From 7471e17daf4e7ec0eed1bef1bf89d78bc2ca42f8 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Tue, 18 Aug 2026 11:18:25 +0200 Subject: [PATCH 1/2] dresources: remove permissions/grants special-casing from plan and migrate Two optional adapter hooks, Configure(resourceType) and PrepareInputConfig(inputConfig, resourceKey), replace the suffix/prefix dispatch that makePlan and BuildStateFromTF each carried. Co-authored-by: Isaac --- bundle/direct/bundle_plan.go | 36 ++-------- bundle/direct/dresources/README.md | 11 +++ bundle/direct/dresources/adapter.go | 54 +++++++++++++++ bundle/direct/dresources/grants.go | 60 +++++++---------- bundle/direct/dresources/permissions.go | 50 ++++++++------ bundle/direct/dresources/permissions_test.go | 67 +++++++++++++++++++ bundle/direct/dresources/secret_scope_acls.go | 26 +++---- bundle/migrate/build_state.go | 38 ++--------- 8 files changed, 209 insertions(+), 133 deletions(-) create mode 100644 bundle/direct/dresources/permissions_test.go diff --git a/bundle/direct/bundle_plan.go b/bundle/direct/bundle_plan.go index 21c29d78d33..5b8829e3f59 100644 --- a/bundle/direct/bundle_plan.go +++ b/bundle/direct/bundle_plan.go @@ -11,7 +11,6 @@ import ( "strings" "github.com/databricks/cli/bundle/config" - "github.com/databricks/cli/bundle/config/resources" "github.com/databricks/cli/bundle/deployplan" "github.com/databricks/cli/bundle/direct/dresources" "github.com/databricks/cli/bundle/direct/dstate" @@ -976,37 +975,12 @@ func (b *DeploymentBundle) makePlan(ctx context.Context, configRoot *config.Root return nil, fmt.Errorf("%s: %w", prefix, err) } - baseRefs := map[string]string{} - - if strings.HasSuffix(node, ".permissions") { - var inputConfigStructVar *structvar.StructVar - var err error - - if strings.HasPrefix(node, "resources.secret_scopes.") { - typedConfig, ok := inputConfig.(*[]resources.SecretScopePermission) - if !ok { - return nil, fmt.Errorf("%s: expected *[]resources.SecretScopePermission, got %T", prefix, inputConfig) - } - inputConfigStructVar, err = dresources.PrepareSecretScopeAclsInputConfig(*typedConfig, node) - } else { - inputConfigStructVar, err = dresources.PreparePermissionsInputConfig(inputConfig, node) - } - - if err != nil { - return nil, err - } - inputConfig = inputConfigStructVar.Value - baseRefs = inputConfigStructVar.Refs - } else if strings.HasSuffix(node, ".grants") { - inputConfigStructVar, err := dresources.PrepareGrantsInputConfig(inputConfig, node) - if err != nil { - return nil, err - } - inputConfig = inputConfigStructVar.Value - baseRefs = inputConfigStructVar.Refs + inputStructVar, err := adapter.PrepareInputConfig(inputConfig, node) + if err != nil { + return nil, fmt.Errorf("%s: %w", prefix, err) } - newStateConfig, err := adapter.PrepareState(inputConfig) + newStateConfig, err := adapter.PrepareState(inputStructVar.Value) if err != nil { return nil, fmt.Errorf("%s: %w", prefix, err) } @@ -1032,7 +1006,7 @@ func (b *DeploymentBundle) makePlan(ctx context.Context, configRoot *config.Root return nil, fmt.Errorf("failed to read references from config for %s: %w", node, err) } - maps.Copy(refs, baseRefs) + maps.Copy(refs, inputStructVar.Refs) var dependsOn []deployplan.DependsOnEntry for _, reference := range refs { diff --git a/bundle/direct/dresources/README.md b/bundle/direct/dresources/README.md index 29e234c85ac..e93d5abcc10 100644 --- a/bundle/direct/dresources/README.md +++ b/bundle/direct/dresources/README.md @@ -48,6 +48,17 @@ If a desired state describes no resource at all (e.g. an empty grants list), imp The planner only consults it for nodes without a state entry: once state exists the node stays in the plan, so emptying it still plans an update, and the update is what removes the entry. +## Config that is not the state: Configure and PrepareInputConfig + +Most resources take their bundle config straight into `PrepareState`. Sub-resources (permissions, grants) cannot: their state is assembled from the parent's config section plus a reference to the parent's ID, which has no source in the config. + +Two optional hooks cover this, and nothing outside the resource needs to know a node is a sub-resource: + + - `Configure(resourceType string) error` is called once per registered type at adapter construction, with the key from `SupportedResources` (e.g. `"jobs.permissions"`). Resolve type-level lookups here — one instance exists per type, so the result can be stored on the receiver, and an unsupported type fails at init rather than at plan time. + - `PrepareInputConfig(inputConfig, resourceKey string) (*structvar.StructVar, error)` converts the node's config into the value handed to `PrepareState`, together with the references needed to complete it. `resourceKey` is the full node (`resources.jobs.foo.permissions`), so the parent node is available for building `${...}` references. + +Declare `inputConfig` with its concrete type when every parent shares one (grants: `*[]catalog.PrivilegeAssignment`); use `any` only where the config types genuinely differ per parent, as with permissions. + ## State backward compatibility The state struct is serialized to JSON and persisted between deploys. Backward incompatible changes will result in a drift, which depending diff --git a/bundle/direct/dresources/adapter.go b/bundle/direct/dresources/adapter.go index 63f56479055..99b061f268d 100644 --- a/bundle/direct/dresources/adapter.go +++ b/bundle/direct/dresources/adapter.go @@ -9,6 +9,7 @@ import ( "github.com/databricks/cli/bundle/deployplan" "github.com/databricks/cli/libs/calladapt" "github.com/databricks/cli/libs/structs/structpath" + "github.com/databricks/cli/libs/structs/structvar" "github.com/databricks/databricks-sdk-go" ) @@ -28,6 +29,19 @@ type IResource interface { // Single instance is reused across all instances, so it must not store any resource-specific state. New(client *databricks.WorkspaceClient) any + // [Optional] Configure passes the resource type this instance is registered under in SupportedResources + // (e.g. "jobs.permissions"). One instance is created per resource type, so type-level lookups belong here + // rather than in per-node methods, and an unsupported type fails at init instead of at plan time. + // Example: func (r *ResourcePermissions) Configure(resourceType string) error + Configure(resourceType string) error + + // [Optional] PrepareInputConfig converts the bundle config for a node into the value passed to PrepareState, + // plus references that complete it but have no source in the config. resourceKey is the full node, + // e.g. "resources.jobs.foo.permissions". Sub-resources use it to reference their parent's id. + // Resources that don't implement it receive their config unchanged and contribute no references. + // Example: func (r *ResourceGrants) PrepareInputConfig(inputConfig *[]catalog.PrivilegeAssignment, resourceKey string) (*structvar.StructVar, error) + PrepareInputConfig(inputConfig any, resourceKey string) (*structvar.StructVar, error) + // PrepareState converts resource's config as defined by bundle schema to the concrete type used by create/update and persisted in the state. // Example: func (*ResourceJob) PrepareState(input *resources.Job) *jobs.JobSettings PrepareState(input any) any @@ -109,6 +123,7 @@ type Adapter struct { doCreate *calladapt.BoundCaller // Optional: + prepareInputConfig *calladapt.BoundCaller isEmptyState *calladapt.BoundCaller doUpdate *calladapt.BoundCaller doUpdateWithID *calladapt.BoundCaller @@ -137,12 +152,19 @@ func NewAdapter(typedNil any, resourceType string, client *databricks.WorkspaceC return nil, fmt.Errorf("internal error: New returned %d values, expected 1", len(outs)) } impl := outs[0] + + err = configureImpl(impl, resourceType) + if err != nil { + return nil, err + } + adapter := &Adapter{ prepareState: nil, remapState: nil, doRefresh: nil, doDelete: nil, doCreate: nil, + prepareInputConfig: nil, isEmptyState: nil, doUpdate: nil, doUpdateWithID: nil, @@ -170,6 +192,19 @@ func NewAdapter(typedNil any, resourceType string, client *databricks.WorkspaceC return adapter, nil } +// configureImpl calls the resource's Configure method, if it has one. +func configureImpl(impl any, resourceType string) error { + call, err := calladapt.PrepareCall(impl, reflect.TypeFor[IResource](), "Configure") + if err != nil { + return err + } + if call == nil { + return nil + } + _, err = call.Call(resourceType) + return err +} + // loadKeyedSlices validates and calls KeyedSlices method, returning the resulting map. func loadKeyedSlices(call *calladapt.BoundCaller) (map[string]any, error) { outs, err := call.Call() @@ -213,6 +248,11 @@ func (a *Adapter) initMethods(resource any) error { // Optional methods with varying signatures: + a.prepareInputConfig, err = calladapt.PrepareCall(resource, reflect.TypeFor[IResource](), "PrepareInputConfig") + if err != nil { + return err + } + a.isEmptyState, err = calladapt.PrepareCall(resource, reflect.TypeFor[IResource](), "IsEmptyState") if err != nil { return err @@ -440,6 +480,20 @@ func (a *Adapter) FieldTriggersRecreate(path *structpath.PathNode) bool { return false } +// PrepareInputConfig converts the node's bundle config into the input for PrepareState and the +// references needed to complete it. Resources without PrepareInputConfig pass their config through. +func (a *Adapter) PrepareInputConfig(inputConfig any, resourceKey string) (*structvar.StructVar, error) { + if a.prepareInputConfig == nil { + return &structvar.StructVar{Value: inputConfig, Refs: nil}, nil + } + + outs, err := a.prepareInputConfig.Call(inputConfig, resourceKey) + if err != nil { + return nil, err + } + return outs[0].(*structvar.StructVar), nil +} + func (a *Adapter) PrepareState(input any) (any, error) { outs, err := a.prepareState.Call(input) if err != nil { diff --git a/bundle/direct/dresources/grants.go b/bundle/direct/dresources/grants.go index eb21b85bc8f..5fd10f41046 100644 --- a/bundle/direct/dresources/grants.go +++ b/bundle/direct/dresources/grants.go @@ -28,36 +28,46 @@ type GrantsState struct { EmbeddedSlice []catalog.PrivilegeAssignment `json:"__embed__,omitempty"` } -func PrepareGrantsInputConfig(inputConfig any, node string) (*structvar.StructVar, error) { - baseNode, ok := strings.CutSuffix(node, ".grants") - if !ok { - return nil, fmt.Errorf("internal error: node %q does not end with .grants", node) - } +type ResourceGrants struct { + client *databricks.WorkspaceClient - resourceType, err := extractGrantResourceType(node) - if err != nil { - return nil, err + // securableType is the UC securable type of the parent resource, e.g. "schema". + securableType string +} + +func (*ResourceGrants) New(client *databricks.WorkspaceClient) *ResourceGrants { + return &ResourceGrants{client: client, securableType: ""} +} + +func (r *ResourceGrants) Configure(resourceType string) error { + parentType, ok := strings.CutSuffix(resourceType, ".grants") + if !ok { + return fmt.Errorf("internal error: resource type %q does not end with .grants", resourceType) } - securableType, ok := grantResourceToSecurableType[resourceType] + r.securableType, ok = grantResourceToSecurableType[parentType] if !ok { - return nil, fmt.Errorf("unsupported grants resource type: %s", resourceType) + return fmt.Errorf("unsupported grants resource type: %s", parentType) } - grantsPtr, ok := inputConfig.(*[]catalog.PrivilegeAssignment) + return nil +} + +func (r *ResourceGrants) PrepareInputConfig(inputConfig *[]catalog.PrivilegeAssignment, resourceKey string) (*structvar.StructVar, error) { + baseNode, ok := strings.CutSuffix(resourceKey, ".grants") if !ok { - return nil, fmt.Errorf("expected *[]catalog.PrivilegeAssignment, got %T", inputConfig) + return nil, fmt.Errorf("internal error: node %q does not end with .grants", resourceKey) } // Normalize the same way as DoRead (sort, collapse ALL_PRIVILEGES) so the // config and the value read back compare equal. - normalizeAssignments(*grantsPtr) + normalizeAssignments(*inputConfig) return &structvar.StructVar{ Value: &GrantsState{ - SecurableType: securableType, + SecurableType: r.securableType, FullName: "", - EmbeddedSlice: *grantsPtr, + EmbeddedSlice: *inputConfig, }, Refs: map[string]string{ "full_name": "${" + baseNode + ".id}", @@ -65,14 +75,6 @@ func PrepareGrantsInputConfig(inputConfig any, node string) (*structvar.StructVa }, nil } -type ResourceGrants struct { - client *databricks.WorkspaceClient -} - -func (*ResourceGrants) New(client *databricks.WorkspaceClient) *ResourceGrants { - return &ResourceGrants{client: client} -} - func (*ResourceGrants) PrepareState(state *GrantsState) *GrantsState { return state } @@ -251,18 +253,6 @@ func normalizeAssignments(assignments []catalog.PrivilegeAssignment) { } } -func extractGrantResourceType(node string) (string, error) { - rest, ok := strings.CutPrefix(node, "resources.") - if !ok { - return "", fmt.Errorf("cannot extract resource type from %q", node) - } - parts := strings.Split(rest, ".") - if len(parts) < 2 { - return "", fmt.Errorf("cannot extract resource type from %q", node) - } - return parts[0], nil -} - func parseGrantsID(id string) (string, string, error) { parts := strings.SplitN(id, "/", 2) if len(parts) != 2 { diff --git a/bundle/direct/dresources/permissions.go b/bundle/direct/dresources/permissions.go index e99311757a2..aa838acfeef 100644 --- a/bundle/direct/dresources/permissions.go +++ b/bundle/direct/dresources/permissions.go @@ -33,6 +33,12 @@ var permissionResourceToObjectType = map[string]string{ type ResourcePermissions struct { client *databricks.WorkspaceClient + + // objectType is the permissions API prefix of the parent resource, e.g. "/jobs/". + objectType string + + // idField is the parent field holding the permissions object ID. + idField string } // StatePermission represents a permission entry in deployment state. @@ -57,7 +63,7 @@ type PermissionsState struct { } // permissionIDFields maps resource types that use a non-standard ID field for -// the permissions API (most resources use "id"). +// the permissions API; resources absent from it use defaultPermissionIDField. var permissionIDFields = map[string]string{ "model_serving_endpoints": "endpoint_id", // internal numeric ID, not the name used in CRUD APIs "models": "model_id", // numeric model ID, not the model name used as CRUD state ID @@ -65,29 +71,35 @@ var permissionIDFields = map[string]string{ "vector_search_endpoints": "endpoint_uuid", // endpoint UUID, not the endpoint name used as deployment ID } -// objectIDRef returns the reference expression for the permissions object ID. -func objectIDRef(prefix, baseNode, resourceType string) string { - if field, ok := permissionIDFields[resourceType]; ok { - return prefix + "${" + baseNode + "." + field + "}" - } - return prefix + "${" + baseNode + ".id}" +const defaultPermissionIDField = "id" + +func (*ResourcePermissions) New(client *databricks.WorkspaceClient) *ResourcePermissions { + return &ResourcePermissions{client: client, objectType: "", idField: ""} } -func PreparePermissionsInputConfig(inputConfig any, node string) (*structvar.StructVar, error) { - baseNode, ok := strings.CutSuffix(node, ".permissions") +func (r *ResourcePermissions) Configure(resourceType string) error { + parentType, ok := strings.CutSuffix(resourceType, ".permissions") if !ok { - return nil, fmt.Errorf("internal error: node %q does not end with .permissions", node) + return fmt.Errorf("internal error: resource type %q does not end with .permissions", resourceType) } - parts := strings.Split(baseNode, ".") - if len(parts) < 2 { - return nil, fmt.Errorf("internal error: unexpected node format %q", baseNode) + r.objectType, ok = permissionResourceToObjectType[parentType] + if !ok { + return fmt.Errorf("unsupported permissions resource type: %s", parentType) } - resourceType := parts[1] - prefix, ok := permissionResourceToObjectType[resourceType] + r.idField, ok = permissionIDFields[parentType] if !ok { - return nil, fmt.Errorf("unsupported permissions resource type: %s", resourceType) + r.idField = defaultPermissionIDField + } + + return nil +} + +func (r *ResourcePermissions) PrepareInputConfig(inputConfig any, resourceKey string) (*structvar.StructVar, error) { + baseNode, ok := strings.CutSuffix(resourceKey, ".permissions") + if !ok { + return nil, fmt.Errorf("internal error: node %q does not end with .permissions", resourceKey) } permissions, err := toStatePermissions(inputConfig) @@ -101,15 +113,11 @@ func PreparePermissionsInputConfig(inputConfig any, node string) (*structvar.Str EmbeddedSlice: permissions, }, Refs: map[string]string{ - "object_id": objectIDRef(prefix, baseNode, resourceType), + "object_id": r.objectType + "${" + baseNode + "." + r.idField + "}", }, }, nil } -func (*ResourcePermissions) New(client *databricks.WorkspaceClient) *ResourcePermissions { - return &ResourcePermissions{client: client} -} - func (*ResourcePermissions) PrepareState(s *PermissionsState) *PermissionsState { return s } diff --git a/bundle/direct/dresources/permissions_test.go b/bundle/direct/dresources/permissions_test.go new file mode 100644 index 00000000000..20ef61b75f0 --- /dev/null +++ b/bundle/direct/dresources/permissions_test.go @@ -0,0 +1,67 @@ +package dresources + +import ( + "testing" + + "github.com/databricks/cli/bundle/config/resources" + "github.com/databricks/databricks-sdk-go/service/catalog" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// The object_id reference is synthesized from the resource type, so it is only exercised +// end-to-end by acceptance tests. Pin both the default id field and an override here. +func TestPermissionsPrepareInputConfig(t *testing.T) { + tests := []struct { + resourceType string + resourceKey string + expectedRef string + }{ + { + resourceType: "jobs.permissions", + resourceKey: "resources.jobs.foo.permissions", + expectedRef: "/jobs/${resources.jobs.foo.id}", + }, + { + // models use a numeric model_id, not the model name recorded as the CRUD state ID + resourceType: "models.permissions", + resourceKey: "resources.models.foo.permissions", + expectedRef: "/registered-models/${resources.models.foo.model_id}", + }, + } + + for _, tt := range tests { + t.Run(tt.resourceType, func(t *testing.T) { + adapter, err := NewAdapter(SupportedResources[tt.resourceType], tt.resourceType, nil) + require.NoError(t, err) + + input := &[]resources.JobPermission{{Level: "CAN_VIEW", UserName: "alice"}} + sv, err := adapter.PrepareInputConfig(input, tt.resourceKey) + require.NoError(t, err) + + assert.Equal(t, &PermissionsState{ + EmbeddedSlice: []StatePermission{{Level: "CAN_VIEW", UserName: "alice"}}, + }, sv.Value) + assert.Equal(t, map[string]string{"object_id": tt.expectedRef}, sv.Refs) + }) + } +} + +// Configure resolves the parent type at init, so an unregistered parent fails before planning. +func TestPermissionsConfigureUnsupportedParent(t *testing.T) { + _, err := NewAdapter(SupportedResources["jobs.permissions"], "unregistered.permissions", nil) + assert.ErrorContains(t, err, "unsupported permissions resource type: unregistered") +} + +// Resources without PrepareInputConfig get their config through unchanged and add no references. +func TestPrepareInputConfigPassthrough(t *testing.T) { + adapter, err := NewAdapter(SupportedResources["schemas"], "schemas", nil) + require.NoError(t, err) + + input := &resources.Schema{CreateSchema: catalog.CreateSchema{Name: "myschema"}} + sv, err := adapter.PrepareInputConfig(input, "resources.schemas.foo") + require.NoError(t, err) + + assert.Same(t, input, sv.Value) + assert.Nil(t, sv.Refs) +} diff --git a/bundle/direct/dresources/secret_scope_acls.go b/bundle/direct/dresources/secret_scope_acls.go index ef04cb7cb6a..c35d53bd267 100644 --- a/bundle/direct/dresources/secret_scope_acls.go +++ b/bundle/direct/dresources/secret_scope_acls.go @@ -22,14 +22,22 @@ type SecretScopeAclsState struct { Acls []workspace.AclItem `json:"acls,omitempty"` } -func PrepareSecretScopeAclsInputConfig(inputConfig []resources.SecretScopePermission, node string) (*structvar.StructVar, error) { - baseNode, ok := strings.CutSuffix(node, ".permissions") +func (*ResourceSecretScopeAcls) New(client *databricks.WorkspaceClient) *ResourceSecretScopeAcls { + return &ResourceSecretScopeAcls{client: client} +} + +func (*ResourceSecretScopeAcls) PrepareState(s *SecretScopeAclsState) *SecretScopeAclsState { + return s +} + +func (*ResourceSecretScopeAcls) PrepareInputConfig(inputConfig *[]resources.SecretScopePermission, resourceKey string) (*structvar.StructVar, error) { + baseNode, ok := strings.CutSuffix(resourceKey, ".permissions") if !ok { - return nil, fmt.Errorf("internal error: node %q does not end with .permissions", node) + return nil, fmt.Errorf("internal error: node %q does not end with .permissions", resourceKey) } - acls := make([]workspace.AclItem, 0, len(inputConfig)) - for _, elem := range inputConfig { + acls := make([]workspace.AclItem, 0, len(*inputConfig)) + for _, elem := range *inputConfig { acl := workspace.AclItem{ Permission: workspace.AclPermission(elem.Level), Principal: "", @@ -55,14 +63,6 @@ func PrepareSecretScopeAclsInputConfig(inputConfig []resources.SecretScopePermis }, nil } -func (*ResourceSecretScopeAcls) New(client *databricks.WorkspaceClient) *ResourceSecretScopeAcls { - return &ResourceSecretScopeAcls{client: client} -} - -func (*ResourceSecretScopeAcls) PrepareState(s *SecretScopeAclsState) *SecretScopeAclsState { - return s -} - func aclItemKey(x workspace.AclItem) (string, string) { return "principal", x.Principal } diff --git a/bundle/migrate/build_state.go b/bundle/migrate/build_state.go index c08cf01c31f..e8b382b370d 100644 --- a/bundle/migrate/build_state.go +++ b/bundle/migrate/build_state.go @@ -8,7 +8,6 @@ import ( "strings" "github.com/databricks/cli/bundle/config" - "github.com/databricks/cli/bundle/config/resources" "github.com/databricks/cli/bundle/deployplan" "github.com/databricks/cli/bundle/direct" "github.com/databricks/cli/bundle/direct/dresources" @@ -80,39 +79,12 @@ func BuildStateFromTF( return warningsSeen, fmt.Errorf("%s: getting config: %w", node, err) } - baseRefs := map[string]string{} - - switch { - case strings.HasSuffix(node, ".permissions"): - var sv *structvar.StructVar - if strings.HasPrefix(node, "resources.secret_scopes.") { - typedConfig, ok := inputConfig.(*[]resources.SecretScopePermission) - if !ok { - return warningsSeen, fmt.Errorf("%s: expected *[]resources.SecretScopePermission, got %T", node, inputConfig) - } - sv, err = dresources.PrepareSecretScopeAclsInputConfig(*typedConfig, node) - if err != nil { - return warningsSeen, fmt.Errorf("%s: preparing secret scope ACLs config: %w", node, err) - } - } else { - sv, err = dresources.PreparePermissionsInputConfig(inputConfig, node) - if err != nil { - return warningsSeen, fmt.Errorf("%s: preparing permissions config: %w", node, err) - } - } - inputConfig = sv.Value - baseRefs = sv.Refs - - case strings.HasSuffix(node, ".grants"): - sv, err := dresources.PrepareGrantsInputConfig(inputConfig, node) - if err != nil { - return warningsSeen, fmt.Errorf("%s: preparing grants config: %w", node, err) - } - inputConfig = sv.Value - baseRefs = sv.Refs + inputSV, err := adapter.PrepareInputConfig(inputConfig, node) + if err != nil { + return warningsSeen, fmt.Errorf("%s: PrepareInputConfig: %w", node, err) } - newStateValue, err := adapter.PrepareState(inputConfig) + newStateValue, err := adapter.PrepareState(inputSV.Value) if err != nil { return warningsSeen, fmt.Errorf("%s: PrepareState: %w", node, err) } @@ -121,7 +93,7 @@ func BuildStateFromTF( if err != nil { return warningsSeen, fmt.Errorf("%s: extracting references: %w", node, err) } - maps.Copy(refs, baseRefs) + maps.Copy(refs, inputSV.Refs) sv := structvar.NewStructVar(newStateValue, refs) From 153a6a6484f0e36339f3eedfa98804fd24f22b0b Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Tue, 18 Aug 2026 12:48:34 +0200 Subject: [PATCH 2/2] dresources: document that PrepareInputConfig never receives nil Co-authored-by: Isaac --- bundle/direct/dresources/adapter.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bundle/direct/dresources/adapter.go b/bundle/direct/dresources/adapter.go index 99b061f268d..6025f5dc0bd 100644 --- a/bundle/direct/dresources/adapter.go +++ b/bundle/direct/dresources/adapter.go @@ -39,6 +39,8 @@ type IResource interface { // plus references that complete it but have no source in the config. resourceKey is the full node, // e.g. "resources.jobs.foo.permissions". Sub-resources use it to reference their parent's id. // Resources that don't implement it receive their config unchanged and contribute no references. + // Like the other resource methods, inputConfig is never nil, so it may be declared as a concrete + // pointer and dereferenced: nodes are discovered from the config tree, so the key always exists. // Example: func (r *ResourceGrants) PrepareInputConfig(inputConfig *[]catalog.PrivilegeAssignment, resourceKey string) (*structvar.StructVar, error) PrepareInputConfig(inputConfig any, resourceKey string) (*structvar.StructVar, error)