Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,6 @@ dist/
# Local development notes, tmp
/pr-*
/tmp/

# Fetched based on recorded hash
.codegen/apischema.json
21 changes: 20 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -162,14 +162,33 @@ generate-validation:
UNIVERSE_DIR ?= $(HOME)/universe
GENKIT_BINARY := $(UNIVERSE_DIR)/bazel-bin/openapi/genkit/genkit_/genkit

generate:
genkit-binary:
@echo "Checking out universe at SHA: $$(cat .codegen/_openapi_sha)"
cd $(UNIVERSE_DIR) && git fetch origin master && git checkout $$(cat $(PWD)/.codegen/_openapi_sha)
@echo "Building genkit..."
cd $(UNIVERSE_DIR) && bazel build //openapi/genkit

.PHONY: genkit-binary

generate: genkit-binary
@echo "Generating CLI code..."
$(GENKIT_BINARY) update-sdk

.codegen/openapi.json: .codegen/_openapi_sha
$(GENKIT_BINARY) get `cat $<` > $@

generate-direct: generate-direct-apitypes generate-direct-resources
generate-direct-apitypes: bundle/direct/dresources/apitypes.generated.yml
generate-direct-resources: bundle/direct/dresources/resources.generated.yml
generate-direct-clean:
rm -f bundle/direct/dresources/apitypes.generated.yml bundle/direct/dresources/resources.generated.yml
.PHONY: generate-direct generate-direct-apitypes generate-direct-resources generate-direct-clean

bundle/direct/dresources/apitypes.generated.yml: ./bundle/direct/tools/generate_apitypes.py .codegen/apischema.json acceptance/bundle/refschema/out.fields.txt
python3 $^ > $@

bundle/direct/dresources/resources.generated.yml: ./bundle/direct/tools/generate_resources.py .codegen/apischema.json bundle/direct/dresources/apitypes.generated.yml acceptance/bundle/refschema/out.fields.txt
python3 $^ > $@

.PHONY: lint lintfull tidy lintcheck fmt fmtfull test test-unit test-acc test-slow test-slow-unit test-slow-acc cover showcover build snapshot snapshot-release schema integration integration-short acc-cover acc-showcover docs ws wsfix links checks test-update test-update-templates generate-out-test-toml test-update-aws test-update-all generate-validation

