mirror of https://github.com/databricks/cli.git
Compare commits
57 Commits
bb3ce48dcb
...
ece4725328
Author | SHA1 | Date |
---|---|---|
shreyas-goenka | ece4725328 | |
Pieter Noordhuis | 75b09ff230 | |
Pieter Noordhuis | 4fea0219fd | |
Shreyas Goenka | 039057fdd7 | |
Shreyas Goenka | 76092ccaa7 | |
Shreyas Goenka | ea6906e88c | |
Shreyas Goenka | f5ea8dac26 | |
Shreyas Goenka | e6723deb9d | |
Shreyas Goenka | b0e527efe8 | |
Shreyas Goenka | 040626589a | |
Shreyas Goenka | 4cc2790300 | |
Shreyas Goenka | 68dc6c1ce4 | |
Shreyas Goenka | 1218178e64 | |
Shreyas Goenka | 250d4265ce | |
Shreyas Goenka | f9287e0101 | |
Shreyas Goenka | 6b122348ad | |
Shreyas Goenka | e32ebd0b48 | |
Shreyas Goenka | 49b2cf2723 | |
Shreyas Goenka | 8a2fe4969c | |
Shreyas Goenka | 1a961eb19c | |
Shreyas Goenka | 810da66809 | |
Shreyas Goenka | 701b1786a8 | |
Shreyas Goenka | 6192835d63 | |
Shreyas Goenka | d241c2b39c | |
Shreyas Goenka | 3e3ddfd0cb | |
Shreyas Goenka | eb94cd6717 | |
Shreyas Goenka | c5a02ef8fb | |
Shreyas Goenka | a9b8575bc3 | |
Shreyas Goenka | 266c26ce09 | |
Shreyas Goenka | 9921263928 | |
Shreyas Goenka | f919e94bce | |
Shreyas Goenka | e43f566579 | |
Shreyas Goenka | 227dfe95ca | |
Shreyas Goenka | d3d5d4c0d6 | |
Shreyas Goenka | 274fd636e0 | |
Shreyas Goenka | bdecd08206 | |
Shreyas Goenka | 13748f177d | |
Shreyas Goenka | 39cb5e8471 | |
Shreyas Goenka | a90eb57a5b | |
Shreyas Goenka | aa2e16d757 | |
Shreyas Goenka | aeab4efda1 | |
Shreyas Goenka | df3bbad70b | |
Shreyas Goenka | f10038a20e | |
Shreyas Goenka | de7eb94e45 | |
Shreyas Goenka | 73826acb2f | |
Shreyas Goenka | fa545777bd | |
Shreyas Goenka | d180bab15d | |
Shreyas Goenka | d47b0d6f47 | |
Shreyas Goenka | 6f9817e194 | |
Shreyas Goenka | 88d0402f44 | |
Shreyas Goenka | 4b22e2d658 | |
Shreyas Goenka | 9b66cd523b | |
Shreyas Goenka | d04b6b08ea | |
Shreyas Goenka | 7c7abeff81 | |
Shreyas Goenka | ce5792c256 | |
Shreyas Goenka | 8f4f3ae9c6 | |
Shreyas Goenka | f772ce4259 |
|
@ -7,8 +7,8 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/databricks/cli/bundle"
|
||||
"github.com/databricks/cli/bundle/bundletest"
|
||||
"github.com/databricks/cli/bundle/config"
|
||||
"github.com/databricks/cli/bundle/internal/bundletest"
|
||||
"github.com/databricks/cli/internal/testutil"
|
||||
"github.com/databricks/cli/libs/dyn"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
|
|
@ -21,18 +21,13 @@ func (m *cleanUp) Name() string {
|
|||
}
|
||||
|
||||
func (m *cleanUp) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics {
|
||||
uploadPath, err := libraries.GetUploadBasePath(b)
|
||||
if err != nil {
|
||||
return diag.FromErr(err)
|
||||
}
|
||||
|
||||
client, err := libraries.GetFilerForLibraries(b.WorkspaceClient(), uploadPath)
|
||||
if err != nil {
|
||||
return diag.FromErr(err)
|
||||
client, uploadPath, diags := libraries.GetFilerForLibraries(ctx, b)
|
||||
if diags.HasError() {
|
||||
return diags
|
||||
}
|
||||
|
||||
// We intentionally ignore the error because it is not critical to the deployment
|
||||
err = client.Delete(ctx, ".", filer.DeleteRecursively)
|
||||
err := client.Delete(ctx, ".", filer.DeleteRecursively)
|
||||
if err != nil {
|
||||
log.Errorf(ctx, "failed to delete %s: %v", uploadPath, err)
|
||||
}
|
||||
|
|
|
@ -125,6 +125,36 @@ func TestApplyPresetsPrefixForUcSchema(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestApplyPresetsUCVolumesShouldNotBePrefixed(t *testing.T) {
|
||||
b := &bundle.Bundle{
|
||||
Config: config.Root{
|
||||
Resources: config.Resources{
|
||||
Volumes: map[string]*resources.Volume{
|
||||
"volume1": {
|
||||
CreateVolumeRequestContent: &catalog.CreateVolumeRequestContent{
|
||||
Name: "volume1",
|
||||
CatalogName: "catalog1",
|
||||
SchemaName: "schema1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Presets: config.Presets{
|
||||
NamePrefix: "[prefix]",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
diag := bundle.Apply(ctx, b, mutator.ApplyPresets())
|
||||
|
||||
if diag.HasError() {
|
||||
t.Fatalf("unexpected error: %v", diag)
|
||||
}
|
||||
|
||||
require.Equal(t, "volume1", b.Config.Resources.Volumes["volume1"].Name)
|
||||
}
|
||||
|
||||
func TestApplyPresetsTags(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
|
|
@ -5,10 +5,10 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/databricks/cli/bundle"
|
||||
"github.com/databricks/cli/bundle/bundletest"
|
||||
"github.com/databricks/cli/bundle/config"
|
||||
"github.com/databricks/cli/bundle/config/mutator"
|
||||
"github.com/databricks/cli/bundle/config/resources"
|
||||
"github.com/databricks/cli/bundle/internal/bundletest"
|
||||
"github.com/databricks/cli/libs/dyn"
|
||||
"github.com/databricks/databricks-sdk-go/service/dashboards"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
|
|
@ -7,9 +7,9 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/databricks/cli/bundle"
|
||||
"github.com/databricks/cli/bundle/bundletest"
|
||||
"github.com/databricks/cli/bundle/config"
|
||||
"github.com/databricks/cli/bundle/config/resources"
|
||||
"github.com/databricks/cli/bundle/internal/bundletest"
|
||||
"github.com/databricks/cli/libs/dyn"
|
||||
"github.com/databricks/databricks-sdk-go/service/compute"
|
||||
"github.com/databricks/databricks-sdk-go/service/pipelines"
|
||||
|
|
|
@ -3,7 +3,7 @@ package mutator
|
|||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
"strings"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"github.com/databricks/cli/bundle"
|
||||
|
@ -128,6 +128,9 @@ func mockBundle(mode config.Mode) *bundle.Bundle {
|
|||
Schemas: map[string]*resources.Schema{
|
||||
"schema1": {CreateSchema: &catalog.CreateSchema{Name: "schema1"}},
|
||||
},
|
||||
Volumes: map[string]*resources.Volume{
|
||||
"volume1": {CreateVolumeRequestContent: &catalog.CreateVolumeRequestContent{Name: "volume1"}},
|
||||
},
|
||||
Clusters: map[string]*resources.Cluster{
|
||||
"cluster1": {ClusterSpec: &compute.ClusterSpec{ClusterName: "cluster1", SparkVersion: "13.2.x", NumWorkers: 1}},
|
||||
},
|
||||
|
@ -307,6 +310,8 @@ func TestProcessTargetModeDefault(t *testing.T) {
|
|||
assert.Equal(t, "servingendpoint1", b.Config.Resources.ModelServingEndpoints["servingendpoint1"].Name)
|
||||
assert.Equal(t, "registeredmodel1", b.Config.Resources.RegisteredModels["registeredmodel1"].Name)
|
||||
assert.Equal(t, "qualityMonitor1", b.Config.Resources.QualityMonitors["qualityMonitor1"].TableName)
|
||||
assert.Equal(t, "schema1", b.Config.Resources.Schemas["schema1"].Name)
|
||||
assert.Equal(t, "volume1", b.Config.Resources.Volumes["volume1"].Name)
|
||||
assert.Equal(t, "cluster1", b.Config.Resources.Clusters["cluster1"].ClusterName)
|
||||
}
|
||||
|
||||
|
@ -351,6 +356,8 @@ func TestProcessTargetModeProduction(t *testing.T) {
|
|||
assert.Equal(t, "servingendpoint1", b.Config.Resources.ModelServingEndpoints["servingendpoint1"].Name)
|
||||
assert.Equal(t, "registeredmodel1", b.Config.Resources.RegisteredModels["registeredmodel1"].Name)
|
||||
assert.Equal(t, "qualityMonitor1", b.Config.Resources.QualityMonitors["qualityMonitor1"].TableName)
|
||||
assert.Equal(t, "schema1", b.Config.Resources.Schemas["schema1"].Name)
|
||||
assert.Equal(t, "volume1", b.Config.Resources.Volumes["volume1"].Name)
|
||||
assert.Equal(t, "cluster1", b.Config.Resources.Clusters["cluster1"].ClusterName)
|
||||
}
|
||||
|
||||
|
@ -384,10 +391,17 @@ func TestAllResourcesMocked(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// Make sure that we at least rename all resources
|
||||
func TestAllResourcesRenamed(t *testing.T) {
|
||||
// Make sure that we at rename all non UC resources
|
||||
func TestAllNonUCResourcesAreRenamed(t *testing.T) {
|
||||
b := mockBundle(config.Development)
|
||||
|
||||
// UC resources should not have a prefix added to their name. Right now
|
||||
// this list only contains the Volume resource since we have yet to remove
|
||||
// prefixing support for UC schemas and registered models.
|
||||
ucFields := []reflect.Type{
|
||||
reflect.TypeOf(&resources.Volume{}),
|
||||
}
|
||||
|
||||
m := bundle.Seq(ProcessTargetMode(), ApplyPresets())
|
||||
diags := bundle.Apply(context.Background(), b, m)
|
||||
require.NoError(t, diags.Error())
|
||||
|
@ -400,14 +414,14 @@ func TestAllResourcesRenamed(t *testing.T) {
|
|||
for _, key := range field.MapKeys() {
|
||||
resource := field.MapIndex(key)
|
||||
nameField := resource.Elem().FieldByName("Name")
|
||||
if nameField.IsValid() && nameField.Kind() == reflect.String {
|
||||
assert.True(
|
||||
t,
|
||||
strings.Contains(nameField.String(), "dev"),
|
||||
"process_target_mode should rename '%s' in '%s'",
|
||||
key,
|
||||
resources.Type().Field(i).Name,
|
||||
)
|
||||
if !nameField.IsValid() || nameField.Kind() != reflect.String {
|
||||
continue
|
||||
}
|
||||
|
||||
if slices.Contains(ucFields, resource.Type()) {
|
||||
assert.NotContains(t, nameField.String(), "dev", "process_target_mode should not rename '%s' in '%s'", key, resources.Type().Field(i).Name)
|
||||
} else {
|
||||
assert.Contains(t, nameField.String(), "dev", "process_target_mode should rename '%s' in '%s'", key, resources.Type().Field(i).Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,9 +6,9 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/databricks/cli/bundle"
|
||||
"github.com/databricks/cli/bundle/bundletest"
|
||||
"github.com/databricks/cli/bundle/config"
|
||||
"github.com/databricks/cli/bundle/config/mutator"
|
||||
"github.com/databricks/cli/bundle/internal/bundletest"
|
||||
"github.com/databricks/cli/libs/dyn"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
|
|
@ -42,6 +42,7 @@ func allResourceTypes(t *testing.T) []string {
|
|||
"quality_monitors",
|
||||
"registered_models",
|
||||
"schemas",
|
||||
"volumes",
|
||||
},
|
||||
resourceTypes,
|
||||
)
|
||||
|
@ -141,6 +142,7 @@ func TestRunAsErrorForUnsupportedResources(t *testing.T) {
|
|||
"registered_models",
|
||||
"experiments",
|
||||
"schemas",
|
||||
"volumes",
|
||||
}
|
||||
|
||||
base := config.Root{
|
||||
|
|
|
@ -6,9 +6,9 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/databricks/cli/bundle"
|
||||
"github.com/databricks/cli/bundle/bundletest"
|
||||
"github.com/databricks/cli/bundle/config"
|
||||
"github.com/databricks/cli/bundle/config/mutator"
|
||||
"github.com/databricks/cli/bundle/internal/bundletest"
|
||||
"github.com/databricks/cli/libs/dyn"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
|
|
@ -6,10 +6,10 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/databricks/cli/bundle"
|
||||
"github.com/databricks/cli/bundle/bundletest"
|
||||
"github.com/databricks/cli/bundle/config"
|
||||
"github.com/databricks/cli/bundle/config/mutator"
|
||||
"github.com/databricks/cli/bundle/config/resources"
|
||||
"github.com/databricks/cli/bundle/internal/bundletest"
|
||||
"github.com/databricks/cli/libs/dyn"
|
||||
"github.com/databricks/cli/libs/vfs"
|
||||
"github.com/databricks/databricks-sdk-go/service/dashboards"
|
||||
|
|
|
@ -8,11 +8,11 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/databricks/cli/bundle"
|
||||
"github.com/databricks/cli/bundle/bundletest"
|
||||
"github.com/databricks/cli/bundle/config"
|
||||
"github.com/databricks/cli/bundle/config/mutator"
|
||||
"github.com/databricks/cli/bundle/config/resources"
|
||||
"github.com/databricks/cli/bundle/config/variable"
|
||||
"github.com/databricks/cli/bundle/internal/bundletest"
|
||||
"github.com/databricks/cli/libs/diag"
|
||||
"github.com/databricks/cli/libs/dyn"
|
||||
"github.com/databricks/cli/libs/vfs"
|
||||
|
|
|
@ -20,6 +20,7 @@ type Resources struct {
|
|||
RegisteredModels map[string]*resources.RegisteredModel `json:"registered_models,omitempty"`
|
||||
QualityMonitors map[string]*resources.QualityMonitor `json:"quality_monitors,omitempty"`
|
||||
Schemas map[string]*resources.Schema `json:"schemas,omitempty"`
|
||||
Volumes map[string]*resources.Volume `json:"volumes,omitempty"`
|
||||
Clusters map[string]*resources.Cluster `json:"clusters,omitempty"`
|
||||
Dashboards map[string]*resources.Dashboard `json:"dashboards,omitempty"`
|
||||
}
|
||||
|
@ -79,6 +80,7 @@ func (r *Resources) AllResources() []ResourceGroup {
|
|||
collectResourceMap(descriptions["schemas"], r.Schemas),
|
||||
collectResourceMap(descriptions["clusters"], r.Clusters),
|
||||
collectResourceMap(descriptions["dashboards"], r.Dashboards),
|
||||
collectResourceMap(descriptions["volumes"], r.Volumes),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -183,5 +185,11 @@ func SupportedResources() map[string]ResourceDescription {
|
|||
SingularTitle: "Dashboard",
|
||||
PluralTitle: "Dashboards",
|
||||
},
|
||||
"volumes": {
|
||||
SingularName: "volume",
|
||||
PluralName: "volumes",
|
||||
SingularTitle: "Volume",
|
||||
PluralTitle: "Volumes",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
package resources
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/databricks/databricks-sdk-go"
|
||||
"github.com/databricks/databricks-sdk-go/marshal"
|
||||
"github.com/databricks/databricks-sdk-go/service/catalog"
|
||||
)
|
||||
|
||||
type Volume struct {
|
||||
// List of grants to apply on this volume.
|
||||
Grants []Grant `json:"grants,omitempty"`
|
||||
|
||||
// Full name of the volume (catalog_name.schema_name.volume_name). This value is read from
|
||||
// the terraform state after deployment succeeds.
|
||||
ID string `json:"id,omitempty" bundle:"readonly"`
|
||||
|
||||
*catalog.CreateVolumeRequestContent
|
||||
|
||||
ModifiedStatus ModifiedStatus `json:"modified_status,omitempty" bundle:"internal"`
|
||||
URL string `json:"url,omitempty" bundle:"internal"`
|
||||
}
|
||||
|
||||
func (v *Volume) UnmarshalJSON(b []byte) error {
|
||||
return marshal.Unmarshal(b, v)
|
||||
}
|
||||
|
||||
func (v Volume) MarshalJSON() ([]byte, error) {
|
||||
return marshal.Marshal(v)
|
||||
}
|
||||
|
||||
func (v *Volume) Exists(ctx context.Context, w *databricks.WorkspaceClient, id string) (bool, error) {
|
||||
return false, fmt.Errorf("volume.Exists() is not supported")
|
||||
}
|
||||
|
||||
func (v *Volume) TerraformResourceName() string {
|
||||
return "databricks_volume"
|
||||
}
|
||||
|
||||
func (v *Volume) InitializeURL(baseURL url.URL) {
|
||||
if v.ID == "" {
|
||||
return
|
||||
}
|
||||
baseURL.Path = fmt.Sprintf("explore/data/volumes/%s", strings.ReplaceAll(v.ID, ".", "/"))
|
||||
v.URL = baseURL.String()
|
||||
}
|
||||
|
||||
func (v *Volume) GetURL() string {
|
||||
return v.URL
|
||||
}
|
||||
|
||||
func (v *Volume) GetName() string {
|
||||
return v.Name
|
||||
}
|
|
@ -5,9 +5,9 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/databricks/cli/bundle"
|
||||
"github.com/databricks/cli/bundle/bundletest"
|
||||
"github.com/databricks/cli/bundle/config"
|
||||
"github.com/databricks/cli/bundle/config/resources"
|
||||
"github.com/databricks/cli/bundle/internal/bundletest"
|
||||
"github.com/databricks/cli/bundle/metadata"
|
||||
"github.com/databricks/cli/libs/dyn"
|
||||
"github.com/databricks/databricks-sdk-go/service/jobs"
|
||||
|
|
|
@ -166,6 +166,16 @@ func TerraformToBundle(state *resourcesState, config *config.Root) error {
|
|||
}
|
||||
cur.ID = instance.Attributes.ID
|
||||
config.Resources.Schemas[resource.Name] = cur
|
||||
case "databricks_volume":
|
||||
if config.Resources.Volumes == nil {
|
||||
config.Resources.Volumes = make(map[string]*resources.Volume)
|
||||
}
|
||||
cur := config.Resources.Volumes[resource.Name]
|
||||
if cur == nil {
|
||||
cur = &resources.Volume{ModifiedStatus: resources.ModifiedStatusDeleted}
|
||||
}
|
||||
cur.ID = instance.Attributes.ID
|
||||
config.Resources.Volumes[resource.Name] = cur
|
||||
case "databricks_cluster":
|
||||
if config.Resources.Clusters == nil {
|
||||
config.Resources.Clusters = make(map[string]*resources.Cluster)
|
||||
|
@ -235,6 +245,11 @@ func TerraformToBundle(state *resourcesState, config *config.Root) error {
|
|||
src.ModifiedStatus = resources.ModifiedStatusCreated
|
||||
}
|
||||
}
|
||||
for _, src := range config.Resources.Volumes {
|
||||
if src.ModifiedStatus == "" && src.ID == "" {
|
||||
src.ModifiedStatus = resources.ModifiedStatusCreated
|
||||
}
|
||||
}
|
||||
for _, src := range config.Resources.Clusters {
|
||||
if src.ModifiedStatus == "" && src.ID == "" {
|
||||
src.ModifiedStatus = resources.ModifiedStatusCreated
|
||||
|
|
|
@ -670,6 +670,14 @@ func TestTerraformToBundleEmptyLocalResources(t *testing.T) {
|
|||
{Attributes: stateInstanceAttributes{ID: "1"}},
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: "databricks_volume",
|
||||
Mode: "managed",
|
||||
Name: "test_volume",
|
||||
Instances: []stateResourceInstance{
|
||||
{Attributes: stateInstanceAttributes{ID: "1"}},
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: "databricks_cluster",
|
||||
Mode: "managed",
|
||||
|
@ -715,6 +723,9 @@ func TestTerraformToBundleEmptyLocalResources(t *testing.T) {
|
|||
assert.Equal(t, "1", config.Resources.Schemas["test_schema"].ID)
|
||||
assert.Equal(t, resources.ModifiedStatusDeleted, config.Resources.Schemas["test_schema"].ModifiedStatus)
|
||||
|
||||
assert.Equal(t, "1", config.Resources.Volumes["test_volume"].ID)
|
||||
assert.Equal(t, resources.ModifiedStatusDeleted, config.Resources.Volumes["test_volume"].ModifiedStatus)
|
||||
|
||||
assert.Equal(t, "1", config.Resources.Clusters["test_cluster"].ID)
|
||||
assert.Equal(t, resources.ModifiedStatusDeleted, config.Resources.Clusters["test_cluster"].ModifiedStatus)
|
||||
|
||||
|
@ -783,6 +794,13 @@ func TestTerraformToBundleEmptyRemoteResources(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
Volumes: map[string]*resources.Volume{
|
||||
"test_volume": {
|
||||
CreateVolumeRequestContent: &catalog.CreateVolumeRequestContent{
|
||||
Name: "test_volume",
|
||||
},
|
||||
},
|
||||
},
|
||||
Clusters: map[string]*resources.Cluster{
|
||||
"test_cluster": {
|
||||
ClusterSpec: &compute.ClusterSpec{
|
||||
|
@ -829,6 +847,9 @@ func TestTerraformToBundleEmptyRemoteResources(t *testing.T) {
|
|||
assert.Equal(t, "", config.Resources.Schemas["test_schema"].ID)
|
||||
assert.Equal(t, resources.ModifiedStatusCreated, config.Resources.Schemas["test_schema"].ModifiedStatus)
|
||||
|
||||
assert.Equal(t, "", config.Resources.Volumes["test_volume"].ID)
|
||||
assert.Equal(t, resources.ModifiedStatusCreated, config.Resources.Volumes["test_volume"].ModifiedStatus)
|
||||
|
||||
assert.Equal(t, "", config.Resources.Clusters["test_cluster"].ID)
|
||||
assert.Equal(t, resources.ModifiedStatusCreated, config.Resources.Clusters["test_cluster"].ModifiedStatus)
|
||||
|
||||
|
@ -937,6 +958,18 @@ func TestTerraformToBundleModifiedResources(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
Volumes: map[string]*resources.Volume{
|
||||
"test_volume": {
|
||||
CreateVolumeRequestContent: &catalog.CreateVolumeRequestContent{
|
||||
Name: "test_volume",
|
||||
},
|
||||
},
|
||||
"test_volume_new": {
|
||||
CreateVolumeRequestContent: &catalog.CreateVolumeRequestContent{
|
||||
Name: "test_volume_new",
|
||||
},
|
||||
},
|
||||
},
|
||||
Clusters: map[string]*resources.Cluster{
|
||||
"test_cluster": {
|
||||
ClusterSpec: &compute.ClusterSpec{
|
||||
|
@ -1093,6 +1126,14 @@ func TestTerraformToBundleModifiedResources(t *testing.T) {
|
|||
{Attributes: stateInstanceAttributes{ID: "2"}},
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: "databricks_volume",
|
||||
Mode: "managed",
|
||||
Name: "test_volume",
|
||||
Instances: []stateResourceInstance{
|
||||
{Attributes: stateInstanceAttributes{ID: "1"}},
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: "databricks_cluster",
|
||||
Mode: "managed",
|
||||
|
@ -1101,6 +1142,14 @@ func TestTerraformToBundleModifiedResources(t *testing.T) {
|
|||
{Attributes: stateInstanceAttributes{ID: "1"}},
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: "databricks_volume",
|
||||
Mode: "managed",
|
||||
Name: "test_volume_old",
|
||||
Instances: []stateResourceInstance{
|
||||
{Attributes: stateInstanceAttributes{ID: "2"}},
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: "databricks_cluster",
|
||||
Mode: "managed",
|
||||
|
@ -1186,6 +1235,13 @@ func TestTerraformToBundleModifiedResources(t *testing.T) {
|
|||
assert.Equal(t, "", config.Resources.Schemas["test_schema_new"].ID)
|
||||
assert.Equal(t, resources.ModifiedStatusCreated, config.Resources.Schemas["test_schema_new"].ModifiedStatus)
|
||||
|
||||
assert.Equal(t, "1", config.Resources.Volumes["test_volume"].ID)
|
||||
assert.Equal(t, "", config.Resources.Volumes["test_volume"].ModifiedStatus)
|
||||
assert.Equal(t, "2", config.Resources.Volumes["test_volume_old"].ID)
|
||||
assert.Equal(t, resources.ModifiedStatusDeleted, config.Resources.Volumes["test_volume_old"].ModifiedStatus)
|
||||
assert.Equal(t, "", config.Resources.Volumes["test_volume_new"].ID)
|
||||
assert.Equal(t, resources.ModifiedStatusCreated, config.Resources.Volumes["test_volume_new"].ModifiedStatus)
|
||||
|
||||
assert.Equal(t, "1", config.Resources.Clusters["test_cluster"].ID)
|
||||
assert.Equal(t, "", config.Resources.Clusters["test_cluster"].ModifiedStatus)
|
||||
assert.Equal(t, "2", config.Resources.Clusters["test_cluster_old"].ID)
|
||||
|
|
|
@ -58,6 +58,8 @@ func (m *interpolateMutator) Apply(ctx context.Context, b *bundle.Bundle) diag.D
|
|||
path = dyn.NewPath(dyn.Key("databricks_quality_monitor")).Append(path[2:]...)
|
||||
case dyn.Key("schemas"):
|
||||
path = dyn.NewPath(dyn.Key("databricks_schema")).Append(path[2:]...)
|
||||
case dyn.Key("volumes"):
|
||||
path = dyn.NewPath(dyn.Key("databricks_volume")).Append(path[2:]...)
|
||||
case dyn.Key("clusters"):
|
||||
path = dyn.NewPath(dyn.Key("databricks_cluster")).Append(path[2:]...)
|
||||
case dyn.Key("dashboards"):
|
||||
|
|
|
@ -31,6 +31,7 @@ func TestInterpolate(t *testing.T) {
|
|||
"other_model_serving": "${resources.model_serving_endpoints.other_model_serving.id}",
|
||||
"other_registered_model": "${resources.registered_models.other_registered_model.id}",
|
||||
"other_schema": "${resources.schemas.other_schema.id}",
|
||||
"other_volume": "${resources.volumes.other_volume.id}",
|
||||
"other_cluster": "${resources.clusters.other_cluster.id}",
|
||||
"other_dashboard": "${resources.dashboards.other_dashboard.id}",
|
||||
},
|
||||
|
@ -69,6 +70,7 @@ func TestInterpolate(t *testing.T) {
|
|||
assert.Equal(t, "${databricks_model_serving.other_model_serving.id}", j.Tags["other_model_serving"])
|
||||
assert.Equal(t, "${databricks_registered_model.other_registered_model.id}", j.Tags["other_registered_model"])
|
||||
assert.Equal(t, "${databricks_schema.other_schema.id}", j.Tags["other_schema"])
|
||||
assert.Equal(t, "${databricks_volume.other_volume.id}", j.Tags["other_volume"])
|
||||
assert.Equal(t, "${databricks_cluster.other_cluster.id}", j.Tags["other_cluster"])
|
||||
assert.Equal(t, "${databricks_dashboard.other_dashboard.id}", j.Tags["other_dashboard"])
|
||||
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
package tfdyn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/databricks/cli/bundle/internal/tf/schema"
|
||||
"github.com/databricks/cli/libs/dyn"
|
||||
"github.com/databricks/cli/libs/dyn/convert"
|
||||
"github.com/databricks/cli/libs/log"
|
||||
)
|
||||
|
||||
func convertVolumeResource(ctx context.Context, vin dyn.Value) (dyn.Value, error) {
|
||||
// Normalize the output value to the target schema.
|
||||
vout, diags := convert.Normalize(schema.ResourceVolume{}, vin)
|
||||
for _, diag := range diags {
|
||||
log.Debugf(ctx, "volume normalization diagnostic: %s", diag.Summary)
|
||||
}
|
||||
|
||||
return vout, nil
|
||||
}
|
||||
|
||||
type volumeConverter struct{}
|
||||
|
||||
func (volumeConverter) Convert(ctx context.Context, key string, vin dyn.Value, out *schema.Resources) error {
|
||||
vout, err := convertVolumeResource(ctx, vin)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Add the converted resource to the output.
|
||||
out.Volume[key] = vout.AsAny()
|
||||
|
||||
// Configure grants for this resource.
|
||||
if grants := convertGrantsResource(ctx, vin); grants != nil {
|
||||
grants.Volume = fmt.Sprintf("${databricks_volume.%s.id}", key)
|
||||
out.Grants["volume_"+key] = grants
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
registerConverter("volumes", volumeConverter{})
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
package tfdyn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/databricks/cli/bundle/config/resources"
|
||||
"github.com/databricks/cli/bundle/internal/tf/schema"
|
||||
"github.com/databricks/cli/libs/dyn"
|
||||
"github.com/databricks/cli/libs/dyn/convert"
|
||||
"github.com/databricks/databricks-sdk-go/service/catalog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestConvertVolume(t *testing.T) {
|
||||
var src = resources.Volume{
|
||||
CreateVolumeRequestContent: &catalog.CreateVolumeRequestContent{
|
||||
CatalogName: "catalog",
|
||||
Comment: "comment",
|
||||
Name: "name",
|
||||
SchemaName: "schema",
|
||||
StorageLocation: "s3://bucket/path",
|
||||
VolumeType: "EXTERNAL",
|
||||
},
|
||||
Grants: []resources.Grant{
|
||||
{
|
||||
Privileges: []string{"READ_VOLUME"},
|
||||
Principal: "jack@gmail.com",
|
||||
},
|
||||
{
|
||||
Privileges: []string{"WRITE_VOLUME"},
|
||||
Principal: "jane@gmail.com",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
vin, err := convert.FromTyped(src, dyn.NilValue)
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := context.Background()
|
||||
out := schema.NewResources()
|
||||
err = volumeConverter{}.Convert(ctx, "my_volume", vin, out)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Assert equality on the volume
|
||||
require.Equal(t, map[string]any{
|
||||
"catalog_name": "catalog",
|
||||
"comment": "comment",
|
||||
"name": "name",
|
||||
"schema_name": "schema",
|
||||
"storage_location": "s3://bucket/path",
|
||||
"volume_type": "EXTERNAL",
|
||||
}, out.Volume["my_volume"])
|
||||
|
||||
// Assert equality on the grants
|
||||
assert.Equal(t, &schema.ResourceGrants{
|
||||
Volume: "${databricks_volume.my_volume.id}",
|
||||
Grant: []schema.ResourceGrantsGrant{
|
||||
{
|
||||
Privileges: []string{"READ_VOLUME"},
|
||||
Principal: "jack@gmail.com",
|
||||
},
|
||||
{
|
||||
Privileges: []string{"WRITE_VOLUME"},
|
||||
Principal: "jane@gmail.com",
|
||||
},
|
||||
},
|
||||
}, out.Grants["volume_my_volume"])
|
||||
}
|
|
@ -6,9 +6,9 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/databricks/cli/bundle"
|
||||
"github.com/databricks/cli/bundle/bundletest"
|
||||
"github.com/databricks/cli/bundle/config"
|
||||
"github.com/databricks/cli/bundle/config/resources"
|
||||
"github.com/databricks/cli/bundle/internal/bundletest"
|
||||
"github.com/databricks/cli/internal/testutil"
|
||||
"github.com/databricks/cli/libs/dyn"
|
||||
"github.com/databricks/databricks-sdk-go/service/compute"
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
package libraries
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/databricks/cli/bundle"
|
||||
"github.com/databricks/cli/libs/diag"
|
||||
"github.com/databricks/cli/libs/filer"
|
||||
)
|
||||
|
||||
// This function returns the right filer to use, to upload artifacts to the configured location.
|
||||
// Supported locations:
|
||||
// 1. WSFS
|
||||
// 2. UC volumes
|
||||
func GetFilerForLibraries(ctx context.Context, b *bundle.Bundle) (filer.Filer, string, diag.Diagnostics) {
|
||||
artifactPath := b.Config.Workspace.ArtifactPath
|
||||
if artifactPath == "" {
|
||||
return nil, "", diag.Errorf("remote artifact path not configured")
|
||||
}
|
||||
|
||||
switch {
|
||||
case IsVolumesPath(artifactPath):
|
||||
return filerForVolume(ctx, b)
|
||||
|
||||
default:
|
||||
return filerForWorkspace(b)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
package libraries
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/databricks/cli/bundle"
|
||||
"github.com/databricks/cli/bundle/config"
|
||||
"github.com/databricks/cli/libs/filer"
|
||||
sdkconfig "github.com/databricks/databricks-sdk-go/config"
|
||||
"github.com/databricks/databricks-sdk-go/experimental/mocks"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGetFilerForLibrariesValidWsfs(t *testing.T) {
|
||||
b := &bundle.Bundle{
|
||||
Config: config.Root{
|
||||
Workspace: config.Workspace{
|
||||
ArtifactPath: "/foo/bar/artifacts",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
client, uploadPath, diags := GetFilerForLibraries(context.Background(), b)
|
||||
require.NoError(t, diags.Error())
|
||||
assert.Equal(t, "/foo/bar/artifacts/.internal", uploadPath)
|
||||
|
||||
assert.IsType(t, &filer.WorkspaceFilesClient{}, client)
|
||||
}
|
||||
|
||||
func TestGetFilerForLibrariesValidUcVolume(t *testing.T) {
|
||||
b := &bundle.Bundle{
|
||||
Config: config.Root{
|
||||
Workspace: config.Workspace{
|
||||
ArtifactPath: "/Volumes/main/my_schema/my_volume",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
m := mocks.NewMockWorkspaceClient(t)
|
||||
m.WorkspaceClient.Config = &sdkconfig.Config{}
|
||||
m.GetMockFilesAPI().EXPECT().GetDirectoryMetadataByDirectoryPath(mock.Anything, "/Volumes/main/my_schema/my_volume").Return(nil)
|
||||
b.SetWorkpaceClient(m.WorkspaceClient)
|
||||
|
||||
client, uploadPath, diags := GetFilerForLibraries(context.Background(), b)
|
||||
require.NoError(t, diags.Error())
|
||||
assert.Equal(t, "/Volumes/main/my_schema/my_volume/.internal", uploadPath)
|
||||
|
||||
assert.IsType(t, &filer.FilesClient{}, client)
|
||||
}
|
||||
|
||||
func TestGetFilerForLibrariesRemotePathNotSet(t *testing.T) {
|
||||
b := &bundle.Bundle{
|
||||
Config: config.Root{
|
||||
Workspace: config.Workspace{},
|
||||
},
|
||||
}
|
||||
|
||||
_, _, diags := GetFilerForLibraries(context.Background(), b)
|
||||
require.EqualError(t, diags.Error(), "remote artifact path not configured")
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
package libraries
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/databricks/cli/bundle"
|
||||
"github.com/databricks/cli/libs/diag"
|
||||
"github.com/databricks/cli/libs/dyn"
|
||||
"github.com/databricks/cli/libs/dyn/dynvar"
|
||||
"github.com/databricks/cli/libs/filer"
|
||||
)
|
||||
|
||||
// This function returns a filer for ".internal" folder inside the directory configured
|
||||
// at `workspace.artifact_path`.
|
||||
// This function also checks if the UC volume exists in the workspace and then:
|
||||
// 1. If the UC volume exists in the workspace:
|
||||
// Returns a filer for the UC volume.
|
||||
// 2. If the UC volume does not exist in the workspace but is (with high confidence) defined in
|
||||
// the bundle configuration:
|
||||
// Returns an error and a warning that instructs the user to deploy the
|
||||
// UC volume before using it in the artifact path.
|
||||
// 3. If the UC volume does not exist in the workspace and is not defined in the bundle configuration:
|
||||
// Returns an error.
|
||||
func filerForVolume(ctx context.Context, b *bundle.Bundle) (filer.Filer, string, diag.Diagnostics) {
|
||||
artifactPath := b.Config.Workspace.ArtifactPath
|
||||
w := b.WorkspaceClient()
|
||||
|
||||
if !strings.HasPrefix(artifactPath, "/Volumes/") {
|
||||
return nil, "", diag.Errorf("expected artifact_path to start with /Volumes/, got %s", artifactPath)
|
||||
}
|
||||
|
||||
parts := strings.Split(artifactPath, "/")
|
||||
volumeFormatErr := fmt.Errorf("expected UC volume path to be in the format /Volumes/<catalog>/<schema>/<volume>/..., got %s", artifactPath)
|
||||
|
||||
// Incorrect format.
|
||||
if len(parts) < 5 {
|
||||
return nil, "", diag.FromErr(volumeFormatErr)
|
||||
}
|
||||
|
||||
catalogName := parts[2]
|
||||
schemaName := parts[3]
|
||||
volumeName := parts[4]
|
||||
|
||||
// Incorrect format.
|
||||
if catalogName == "" || schemaName == "" || volumeName == "" {
|
||||
return nil, "", diag.FromErr(volumeFormatErr)
|
||||
}
|
||||
|
||||
// Check if the UC volume exists in the workspace.
|
||||
volumePath := fmt.Sprintf("/Volumes/%s/%s/%s", catalogName, schemaName, volumeName)
|
||||
err := w.Files.GetDirectoryMetadataByDirectoryPath(ctx, volumePath)
|
||||
|
||||
// If the volume exists already, directly return the filer for the path to
|
||||
// upload the artifacts to.
|
||||
if err == nil {
|
||||
uploadPath := path.Join(artifactPath, ".internal")
|
||||
f, err := filer.NewFilesClient(w, uploadPath)
|
||||
return f, uploadPath, diag.FromErr(err)
|
||||
}
|
||||
|
||||
diags := diag.Errorf("failed to fetch metadata for the UC volume %s that is configured in the artifact_path: %s", volumePath, err)
|
||||
|
||||
path, locations, ok := findVolumeInBundle(b, catalogName, schemaName, volumeName)
|
||||
if !ok {
|
||||
return nil, "", diags
|
||||
}
|
||||
|
||||
warning := diag.Diagnostic{
|
||||
Severity: diag.Warning,
|
||||
Summary: `You might be using a UC volume in your artifact_path that is managed by this bundle but which has not been deployed yet. Please deploy the UC volume in a separate bundle deploy before using it in the artifact_path.`,
|
||||
Locations: locations,
|
||||
Paths: []dyn.Path{path},
|
||||
}
|
||||
return nil, "", diags.Append(warning)
|
||||
}
|
||||
|
||||
func findVolumeInBundle(b *bundle.Bundle, catalogName, schemaName, volumeName string) (dyn.Path, []dyn.Location, bool) {
|
||||
volumes := b.Config.Resources.Volumes
|
||||
for k, v := range volumes {
|
||||
if v.CatalogName != catalogName || v.Name != volumeName {
|
||||
continue
|
||||
}
|
||||
// UC schemas can be defined in the bundle itself, and thus might be interpolated
|
||||
// at runtime via the ${resources.schemas.<name>} syntax. Thus we match the volume
|
||||
// definition if the schema name is the same as the one in the bundle, or if the
|
||||
// schema name is interpolated.
|
||||
// We only have to check for ${resources.schemas...} references because any
|
||||
// other valid reference (like ${var.foo}) would have been interpolated by this point.
|
||||
p, ok := dynvar.PureReferenceToPath(v.SchemaName)
|
||||
isSchemaDefinedInBundle := ok && p.HasPrefix(dyn.Path{dyn.Key("resources"), dyn.Key("schemas")})
|
||||
if v.SchemaName != schemaName && !isSchemaDefinedInBundle {
|
||||
continue
|
||||
}
|
||||
pathString := fmt.Sprintf("resources.volumes.%s", k)
|
||||
return dyn.MustPathFromString(pathString), b.Config.GetLocations(pathString), true
|
||||
}
|
||||
return nil, nil, false
|
||||
}
|
|
@ -0,0 +1,223 @@
|
|||
package libraries
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"github.com/databricks/cli/bundle"
|
||||
"github.com/databricks/cli/bundle/bundletest"
|
||||
"github.com/databricks/cli/bundle/config"
|
||||
"github.com/databricks/cli/bundle/config/resources"
|
||||
"github.com/databricks/cli/libs/diag"
|
||||
"github.com/databricks/cli/libs/dyn"
|
||||
"github.com/databricks/cli/libs/filer"
|
||||
sdkconfig "github.com/databricks/databricks-sdk-go/config"
|
||||
"github.com/databricks/databricks-sdk-go/experimental/mocks"
|
||||
"github.com/databricks/databricks-sdk-go/service/catalog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestFindVolumeInBundle(t *testing.T) {
|
||||
b := &bundle.Bundle{
|
||||
Config: config.Root{
|
||||
Resources: config.Resources{
|
||||
Volumes: map[string]*resources.Volume{
|
||||
"foo": {
|
||||
CreateVolumeRequestContent: &catalog.CreateVolumeRequestContent{
|
||||
CatalogName: "main",
|
||||
Name: "my_volume",
|
||||
SchemaName: "my_schema",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
bundletest.SetLocation(b, "resources.volumes.foo", []dyn.Location{
|
||||
{
|
||||
File: "volume.yml",
|
||||
Line: 1,
|
||||
Column: 2,
|
||||
},
|
||||
})
|
||||
|
||||
// volume is in DAB.
|
||||
path, locations, ok := findVolumeInBundle(b, "main", "my_schema", "my_volume")
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, []dyn.Location{{
|
||||
File: "volume.yml",
|
||||
Line: 1,
|
||||
Column: 2,
|
||||
}}, locations)
|
||||
assert.Equal(t, dyn.MustPathFromString("resources.volumes.foo"), path)
|
||||
|
||||
// wrong volume name
|
||||
_, _, ok = findVolumeInBundle(b, "main", "my_schema", "doesnotexist")
|
||||
assert.False(t, ok)
|
||||
|
||||
// wrong schema name
|
||||
_, _, ok = findVolumeInBundle(b, "main", "doesnotexist", "my_volume")
|
||||
assert.False(t, ok)
|
||||
|
||||
// wrong catalog name
|
||||
_, _, ok = findVolumeInBundle(b, "doesnotexist", "my_schema", "my_volume")
|
||||
assert.False(t, ok)
|
||||
|
||||
// schema name is interpolated but does not have the right prefix. In this case
|
||||
// we should not match the volume.
|
||||
b.Config.Resources.Volumes["foo"].SchemaName = "${foo.bar.baz}"
|
||||
_, _, ok = findVolumeInBundle(b, "main", "my_schema", "my_volume")
|
||||
assert.False(t, ok)
|
||||
|
||||
// schema name is interpolated.
|
||||
b.Config.Resources.Volumes["foo"].SchemaName = "${resources.schemas.my_schema.name}"
|
||||
path, locations, ok = findVolumeInBundle(b, "main", "valuedoesnotmatter", "my_volume")
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, []dyn.Location{{
|
||||
File: "volume.yml",
|
||||
Line: 1,
|
||||
Column: 2,
|
||||
}}, locations)
|
||||
assert.Equal(t, dyn.MustPathFromString("resources.volumes.foo"), path)
|
||||
}
|
||||
|
||||
func TestFilerForVolumeNotInBundle(t *testing.T) {
|
||||
b := &bundle.Bundle{
|
||||
Config: config.Root{
|
||||
Workspace: config.Workspace{
|
||||
ArtifactPath: "/Volumes/main/my_schema/doesnotexist",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
m := mocks.NewMockWorkspaceClient(t)
|
||||
m.WorkspaceClient.Config = &sdkconfig.Config{}
|
||||
m.GetMockFilesAPI().EXPECT().GetDirectoryMetadataByDirectoryPath(mock.Anything, "/Volumes/main/my_schema/doesnotexist").Return(fmt.Errorf("error from API"))
|
||||
b.SetWorkpaceClient(m.WorkspaceClient)
|
||||
|
||||
_, _, diags := filerForVolume(context.Background(), b)
|
||||
assert.EqualError(t, diags.Error(), "failed to fetch metadata for the UC volume /Volumes/main/my_schema/doesnotexist that is configured in the artifact_path: error from API")
|
||||
assert.Len(t, diags, 1)
|
||||
}
|
||||
|
||||
func TestFilerForVolumeInBundle(t *testing.T) {
|
||||
b := &bundle.Bundle{
|
||||
Config: config.Root{
|
||||
Workspace: config.Workspace{
|
||||
ArtifactPath: "/Volumes/main/my_schema/my_volume",
|
||||
},
|
||||
Resources: config.Resources{
|
||||
Volumes: map[string]*resources.Volume{
|
||||
"foo": {
|
||||
CreateVolumeRequestContent: &catalog.CreateVolumeRequestContent{
|
||||
CatalogName: "main",
|
||||
Name: "my_volume",
|
||||
VolumeType: "MANAGED",
|
||||
SchemaName: "my_schema",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
bundletest.SetLocation(b, "resources.volumes.foo", []dyn.Location{
|
||||
{
|
||||
File: "volume.yml",
|
||||
Line: 1,
|
||||
Column: 2,
|
||||
},
|
||||
})
|
||||
|
||||
m := mocks.NewMockWorkspaceClient(t)
|
||||
m.WorkspaceClient.Config = &sdkconfig.Config{}
|
||||
m.GetMockFilesAPI().EXPECT().GetDirectoryMetadataByDirectoryPath(mock.Anything, "/Volumes/main/my_schema/my_volume").Return(fmt.Errorf("error from API"))
|
||||
b.SetWorkpaceClient(m.WorkspaceClient)
|
||||
|
||||
_, _, diags := GetFilerForLibraries(context.Background(), b)
|
||||
assert.EqualError(t, diags.Error(), "failed to fetch metadata for the UC volume /Volumes/main/my_schema/my_volume that is configured in the artifact_path: error from API")
|
||||
assert.Contains(t, diags, diag.Diagnostic{
|
||||
Severity: diag.Warning,
|
||||
Summary: "You might be using a UC volume in your artifact_path that is managed by this bundle but which has not been deployed yet. Please deploy the UC volume in a separate bundle deploy before using it in the artifact_path.",
|
||||
Locations: []dyn.Location{{
|
||||
File: "volume.yml",
|
||||
Line: 1,
|
||||
Column: 2,
|
||||
}},
|
||||
Paths: []dyn.Path{dyn.MustPathFromString("resources.volumes.foo")},
|
||||
})
|
||||
}
|
||||
|
||||
func TestFilerForVolumeWithInvalidVolumePaths(t *testing.T) {
|
||||
invalidPaths := []string{
|
||||
"/Volumes/",
|
||||
"/Volumes/main",
|
||||
"/Volumes/main/",
|
||||
"/Volumes/main//",
|
||||
"/Volumes/main//my_schema",
|
||||
"/Volumes/main/my_schema",
|
||||
"/Volumes/main/my_schema/",
|
||||
"/Volumes/main/my_schema//",
|
||||
"/Volumes//my_schema/my_volume",
|
||||
}
|
||||
|
||||
for _, p := range invalidPaths {
|
||||
b := &bundle.Bundle{
|
||||
Config: config.Root{
|
||||
Workspace: config.Workspace{
|
||||
ArtifactPath: p,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_, _, diags := GetFilerForLibraries(context.Background(), b)
|
||||
require.EqualError(t, diags.Error(), fmt.Sprintf("expected UC volume path to be in the format /Volumes/<catalog>/<schema>/<volume>/..., got %s", p))
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilerForVolumeWithInvalidPrefix(t *testing.T) {
|
||||
b := &bundle.Bundle{
|
||||
Config: config.Root{
|
||||
Workspace: config.Workspace{
|
||||
ArtifactPath: "/Volume/main/my_schema/my_volume",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_, _, diags := filerForVolume(context.Background(), b)
|
||||
require.EqualError(t, diags.Error(), "expected artifact_path to start with /Volumes/, got /Volume/main/my_schema/my_volume")
|
||||
}
|
||||
|
||||
func TestFilerForVolumeWithValidlVolumePaths(t *testing.T) {
|
||||
validPaths := []string{
|
||||
"/Volumes/main/my_schema/my_volume",
|
||||
"/Volumes/main/my_schema/my_volume/",
|
||||
"/Volumes/main/my_schema/my_volume/a/b/c",
|
||||
"/Volumes/main/my_schema/my_volume/a/a/a",
|
||||
}
|
||||
|
||||
for _, p := range validPaths {
|
||||
b := &bundle.Bundle{
|
||||
Config: config.Root{
|
||||
Workspace: config.Workspace{
|
||||
ArtifactPath: p,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
m := mocks.NewMockWorkspaceClient(t)
|
||||
m.WorkspaceClient.Config = &sdkconfig.Config{}
|
||||
m.GetMockFilesAPI().EXPECT().GetDirectoryMetadataByDirectoryPath(mock.Anything, "/Volumes/main/my_schema/my_volume").Return(nil)
|
||||
b.SetWorkpaceClient(m.WorkspaceClient)
|
||||
|
||||
client, uploadPath, diags := filerForVolume(context.Background(), b)
|
||||
require.NoError(t, diags.Error())
|
||||
assert.Equal(t, path.Join(p, ".internal"), uploadPath)
|
||||
assert.IsType(t, &filer.FilesClient{}, client)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package libraries
|
||||
|
||||
import (
|
||||
"path"
|
||||
|
||||
"github.com/databricks/cli/bundle"
|
||||
"github.com/databricks/cli/libs/diag"
|
||||
"github.com/databricks/cli/libs/filer"
|
||||
)
|
||||
|
||||
func filerForWorkspace(b *bundle.Bundle) (filer.Filer, string, diag.Diagnostics) {
|
||||
uploadPath := path.Join(b.Config.Workspace.ArtifactPath, ".internal")
|
||||
f, err := filer.NewWorkspaceFilesClient(b.WorkspaceClient(), uploadPath)
|
||||
return f, uploadPath, diag.FromErr(err)
|
||||
}
|
|
@ -16,8 +16,6 @@ import (
|
|||
"github.com/databricks/cli/libs/filer"
|
||||
"github.com/databricks/cli/libs/log"
|
||||
|
||||
"github.com/databricks/databricks-sdk-go"
|
||||
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
|
@ -130,24 +128,17 @@ func collectLocalLibraries(b *bundle.Bundle) (map[string][]configLocation, error
|
|||
}
|
||||
|
||||
func (u *upload) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics {
|
||||
uploadPath, err := GetUploadBasePath(b)
|
||||
if err != nil {
|
||||
return diag.FromErr(err)
|
||||
client, uploadPath, diags := GetFilerForLibraries(ctx, b)
|
||||
if diags.HasError() {
|
||||
return diags
|
||||
}
|
||||
|
||||
// If the client is not initialized, initialize it
|
||||
// We use client field in mutator to allow for mocking client in testing
|
||||
// Only set the filer client if it's not already set. We use the client field
|
||||
// in the mutator to mock the filer client in testing
|
||||
if u.client == nil {
|
||||
filer, err := GetFilerForLibraries(b.WorkspaceClient(), uploadPath)
|
||||
if err != nil {
|
||||
return diag.FromErr(err)
|
||||
}
|
||||
|
||||
u.client = filer
|
||||
u.client = client
|
||||
}
|
||||
|
||||
var diags diag.Diagnostics
|
||||
|
||||
libs, err := collectLocalLibraries(b)
|
||||
if err != nil {
|
||||
return diag.FromErr(err)
|
||||
|
@ -197,17 +188,6 @@ func (u *upload) Name() string {
|
|||
return "libraries.Upload"
|
||||
}
|
||||
|
||||
func GetFilerForLibraries(w *databricks.WorkspaceClient, uploadPath string) (filer.Filer, error) {
|
||||
if isVolumesPath(uploadPath) {
|
||||
return filer.NewFilesClient(w, uploadPath)
|
||||
}
|
||||
return filer.NewWorkspaceFilesClient(w, uploadPath)
|
||||
}
|
||||
|
||||
func isVolumesPath(path string) bool {
|
||||
return strings.HasPrefix(path, "/Volumes/")
|
||||
}
|
||||
|
||||
// Function to upload file (a library, artifact and etc) to Workspace or UC volume
|
||||
func UploadFile(ctx context.Context, file string, client filer.Filer) error {
|
||||
filename := filepath.Base(file)
|
||||
|
@ -227,12 +207,3 @@ func UploadFile(ctx context.Context, file string, client filer.Filer) error {
|
|||
log.Infof(ctx, "Upload succeeded")
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetUploadBasePath(b *bundle.Bundle) (string, error) {
|
||||
artifactPath := b.Config.Workspace.ArtifactPath
|
||||
if artifactPath == "" {
|
||||
return "", fmt.Errorf("remote artifact path not configured")
|
||||
}
|
||||
|
||||
return path.Join(artifactPath, ".internal"), nil
|
||||
}
|
||||
|
|
|
@ -11,6 +11,8 @@ import (
|
|||
mockfiler "github.com/databricks/cli/internal/mocks/libs/filer"
|
||||
"github.com/databricks/cli/internal/testutil"
|
||||
"github.com/databricks/cli/libs/filer"
|
||||
sdkconfig "github.com/databricks/databricks-sdk-go/config"
|
||||
"github.com/databricks/databricks-sdk-go/experimental/mocks"
|
||||
"github.com/databricks/databricks-sdk-go/service/compute"
|
||||
"github.com/databricks/databricks-sdk-go/service/jobs"
|
||||
"github.com/stretchr/testify/mock"
|
||||
|
@ -181,6 +183,11 @@ func TestArtifactUploadForVolumes(t *testing.T) {
|
|||
filer.CreateParentDirectories,
|
||||
).Return(nil)
|
||||
|
||||
m := mocks.NewMockWorkspaceClient(t)
|
||||
m.WorkspaceClient.Config = &sdkconfig.Config{}
|
||||
m.GetMockFilesAPI().EXPECT().GetDirectoryMetadataByDirectoryPath(mock.Anything, "/Volumes/foo/bar/artifacts").Return(nil)
|
||||
b.SetWorkpaceClient(m.WorkspaceClient)
|
||||
|
||||
diags := bundle.Apply(context.Background(), b, bundle.Seq(ExpandGlobReferences(), UploadWithClient(mockFiler)))
|
||||
require.NoError(t, diags.Error())
|
||||
|
||||
|
|
|
@ -23,10 +23,10 @@ import (
|
|||
tfjson "github.com/hashicorp/terraform-json"
|
||||
)
|
||||
|
||||
func parseTerraformActions(changes []*tfjson.ResourceChange, toInclude func(typ string, actions tfjson.Actions) bool) []terraformlib.Action {
|
||||
func filterDeleteOrRecreateActions(changes []*tfjson.ResourceChange, resourceType string) []terraformlib.Action {
|
||||
res := make([]terraformlib.Action, 0)
|
||||
for _, rc := range changes {
|
||||
if !toInclude(rc.Type, rc.Change.Actions) {
|
||||
if rc.Type != resourceType {
|
||||
continue
|
||||
}
|
||||
|
||||
|
@ -37,7 +37,7 @@ func parseTerraformActions(changes []*tfjson.ResourceChange, toInclude func(typ
|
|||
case rc.Change.Actions.Replace():
|
||||
actionType = terraformlib.ActionTypeRecreate
|
||||
default:
|
||||
// No use case for other action types yet.
|
||||
// Filter other action types..
|
||||
continue
|
||||
}
|
||||
|
||||
|
@ -63,30 +63,12 @@ func approvalForDeploy(ctx context.Context, b *bundle.Bundle) (bool, error) {
|
|||
return false, err
|
||||
}
|
||||
|
||||
schemaActions := parseTerraformActions(plan.ResourceChanges, func(typ string, actions tfjson.Actions) bool {
|
||||
// Filter in only UC schema resources.
|
||||
if typ != "databricks_schema" {
|
||||
return false
|
||||
}
|
||||
|
||||
// We only display prompts for destructive actions like deleting or
|
||||
// recreating a schema.
|
||||
return actions.Delete() || actions.Replace()
|
||||
})
|
||||
|
||||
dltActions := parseTerraformActions(plan.ResourceChanges, func(typ string, actions tfjson.Actions) bool {
|
||||
// Filter in only DLT pipeline resources.
|
||||
if typ != "databricks_pipeline" {
|
||||
return false
|
||||
}
|
||||
|
||||
// Recreating DLT pipeline leads to metadata loss and for a transient period
|
||||
// the underling tables will be unavailable.
|
||||
return actions.Replace() || actions.Delete()
|
||||
})
|
||||
schemaActions := filterDeleteOrRecreateActions(plan.ResourceChanges, "databricks_schema")
|
||||
dltActions := filterDeleteOrRecreateActions(plan.ResourceChanges, "databricks_pipeline")
|
||||
volumeActions := filterDeleteOrRecreateActions(plan.ResourceChanges, "databricks_volume")
|
||||
|
||||
// We don't need to display any prompts in this case.
|
||||
if len(dltActions) == 0 && len(schemaActions) == 0 {
|
||||
if len(schemaActions) == 0 && len(dltActions) == 0 && len(volumeActions) == 0 {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
|
@ -111,6 +93,19 @@ properties such as the 'catalog' or 'storage' are changed:`
|
|||
}
|
||||
}
|
||||
|
||||
// One or more volumes is being recreated.
|
||||
if len(volumeActions) != 0 {
|
||||
msg := `
|
||||
This action will result in the deletion or recreation of the following Volumes.
|
||||
For managed volumes, the files stored in the volume are also deleted from your
|
||||
cloud tenant within 30 days. For external volumes, the metadata about the volume
|
||||
is removed from the catalog, but the underlying files are not deleted:`
|
||||
cmdio.LogString(ctx, msg)
|
||||
for _, action := range volumeActions {
|
||||
cmdio.Log(ctx, action)
|
||||
}
|
||||
}
|
||||
|
||||
if b.AutoApprove {
|
||||
return true, nil
|
||||
}
|
||||
|
|
|
@ -40,17 +40,7 @@ func TestParseTerraformActions(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
res := parseTerraformActions(changes, func(typ string, actions tfjson.Actions) bool {
|
||||
if typ != "databricks_pipeline" {
|
||||
return false
|
||||
}
|
||||
|
||||
if actions.Delete() || actions.Replace() {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
res := filterDeleteOrRecreateActions(changes, "databricks_pipeline")
|
||||
|
||||
assert.Equal(t, []terraformlib.Action{
|
||||
{
|
||||
|
|
|
@ -3,6 +3,7 @@ package bundle
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
|
@ -109,6 +110,24 @@ func getUrlForNativeTemplate(name string) string {
|
|||
return ""
|
||||
}
|
||||
|
||||
func getFsForNativeTemplate(name string) (fs.FS, error) {
|
||||
builtin, err := template.Builtin()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If this is a built-in template, the return value will be non-nil.
|
||||
var templateFS fs.FS
|
||||
for _, entry := range builtin {
|
||||
if entry.Name == name {
|
||||
templateFS = entry.FS
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return templateFS, nil
|
||||
}
|
||||
|
||||
func isRepoUrl(url string) bool {
|
||||
result := false
|
||||
for _, prefix := range gitUrlPrefixes {
|
||||
|
@ -198,9 +217,20 @@ See https://docs.databricks.com/en/dev-tools/bundles/templates.html for more inf
|
|||
if templateDir != "" {
|
||||
return errors.New("--template-dir can only be used with a Git repository URL")
|
||||
}
|
||||
|
||||
templateFS, err := getFsForNativeTemplate(templatePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If this is not a built-in template, then it must be a local file system path.
|
||||
if templateFS == nil {
|
||||
templateFS = os.DirFS(templatePath)
|
||||
}
|
||||
|
||||
// skip downloading the repo because input arg is not a URL. We assume
|
||||
// it's a path on the local file system in that case
|
||||
return template.Materialize(ctx, configFile, templatePath, outputDir)
|
||||
return template.Materialize(ctx, configFile, templateFS, outputDir)
|
||||
}
|
||||
|
||||
// Create a temporary directory with the name of the repository. The '*'
|
||||
|
@ -224,7 +254,8 @@ See https://docs.databricks.com/en/dev-tools/bundles/templates.html for more inf
|
|||
|
||||
// Clean up downloaded repository once the template is materialized.
|
||||
defer os.RemoveAll(repoDir)
|
||||
return template.Materialize(ctx, configFile, filepath.Join(repoDir, templateDir), outputDir)
|
||||
templateFS := os.DirFS(filepath.Join(repoDir, templateDir))
|
||||
return template.Materialize(ctx, configFile, templateFS, outputDir)
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package bundle
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
|
@ -8,13 +9,18 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/databricks/cli/bundle"
|
||||
"github.com/databricks/cli/bundle/bundletest"
|
||||
"github.com/databricks/cli/bundle/config"
|
||||
"github.com/databricks/cli/bundle/config/resources"
|
||||
"github.com/databricks/cli/bundle/libraries"
|
||||
"github.com/databricks/cli/internal"
|
||||
"github.com/databricks/cli/internal/acc"
|
||||
"github.com/databricks/cli/libs/diag"
|
||||
"github.com/databricks/cli/libs/dyn"
|
||||
"github.com/databricks/databricks-sdk-go/service/catalog"
|
||||
"github.com/databricks/databricks-sdk-go/service/compute"
|
||||
"github.com/databricks/databricks-sdk-go/service/jobs"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
|
@ -225,3 +231,112 @@ func TestAccUploadArtifactFileToCorrectRemotePathForVolumes(t *testing.T) {
|
|||
b.Config.Resources.Jobs["test"].JobSettings.Tasks[0].Libraries[0].Whl,
|
||||
)
|
||||
}
|
||||
|
||||
func TestAccUploadArtifactFileToInvalidVolume(t *testing.T) {
|
||||
ctx, wt := acc.UcWorkspaceTest(t)
|
||||
w := wt.W
|
||||
|
||||
schemaName := internal.RandomName("schema-")
|
||||
|
||||
_, err := w.Schemas.Create(ctx, catalog.CreateSchema{
|
||||
CatalogName: "main",
|
||||
Comment: "test schema",
|
||||
Name: schemaName,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Cleanup(func() {
|
||||
err = w.Schemas.DeleteByFullName(ctx, "main."+schemaName)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("volume not in DAB", func(t *testing.T) {
|
||||
volumePath := fmt.Sprintf("/Volumes/main/%s/doesnotexist", schemaName)
|
||||
dir := t.TempDir()
|
||||
|
||||
b := &bundle.Bundle{
|
||||
BundleRootPath: dir,
|
||||
SyncRootPath: dir,
|
||||
Config: config.Root{
|
||||
Bundle: config.Bundle{
|
||||
Target: "whatever",
|
||||
},
|
||||
Workspace: config.Workspace{
|
||||
ArtifactPath: volumePath,
|
||||
},
|
||||
Resources: config.Resources{
|
||||
Volumes: map[string]*resources.Volume{
|
||||
"foo": {
|
||||
CreateVolumeRequestContent: &catalog.CreateVolumeRequestContent{
|
||||
CatalogName: "main",
|
||||
Name: "my_volume",
|
||||
VolumeType: "MANAGED",
|
||||
SchemaName: schemaName,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
diags := bundle.Apply(ctx, b, libraries.Upload())
|
||||
assert.ErrorContains(t, diags.Error(), fmt.Sprintf("failed to fetch metadata for the UC volume %s that is configured in the artifact_path:", volumePath))
|
||||
})
|
||||
|
||||
t.Run("volume in DAB config", func(t *testing.T) {
|
||||
volumePath := fmt.Sprintf("/Volumes/main/%s/my_volume", schemaName)
|
||||
dir := t.TempDir()
|
||||
|
||||
b := &bundle.Bundle{
|
||||
BundleRootPath: dir,
|
||||
SyncRootPath: dir,
|
||||
Config: config.Root{
|
||||
Bundle: config.Bundle{
|
||||
Target: "whatever",
|
||||
},
|
||||
Workspace: config.Workspace{
|
||||
ArtifactPath: volumePath,
|
||||
},
|
||||
Resources: config.Resources{
|
||||
Volumes: map[string]*resources.Volume{
|
||||
"foo": {
|
||||
CreateVolumeRequestContent: &catalog.CreateVolumeRequestContent{
|
||||
CatalogName: "main",
|
||||
Name: "my_volume",
|
||||
VolumeType: "MANAGED",
|
||||
SchemaName: schemaName,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// set location of volume definition in config.
|
||||
bundletest.SetLocation(b, "resources.volumes.foo", []dyn.Location{{
|
||||
File: filepath.Join(dir, "databricks.yml"),
|
||||
Line: 1,
|
||||
Column: 2,
|
||||
}})
|
||||
|
||||
diags := bundle.Apply(ctx, b, libraries.Upload())
|
||||
assert.Contains(t, diags, diag.Diagnostic{
|
||||
Severity: diag.Error,
|
||||
Summary: fmt.Sprintf("failed to fetch metadata for the UC volume %s that is configured in the artifact_path: Not Found", volumePath),
|
||||
})
|
||||
assert.Contains(t, diags, diag.Diagnostic{
|
||||
Severity: diag.Warning,
|
||||
Summary: "You might be using a UC volume in your artifact_path that is managed by this bundle but which has not been deployed yet. Please deploy the UC volume in a separate bundle deploy before using it in the artifact_path.",
|
||||
Locations: []dyn.Location{
|
||||
{
|
||||
File: filepath.Join(dir, "databricks.yml"),
|
||||
Line: 1,
|
||||
Column: 2,
|
||||
},
|
||||
},
|
||||
Paths: []dyn.Path{
|
||||
dyn.MustPathFromString("resources.volumes.foo"),
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"properties": {
|
||||
"unique_id": {
|
||||
"type": "string",
|
||||
"description": "Unique ID for the schema names"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
bundle:
|
||||
name: test-uc-volumes-{{.unique_id}}
|
||||
|
||||
variables:
|
||||
schema_name:
|
||||
default: ${resources.schemas.schema1.name}
|
||||
|
||||
resources:
|
||||
schemas:
|
||||
schema1:
|
||||
name: schema1-{{.unique_id}}
|
||||
catalog_name: main
|
||||
comment: This schema was created from DABs
|
||||
|
||||
schema2:
|
||||
name: schema2-{{.unique_id}}
|
||||
catalog_name: main
|
||||
comment: This schema was created from DABs
|
||||
|
||||
volumes:
|
||||
foo:
|
||||
catalog_name: main
|
||||
name: my_volume
|
||||
schema_name: ${var.schema_name}
|
||||
volume_type: MANAGED
|
||||
comment: This volume was created from DABs.
|
||||
|
||||
grants:
|
||||
- principal: account users
|
||||
privileges:
|
||||
- WRITE_VOLUME
|
|
@ -0,0 +1,2 @@
|
|||
-- Databricks notebook source
|
||||
select 1
|
|
@ -243,3 +243,73 @@ func TestAccDeployBasicBundleLogs(t *testing.T) {
|
|||
}, "\n"), stderr)
|
||||
assert.Equal(t, "", stdout)
|
||||
}
|
||||
|
||||
func TestAccDeployUcVolume(t *testing.T) {
|
||||
ctx, wt := acc.UcWorkspaceTest(t)
|
||||
w := wt.W
|
||||
|
||||
uniqueId := uuid.New().String()
|
||||
bundleRoot, err := initTestTemplate(t, ctx, "volume", map[string]any{
|
||||
"unique_id": uniqueId,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = deployBundle(t, ctx, bundleRoot)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Cleanup(func() {
|
||||
destroyBundle(t, ctx, bundleRoot)
|
||||
})
|
||||
|
||||
// Assert the volume is created successfully
|
||||
catalogName := "main"
|
||||
schemaName := "schema1-" + uniqueId
|
||||
volumeName := "my_volume"
|
||||
fullName := fmt.Sprintf("%s.%s.%s", catalogName, schemaName, volumeName)
|
||||
volume, err := w.Volumes.ReadByName(ctx, fullName)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, volume.Name, volumeName)
|
||||
require.Equal(t, catalogName, volume.CatalogName)
|
||||
require.Equal(t, schemaName, volume.SchemaName)
|
||||
|
||||
// Assert that the grants were successfully applied.
|
||||
grants, err := w.Grants.GetBySecurableTypeAndFullName(ctx, catalog.SecurableTypeVolume, fullName)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, grants.PrivilegeAssignments, 1)
|
||||
assert.Equal(t, "account users", grants.PrivilegeAssignments[0].Principal)
|
||||
assert.Equal(t, []catalog.Privilege{catalog.PrivilegeWriteVolume}, grants.PrivilegeAssignments[0].Privileges)
|
||||
|
||||
// Recreation of the volume without --auto-approve should fail since prompting is not possible
|
||||
t.Setenv("TERM", "dumb")
|
||||
t.Setenv("BUNDLE_ROOT", bundleRoot)
|
||||
stdout, stderr, err := internal.NewCobraTestRunnerWithContext(t, ctx, "bundle", "deploy", "--var=schema_name=${resources.schemas.schema2.name}").Run()
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, stderr.String(), `This action will result in the deletion or recreation of the following Volumes.
|
||||
For managed volumes, the files stored in the volume are also deleted from your
|
||||
cloud tenant within 30 days. For external volumes, the metadata about the volume
|
||||
is removed from the catalog, but the underlying files are not deleted:
|
||||
recreate volume foo`)
|
||||
assert.Contains(t, stdout.String(), "the deployment requires destructive actions, but current console does not support prompting. Please specify --auto-approve if you would like to skip prompts and proceed")
|
||||
|
||||
// Successfully recreate the volume with --auto-approve
|
||||
t.Setenv("TERM", "dumb")
|
||||
t.Setenv("BUNDLE_ROOT", bundleRoot)
|
||||
_, _, err = internal.NewCobraTestRunnerWithContext(t, ctx, "bundle", "deploy", "--var=schema_name=${resources.schemas.schema2.name}", "--auto-approve").Run()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Assert the volume is updated successfully
|
||||
schemaName = "schema2-" + uniqueId
|
||||
fullName = fmt.Sprintf("%s.%s.%s", catalogName, schemaName, volumeName)
|
||||
volume, err = w.Volumes.ReadByName(ctx, fullName)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, volume.Name, volumeName)
|
||||
require.Equal(t, catalogName, volume.CatalogName)
|
||||
require.Equal(t, schemaName, volume.SchemaName)
|
||||
|
||||
// assert that the grants were applied / retained on recreate.
|
||||
grants, err = w.Grants.GetBySecurableTypeAndFullName(ctx, catalog.SecurableTypeVolume, fullName)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, grants.PrivilegeAssignments, 1)
|
||||
assert.Equal(t, "account users", grants.PrivilegeAssignments[0].Principal)
|
||||
assert.Equal(t, []catalog.Privilege{catalog.PrivilegeWriteVolume}, grants.PrivilegeAssignments[0].Privileges)
|
||||
}
|
||||
|
|
|
@ -42,7 +42,7 @@ func initTestTemplateWithBundleRoot(t *testing.T, ctx context.Context, templateN
|
|||
cmd := cmdio.NewIO(flags.OutputJSON, strings.NewReader(""), os.Stdout, os.Stderr, "", "bundles")
|
||||
ctx = cmdio.InContext(ctx, cmd)
|
||||
|
||||
err = template.Materialize(ctx, configFilePath, templateRoot, bundleRoot)
|
||||
err = template.Materialize(ctx, configFilePath, os.DirFS(templateRoot), bundleRoot)
|
||||
return bundleRoot, err
|
||||
}
|
||||
|
||||
|
|
|
@ -71,3 +71,23 @@ func (v ref) references() []string {
|
|||
func IsPureVariableReference(s string) bool {
|
||||
return len(s) > 0 && re.FindString(s) == s
|
||||
}
|
||||
|
||||
// If s is a pure variable reference, this function returns the corresponding
|
||||
// dyn.Path. Otherwise, it returns false.
|
||||
func PureReferenceToPath(s string) (dyn.Path, bool) {
|
||||
ref, ok := newRef(dyn.V(s))
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
if !ref.isPure() {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
p, err := dyn.NewPathFromString(ref.references()[0])
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return p, true
|
||||
}
|
||||
|
|
|
@ -51,3 +51,34 @@ func TestIsPureVariableReference(t *testing.T) {
|
|||
assert.False(t, IsPureVariableReference("prefix ${foo.bar}"))
|
||||
assert.True(t, IsPureVariableReference("${foo.bar}"))
|
||||
}
|
||||
|
||||
func TestPureReferenceToPath(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
in string
|
||||
out string
|
||||
ok bool
|
||||
}{
|
||||
{"${foo.bar}", "foo.bar", true},
|
||||
{"${foo.bar.baz}", "foo.bar.baz", true},
|
||||
{"${foo.bar.baz[0]}", "foo.bar.baz[0]", true},
|
||||
{"${foo.bar.baz[0][1]}", "foo.bar.baz[0][1]", true},
|
||||
{"${foo.bar.baz[0][1].qux}", "foo.bar.baz[0][1].qux", true},
|
||||
|
||||
{"${foo.one}${foo.two}", "", false},
|
||||
{"prefix ${foo.bar}", "", false},
|
||||
{"${foo.bar} suffix", "", false},
|
||||
{"${foo.bar", "", false},
|
||||
{"foo.bar}", "", false},
|
||||
{"foo.bar", "", false},
|
||||
{"{foo.bar}", "", false},
|
||||
{"", "", false},
|
||||
} {
|
||||
path, ok := PureReferenceToPath(tc.in)
|
||||
if tc.ok {
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, dyn.MustPathFromString(tc.out), path)
|
||||
} else {
|
||||
assert.False(t, ok)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,13 +7,24 @@ import (
|
|||
"io/fs"
|
||||
)
|
||||
|
||||
// WriteMode captures intent when writing a file.
|
||||
//
|
||||
// The first 9 bits are reserved for the [fs.FileMode] permission bits.
|
||||
// These are used only by the local filer implementation and have
|
||||
// no effect for the other implementations.
|
||||
type WriteMode int
|
||||
|
||||
// writeModePerm is a mask to extract permission bits from a WriteMode.
|
||||
const writeModePerm = WriteMode(fs.ModePerm)
|
||||
|
||||
const (
|
||||
OverwriteIfExists WriteMode = 1 << iota
|
||||
// Note: these constants are defined as powers of 2 to support combining them using a bit-wise OR.
|
||||
// They starts from the 10th bit (permission mask + 1) to avoid conflicts with the permission bits.
|
||||
OverwriteIfExists WriteMode = (writeModePerm + 1) << iota
|
||||
CreateParentDirectories
|
||||
)
|
||||
|
||||
// DeleteMode captures intent when deleting a file.
|
||||
type DeleteMode int
|
||||
|
||||
const (
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
package filer
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestWriteMode(t *testing.T) {
|
||||
assert.Equal(t, 512, int(OverwriteIfExists))
|
||||
assert.Equal(t, 1024, int(CreateParentDirectories))
|
||||
}
|
|
@ -28,6 +28,15 @@ func (w *LocalClient) Write(ctx context.Context, name string, reader io.Reader,
|
|||
return err
|
||||
}
|
||||
|
||||
// Retrieve permission mask from the [WriteMode], if present.
|
||||
perm := fs.FileMode(0644)
|
||||
for _, m := range mode {
|
||||
bits := m & writeModePerm
|
||||
if bits != 0 {
|
||||
perm = fs.FileMode(bits)
|
||||
}
|
||||
}
|
||||
|
||||
flags := os.O_WRONLY | os.O_CREATE
|
||||
if slices.Contains(mode, OverwriteIfExists) {
|
||||
flags |= os.O_TRUNC
|
||||
|
@ -35,7 +44,7 @@ func (w *LocalClient) Write(ctx context.Context, name string, reader io.Reader,
|
|||
flags |= os.O_EXCL
|
||||
}
|
||||
|
||||
f, err := os.OpenFile(absPath, flags, 0644)
|
||||
f, err := os.OpenFile(absPath, flags, perm)
|
||||
if errors.Is(err, fs.ErrNotExist) && slices.Contains(mode, CreateParentDirectories) {
|
||||
// Create parent directories if they don't exist.
|
||||
err = os.MkdirAll(filepath.Dir(absPath), 0755)
|
||||
|
@ -43,7 +52,7 @@ func (w *LocalClient) Write(ctx context.Context, name string, reader io.Reader,
|
|||
return err
|
||||
}
|
||||
// Try again.
|
||||
f, err = os.OpenFile(absPath, flags, 0644)
|
||||
f, err = os.OpenFile(absPath, flags, perm)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
|
|
|
@ -114,7 +114,7 @@ type apiClient interface {
|
|||
|
||||
// NOTE: This API is available for files under /Repos if a workspace has files-in-repos enabled.
|
||||
// It can access any workspace path if files-in-workspace is enabled.
|
||||
type workspaceFilesClient struct {
|
||||
type WorkspaceFilesClient struct {
|
||||
workspaceClient *databricks.WorkspaceClient
|
||||
apiClient apiClient
|
||||
|
||||
|
@ -128,7 +128,7 @@ func NewWorkspaceFilesClient(w *databricks.WorkspaceClient, root string) (Filer,
|
|||
return nil, err
|
||||
}
|
||||
|
||||
return &workspaceFilesClient{
|
||||
return &WorkspaceFilesClient{
|
||||
workspaceClient: w,
|
||||
apiClient: apiClient,
|
||||
|
||||
|
@ -136,7 +136,7 @@ func NewWorkspaceFilesClient(w *databricks.WorkspaceClient, root string) (Filer,
|
|||
}, nil
|
||||
}
|
||||
|
||||
func (w *workspaceFilesClient) Write(ctx context.Context, name string, reader io.Reader, mode ...WriteMode) error {
|
||||
func (w *WorkspaceFilesClient) Write(ctx context.Context, name string, reader io.Reader, mode ...WriteMode) error {
|
||||
absPath, err := w.root.Join(name)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -214,7 +214,7 @@ func (w *workspaceFilesClient) Write(ctx context.Context, name string, reader io
|
|||
return err
|
||||
}
|
||||
|
||||
func (w *workspaceFilesClient) Read(ctx context.Context, name string) (io.ReadCloser, error) {
|
||||
func (w *WorkspaceFilesClient) Read(ctx context.Context, name string) (io.ReadCloser, error) {
|
||||
absPath, err := w.root.Join(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -238,7 +238,7 @@ func (w *workspaceFilesClient) Read(ctx context.Context, name string) (io.ReadCl
|
|||
return w.workspaceClient.Workspace.Download(ctx, absPath)
|
||||
}
|
||||
|
||||
func (w *workspaceFilesClient) Delete(ctx context.Context, name string, mode ...DeleteMode) error {
|
||||
func (w *WorkspaceFilesClient) Delete(ctx context.Context, name string, mode ...DeleteMode) error {
|
||||
absPath, err := w.root.Join(name)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -282,7 +282,7 @@ func (w *workspaceFilesClient) Delete(ctx context.Context, name string, mode ...
|
|||
return err
|
||||
}
|
||||
|
||||
func (w *workspaceFilesClient) ReadDir(ctx context.Context, name string) ([]fs.DirEntry, error) {
|
||||
func (w *WorkspaceFilesClient) ReadDir(ctx context.Context, name string) ([]fs.DirEntry, error) {
|
||||
absPath, err := w.root.Join(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -315,7 +315,7 @@ func (w *workspaceFilesClient) ReadDir(ctx context.Context, name string) ([]fs.D
|
|||
return wsfsDirEntriesFromObjectInfos(objects), nil
|
||||
}
|
||||
|
||||
func (w *workspaceFilesClient) Mkdir(ctx context.Context, name string) error {
|
||||
func (w *WorkspaceFilesClient) Mkdir(ctx context.Context, name string) error {
|
||||
dirPath, err := w.root.Join(name)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -325,7 +325,7 @@ func (w *workspaceFilesClient) Mkdir(ctx context.Context, name string) error {
|
|||
})
|
||||
}
|
||||
|
||||
func (w *workspaceFilesClient) Stat(ctx context.Context, name string) (fs.FileInfo, error) {
|
||||
func (w *WorkspaceFilesClient) Stat(ctx context.Context, name string) (fs.FileInfo, error) {
|
||||
absPath, err := w.root.Join(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
|
@ -174,7 +174,7 @@ func TestFilerWorkspaceFilesExtensionsErrorsOnDupName(t *testing.T) {
|
|||
"return_export_info": "true",
|
||||
}, mock.AnythingOfType("*filer.wsfsFileInfo"), []func(*http.Request) error(nil)).Return(nil, statNotebook)
|
||||
|
||||
workspaceFilesClient := workspaceFilesClient{
|
||||
workspaceFilesClient := WorkspaceFilesClient{
|
||||
workspaceClient: mockedWorkspaceClient.WorkspaceClient,
|
||||
apiClient: &mockedApiClient,
|
||||
root: NewWorkspaceRootPath("/dir"),
|
||||
|
|
|
@ -3,7 +3,9 @@ package jsonschema
|
|||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"slices"
|
||||
|
||||
|
@ -255,7 +257,12 @@ func (schema *Schema) validate() error {
|
|||
}
|
||||
|
||||
func Load(path string) (*Schema, error) {
|
||||
b, err := os.ReadFile(path)
|
||||
dir, file := filepath.Split(path)
|
||||
return LoadFS(os.DirFS(dir), file)
|
||||
}
|
||||
|
||||
func LoadFS(fsys fs.FS, path string) (*Schema, error) {
|
||||
b, err := fs.ReadFile(fsys, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package jsonschema
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
@ -305,3 +306,9 @@ func TestValidateSchemaSkippedPropertiesHaveDefaults(t *testing.T) {
|
|||
err = s.validate()
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestSchema_LoadFS(t *testing.T) {
|
||||
fsys := os.DirFS("./testdata/schema-load-int")
|
||||
_, err := LoadFS(fsys, "schema-valid.json")
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
package template
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"io/fs"
|
||||
)
|
||||
|
||||
//go:embed all:templates
|
||||
var builtinTemplates embed.FS
|
||||
|
||||
// BuiltinTemplate represents a template that is built into the CLI.
|
||||
type BuiltinTemplate struct {
|
||||
Name string
|
||||
FS fs.FS
|
||||
}
|
||||
|
||||
// Builtin returns the list of all built-in templates.
|
||||
func Builtin() ([]BuiltinTemplate, error) {
|
||||
templates, err := fs.Sub(builtinTemplates, "templates")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
entries, err := fs.ReadDir(templates, ".")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var out []BuiltinTemplate
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
templateFS, err := fs.Sub(templates, entry.Name())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
out = append(out, BuiltinTemplate{
|
||||
Name: entry.Name(),
|
||||
FS: templateFS,
|
||||
})
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
package template
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestBuiltin(t *testing.T) {
|
||||
out, err := Builtin()
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, out, 3)
|
||||
|
||||
// Confirm names.
|
||||
assert.Equal(t, "dbt-sql", out[0].Name)
|
||||
assert.Equal(t, "default-python", out[1].Name)
|
||||
assert.Equal(t, "default-sql", out[2].Name)
|
||||
|
||||
// Confirm that the filesystems work.
|
||||
_, err = fs.Stat(out[0].FS, `template/{{.project_name}}/dbt_project.yml.tmpl`)
|
||||
assert.NoError(t, err)
|
||||
_, err = fs.Stat(out[1].FS, `template/{{.project_name}}/tests/main_test.py.tmpl`)
|
||||
assert.NoError(t, err)
|
||||
_, err = fs.Stat(out[2].FS, `template/{{.project_name}}/src/orders_daily.sql.tmpl`)
|
||||
assert.NoError(t, err)
|
||||
}
|
|
@ -4,6 +4,7 @@ import (
|
|||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
|
||||
"github.com/databricks/cli/libs/cmdio"
|
||||
"github.com/databricks/cli/libs/jsonschema"
|
||||
|
@ -28,9 +29,8 @@ type config struct {
|
|||
schema *jsonschema.Schema
|
||||
}
|
||||
|
||||
func newConfig(ctx context.Context, schemaPath string) (*config, error) {
|
||||
// Read config schema
|
||||
schema, err := jsonschema.Load(schemaPath)
|
||||
func newConfig(ctx context.Context, templateFS fs.FS, schemaPath string) (*config, error) {
|
||||
schema, err := jsonschema.LoadFS(templateFS, schemaPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -3,6 +3,8 @@ package template
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"text/template"
|
||||
|
@ -16,7 +18,7 @@ func TestTemplateConfigAssignValuesFromFile(t *testing.T) {
|
|||
testDir := "./testdata/config-assign-from-file"
|
||||
|
||||
ctx := context.Background()
|
||||
c, err := newConfig(ctx, filepath.Join(testDir, "schema.json"))
|
||||
c, err := newConfig(ctx, os.DirFS(testDir), "schema.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
err = c.assignValuesFromFile(filepath.Join(testDir, "config.json"))
|
||||
|
@ -32,7 +34,7 @@ func TestTemplateConfigAssignValuesFromFileDoesNotOverwriteExistingConfigs(t *te
|
|||
testDir := "./testdata/config-assign-from-file"
|
||||
|
||||
ctx := context.Background()
|
||||
c, err := newConfig(ctx, filepath.Join(testDir, "schema.json"))
|
||||
c, err := newConfig(ctx, os.DirFS(testDir), "schema.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
c.values = map[string]any{
|
||||
|
@ -52,7 +54,7 @@ func TestTemplateConfigAssignValuesFromFileForInvalidIntegerValue(t *testing.T)
|
|||
testDir := "./testdata/config-assign-from-file-invalid-int"
|
||||
|
||||
ctx := context.Background()
|
||||
c, err := newConfig(ctx, filepath.Join(testDir, "schema.json"))
|
||||
c, err := newConfig(ctx, os.DirFS(testDir), "schema.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
err = c.assignValuesFromFile(filepath.Join(testDir, "config.json"))
|
||||
|
@ -63,7 +65,7 @@ func TestTemplateConfigAssignValuesFromFileFiltersPropertiesNotInTheSchema(t *te
|
|||
testDir := "./testdata/config-assign-from-file-unknown-property"
|
||||
|
||||
ctx := context.Background()
|
||||
c, err := newConfig(ctx, filepath.Join(testDir, "schema.json"))
|
||||
c, err := newConfig(ctx, os.DirFS(testDir), "schema.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
err = c.assignValuesFromFile(filepath.Join(testDir, "config.json"))
|
||||
|
@ -78,10 +80,10 @@ func TestTemplateConfigAssignValuesFromDefaultValues(t *testing.T) {
|
|||
testDir := "./testdata/config-assign-from-default-value"
|
||||
|
||||
ctx := context.Background()
|
||||
c, err := newConfig(ctx, filepath.Join(testDir, "schema.json"))
|
||||
c, err := newConfig(ctx, os.DirFS(testDir), "schema.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
r, err := newRenderer(ctx, nil, nil, "./testdata/empty/template", "./testdata/empty/library", t.TempDir())
|
||||
r, err := newRenderer(ctx, nil, nil, os.DirFS("."), "./testdata/empty/template", "./testdata/empty/library")
|
||||
require.NoError(t, err)
|
||||
|
||||
err = c.assignDefaultValues(r)
|
||||
|
@ -97,10 +99,10 @@ func TestTemplateConfigAssignValuesFromTemplatedDefaultValues(t *testing.T) {
|
|||
testDir := "./testdata/config-assign-from-templated-default-value"
|
||||
|
||||
ctx := context.Background()
|
||||
c, err := newConfig(ctx, filepath.Join(testDir, "schema.json"))
|
||||
c, err := newConfig(ctx, os.DirFS(testDir), "schema.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
r, err := newRenderer(ctx, nil, nil, filepath.Join(testDir, "template/template"), filepath.Join(testDir, "template/library"), t.TempDir())
|
||||
r, err := newRenderer(ctx, nil, nil, os.DirFS("."), path.Join(testDir, "template/template"), path.Join(testDir, "template/library"))
|
||||
require.NoError(t, err)
|
||||
|
||||
// Note: only the string value is templated.
|
||||
|
@ -116,7 +118,7 @@ func TestTemplateConfigAssignValuesFromTemplatedDefaultValues(t *testing.T) {
|
|||
|
||||
func TestTemplateConfigValidateValuesDefined(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
c, err := newConfig(ctx, "testdata/config-test-schema/test-schema.json")
|
||||
c, err := newConfig(ctx, os.DirFS("testdata/config-test-schema"), "test-schema.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
c.values = map[string]any{
|
||||
|
@ -131,7 +133,7 @@ func TestTemplateConfigValidateValuesDefined(t *testing.T) {
|
|||
|
||||
func TestTemplateConfigValidateTypeForValidConfig(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
c, err := newConfig(ctx, "testdata/config-test-schema/test-schema.json")
|
||||
c, err := newConfig(ctx, os.DirFS("testdata/config-test-schema"), "test-schema.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
c.values = map[string]any{
|
||||
|
@ -147,7 +149,7 @@ func TestTemplateConfigValidateTypeForValidConfig(t *testing.T) {
|
|||
|
||||
func TestTemplateConfigValidateTypeForUnknownField(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
c, err := newConfig(ctx, "testdata/config-test-schema/test-schema.json")
|
||||
c, err := newConfig(ctx, os.DirFS("testdata/config-test-schema"), "test-schema.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
c.values = map[string]any{
|
||||
|
@ -164,7 +166,7 @@ func TestTemplateConfigValidateTypeForUnknownField(t *testing.T) {
|
|||
|
||||
func TestTemplateConfigValidateTypeForInvalidType(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
c, err := newConfig(ctx, "testdata/config-test-schema/test-schema.json")
|
||||
c, err := newConfig(ctx, os.DirFS("testdata/config-test-schema"), "test-schema.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
c.values = map[string]any{
|
||||
|
@ -271,7 +273,8 @@ func TestTemplateEnumValidation(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestTemplateSchemaErrorsWithEmptyDescription(t *testing.T) {
|
||||
_, err := newConfig(context.Background(), "./testdata/config-test-schema/invalid-test-schema.json")
|
||||
ctx := context.Background()
|
||||
_, err := newConfig(ctx, os.DirFS("./testdata/config-test-schema"), "invalid-test-schema.json")
|
||||
assert.EqualError(t, err, "template property property-without-description is missing a description")
|
||||
}
|
||||
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
package template
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
|
||||
"github.com/databricks/cli/libs/filer"
|
||||
)
|
||||
|
@ -13,89 +12,69 @@ import (
|
|||
// Interface representing a file to be materialized from a template into a project
|
||||
// instance
|
||||
type file interface {
|
||||
// Destination path for file. This is where the file will be created when
|
||||
// PersistToDisk is called.
|
||||
DstPath() *destinationPath
|
||||
// Path of the file relative to the root of the instantiated template.
|
||||
// This is where the file is written to when persisting the template to disk.
|
||||
// Must be slash-separated.
|
||||
RelPath() string
|
||||
|
||||
// Write file to disk at the destination path.
|
||||
PersistToDisk() error
|
||||
}
|
||||
Write(ctx context.Context, out filer.Filer) error
|
||||
|
||||
type destinationPath struct {
|
||||
// Root path for the project instance. This path uses the system's default
|
||||
// file separator. For example /foo/bar on Unix and C:\foo\bar on windows
|
||||
root string
|
||||
|
||||
// Unix like file path relative to the "root" of the instantiated project. Is used to
|
||||
// evaluate whether the file should be skipped by comparing it to a list of
|
||||
// skip glob patterns.
|
||||
relPath string
|
||||
}
|
||||
|
||||
// Absolute path of the file, in the os native format. For example /foo/bar on
|
||||
// Unix and C:\foo\bar on windows
|
||||
func (f *destinationPath) absPath() string {
|
||||
return filepath.Join(f.root, filepath.FromSlash(f.relPath))
|
||||
// contents returns the file contents as a byte slice.
|
||||
// This is used for testing purposes.
|
||||
contents() ([]byte, error)
|
||||
}
|
||||
|
||||
type copyFile struct {
|
||||
ctx context.Context
|
||||
|
||||
// Permissions bits for the destination file
|
||||
perm fs.FileMode
|
||||
|
||||
dstPath *destinationPath
|
||||
// Destination path for the file.
|
||||
relPath string
|
||||
|
||||
// Filer rooted at template root. Used to read srcPath.
|
||||
srcFiler filer.Filer
|
||||
// [fs.FS] rooted at template root. Used to read srcPath.
|
||||
srcFS fs.FS
|
||||
|
||||
// Relative path from template root for file to be copied.
|
||||
srcPath string
|
||||
}
|
||||
|
||||
func (f *copyFile) DstPath() *destinationPath {
|
||||
return f.dstPath
|
||||
func (f *copyFile) RelPath() string {
|
||||
return f.relPath
|
||||
}
|
||||
|
||||
func (f *copyFile) PersistToDisk() error {
|
||||
path := f.DstPath().absPath()
|
||||
err := os.MkdirAll(filepath.Dir(path), 0755)
|
||||
func (f *copyFile) Write(ctx context.Context, out filer.Filer) error {
|
||||
src, err := f.srcFS.Open(f.srcPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
srcFile, err := f.srcFiler.Read(f.ctx, f.srcPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer srcFile.Close()
|
||||
dstFile, err := os.OpenFile(path, os.O_CREATE|os.O_EXCL|os.O_WRONLY, f.perm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer dstFile.Close()
|
||||
_, err = io.Copy(dstFile, srcFile)
|
||||
return err
|
||||
defer src.Close()
|
||||
return out.Write(ctx, f.relPath, src, filer.CreateParentDirectories, filer.WriteMode(f.perm))
|
||||
}
|
||||
|
||||
func (f *copyFile) contents() ([]byte, error) {
|
||||
return fs.ReadFile(f.srcFS, f.srcPath)
|
||||
}
|
||||
|
||||
type inMemoryFile struct {
|
||||
dstPath *destinationPath
|
||||
|
||||
content []byte
|
||||
|
||||
// Permissions bits for the destination file
|
||||
perm fs.FileMode
|
||||
|
||||
// Destination path for the file.
|
||||
relPath string
|
||||
|
||||
// Contents of the file.
|
||||
content []byte
|
||||
}
|
||||
|
||||
func (f *inMemoryFile) DstPath() *destinationPath {
|
||||
return f.dstPath
|
||||
func (f *inMemoryFile) RelPath() string {
|
||||
return f.relPath
|
||||
}
|
||||
|
||||
func (f *inMemoryFile) PersistToDisk() error {
|
||||
path := f.DstPath().absPath()
|
||||
|
||||
err := os.MkdirAll(filepath.Dir(path), 0755)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(path, f.content, f.perm)
|
||||
func (f *inMemoryFile) Write(ctx context.Context, out filer.Filer) error {
|
||||
return out.Write(ctx, f.relPath, bytes.NewReader(f.content), filer.CreateParentDirectories, filer.WriteMode(f.perm))
|
||||
}
|
||||
|
||||
func (f *inMemoryFile) contents() ([]byte, error) {
|
||||
return slices.Clone(f.content), nil
|
||||
}
|
||||
|
|
|
@ -13,76 +13,51 @@ import (
|
|||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func testInMemoryFile(t *testing.T, perm fs.FileMode) {
|
||||
func testInMemoryFile(t *testing.T, ctx context.Context, perm fs.FileMode) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
f := &inMemoryFile{
|
||||
dstPath: &destinationPath{
|
||||
root: tmpDir,
|
||||
relPath: "a/b/c",
|
||||
},
|
||||
perm: perm,
|
||||
relPath: "a/b/c",
|
||||
content: []byte("123"),
|
||||
}
|
||||
err := f.PersistToDisk()
|
||||
|
||||
out, err := filer.NewLocalClient(tmpDir)
|
||||
require.NoError(t, err)
|
||||
err = f.Write(ctx, out)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assertFileContent(t, filepath.Join(tmpDir, "a/b/c"), "123")
|
||||
assertFilePermissions(t, filepath.Join(tmpDir, "a/b/c"), perm)
|
||||
}
|
||||
|
||||
func testCopyFile(t *testing.T, perm fs.FileMode) {
|
||||
func testCopyFile(t *testing.T, ctx context.Context, perm fs.FileMode) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
templateFiler, err := filer.NewLocalClient(tmpDir)
|
||||
require.NoError(t, err)
|
||||
err = os.WriteFile(filepath.Join(tmpDir, "source"), []byte("qwerty"), perm)
|
||||
err := os.WriteFile(filepath.Join(tmpDir, "source"), []byte("qwerty"), perm)
|
||||
require.NoError(t, err)
|
||||
|
||||
f := ©File{
|
||||
ctx: context.Background(),
|
||||
dstPath: &destinationPath{
|
||||
root: tmpDir,
|
||||
relPath: "a/b/c",
|
||||
},
|
||||
perm: perm,
|
||||
srcPath: "source",
|
||||
srcFiler: templateFiler,
|
||||
perm: perm,
|
||||
relPath: "a/b/c",
|
||||
srcFS: os.DirFS(tmpDir),
|
||||
srcPath: "source",
|
||||
}
|
||||
err = f.PersistToDisk()
|
||||
|
||||
out, err := filer.NewLocalClient(tmpDir)
|
||||
require.NoError(t, err)
|
||||
err = f.Write(ctx, out)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assertFileContent(t, filepath.Join(tmpDir, "a/b/c"), "qwerty")
|
||||
assertFilePermissions(t, filepath.Join(tmpDir, "a/b/c"), perm)
|
||||
}
|
||||
|
||||
func TestTemplateFileDestinationPath(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.SkipNow()
|
||||
}
|
||||
f := &destinationPath{
|
||||
root: `a/b/c`,
|
||||
relPath: "d/e",
|
||||
}
|
||||
assert.Equal(t, `a/b/c/d/e`, f.absPath())
|
||||
}
|
||||
|
||||
func TestTemplateFileDestinationPathForWindows(t *testing.T) {
|
||||
if runtime.GOOS != "windows" {
|
||||
t.SkipNow()
|
||||
}
|
||||
f := &destinationPath{
|
||||
root: `c:\a\b\c`,
|
||||
relPath: "d/e",
|
||||
}
|
||||
assert.Equal(t, `c:\a\b\c\d\e`, f.absPath())
|
||||
}
|
||||
|
||||
func TestTemplateInMemoryFilePersistToDisk(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.SkipNow()
|
||||
}
|
||||
testInMemoryFile(t, 0755)
|
||||
ctx := context.Background()
|
||||
testInMemoryFile(t, ctx, 0755)
|
||||
}
|
||||
|
||||
func TestTemplateInMemoryFilePersistToDiskForWindows(t *testing.T) {
|
||||
|
@ -91,14 +66,16 @@ func TestTemplateInMemoryFilePersistToDiskForWindows(t *testing.T) {
|
|||
}
|
||||
// we have separate tests for windows because of differences in valid
|
||||
// fs.FileMode values we can use for different operating systems.
|
||||
testInMemoryFile(t, 0666)
|
||||
ctx := context.Background()
|
||||
testInMemoryFile(t, ctx, 0666)
|
||||
}
|
||||
|
||||
func TestTemplateCopyFilePersistToDisk(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.SkipNow()
|
||||
}
|
||||
testCopyFile(t, 0644)
|
||||
ctx := context.Background()
|
||||
testCopyFile(t, ctx, 0644)
|
||||
}
|
||||
|
||||
func TestTemplateCopyFilePersistToDiskForWindows(t *testing.T) {
|
||||
|
@ -107,5 +84,6 @@ func TestTemplateCopyFilePersistToDiskForWindows(t *testing.T) {
|
|||
}
|
||||
// we have separate tests for windows because of differences in valid
|
||||
// fs.FileMode values we can use for different operating systems.
|
||||
testCopyFile(t, 0666)
|
||||
ctx := context.Background()
|
||||
testCopyFile(t, ctx, 0666)
|
||||
}
|
||||
|
|
|
@ -18,11 +18,10 @@ import (
|
|||
|
||||
func TestTemplatePrintStringWithoutProcessing(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
ctx = root.SetWorkspaceClient(ctx, nil)
|
||||
helpers := loadHelpers(ctx)
|
||||
r, err := newRenderer(ctx, nil, helpers, "./testdata/print-without-processing/template", "./testdata/print-without-processing/library", tmpDir)
|
||||
r, err := newRenderer(ctx, nil, helpers, os.DirFS("."), "./testdata/print-without-processing/template", "./testdata/print-without-processing/library")
|
||||
require.NoError(t, err)
|
||||
|
||||
err = r.walk()
|
||||
|
@ -35,11 +34,10 @@ func TestTemplatePrintStringWithoutProcessing(t *testing.T) {
|
|||
|
||||
func TestTemplateRegexpCompileFunction(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
ctx = root.SetWorkspaceClient(ctx, nil)
|
||||
helpers := loadHelpers(ctx)
|
||||
r, err := newRenderer(ctx, nil, helpers, "./testdata/regexp-compile/template", "./testdata/regexp-compile/library", tmpDir)
|
||||
r, err := newRenderer(ctx, nil, helpers, os.DirFS("."), "./testdata/regexp-compile/template", "./testdata/regexp-compile/library")
|
||||
require.NoError(t, err)
|
||||
|
||||
err = r.walk()
|
||||
|
@ -53,11 +51,10 @@ func TestTemplateRegexpCompileFunction(t *testing.T) {
|
|||
|
||||
func TestTemplateRandIntFunction(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
ctx = root.SetWorkspaceClient(ctx, nil)
|
||||
helpers := loadHelpers(ctx)
|
||||
r, err := newRenderer(ctx, nil, helpers, "./testdata/random-int/template", "./testdata/random-int/library", tmpDir)
|
||||
r, err := newRenderer(ctx, nil, helpers, os.DirFS("."), "./testdata/random-int/template", "./testdata/random-int/library")
|
||||
require.NoError(t, err)
|
||||
|
||||
err = r.walk()
|
||||
|
@ -71,11 +68,10 @@ func TestTemplateRandIntFunction(t *testing.T) {
|
|||
|
||||
func TestTemplateUuidFunction(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
ctx = root.SetWorkspaceClient(ctx, nil)
|
||||
helpers := loadHelpers(ctx)
|
||||
r, err := newRenderer(ctx, nil, helpers, "./testdata/uuid/template", "./testdata/uuid/library", tmpDir)
|
||||
r, err := newRenderer(ctx, nil, helpers, os.DirFS("."), "./testdata/uuid/template", "./testdata/uuid/library")
|
||||
require.NoError(t, err)
|
||||
|
||||
err = r.walk()
|
||||
|
@ -88,11 +84,10 @@ func TestTemplateUuidFunction(t *testing.T) {
|
|||
|
||||
func TestTemplateUrlFunction(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
ctx = root.SetWorkspaceClient(ctx, nil)
|
||||
helpers := loadHelpers(ctx)
|
||||
r, err := newRenderer(ctx, nil, helpers, "./testdata/urlparse-function/template", "./testdata/urlparse-function/library", tmpDir)
|
||||
r, err := newRenderer(ctx, nil, helpers, os.DirFS("."), "./testdata/urlparse-function/template", "./testdata/urlparse-function/library")
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
|
@ -105,11 +100,10 @@ func TestTemplateUrlFunction(t *testing.T) {
|
|||
|
||||
func TestTemplateMapPairFunction(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
ctx = root.SetWorkspaceClient(ctx, nil)
|
||||
helpers := loadHelpers(ctx)
|
||||
r, err := newRenderer(ctx, nil, helpers, "./testdata/map-pair/template", "./testdata/map-pair/library", tmpDir)
|
||||
r, err := newRenderer(ctx, nil, helpers, os.DirFS("."), "./testdata/map-pair/template", "./testdata/map-pair/library")
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
|
@ -122,7 +116,6 @@ func TestTemplateMapPairFunction(t *testing.T) {
|
|||
|
||||
func TestWorkspaceHost(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
w := &databricks.WorkspaceClient{
|
||||
Config: &workspaceConfig.Config{
|
||||
|
@ -132,7 +125,7 @@ func TestWorkspaceHost(t *testing.T) {
|
|||
ctx = root.SetWorkspaceClient(ctx, w)
|
||||
|
||||
helpers := loadHelpers(ctx)
|
||||
r, err := newRenderer(ctx, nil, helpers, "./testdata/workspace-host/template", "./testdata/map-pair/library", tmpDir)
|
||||
r, err := newRenderer(ctx, nil, helpers, os.DirFS("."), "./testdata/workspace-host/template", "./testdata/map-pair/library")
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
|
@ -149,7 +142,6 @@ func TestWorkspaceHostNotConfigured(t *testing.T) {
|
|||
ctx := context.Background()
|
||||
cmd := cmdio.NewIO(flags.OutputJSON, strings.NewReader(""), os.Stdout, os.Stderr, "", "template")
|
||||
ctx = cmdio.InContext(ctx, cmd)
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
w := &databricks.WorkspaceClient{
|
||||
Config: &workspaceConfig.Config{},
|
||||
|
@ -157,7 +149,7 @@ func TestWorkspaceHostNotConfigured(t *testing.T) {
|
|||
ctx = root.SetWorkspaceClient(ctx, w)
|
||||
|
||||
helpers := loadHelpers(ctx)
|
||||
r, err := newRenderer(ctx, nil, helpers, "./testdata/workspace-host/template", "./testdata/map-pair/library", tmpDir)
|
||||
r, err := newRenderer(ctx, nil, helpers, os.DirFS("."), "./testdata/workspace-host/template", "./testdata/map-pair/library")
|
||||
|
||||
assert.NoError(t, err)
|
||||
|
||||
|
|
|
@ -2,54 +2,32 @@ package template
|
|||
|
||||
import (
|
||||
"context"
|
||||
"embed"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/databricks/cli/libs/cmdio"
|
||||
"github.com/databricks/cli/libs/filer"
|
||||
)
|
||||
|
||||
const libraryDirName = "library"
|
||||
const templateDirName = "template"
|
||||
const schemaFileName = "databricks_template_schema.json"
|
||||
|
||||
//go:embed all:templates
|
||||
var builtinTemplates embed.FS
|
||||
|
||||
// This function materializes the input templates as a project, using user defined
|
||||
// configurations.
|
||||
// Parameters:
|
||||
//
|
||||
// ctx: context containing a cmdio object. This is used to prompt the user
|
||||
// configFilePath: file path containing user defined config values
|
||||
// templateRoot: root of the template definition
|
||||
// templateFS: root of the template definition
|
||||
// outputDir: root of directory where to initialize the template
|
||||
func Materialize(ctx context.Context, configFilePath, templateRoot, outputDir string) error {
|
||||
// Use a temporary directory in case any builtin templates like default-python are used
|
||||
tempDir, err := os.MkdirTemp("", "templates")
|
||||
defer os.RemoveAll(tempDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
templateRoot, err = prepareBuiltinTemplates(templateRoot, tempDir)
|
||||
if err != nil {
|
||||
return err
|
||||
func Materialize(ctx context.Context, configFilePath string, templateFS fs.FS, outputDir string) error {
|
||||
if _, err := fs.Stat(templateFS, schemaFileName); errors.Is(err, fs.ErrNotExist) {
|
||||
return fmt.Errorf("not a bundle template: expected to find a template schema file at %s", schemaFileName)
|
||||
}
|
||||
|
||||
templatePath := filepath.Join(templateRoot, templateDirName)
|
||||
libraryPath := filepath.Join(templateRoot, libraryDirName)
|
||||
schemaPath := filepath.Join(templateRoot, schemaFileName)
|
||||
helpers := loadHelpers(ctx)
|
||||
|
||||
if _, err := os.Stat(schemaPath); errors.Is(err, fs.ErrNotExist) {
|
||||
return fmt.Errorf("not a bundle template: expected to find a template schema file at %s", schemaPath)
|
||||
}
|
||||
|
||||
config, err := newConfig(ctx, schemaPath)
|
||||
config, err := newConfig(ctx, templateFS, schemaFileName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -62,7 +40,8 @@ func Materialize(ctx context.Context, configFilePath, templateRoot, outputDir st
|
|||
}
|
||||
}
|
||||
|
||||
r, err := newRenderer(ctx, config.values, helpers, templatePath, libraryPath, outputDir)
|
||||
helpers := loadHelpers(ctx)
|
||||
r, err := newRenderer(ctx, config.values, helpers, templateFS, templateDirName, libraryDirName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -94,7 +73,12 @@ func Materialize(ctx context.Context, configFilePath, templateRoot, outputDir st
|
|||
return err
|
||||
}
|
||||
|
||||
err = r.persistToDisk()
|
||||
out, err := filer.NewLocalClient(outputDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = r.persistToDisk(ctx, out)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -111,44 +95,3 @@ func Materialize(ctx context.Context, configFilePath, templateRoot, outputDir st
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// If the given templateRoot matches
|
||||
func prepareBuiltinTemplates(templateRoot string, tempDir string) (string, error) {
|
||||
// Check that `templateRoot` is a clean basename, i.e. `some_path` and not `./some_path` or "."
|
||||
// Return early if that's not the case.
|
||||
if templateRoot == "." || path.Base(templateRoot) != templateRoot {
|
||||
return templateRoot, nil
|
||||
}
|
||||
|
||||
_, err := fs.Stat(builtinTemplates, path.Join("templates", templateRoot))
|
||||
if err != nil {
|
||||
// The given path doesn't appear to be using out built-in templates
|
||||
return templateRoot, nil
|
||||
}
|
||||
|
||||
// We have a built-in template with the same name as templateRoot!
|
||||
// Now we need to make a fully copy of the builtin templates to a real file system
|
||||
// since template.Parse() doesn't support embed.FS.
|
||||
err = fs.WalkDir(builtinTemplates, "templates", func(path string, entry fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
targetPath := filepath.Join(tempDir, path)
|
||||
if entry.IsDir() {
|
||||
return os.Mkdir(targetPath, 0755)
|
||||
} else {
|
||||
content, err := fs.ReadFile(builtinTemplates, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(targetPath, content, 0644)
|
||||
}
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return filepath.Join(tempDir, "templates", templateRoot), nil
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ package template
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/databricks/cli/cmd/root"
|
||||
|
@ -19,6 +19,6 @@ func TestMaterializeForNonTemplateDirectory(t *testing.T) {
|
|||
ctx := root.SetWorkspaceClient(context.Background(), w)
|
||||
|
||||
// Try to materialize a non-template directory.
|
||||
err = Materialize(ctx, "", tmpDir, "")
|
||||
assert.EqualError(t, err, fmt.Sprintf("not a bundle template: expected to find a template schema file at %s", filepath.Join(tmpDir, schemaFileName)))
|
||||
err = Materialize(ctx, "", os.DirFS(tmpDir), "")
|
||||
assert.EqualError(t, err, fmt.Sprintf("not a bundle template: expected to find a template schema file at %s", schemaFileName))
|
||||
}
|
||||
|
|
|
@ -6,9 +6,7 @@ import (
|
|||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"slices"
|
||||
"sort"
|
||||
|
@ -52,32 +50,38 @@ type renderer struct {
|
|||
// do not match any glob patterns from this list
|
||||
skipPatterns []string
|
||||
|
||||
// Filer rooted at template root. The file tree from this root is walked to
|
||||
// generate the project
|
||||
templateFiler filer.Filer
|
||||
|
||||
// Root directory for the project instantiated from the template
|
||||
instanceRoot string
|
||||
// [fs.FS] that holds the template's file tree.
|
||||
srcFS fs.FS
|
||||
}
|
||||
|
||||
func newRenderer(ctx context.Context, config map[string]any, helpers template.FuncMap, templateRoot, libraryRoot, instanceRoot string) (*renderer, error) {
|
||||
func newRenderer(
|
||||
ctx context.Context,
|
||||
config map[string]any,
|
||||
helpers template.FuncMap,
|
||||
templateFS fs.FS,
|
||||
templateDir string,
|
||||
libraryDir string,
|
||||
) (*renderer, error) {
|
||||
// Initialize new template, with helper functions loaded
|
||||
tmpl := template.New("").Funcs(helpers)
|
||||
|
||||
// Load user defined associated templates from the library root
|
||||
libraryGlob := filepath.Join(libraryRoot, "*")
|
||||
matches, err := filepath.Glob(libraryGlob)
|
||||
// Find user-defined templates in the library directory
|
||||
matches, err := fs.Glob(templateFS, path.Join(libraryDir, "*"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Parse user-defined templates.
|
||||
// Note: we do not call [ParseFS] with the glob directly because
|
||||
// it returns an error if no files match the pattern.
|
||||
if len(matches) != 0 {
|
||||
tmpl, err = tmpl.ParseFiles(matches...)
|
||||
tmpl, err = tmpl.ParseFS(templateFS, matches...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
templateFiler, err := filer.NewLocalClient(templateRoot)
|
||||
srcFS, err := fs.Sub(templateFS, path.Clean(templateDir))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -85,13 +89,12 @@ func newRenderer(ctx context.Context, config map[string]any, helpers template.Fu
|
|||
ctx = log.NewContext(ctx, log.GetLogger(ctx).With("action", "initialize-template"))
|
||||
|
||||
return &renderer{
|
||||
ctx: ctx,
|
||||
config: config,
|
||||
baseTemplate: tmpl,
|
||||
files: make([]file, 0),
|
||||
skipPatterns: make([]string, 0),
|
||||
templateFiler: templateFiler,
|
||||
instanceRoot: instanceRoot,
|
||||
ctx: ctx,
|
||||
config: config,
|
||||
baseTemplate: tmpl,
|
||||
files: make([]file, 0),
|
||||
skipPatterns: make([]string, 0),
|
||||
srcFS: srcFS,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -141,7 +144,7 @@ func (r *renderer) executeTemplate(templateDefinition string) (string, error) {
|
|||
|
||||
func (r *renderer) computeFile(relPathTemplate string) (file, error) {
|
||||
// read file permissions
|
||||
info, err := r.templateFiler.Stat(r.ctx, relPathTemplate)
|
||||
info, err := fs.Stat(r.srcFS, relPathTemplate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -157,14 +160,10 @@ func (r *renderer) computeFile(relPathTemplate string) (file, error) {
|
|||
// over as is, without treating it as a template
|
||||
if !strings.HasSuffix(relPathTemplate, templateExtension) {
|
||||
return ©File{
|
||||
dstPath: &destinationPath{
|
||||
root: r.instanceRoot,
|
||||
relPath: relPath,
|
||||
},
|
||||
perm: perm,
|
||||
ctx: r.ctx,
|
||||
srcPath: relPathTemplate,
|
||||
srcFiler: r.templateFiler,
|
||||
perm: perm,
|
||||
relPath: relPath,
|
||||
srcFS: r.srcFS,
|
||||
srcPath: relPathTemplate,
|
||||
}, nil
|
||||
} else {
|
||||
// Trim the .tmpl suffix from file name, if specified in the template
|
||||
|
@ -173,7 +172,7 @@ func (r *renderer) computeFile(relPathTemplate string) (file, error) {
|
|||
}
|
||||
|
||||
// read template file's content
|
||||
templateReader, err := r.templateFiler.Read(r.ctx, relPathTemplate)
|
||||
templateReader, err := r.srcFS.Open(relPathTemplate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -194,11 +193,8 @@ func (r *renderer) computeFile(relPathTemplate string) (file, error) {
|
|||
}
|
||||
|
||||
return &inMemoryFile{
|
||||
dstPath: &destinationPath{
|
||||
root: r.instanceRoot,
|
||||
relPath: relPath,
|
||||
},
|
||||
perm: perm,
|
||||
relPath: relPath,
|
||||
content: []byte(content),
|
||||
}, nil
|
||||
}
|
||||
|
@ -263,7 +259,7 @@ func (r *renderer) walk() error {
|
|||
//
|
||||
// 2. For directories: They are appended to a slice, which acts as a queue
|
||||
// allowing BFS traversal of the template file tree
|
||||
entries, err := r.templateFiler.ReadDir(r.ctx, currentDirectory)
|
||||
entries, err := fs.ReadDir(r.srcFS, currentDirectory)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -283,7 +279,7 @@ func (r *renderer) walk() error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
logger.Infof(r.ctx, "added file to list of possible project files: %s", f.DstPath().relPath)
|
||||
logger.Infof(r.ctx, "added file to list of possible project files: %s", f.RelPath())
|
||||
r.files = append(r.files, f)
|
||||
}
|
||||
|
||||
|
@ -291,17 +287,17 @@ func (r *renderer) walk() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (r *renderer) persistToDisk() error {
|
||||
func (r *renderer) persistToDisk(ctx context.Context, out filer.Filer) error {
|
||||
// Accumulate files which we will persist, skipping files whose path matches
|
||||
// any of the skip patterns
|
||||
filesToPersist := make([]file, 0)
|
||||
for _, file := range r.files {
|
||||
match, err := isSkipped(file.DstPath().relPath, r.skipPatterns)
|
||||
match, err := isSkipped(file.RelPath(), r.skipPatterns)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if match {
|
||||
log.Infof(r.ctx, "skipping file: %s", file.DstPath())
|
||||
log.Infof(r.ctx, "skipping file: %s", file.RelPath())
|
||||
continue
|
||||
}
|
||||
filesToPersist = append(filesToPersist, file)
|
||||
|
@ -309,8 +305,8 @@ func (r *renderer) persistToDisk() error {
|
|||
|
||||
// Assert no conflicting files exist
|
||||
for _, file := range filesToPersist {
|
||||
path := file.DstPath().absPath()
|
||||
_, err := os.Stat(path)
|
||||
path := file.RelPath()
|
||||
_, err := out.Stat(ctx, path)
|
||||
if err == nil {
|
||||
return fmt.Errorf("failed to initialize template, one or more files already exist: %s", path)
|
||||
}
|
||||
|
@ -321,7 +317,7 @@ func (r *renderer) persistToDisk() error {
|
|||
|
||||
// Persist files to disk
|
||||
for _, file := range filesToPersist {
|
||||
err := file.PersistToDisk()
|
||||
err := file.Write(ctx, out)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -3,9 +3,9 @@ package template
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
@ -18,6 +18,7 @@ import (
|
|||
"github.com/databricks/cli/cmd/root"
|
||||
"github.com/databricks/cli/internal/testutil"
|
||||
"github.com/databricks/cli/libs/diag"
|
||||
"github.com/databricks/cli/libs/filer"
|
||||
"github.com/databricks/cli/libs/tags"
|
||||
"github.com/databricks/databricks-sdk-go"
|
||||
workspaceConfig "github.com/databricks/databricks-sdk-go/config"
|
||||
|
@ -41,9 +42,8 @@ func assertFilePermissions(t *testing.T, path string, perm fs.FileMode) {
|
|||
func assertBuiltinTemplateValid(t *testing.T, template string, settings map[string]any, target string, isServicePrincipal bool, build bool, tempDir string) {
|
||||
ctx := context.Background()
|
||||
|
||||
templatePath, err := prepareBuiltinTemplates(template, tempDir)
|
||||
templateFS, err := fs.Sub(builtinTemplates, path.Join("templates", template))
|
||||
require.NoError(t, err)
|
||||
libraryPath := filepath.Join(templatePath, "library")
|
||||
|
||||
w := &databricks.WorkspaceClient{
|
||||
Config: &workspaceConfig.Config{Host: "https://myhost.com"},
|
||||
|
@ -58,16 +58,18 @@ func assertBuiltinTemplateValid(t *testing.T, template string, settings map[stri
|
|||
ctx = root.SetWorkspaceClient(ctx, w)
|
||||
helpers := loadHelpers(ctx)
|
||||
|
||||
renderer, err := newRenderer(ctx, settings, helpers, templatePath, libraryPath, tempDir)
|
||||
renderer, err := newRenderer(ctx, settings, helpers, templateFS, templateDirName, libraryDirName)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Evaluate template
|
||||
err = renderer.walk()
|
||||
require.NoError(t, err)
|
||||
err = renderer.persistToDisk()
|
||||
out, err := filer.NewLocalClient(tempDir)
|
||||
require.NoError(t, err)
|
||||
err = renderer.persistToDisk(ctx, out)
|
||||
require.NoError(t, err)
|
||||
|
||||
b, err := bundle.Load(ctx, filepath.Join(tempDir, "template", "my_project"))
|
||||
b, err := bundle.Load(ctx, filepath.Join(tempDir, "my_project"))
|
||||
require.NoError(t, err)
|
||||
diags := bundle.Apply(ctx, b, phases.LoadNamedTarget(target))
|
||||
require.NoError(t, diags.Error())
|
||||
|
@ -96,18 +98,6 @@ func assertBuiltinTemplateValid(t *testing.T, template string, settings map[stri
|
|||
}
|
||||
}
|
||||
|
||||
func TestPrepareBuiltInTemplatesWithRelativePaths(t *testing.T) {
|
||||
// CWD should not be resolved as a built in template
|
||||
dir, err := prepareBuiltinTemplates(".", t.TempDir())
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, ".", dir)
|
||||
|
||||
// relative path should not be resolved as a built in template
|
||||
dir, err = prepareBuiltinTemplates("./default-python", t.TempDir())
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "./default-python", dir)
|
||||
}
|
||||
|
||||
func TestBuiltinPythonTemplateValid(t *testing.T) {
|
||||
// Test option combinations
|
||||
options := []string{"yes", "no"}
|
||||
|
@ -194,13 +184,14 @@ func TestRendererWithAssociatedTemplateInLibrary(t *testing.T) {
|
|||
ctx := context.Background()
|
||||
ctx = root.SetWorkspaceClient(ctx, nil)
|
||||
helpers := loadHelpers(ctx)
|
||||
r, err := newRenderer(ctx, nil, helpers, "./testdata/email/template", "./testdata/email/library", tmpDir)
|
||||
r, err := newRenderer(ctx, nil, helpers, os.DirFS("."), "./testdata/email/template", "./testdata/email/library")
|
||||
require.NoError(t, err)
|
||||
|
||||
err = r.walk()
|
||||
require.NoError(t, err)
|
||||
|
||||
err = r.persistToDisk()
|
||||
out, err := filer.NewLocalClient(tmpDir)
|
||||
require.NoError(t, err)
|
||||
err = r.persistToDisk(ctx, out)
|
||||
require.NoError(t, err)
|
||||
|
||||
b, err := os.ReadFile(filepath.Join(tmpDir, "my_email"))
|
||||
|
@ -325,45 +316,34 @@ func TestRendererPersistToDisk(t *testing.T) {
|
|||
|
||||
r := &renderer{
|
||||
ctx: ctx,
|
||||
instanceRoot: tmpDir,
|
||||
skipPatterns: []string{"a/b/c", "mn*"},
|
||||
files: []file{
|
||||
&inMemoryFile{
|
||||
dstPath: &destinationPath{
|
||||
root: tmpDir,
|
||||
relPath: "a/b/c",
|
||||
},
|
||||
perm: 0444,
|
||||
relPath: "a/b/c",
|
||||
content: nil,
|
||||
},
|
||||
&inMemoryFile{
|
||||
dstPath: &destinationPath{
|
||||
root: tmpDir,
|
||||
relPath: "mno",
|
||||
},
|
||||
perm: 0444,
|
||||
relPath: "mno",
|
||||
content: nil,
|
||||
},
|
||||
&inMemoryFile{
|
||||
dstPath: &destinationPath{
|
||||
root: tmpDir,
|
||||
relPath: "a/b/d",
|
||||
},
|
||||
perm: 0444,
|
||||
relPath: "a/b/d",
|
||||
content: []byte("123"),
|
||||
},
|
||||
&inMemoryFile{
|
||||
dstPath: &destinationPath{
|
||||
root: tmpDir,
|
||||
relPath: "mmnn",
|
||||
},
|
||||
perm: 0444,
|
||||
relPath: "mmnn",
|
||||
content: []byte("456"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err := r.persistToDisk()
|
||||
out, err := filer.NewLocalClient(tmpDir)
|
||||
require.NoError(t, err)
|
||||
err = r.persistToDisk(ctx, out)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.NoFileExists(t, filepath.Join(tmpDir, "a", "b", "c"))
|
||||
|
@ -378,10 +358,9 @@ func TestRendererPersistToDisk(t *testing.T) {
|
|||
func TestRendererWalk(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx = root.SetWorkspaceClient(ctx, nil)
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
helpers := loadHelpers(ctx)
|
||||
r, err := newRenderer(ctx, nil, helpers, "./testdata/walk/template", "./testdata/walk/library", tmpDir)
|
||||
r, err := newRenderer(ctx, nil, helpers, os.DirFS("."), "./testdata/walk/template", "./testdata/walk/library")
|
||||
require.NoError(t, err)
|
||||
|
||||
err = r.walk()
|
||||
|
@ -389,21 +368,12 @@ func TestRendererWalk(t *testing.T) {
|
|||
|
||||
getContent := func(r *renderer, path string) string {
|
||||
for _, f := range r.files {
|
||||
if f.DstPath().relPath != path {
|
||||
if f.RelPath() != path {
|
||||
continue
|
||||
}
|
||||
switch v := f.(type) {
|
||||
case *inMemoryFile:
|
||||
return strings.Trim(string(v.content), "\r\n")
|
||||
case *copyFile:
|
||||
r, err := r.templateFiler.Read(context.Background(), v.srcPath)
|
||||
require.NoError(t, err)
|
||||
b, err := io.ReadAll(r)
|
||||
require.NoError(t, err)
|
||||
return strings.Trim(string(b), "\r\n")
|
||||
default:
|
||||
require.FailNow(t, "execution should not reach here")
|
||||
}
|
||||
b, err := f.contents()
|
||||
require.NoError(t, err)
|
||||
return strings.Trim(string(b), "\r\n")
|
||||
}
|
||||
require.FailNow(t, "file is absent: "+path)
|
||||
return ""
|
||||
|
@ -419,10 +389,9 @@ func TestRendererWalk(t *testing.T) {
|
|||
func TestRendererFailFunction(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx = root.SetWorkspaceClient(ctx, nil)
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
helpers := loadHelpers(ctx)
|
||||
r, err := newRenderer(ctx, nil, helpers, "./testdata/fail/template", "./testdata/fail/library", tmpDir)
|
||||
r, err := newRenderer(ctx, nil, helpers, os.DirFS("."), "./testdata/fail/template", "./testdata/fail/library")
|
||||
require.NoError(t, err)
|
||||
|
||||
err = r.walk()
|
||||
|
@ -432,10 +401,9 @@ func TestRendererFailFunction(t *testing.T) {
|
|||
func TestRendererSkipsDirsEagerly(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx = root.SetWorkspaceClient(ctx, nil)
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
helpers := loadHelpers(ctx)
|
||||
r, err := newRenderer(ctx, nil, helpers, "./testdata/skip-dir-eagerly/template", "./testdata/skip-dir-eagerly/library", tmpDir)
|
||||
r, err := newRenderer(ctx, nil, helpers, os.DirFS("."), "./testdata/skip-dir-eagerly/template", "./testdata/skip-dir-eagerly/library")
|
||||
require.NoError(t, err)
|
||||
|
||||
err = r.walk()
|
||||
|
@ -452,7 +420,7 @@ func TestRendererSkipAllFilesInCurrentDirectory(t *testing.T) {
|
|||
tmpDir := t.TempDir()
|
||||
|
||||
helpers := loadHelpers(ctx)
|
||||
r, err := newRenderer(ctx, nil, helpers, "./testdata/skip-all-files-in-cwd/template", "./testdata/skip-all-files-in-cwd/library", tmpDir)
|
||||
r, err := newRenderer(ctx, nil, helpers, os.DirFS("."), "./testdata/skip-all-files-in-cwd/template", "./testdata/skip-all-files-in-cwd/library")
|
||||
require.NoError(t, err)
|
||||
|
||||
err = r.walk()
|
||||
|
@ -460,7 +428,9 @@ func TestRendererSkipAllFilesInCurrentDirectory(t *testing.T) {
|
|||
// All 3 files are executed and have in memory representations
|
||||
require.Len(t, r.files, 3)
|
||||
|
||||
err = r.persistToDisk()
|
||||
out, err := filer.NewLocalClient(tmpDir)
|
||||
require.NoError(t, err)
|
||||
err = r.persistToDisk(ctx, out)
|
||||
require.NoError(t, err)
|
||||
|
||||
entries, err := os.ReadDir(tmpDir)
|
||||
|
@ -472,10 +442,9 @@ func TestRendererSkipAllFilesInCurrentDirectory(t *testing.T) {
|
|||
func TestRendererSkipPatternsAreRelativeToFileDirectory(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx = root.SetWorkspaceClient(ctx, nil)
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
helpers := loadHelpers(ctx)
|
||||
r, err := newRenderer(ctx, nil, helpers, "./testdata/skip-is-relative/template", "./testdata/skip-is-relative/library", tmpDir)
|
||||
r, err := newRenderer(ctx, nil, helpers, os.DirFS("."), "./testdata/skip-is-relative/template", "./testdata/skip-is-relative/library")
|
||||
require.NoError(t, err)
|
||||
|
||||
err = r.walk()
|
||||
|
@ -493,7 +462,7 @@ func TestRendererSkip(t *testing.T) {
|
|||
tmpDir := t.TempDir()
|
||||
|
||||
helpers := loadHelpers(ctx)
|
||||
r, err := newRenderer(ctx, nil, helpers, "./testdata/skip/template", "./testdata/skip/library", tmpDir)
|
||||
r, err := newRenderer(ctx, nil, helpers, os.DirFS("."), "./testdata/skip/template", "./testdata/skip/library")
|
||||
require.NoError(t, err)
|
||||
|
||||
err = r.walk()
|
||||
|
@ -502,7 +471,9 @@ func TestRendererSkip(t *testing.T) {
|
|||
// This is because "dir2/*" matches the files in dir2, but not dir2 itself
|
||||
assert.Len(t, r.files, 6)
|
||||
|
||||
err = r.persistToDisk()
|
||||
out, err := filer.NewLocalClient(tmpDir)
|
||||
require.NoError(t, err)
|
||||
err = r.persistToDisk(ctx, out)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.FileExists(t, filepath.Join(tmpDir, "file1"))
|
||||
|
@ -520,12 +491,11 @@ func TestRendererReadsPermissionsBits(t *testing.T) {
|
|||
if runtime.GOOS != "linux" && runtime.GOOS != "darwin" {
|
||||
t.SkipNow()
|
||||
}
|
||||
tmpDir := t.TempDir()
|
||||
ctx := context.Background()
|
||||
ctx = root.SetWorkspaceClient(ctx, nil)
|
||||
|
||||
helpers := loadHelpers(ctx)
|
||||
r, err := newRenderer(ctx, nil, helpers, "./testdata/executable-bit-read/template", "./testdata/executable-bit-read/library", tmpDir)
|
||||
r, err := newRenderer(ctx, nil, helpers, os.DirFS("."), "./testdata/executable-bit-read/template", "./testdata/executable-bit-read/library")
|
||||
require.NoError(t, err)
|
||||
|
||||
err = r.walk()
|
||||
|
@ -533,7 +503,7 @@ func TestRendererReadsPermissionsBits(t *testing.T) {
|
|||
|
||||
getPermissions := func(r *renderer, path string) fs.FileMode {
|
||||
for _, f := range r.files {
|
||||
if f.DstPath().relPath != path {
|
||||
if f.RelPath() != path {
|
||||
continue
|
||||
}
|
||||
switch v := f.(type) {
|
||||
|
@ -556,6 +526,7 @@ func TestRendererReadsPermissionsBits(t *testing.T) {
|
|||
|
||||
func TestRendererErrorOnConflictingFile(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
ctx := context.Background()
|
||||
|
||||
f, err := os.Create(filepath.Join(tmpDir, "a"))
|
||||
require.NoError(t, err)
|
||||
|
@ -566,17 +537,16 @@ func TestRendererErrorOnConflictingFile(t *testing.T) {
|
|||
skipPatterns: []string{},
|
||||
files: []file{
|
||||
&inMemoryFile{
|
||||
dstPath: &destinationPath{
|
||||
root: tmpDir,
|
||||
relPath: "a",
|
||||
},
|
||||
perm: 0444,
|
||||
relPath: "a",
|
||||
content: []byte("123"),
|
||||
},
|
||||
},
|
||||
}
|
||||
err = r.persistToDisk()
|
||||
assert.EqualError(t, err, fmt.Sprintf("failed to initialize template, one or more files already exist: %s", filepath.Join(tmpDir, "a")))
|
||||
out, err := filer.NewLocalClient(tmpDir)
|
||||
require.NoError(t, err)
|
||||
err = r.persistToDisk(ctx, out)
|
||||
assert.EqualError(t, err, fmt.Sprintf("failed to initialize template, one or more files already exist: %s", "a"))
|
||||
}
|
||||
|
||||
func TestRendererNoErrorOnConflictingFileIfSkipped(t *testing.T) {
|
||||
|
@ -593,16 +563,15 @@ func TestRendererNoErrorOnConflictingFileIfSkipped(t *testing.T) {
|
|||
skipPatterns: []string{"a"},
|
||||
files: []file{
|
||||
&inMemoryFile{
|
||||
dstPath: &destinationPath{
|
||||
root: tmpDir,
|
||||
relPath: "a",
|
||||
},
|
||||
perm: 0444,
|
||||
relPath: "a",
|
||||
content: []byte("123"),
|
||||
},
|
||||
},
|
||||
}
|
||||
err = r.persistToDisk()
|
||||
out, err := filer.NewLocalClient(tmpDir)
|
||||
require.NoError(t, err)
|
||||
err = r.persistToDisk(ctx, out)
|
||||
// No error is returned even though a conflicting file exists. This is because
|
||||
// the generated file is being skipped
|
||||
assert.NoError(t, err)
|
||||
|
@ -612,10 +581,9 @@ func TestRendererNoErrorOnConflictingFileIfSkipped(t *testing.T) {
|
|||
func TestRendererNonTemplatesAreCreatedAsCopyFiles(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx = root.SetWorkspaceClient(ctx, nil)
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
helpers := loadHelpers(ctx)
|
||||
r, err := newRenderer(ctx, nil, helpers, "./testdata/copy-file-walk/template", "./testdata/copy-file-walk/library", tmpDir)
|
||||
r, err := newRenderer(ctx, nil, helpers, os.DirFS("."), "./testdata/copy-file-walk/template", "./testdata/copy-file-walk/library")
|
||||
require.NoError(t, err)
|
||||
|
||||
err = r.walk()
|
||||
|
@ -623,7 +591,7 @@ func TestRendererNonTemplatesAreCreatedAsCopyFiles(t *testing.T) {
|
|||
|
||||
assert.Len(t, r.files, 1)
|
||||
assert.Equal(t, r.files[0].(*copyFile).srcPath, "not-a-template")
|
||||
assert.Equal(t, r.files[0].DstPath().absPath(), filepath.Join(tmpDir, "not-a-template"))
|
||||
assert.Equal(t, r.files[0].RelPath(), "not-a-template")
|
||||
}
|
||||
|
||||
func TestRendererFileTreeRendering(t *testing.T) {
|
||||
|
@ -635,7 +603,7 @@ func TestRendererFileTreeRendering(t *testing.T) {
|
|||
r, err := newRenderer(ctx, map[string]any{
|
||||
"dir_name": "my_directory",
|
||||
"file_name": "my_file",
|
||||
}, helpers, "./testdata/file-tree-rendering/template", "./testdata/file-tree-rendering/library", tmpDir)
|
||||
}, helpers, os.DirFS("."), "./testdata/file-tree-rendering/template", "./testdata/file-tree-rendering/library")
|
||||
require.NoError(t, err)
|
||||
|
||||
err = r.walk()
|
||||
|
@ -643,9 +611,11 @@ func TestRendererFileTreeRendering(t *testing.T) {
|
|||
|
||||
// Assert in memory representation is created.
|
||||
assert.Len(t, r.files, 1)
|
||||
assert.Equal(t, r.files[0].DstPath().absPath(), filepath.Join(tmpDir, "my_directory", "my_file"))
|
||||
assert.Equal(t, r.files[0].RelPath(), "my_directory/my_file")
|
||||
|
||||
err = r.persistToDisk()
|
||||
out, err := filer.NewLocalClient(tmpDir)
|
||||
require.NoError(t, err)
|
||||
err = r.persistToDisk(ctx, out)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Assert files and directories are correctly materialized.
|
||||
|
@ -667,8 +637,7 @@ func TestRendererSubTemplateInPath(t *testing.T) {
|
|||
// https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file.
|
||||
testutil.Touch(t, filepath.Join(templateDir, "template/{{template `dir_name`}}/{{template `file_name`}}"))
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
r, err := newRenderer(ctx, nil, nil, filepath.Join(templateDir, "template"), filepath.Join(templateDir, "library"), tmpDir)
|
||||
r, err := newRenderer(ctx, nil, nil, os.DirFS(templateDir), "template", "library")
|
||||
require.NoError(t, err)
|
||||
|
||||
err = r.walk()
|
||||
|
@ -676,7 +645,6 @@ func TestRendererSubTemplateInPath(t *testing.T) {
|
|||
|
||||
if assert.Len(t, r.files, 2) {
|
||||
f := r.files[1]
|
||||
assert.Equal(t, filepath.Join(tmpDir, "my_directory", "my_file"), f.DstPath().absPath())
|
||||
assert.Equal(t, "my_directory/my_file", f.DstPath().relPath)
|
||||
assert.Equal(t, "my_directory/my_file", f.RelPath())
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue