Skip to content

Commit f36a674

Browse files
Add cross-resource references to Function, Alias, FunctionUrlConfig and LayerVersion (#232)
Adds cross-resource references to 4 fields across 4 resources in `lambda-controller`. These come from a fleet-wide audit for CRD spec fields that hold another AWS resource's identifier but have no `references` block, each of which forces a user to hardcode an ARN, ID, or name they cannot know until the other resource exists. ### Fields Added | # | Field (`generator.yaml` path) | Target | `path` | | --- | --- | --- | --- | | 1 | `Function.Layers` | lambda `LayerVersion` (same service) | `Status.ACKResourceMetadata.ARN` | | 2 | `Alias.FunctionVersion` | lambda `Version` (same service) | `Status.Version` | | 3 | `FunctionUrlConfig.Qualifier` | lambda `Alias` (same service) | `Spec.Name` | | 4 | `LayerVersion.Content.S3Bucket` | s3 `Bucket` | `Spec.Name` | Notes on individual fields: - **(1) `Function.Layers`** requires the versioned layer ARN ("Specify each layer by its ARN, including the version"). `LayerVersion` sets `ACKResourceMetadata.ARN` from `LayerVersionArn`, which carries the trailing version; `Status.LayerARN` is the *unversioned* ARN and would be the wrong choice. The field already carried `set: ignore` on Create and ReadOne, with `LayerStatuses` as the read-only echo, so the new `layerRefs` companion is not clobbered by the response. - **(2) `Alias.FunctionVersion`** holds the bare numeric version (`pattern: ^(\$LATEST(\.PUBLISHED)?|[0-9]+)$`), not an ARN, so it targets `Status.Version` on `Version`. Being API-required, the field correctly leaves the CRD `required` list now that either form is acceptable. `$LATEST` remains expressible through the concrete field. - **(3) `FunctionUrlConfig.Qualifier`** is documented as "The alias name" and its pattern (`^((?!^\d+$)^[0-9a-zA-Z-_]+$)$`) rejects an all-digits value, ruling out a version number and leaving an alias name as the only legal content — hence `Spec.Name` on `Alias`. - **(4) `LayerVersion.Content.S3Bucket`** mirrors `Function.Code.S3Bucket`, already wired to s3 `Bucket` at `Spec.Name`. **This one needed more than a `references` block:** `PublishLayerVersion` returns a `LayerVersionContentOutput` that shares no members with the `LayerVersionContentInput` held in the spec, so the generated write-back replaced `Spec.Content` with an *empty struct* and would have dropped the new `s3BucketRef` on the first reconcile. `set: ignore` is therefore added for Create and Update (ReadOne was already ignored). `Content` is user-supplied with no server default, so no `late_initialize` is needed. Side benefit: a `LayerVersion` created with a concrete `content.s3Bucket` now keeps it in `.spec` instead of having it wiped to `{}` after create. No new module dependency: `s3-controller` is already required by the existing `Function.Code.S3Bucket` reference, so `go.mod` is unchanged and `ATTRIBUTION.md` does not need regenerating. ### Verification Deployed this branch to `ack-dev-auto` (`us-west-2`) with a 60s resync period (`ack-workspace deploy lambda --resync-period 60`), alongside `s3-controller` for the cross-service target, and tested each field. PASS requires all four conditions, held delta-free across at least 3 reconciles: `ACK.ReferencesResolved=True`, `ACK.ResourceSynced=True`, concrete field absent from `.spec`, `*Ref` present in `.spec`. | # | Resource.field | RefsResolved | Synced | Concrete absent | `*Ref` present | Deltas / 4 resyncs | Result | | --- | --- | --- | --- | --- | --- | --- | --- | | 1 | `Function.layerRefs` | ✅ | ✅ | ✅ | ✅ | 0 | **PASS** | | 2 | `Alias.functionVersionRef` | ✅ | ✅ | ✅ | ✅ | 0 | **PASS** | | 3 | `FunctionUrlConfig.qualifierRef` | ✅ | ✅ | ✅ | ✅ | 0 | **PASS** | | 4 | `LayerVersion.content.s3BucketRef` | ✅ | ✅ | ✅ | ✅ | 0 | **PASS** | **Summary: 4/4 PASS** — resync period 60s, delta-free across 4 reconciles. Each reference was additionally confirmed against AWS rather than only against the CR, since three of these paths are echoed back in a form that would expose a wrong `path`: - **(1)** `status.layerStatuses[0].arn` = `arn:aws:lambda:us-west-2:…:layer:ack-ref-test-layer:1` — the **versioned** ARN reached AWS and was attached, which is what `Status.ACKResourceMetadata.ARN` resolves to. `Status.LayerARN` would have produced the unversioned form and been rejected. - **(2)** `aws lambda get-alias` returns `FunctionVersion = 1` — the bare version string was sent, not an ARN. Note `Version.status.version = "1"` while `status.qualifier` is `null`, confirming `Status.Version` is the populated field and the right choice of `path`. - **(3)** `status.functionARN` = `…function:ack-ref-test-fn:ack-ref-test-alias` — the URL is scoped to the referenced alias. `GetFunctionUrlConfig` does not echo `Qualifier`, so this is the only way to observe that the resolved alias name was sent. - **(4)** The layer published successfully from the referenced bucket (`…layer:ack-ref-test-layer:1`), which is the available signal because `Content` is write-only. **The zero-delta counts are not vacuous.** With no delta the runtime logs nothing per reconcile, so the log signal was proven live by injecting drift out-of-band (`aws lambda update-function-configuration --description …`). The controller detected it 33s later, logged `desired resource state has changed` with `diff paths: [['Spec','Description']]`, and reverted it. So reconciles were running during the measurement window and the delta log appears when there is one — the only diff observed on any test resource was the injected one, never a reference field. <details> <summary>Test manifests and observed state</summary> Reference chain: s3 `Bucket` → `LayerVersion` → `Function` → `Version` → `Alias` → `FunctionUrlConfig`. Every manifest sets the `*Ref` and omits the concrete field. ```yaml apiVersion: lambda.services.k8s.aws/v1alpha1 kind: LayerVersion metadata: name: ack-ref-test-layer spec: layerName: ack-ref-test-layer compatibleRuntimes: [python3.12] content: s3BucketRef: # gap 4; content.s3Bucket omitted from: name: ack-lambda-ref-test-bucket s3Key: layer.zip --- apiVersion: lambda.services.k8s.aws/v1alpha1 kind: Function metadata: name: ack-ref-test-fn spec: name: ack-ref-test-fn role: arn:aws:iam::…:role/ref-test-lambda-role runtime: python3.12 handler: lambda_function.handler code: s3BucketRef: from: name: ack-lambda-ref-test-bucket s3Key: fn.zip layerRefs: # gap 1; spec.layers omitted - from: name: ack-ref-test-layer --- apiVersion: lambda.services.k8s.aws/v1alpha1 kind: Alias metadata: name: ack-ref-test-alias spec: name: ack-ref-test-alias functionRef: from: name: ack-ref-test-fn functionVersionRef: # gap 2; spec.functionVersion omitted from: name: ack-ref-test-version --- apiVersion: lambda.services.k8s.aws/v1alpha1 kind: FunctionURLConfig metadata: name: ack-ref-test-furl spec: functionRef: from: name: ack-ref-test-fn qualifierRef: # gap 3; spec.qualifier omitted from: name: ack-ref-test-alias authType: NONE ``` Observed state — conditions, concrete field absent, `*Ref` present: ``` --- layerversion/ack-ref-test-layer {"refsResolved":"True","synced":"True","concrete":null,"ref":{"from":{"name":"ack-lambda-ref-test-bucket"}}} --- function/ack-ref-test-fn {"refsResolved":"True","synced":"True","concrete":null,"ref":[{"from":{"name":"ack-ref-test-layer"}}]} --- alias.lambda.services.k8s.aws/ack-ref-test-alias {"refsResolved":"True","synced":"True","concrete":null,"ref":{"from":{"name":"ack-ref-test-version"}}} --- functionurlconfig/ack-ref-test-furl {"refsResolved":"True","synced":"True","concrete":null,"ref":{"from":{"name":"ack-ref-test-alias"}}} ``` AWS-side confirmation: ``` $ kubectl get layerversion ack-ref-test-layer -o json | jq -c '.status' {"arn":"arn:aws:lambda:us-west-2:…:layer:ack-ref-test-layer:1","versionNumber":1} $ kubectl get function ack-ref-test-fn -o json | jq -c '.status.layerStatuses' [{"arn":"arn:aws:lambda:us-west-2:…:layer:ack-ref-test-layer:1","codeSize":343}] $ aws lambda get-alias --function-name ack-ref-test-fn --name ack-ref-test-alias \ --query '[FunctionVersion,AliasArn]' --output text 1 arn:aws:lambda:us-west-2:…:function:ack-ref-test-fn:ack-ref-test-alias $ kubectl get functionurlconfig ack-ref-test-furl -o json | jq -c '.status' {"functionARN":"arn:aws:lambda:us-west-2:…:function:ack-ref-test-fn:ack-ref-test-alias", "functionURL":"https://….lambda-url.us-west-2.on.aws/"} ``` Delta window (resync 60s, 21:35:55Z → 21:40:35Z, 4 resyncs): ``` ack-ref-test-layer: 0 ack-ref-test-layer-concrete: 0 ack-ref-test-fn: 0 ack-ref-test-version: 0 ack-ref-test-alias: 0 ack-ref-test-furl: 0 ``` Drift-injection control, proving the delta log is live and reconciles were running: ``` $ aws lambda update-function-configuration --function-name ack-ref-test-fn \ --description "DRIFT-INJECTED-1786483918" # 33s later: {"level":"info","ts":"2026-08-11T21:32:32.839Z","logger":"ackrt", "msg":"desired resource state has changed","kind":"Function","name":"ack-ref-test-fn", "generation":2,"diff":[{"Path":{"Parts":["Spec","Description"]} ... diff paths: [['Spec', 'Description']] $ aws lambda get-function-configuration --function-name ack-ref-test-fn --query Description reference audit remediation test function # reverted ``` Input matrix for gap 4 (`LayerVersion.Content.S3Bucket`), which carries the `set: ignore` change: | Input | Result | | --- | --- | | `s3BucketRef` supplied, concrete omitted | `Synced=True`, ref preserved in `.spec`, concrete absent, layer published from the referenced bucket | | concrete `s3Bucket` supplied, no ref | `Synced=True`, `.spec.content` retains `{"s3Bucket":"…","s3Key":"layer.zip"}` and publishes — previously this was wiped to `{}` after create | | neither | n/a — `Content` is API-required and has no server default, so there is nothing to late-initialize | </details> ### Audit findings deliberately *not* wired Four further fields on these resources hold another resource's identifier but are **polymorphic** — they accept more than one resource type. The code-generator takes exactly one `resource` per field and the generated resolver instantiates one concrete Kind, so wiring one arm would privilege it invisibly, and because the ref/concrete choice is per field, it would lock out any user needing a mixed-type list. Reported here so these are not read as overlooked, with the alternatives enumerated for whoever decides how to express unions: | Field | Accepted types | | --- | --- | | `Function.DeadLetterConfig.TargetARN` | sqs `Queue`, sns `Topic` | | `Function.FileSystemConfigs.ARN` | efs `AccessPoint`, s3files `AccessPoint` (the validation pattern admits both) | | `EventSourceMapping.SourceAccessConfigurations.URI` | secretsmanager `Secret`, ec2 `Subnet`, ec2 `SecurityGroup`, or a plain RabbitMQ virtual-host name — selected by the sibling `type_` | | `{Function,Alias,Version}.FunctionEventInvokeConfig.DestinationConfig.On{Failure,Success}.Destination` | sns `Topic`, sqs `Queue`, s3 `Bucket`, lambda `Function`, eventbridge `EventBus` | The concrete field already accepts every one of these types, so omitting the reference costs users nothing they have today. Three further items surfaced and are **out of scope**, each deserving its own change: - **`EventSourceMapping.SelfManagedKafkaEventSourceConfig.SchemaRegistryConfig.AccessConfigs.URI`** → secretsmanager `Secret`. This was wired in an earlier revision of this PR and has been **removed again**, because it cannot be verified end to end. The reference resolves correctly, but the resource can never reach `Synced`: the API rejects it with `SchemaRegistryConfig is only available for Provisioned Mode. To configure Schema Registry, please enable Provisioned Mode by specifying MinimumPollers in ProvisionedPollerConfig`, and `ProvisionedPollerConfig` is held out of the CRD by `ignore.field_paths`, so `MinimumPollers` is unreachable through this controller. Worth noting for whoever picks this up: because both Kafka branches share the `KafkaSchemaRegistryAccessConfig` shape, the CRD already advertises `selfManagedKafkaEventSourceConfig.…accessConfigs[].uriRef` today with no resolver behind it, so setting it is currently silently ignored. Un-suppressing `ProvisionedPollerConfig` and adding the resolver belong together in one change — which would also make the already-merged `AmazonManagedKafka` twin usable, since it is blocked by the same gate. - **`Function.LoggingConfig.LogGroup`** → cloudwatchlogs `LogGroup` is a genuine, monomorphic gap. `sdkCreate` writes `Spec.LoggingConfig` back, so it needs `set: ignore`, which in turn stops the server-side defaults for the three sibling enum leaves (`LogFormat`, `ApplicationLogLevel`, `SystemLogLevel`) from landing in the spec and requires a `late_initialize`/`skip_incomplete_check` cascade across the whole struct. That changes convergence behaviour for every existing `Function` and adds a new `cloudwatchlogs-controller` dependency. - **`FunctionCode.SourceKMSKeyArn`** and **`CreateEventSourceMappingInput.KMSKeyArn`** are both suppressed via `ignore.field_paths` and are textbook kms `Key` references. They cannot be fixed with a `references` block while suppressed; un-ignoring them is a separate, larger change. ### Checklist - [x] Workspace refreshed (runtime, code-generator, controller) before generating - [x] `service_name` omitted for same-service targets, set for cross-service - [x] `path` matches the form the resource's Describe response returns - [x] Regenerated with `ack-workspace build lambda`; controller compiles - [x] Generated artifacts committed (`apis/`, `pkg/resource/`, `config/crd/`, `helm/`) - [x] `go.mod` updated and pinned (cross-service only) — n/a, no new dependency - [x] `ATTRIBUTION.md` regenerated (cross-service only) — n/a, no new dependency - [x] All 4 fields verified against the four pass criteria, delta-free across 4 reconciles /label release/minor By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.
1 parent 24bbc64 commit f36a674

24 files changed

Lines changed: 682 additions & 27 deletions
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
ack_generate_info:
2-
build_date: "2026-08-05T23:30:52Z"
2+
build_date: "2026-08-11T21:59:48Z"
33
build_hash: db232581a560896c2dc461a244f96d5bf3191ec6
44
go_version: go1.26.0
55
version: v0.62.0
6-
api_directory_checksum: ce4e1b9e43ddbbd1de4d42cb5734359c61a0040c
6+
api_directory_checksum: 5868fc26988d19c4bcd0d5b5c8185065082453ba
77
api_version: v1alpha1
88
aws_sdk_go_version: v1.41.5
99
generator_config_info:
10-
file_checksum: 84db182faab650d546e93c61de79c969df4a9c50
10+
file_checksum: ea85efd6223acfed3d4ff9326facbf6aee9dc9f2
1111
original_file_name: generator.yaml
1212
last_modification:
1313
reason: API generation

apis/v1alpha1/alias.go

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apis/v1alpha1/function.go

Lines changed: 3 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apis/v1alpha1/function_url_config.go

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apis/v1alpha1/generator.yaml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,9 @@ resources:
126126
- ignore: true
127127
method: ReadOne
128128
Layers:
129+
references:
130+
resource: LayerVersion
131+
path: Status.ACKResourceMetadata.ARN
129132
set:
130133
- method: Create
131134
ignore: true
@@ -179,6 +182,9 @@ resources:
179182
path: Spec.Name
180183
FunctionVersion:
181184
is_required: true
185+
references:
186+
resource: Version
187+
path: Status.Version
182188
FunctionEventInvokeConfig:
183189
from:
184190
operation: PutFunctionEventInvokeConfig
@@ -266,6 +272,10 @@ resources:
266272
resource: Function
267273
path: Spec.Name
268274
is_primary_key: true
275+
Qualifier:
276+
references:
277+
resource: Alias
278+
path: Spec.Name
269279
LayerVersion:
270280
fields:
271281
LayerName:
@@ -275,9 +285,23 @@ resources:
275285
is_required: true
276286
compare:
277287
is_ignored: true
288+
# PublishLayerVersion returns a LayerVersionContentOutput, which shares no
289+
# members with the LayerVersionContentInput held in the spec, so the
290+
# generated write-back replaces Spec.Content with an empty struct. That
291+
# would drop the s3BucketRef companion below, so ignore Content on the
292+
# response-consuming methods and keep the user's desired value.
278293
set:
279294
- ignore: true
280295
method: ReadOne
296+
- ignore: true
297+
method: Create
298+
- ignore: true
299+
method: Update
300+
Content.S3Bucket:
301+
references:
302+
resource: Bucket
303+
service_name: s3
304+
path: Spec.Name
281305
tags:
282306
ignore: true
283307
hooks:

apis/v1alpha1/types.go

Lines changed: 6 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apis/v1alpha1/zz_generated.deepcopy.go

Lines changed: 26 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

config/crd/bases/lambda.services.k8s.aws_aliases.yaml

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,23 @@ spec:
139139
140140
Regex Pattern: `^(\$LATEST(\.PUBLISHED)?|[0-9]+)$`
141141
type: string
142+
functionVersionRef:
143+
description: "AWSResourceReferenceWrapper provides a wrapper around
144+
*AWSResourceReference\ntype to provide more user friendly syntax
145+
for references using 'from' field\nEx:\nAPIIDRef:\n\n\tfrom:\n\t
146+
\ name: my-api"
147+
properties:
148+
from:
149+
description: |-
150+
AWSResourceReference provides all the values necessary to reference another
151+
k8s resource for finding the identifier(Id/ARN/Name)
152+
properties:
153+
name:
154+
type: string
155+
namespace:
156+
type: string
157+
type: object
158+
type: object
142159
name:
143160
description: |-
144161
The name of the alias.
@@ -200,7 +217,6 @@ spec:
200217
type: object
201218
type: object
202219
required:
203-
- functionVersion
204220
- name
205221
type: object
206222
status:

config/crd/bases/lambda.services.k8s.aws_functions.yaml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,25 @@ spec:
286286
type: string
287287
type: object
288288
type: object
289+
layerRefs:
290+
items:
291+
description: "AWSResourceReferenceWrapper provides a wrapper around
292+
*AWSResourceReference\ntype to provide more user friendly syntax
293+
for references using 'from' field\nEx:\nAPIIDRef:\n\n\tfrom:\n\t
294+
\ name: my-api"
295+
properties:
296+
from:
297+
description: |-
298+
AWSResourceReference provides all the values necessary to reference another
299+
k8s resource for finding the identifier(Id/ARN/Name)
300+
properties:
301+
name:
302+
type: string
303+
namespace:
304+
type: string
305+
type: object
306+
type: object
307+
type: array
289308
layers:
290309
description: |-
291310
A list of function layers (https://docs.aws.amazon.com/lambda/latest/dg/configuration-layers.html)

config/crd/bases/lambda.services.k8s.aws_functionurlconfigs.yaml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,23 @@ spec:
116116
117117
Regex Pattern: `^((?!^\d+$)^[0-9a-zA-Z-_]+$)$`
118118
type: string
119+
qualifierRef:
120+
description: "AWSResourceReferenceWrapper provides a wrapper around
121+
*AWSResourceReference\ntype to provide more user friendly syntax
122+
for references using 'from' field\nEx:\nAPIIDRef:\n\n\tfrom:\n\t
123+
\ name: my-api"
124+
properties:
125+
from:
126+
description: |-
127+
AWSResourceReference provides all the values necessary to reference another
128+
k8s resource for finding the identifier(Id/ARN/Name)
129+
properties:
130+
name:
131+
type: string
132+
namespace:
133+
type: string
134+
type: object
135+
type: object
119136
required:
120137
- authType
121138
type: object

0 commit comments

Comments
 (0)