Expand Down
1 change: 1 addition & 0 deletions bundle/deployplan/plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ const (
ReasonAlias = "alias"
ReasonRemoteAlreadySet = "remote_already_set"
ReasonBuiltinRule = "builtin_rule"
ReasonAPISchema = "apischema"
ReasonConfigOnly = "config_only"
ReasonEmptySlice = "empty_slice"
ReasonEmptyMap = "empty_map"
Expand Down
7 changes: 7 additions & 0 deletions bundle/direct/bundle_plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,7 @@ func prepareChanges(ctx context.Context, adapter *dresources.Adapter, localDiff,

func addPerFieldActions(ctx context.Context, adapter *dresources.Adapter, changes deployplan.Changes, remoteState any) error {
cfg := adapter.ResourceConfig()
generatedCfg := adapter.GeneratedResourceConfig()

for pathString, ch := range changes {
path, err := structpath.Parse(pathString)
Expand All @@ -386,6 +387,9 @@ func addPerFieldActions(ctx context.Context, adapter *dresources.Adapter, change
} else if shouldSkip(cfg, path, ch) {
ch.Action = deployplan.Skip
ch.Reason = deployplan.ReasonBuiltinRule
} else if shouldSkip(generatedCfg, path, ch) {
ch.Action = deployplan.Skip
ch.Reason = deployplan.ReasonAPISchema
} else if ch.New == nil && ch.Old == nil && ch.Remote != nil && path.IsDotString() {
// The field was not set by us, but comes from the remote state.
// This could either be server-side default or a policy.
Expand All @@ -396,6 +400,9 @@ func addPerFieldActions(ctx context.Context, adapter *dresources.Adapter, change
} else if action := shouldUpdateOrRecreate(cfg, path); action != deployplan.Undefined {
ch.Action = action
ch.Reason = deployplan.ReasonBuiltinRule
} else if action := shouldUpdateOrRecreate(generatedCfg, path); action != deployplan.Undefined {
ch.Action = action
ch.Reason = deployplan.ReasonAPISchema
} else {
ch.Action = deployplan.Update
}
Expand Down
36 changes: 21 additions & 15 deletions bundle/direct/dresources/adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,9 @@ type Adapter struct {
overrideChangeDesc *calladapt.BoundCaller
doResize *calladapt.BoundCaller

resourceConfig *ResourceLifecycleConfig
keyedSlices map[string]any
resourceConfig *ResourceLifecycleConfig
generatedResourceConfig *ResourceLifecycleConfig
keyedSlices map[string]any
}

func NewAdapter(typedNil any, resourceType string, client *databricks.WorkspaceClient) (*Adapter, error) {
Expand All @@ -112,19 +113,20 @@ func NewAdapter(typedNil any, resourceType string, client *databricks.WorkspaceC
}
impl := outs[0]
adapter := &Adapter{
prepareState: nil,
remapState: nil,
doRefresh: nil,
doDelete: nil,
doCreate: nil,
doUpdate: nil,
doUpdateWithID: nil,
doResize: nil,
waitAfterCreate: nil,
waitAfterUpdate: nil,
overrideChangeDesc: nil,
resourceConfig: GetResourceConfig(resourceType),
keyedSlices: nil,
prepareState: nil,
remapState: nil,
doRefresh: nil,
doDelete: nil,
doCreate: nil,
doUpdate: nil,
doUpdateWithID: nil,
doResize: nil,
waitAfterCreate: nil,
waitAfterUpdate: nil,
overrideChangeDesc: nil,
resourceConfig: GetResourceConfig(resourceType),
generatedResourceConfig: GetGeneratedResourceConfig(resourceType),
keyedSlices: nil,
}

err = adapter.initMethods(impl)
Expand Down Expand Up @@ -355,6 +357,10 @@ func (a *Adapter) ResourceConfig() *ResourceLifecycleConfig {
return a.resourceConfig
}

func (a *Adapter) GeneratedResourceConfig() *ResourceLifecycleConfig {
return a.generatedResourceConfig
}

func (a *Adapter) PrepareState(input any) (any, error) {
outs, err := a.prepareState.Call(input)
if err != nil {
Expand Down
55 changes: 29 additions & 26 deletions bundle/direct/dresources/all_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -639,51 +639,54 @@ func testCRUD(t *testing.T, group string, adapter *Adapter, client *databricks.W
}
}

// validateFields uses structwalk to generate all valid field paths and checks membership.
func validateFields(t *testing.T, configType reflect.Type, fields map[string]deployplan.ActionType) {
validPaths := make(map[string]struct{})

err := structwalk.WalkType(configType, func(path *structpath.PathNode, typ reflect.Type, field *reflect.StructField) bool {
validPaths[path.String()] = struct{}{}
return true // continue walking
})
require.NoError(t, err)
// TestResourceConfig validates that all field patterns in resource config
// exist in the corresponding StateType for each resource.
func TestResourceConfig(t *testing.T) {
for resourceType, resource := range SupportedResources {
adapter, err := NewAdapter(resource, resourceType, nil)
require.NoError(t, err)

for fieldPath := range fields {
if _, exists := validPaths[fieldPath]; !exists {
t.Errorf("invalid field '%s' for %s", fieldPath, configType)
cfg := adapter.ResourceConfig()
if cfg == nil {
continue
}

t.Run(resourceType, func(t *testing.T) {
validateResourceConfig(t, adapter.StateType(), cfg)
})
}
}

// TestResourceConfig validates that all field patterns in resource config
// TestGeneratedResourceConfig validates that all field patterns in generated resource config
// exist in the corresponding StateType for each resource.
func TestResourceConfig(t *testing.T) {
func TestGeneratedResourceConfig(t *testing.T) {
for resourceType, resource := range SupportedResources {
adapter, err := NewAdapter(resource, resourceType, nil)
require.NoError(t, err)

cfg := adapter.ResourceConfig()
cfg := adapter.GeneratedResourceConfig()
if cfg == nil {
continue
}

t.Run(resourceType, func(t *testing.T) {
fieldMap := make(map[string]deployplan.ActionType)
for _, p := range cfg.RecreateOnChanges {
fieldMap[p.String()] = deployplan.Recreate
}
for _, p := range cfg.UpdateIDOnChanges {
fieldMap[p.String()] = deployplan.UpdateWithID
}
for _, p := range cfg.IgnoreRemoteChanges {
fieldMap[p.String()] = deployplan.Skip
}
validateFields(t, adapter.StateType(), fieldMap)
validateResourceConfig(t, adapter.StateType(), cfg)
})
}
}

func validateResourceConfig(t *testing.T, stateType reflect.Type, cfg *ResourceLifecycleConfig) {
for _, p := range cfg.RecreateOnChanges {
assert.NoError(t, structaccess.Validate(stateType, p), "RecreateOnChanges: %s", p)
}
for _, p := range cfg.UpdateIDOnChanges {
assert.NoError(t, structaccess.Validate(stateType, p), "UpdateIDOnChanges: %s", p)
}
for _, p := range cfg.IgnoreRemoteChanges {
assert.NoError(t, structaccess.Validate(stateType, p), "IgnoreRemoteChanges: %s", p)
}
}

func setupTestServerClient(t *testing.T) (*testserver.Server, *databricks.WorkspaceClient) {
server := testserver.New(t)
testserver.AddDefaultHandlers(server)
Expand Down
37 changes: 37 additions & 0 deletions bundle/direct/dresources/apitypes.generated.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Generated, do not edit. Override via apitypes.yml

alerts: sql.AlertV2

apps: apps.App

clusters: compute.ClusterSpec

dashboards: dashboards.Dashboard

database_catalogs: database.DatabaseCatalog

database_instances: database.DatabaseInstance

experiments: ml.CreateExperiment

jobs: jobs.JobSettings

model_serving_endpoints: serving.CreateServingEndpoint

models: ml.CreateModelRequest

pipelines: pipelines.CreatePipeline

quality_monitors: catalog.CreateMonitor

registered_models: catalog.RegisteredModelInfo

schemas: catalog.CreateSchema

secret_scopes: workspace.CreateScope

sql_warehouses: sql.EditWarehouseRequest

synced_database_tables: database.SyncedDatabaseTable

volumes: catalog.CreateVolumeRequestContent
34 changes: 32 additions & 2 deletions bundle/direct/dresources/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,14 @@ type Config struct {
//go:embed resources.yml
var resourcesYAML []byte

//go:embed resources.generated.yml
var resourcesGeneratedYAML []byte

var (
configOnce sync.Once
globalConfig *Config
configOnce sync.Once
globalConfig *Config
generatedConfigOnce sync.Once
generatedConfig *Config
)

// MustLoadConfig loads and parses the embedded resources.yml configuration.
Expand All @@ -51,6 +56,21 @@ func MustLoadConfig() *Config {
return globalConfig
}

// MustLoadGeneratedConfig loads and parses the embedded resources.generated.yml configuration.
// The config is loaded once and cached for subsequent calls.
// Panics if the embedded YAML is invalid.
func MustLoadGeneratedConfig() *Config {
generatedConfigOnce.Do(func() {
generatedConfig = &Config{
Resources: nil,
}
if err := yaml.Unmarshal(resourcesGeneratedYAML, generatedConfig); err != nil {
panic(err)
}
})
return generatedConfig
}

// GetResourceConfig returns the lifecycle config for a given resource type.
// Returns nil if the resource type has no configuration.
func GetResourceConfig(resourceType string) *ResourceLifecycleConfig {
Expand All @@ -60,3 +80,13 @@ func GetResourceConfig(resourceType string) *ResourceLifecycleConfig {
}
return nil
}

// GetGeneratedResourceConfig returns the generated lifecycle config for a given resource type.
// Returns nil if the resource type has no configuration.
func GetGeneratedResourceConfig(resourceType string) *ResourceLifecycleConfig {
cfg := MustLoadGeneratedConfig()
if rc, ok := cfg.Resources[resourceType]; ok {
return &rc
}
return nil
}
Loading