Skip to content

Commit b32665e

Browse files
authored
feat(carvel): emit cross-deployment consumes on runtime config addon job (#666)
## Summary - Extends the \`job-spec-overlay\` sidecar format with two optional fields: \`from\` and \`deployment\` - When set on a \`boshLinkConsumer\` entry, \`generateRuntimeConfigs()\` now includes the link in the \`registry-data\` addon job's \`consumes:\` map using the BOSH cross-deployment link resolution schema - Extracts a \`readJobSpecOverlays()\` helper so overlay-reading is shared between \`generateBoshReleaseDir()\` and \`generateRuntimeConfigs()\` rather than duplicated ## Motivation Tile authors currently have to post-process the built \`.pivotal\` to inject a \`consumes:\` block into the runtime config addon job after \`kiln carvel bake\`. This change makes it possible to declare cross-deployment link resolution natively in the sidecar files, eliminating the need for that patch step. ## Example overlay ```yaml consumes: - name: nats-tls type: nats-tls optional: false from: nats-tls deployment: "(( ..cf.deployment_name ))" - name: binding_cache type: binding_cache optional: true from: binding_cache deployment: "(( ..cf.deployment_name ))" ``` Generated runtime config addon job will include: ```yaml consumes: nats-tls: from: nats-tls deployment: (( ..cf.deployment_name )) binding_cache: from: binding_cache deployment: (( ..cf.deployment_name )) ``` ## Test plan - [ ] \`go test ./internal/carvel/...\` passes (unit + integration tests) - [ ] New unit tests: \`jobSpecOverlay\` parses \`from\`/\`deployment\` fields correctly, including partial presence (only \`from\` set, only \`deployment\` set) - [ ] Updated integration test: \`addon.Jobs[0].Consumes["binding_cache"]\` has expected \`From\`/\`Deployment\` values in the generated runtime config - [ ] Entries without \`from\`/\`deployment\` are unaffected (no \`consumes:\` key emitted) 🤖 Generated with [Claude Code](https://claude.com/claude-code)
2 parents 6862b6f + 5971606 commit b32665e

4 files changed

Lines changed: 145 additions & 26 deletions

File tree

internal/carvel/baker.go

Lines changed: 75 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,7 @@ func (b *baker) progress(message string) {
257257

258258
// deduplicateConsumes removes duplicate BOSH link consumer entries by name.
259259
// Identical duplicates are dropped silently. If two entries share a name but
260-
// differ in type or optional, the first is kept and a WARNING is emitted —
260+
// differ in any field, the first is kept and a WARNING is emitted —
261261
// BOSH rejects duplicate link names in job.MF, so the second is always ignored.
262262
func (b *baker) deduplicateConsumes(consumes []boshLinkConsumer) []boshLinkConsumer {
263263
seen := make(map[string]boshLinkConsumer)
@@ -272,19 +272,59 @@ func (b *baker) deduplicateConsumes(consumes []boshLinkConsumer) []boshLinkConsu
272272
if existing != c {
273273
b.progress(fmt.Sprintf(
274274
"WARNING: duplicate BOSH link consumer name %q found across packageinstalls.\n"+
275-
" Keeping: {type: %s, optional: %v}\n"+
276-
" Ignoring: {type: %s, optional: %v}\n"+
275+
" Keeping: {type: %s, optional: %v, from: %s, deployment: %s}\n"+
276+
" Ignoring: {type: %s, optional: %v, from: %s, deployment: %s}\n"+
277277
" Ensure all packageinstalls agree on the link definition.",
278-
c.Name, existing.Type, existing.Optional, c.Type, c.Optional,
278+
c.Name,
279+
existing.Type, existing.Optional, existing.From, existing.Deployment,
280+
c.Type, c.Optional, c.From, c.Deployment,
279281
))
280282
}
281283
}
282284
return deduped
283285
}
284286

287+
// readJobSpecOverlays reads all *.job-spec-overlay.yml sidecars for the tile's
288+
// package installs and returns the merged slice of boshLinkConsumer entries.
289+
func (b *baker) readJobSpecOverlays() ([]boshLinkConsumer, error) {
290+
var all []boshLinkConsumer
291+
for _, entry := range b.metadata.PackageInstalls {
292+
entry = strings.Trim(entry, "$() ")
293+
entry = strings.TrimPrefix(entry, "package")
294+
entry = strings.Trim(entry, `"' `)
295+
296+
overlayPath := path.Join(b.source, "packageinstalls", entry+".job-spec-overlay.yml")
297+
data, err := os.ReadFile(overlayPath)
298+
if errors.Is(err, os.ErrNotExist) {
299+
continue
300+
}
301+
if err != nil {
302+
return nil, fmt.Errorf("reading %s: %w", overlayPath, err)
303+
}
304+
var overlay jobSpecOverlay
305+
if err := yaml.Unmarshal(data, &overlay); err != nil {
306+
return nil, fmt.Errorf("parsing %s: %w", overlayPath, err)
307+
}
308+
all = append(all, overlay.Consumes...)
309+
}
310+
return all, nil
311+
}
312+
285313
// boshLinkConsumer declares a BOSH link the registry-data job should consume.
286314
// Populated from per-packageinstall *.job-spec-overlay.yml sidecar files.
315+
// When From or Deployment is set, kiln also emits a cross-deployment consumes
316+
// entry for the link in the runtime config addon job.
287317
type boshLinkConsumer struct {
318+
Name string `yaml:"name"`
319+
Type string `yaml:"type"`
320+
Optional bool `yaml:"optional"`
321+
From string `yaml:"from,omitempty"`
322+
Deployment string `yaml:"deployment,omitempty"`
323+
}
324+
325+
// boshConsumes is the BOSH job spec consumes schema: name, type, and optional only.
326+
// From/Deployment are runtime-config-only and must not appear in the job spec.
327+
type boshConsumes struct {
288328
Name string `yaml:"name"`
289329
Type string `yaml:"type"`
290330
Optional bool `yaml:"optional"`
@@ -293,6 +333,8 @@ type boshLinkConsumer struct {
293333
// jobSpecOverlay is the schema for <entry>.job-spec-overlay.yml sidecar files.
294334
// kiln reads these from packageinstalls/ and merges the consumes entries into
295335
// the generated registry-data job.MF alongside the hardcoded cluster-info link.
336+
// Entries that set from or deployment are also emitted as cross-deployment
337+
// consumes on the runtime config addon job.
296338
type jobSpecOverlay struct {
297339
Consumes []boshLinkConsumer `yaml:"consumes"`
298340
}
@@ -355,7 +397,6 @@ files:
355397

356398
registryDataTemplates := ""
357399
registryDataProperties := ""
358-
var allConsumes []boshLinkConsumer
359400

360401
b.progress(" Configuring package installs")
361402
for _, entry := range b.metadata.PackageInstalls {
@@ -416,21 +457,6 @@ files:
416457
overlayContent = string(overlayData)
417458
}
418459

419-
// Read optional job-spec-overlay sidecar to discover additional BOSH link consumptions.
420-
jobSpecOverlayPath := path.Join(b.source, "packageinstalls", entry+".job-spec-overlay.yml")
421-
overlayData, overlayErr = os.ReadFile(jobSpecOverlayPath)
422-
if overlayErr != nil {
423-
if !errors.Is(overlayErr, os.ErrNotExist) {
424-
return overlayErr
425-
}
426-
} else {
427-
var overlay jobSpecOverlay
428-
if parseErr := yaml.Unmarshal(overlayData, &overlay); parseErr != nil {
429-
return fmt.Errorf("parsing %s: %w", jobSpecOverlayPath, parseErr)
430-
}
431-
allConsumes = append(allConsumes, overlay.Consumes...)
432-
}
433-
434460
manifestTemplate := generateManifestTemplate(entry, overlayContent)
435461

436462
err = os.WriteFile(
@@ -443,9 +469,17 @@ files:
443469
}
444470
}
445471

472+
allConsumes, err := b.readJobSpecOverlays()
473+
if err != nil {
474+
return err
475+
}
446476
deduped := b.deduplicateConsumes(allConsumes)
447477

448-
registryDataSpec, err := buildRegistryDataSpec(registryDataTemplates, registryDataProperties, deduped)
478+
boshLinks := make([]boshConsumes, len(deduped))
479+
for i, c := range deduped {
480+
boshLinks[i] = boshConsumes{Name: c.Name, Type: c.Type, Optional: c.Optional}
481+
}
482+
registryDataSpec, err := buildRegistryDataSpec(registryDataTemplates, registryDataProperties, boshLinks)
449483
if err != nil {
450484
return err
451485
}
@@ -461,7 +495,7 @@ files:
461495
// buildRegistryDataSpec constructs the job.MF content for the registry-data BOSH job.
462496
// It always includes the hardcoded cluster-info link and appends any additional links
463497
// collected from *.job-spec-overlay.yml sidecars in the packageinstalls/ directory.
464-
func buildRegistryDataSpec(templates, properties string, additionalLinks []boshLinkConsumer) (string, error) {
498+
func buildRegistryDataSpec(templates, properties string, additionalLinks []boshConsumes) (string, error) {
465499
extraLinks := ""
466500
if len(additionalLinks) > 0 {
467501
data, err := yaml.Marshal(additionalLinks)
@@ -717,11 +751,30 @@ func (b *baker) generateRuntimeConfigs() error {
717751
}
718752
}
719753

754+
allConsumes, err := b.readJobSpecOverlays()
755+
if err != nil {
756+
return err
757+
}
758+
deduped := b.deduplicateConsumes(allConsumes)
759+
760+
consumesMap := make(map[string]models.JobConsumes)
761+
for _, c := range deduped {
762+
if c.From != "" || c.Deployment != "" {
763+
consumesMap[c.Name] = models.JobConsumes{
764+
From: c.From,
765+
Deployment: c.Deployment,
766+
}
767+
}
768+
}
769+
720770
registryDataJob := models.Job{
721771
Name: "registry-data",
722772
Release: b.metadata.Name,
723773
Properties: registryDataProps,
724774
}
775+
if len(consumesMap) > 0 {
776+
registryDataJob.Consumes = consumesMap
777+
}
725778

726779
inner := models.RuntimeConfigInner{
727780
Releases: []string{

internal/carvel/baker_test.go

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ var _ = Describe("Carvel Baker", func() {
124124

125125
Context("buildRegistryDataSpec", func() {
126126
It("includes user-declared additional links after cluster-info", func() {
127-
links := []boshLinkConsumer{
127+
links := []boshConsumes{
128128
{Name: "binding_cache", Type: "binding_cache", Optional: false},
129129
}
130130
spec, err := buildRegistryDataSpec("", "", links)
@@ -136,7 +136,7 @@ var _ = Describe("Carvel Baker", func() {
136136
})
137137

138138
It("marks optional links correctly", func() {
139-
links := []boshLinkConsumer{
139+
links := []boshConsumes{
140140
{Name: "optional-link", Type: "some-type", Optional: true},
141141
}
142142
spec, err := buildRegistryDataSpec("", "", links)
@@ -154,7 +154,7 @@ var _ = Describe("Carvel Baker", func() {
154154
It("safely encodes link names containing YAML-special characters", func() {
155155
// yaml.Marshal quotes/blocks the value so it cannot inject extra YAML keys.
156156
// The real type field ("legit-type") must still appear at the correct level.
157-
links := []boshLinkConsumer{
157+
links := []boshConsumes{
158158
{Name: "name: injected\ntype: evil", Type: "legit-type", Optional: false},
159159
}
160160
spec, err := buildRegistryDataSpec("", "", links)
@@ -163,7 +163,7 @@ var _ = Describe("Carvel Baker", func() {
163163
})
164164

165165
It("emits each unique link name only once given pre-deduplicated input", func() {
166-
links := []boshLinkConsumer{
166+
links := []boshConsumes{
167167
{Name: "binding_cache", Type: "binding_cache", Optional: false},
168168
}
169169
spec, err := buildRegistryDataSpec("", "", links)
@@ -280,6 +280,55 @@ consumes:
280280
err := yaml.Unmarshal([]byte("consumes: [\ninvalid"), &overlay)
281281
Expect(err).To(HaveOccurred())
282282
})
283+
284+
It("parses an entry with only from set (no deployment)", func() {
285+
content := `
286+
consumes:
287+
- name: nats-tls
288+
type: nats-tls
289+
optional: false
290+
from: nats-tls
291+
`
292+
var overlay jobSpecOverlay
293+
err := yaml.Unmarshal([]byte(content), &overlay)
294+
Expect(err).NotTo(HaveOccurred())
295+
Expect(overlay.Consumes[0].From).To(Equal("nats-tls"))
296+
Expect(overlay.Consumes[0].Deployment).To(BeEmpty())
297+
})
298+
299+
It("parses an entry with only deployment set (no from)", func() {
300+
content := `
301+
consumes:
302+
- name: nats-tls
303+
type: nats-tls
304+
optional: false
305+
deployment: "(( ..cf.deployment_name ))"
306+
`
307+
var overlay jobSpecOverlay
308+
err := yaml.Unmarshal([]byte(content), &overlay)
309+
Expect(err).NotTo(HaveOccurred())
310+
Expect(overlay.Consumes[0].From).To(BeEmpty())
311+
Expect(overlay.Consumes[0].Deployment).To(Equal("(( ..cf.deployment_name ))"))
312+
})
313+
314+
It("parses from and deployment fields for cross-deployment link resolution", func() {
315+
content := `
316+
consumes:
317+
- name: nats-tls
318+
type: nats-tls
319+
optional: false
320+
from: nats-tls
321+
deployment: "(( ..cf.deployment_name ))"
322+
`
323+
var overlay jobSpecOverlay
324+
err := yaml.Unmarshal([]byte(content), &overlay)
325+
Expect(err).NotTo(HaveOccurred())
326+
Expect(overlay.Consumes).To(HaveLen(1))
327+
c := overlay.Consumes[0]
328+
Expect(c.Name).To(Equal("nats-tls"))
329+
Expect(c.From).To(Equal("nats-tls"))
330+
Expect(c.Deployment).To(Equal("(( ..cf.deployment_name ))"))
331+
})
283332
})
284333

285334
Context("Bake", func() {
@@ -455,6 +504,12 @@ consumes:
455504
props := addon.Jobs[0].Properties["test-install"]
456505
Expect(props.Name).To(Equal("something-test.tanzu.vmware.com"))
457506
Expect(props.Version).To(Equal("0.1.5"))
507+
508+
By("emitting cross-deployment consumes from job-spec-overlay from/deployment fields")
509+
Expect(addon.Jobs[0].Consumes).To(HaveKey("binding_cache"))
510+
bc := addon.Jobs[0].Consumes["binding_cache"]
511+
Expect(bc.From).To(Equal("binding_cache"))
512+
Expect(bc.Deployment).To(Equal("(( ..cf.deployment_name ))"))
458513
})
459514
It("can be kiln baked", func() {
460515
if !kilnInstalled() {

internal/carvel/models/job.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,14 @@ package models
33
type Job struct {
44
Name string `yaml:"name"`
55
Release string `yaml:"release"`
6+
Consumes map[string]JobConsumes `yaml:"consumes,omitempty"`
67
Properties map[string]PackageInstallProps `yaml:"properties,omitempty"`
78
}
9+
10+
// JobConsumes declares cross-deployment link resolution for a runtime config addon job.
11+
// When a job-spec-overlay entry sets from or deployment, kiln includes it in the
12+
// addon job's consumes map in the generated metadata.
13+
type JobConsumes struct {
14+
From string `yaml:"from,omitempty"`
15+
Deployment string `yaml:"deployment,omitempty"`
16+
}

internal/carvel/testdata/sample-tile/packageinstalls/test-install.job-spec-overlay.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,5 @@ consumes:
22
- name: binding_cache
33
type: binding_cache
44
optional: false
5+
from: binding_cache
6+
deployment: "(( ..cf.deployment_name ))"

0 commit comments

Comments
 (0)