[Draft] Implement remote fetch, carvel publish, and hook synthesis for carvel tiles - #672
[Draft] Implement remote fetch, carvel publish, and hook synthesis for carvel tiles#672ichandrabhatta wants to merge 4 commits into
Conversation
… tiles - Add --from-lockfile, --skip-fetch, and --releases-directory flags to carvel bake - Modify DownloadRelease to check local cache before fetching - Update update-release to append new releases to Kilnfile.lock - Allow carvel upload and publish to coexist with additional_releases - Add skip-fetch flag to carvel upload and publish commands - Fix code review findings on carvel remote-fetch/publish/hook-synthesis work - Fix additional_releases cache/skip-fetch lookup for s3/artifactory sources Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
Pull request overview
Extends the Carvel tile workflow to match classic kiln bake behavior by enabling remote fetching of additional_releases via Kilnfile/Kilnfile.lock, synthesizing pre/post-install hook adapter jobs into the tile’s generated BOSH release, and updating carvel upload/publish/rebake to rely on name-based lockfile resolution and reproducible cached release artifacts.
Changes:
- Add
additional_releases,pre_install_hooks, andpost_install_hooksto Carvelbase.ymlmetadata and thread these into runtime-config/release generation. - Implement additional release fetching (with cache-hit +
--skip-fetchbehavior) and hook job synthesis into the tile’s own release. - Update Carvel commands and lockfile handling to support name-based lookups and lockfile upserts/preservation.
Reviewed changes
Copilot reviewed 19 out of 21 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| pr_description.md | Adds a repo-stored PR/feature description for the Carvel enhancements. |
| pkg/cargo/kilnfile.go | Changes lockfile update helper to pointer receiver and “upsert” semantics. |
| pkg/cargo/kilnfile_test.go | Updates unit tests to reflect new lockfile upsert behavior. |
| internal/commands/update_release.go | Adjusts update-release to tolerate missing lock entries and improves error hinting. |
| internal/commands/update_release_test.go | Updates expectations for the revised update-release error behavior. |
| internal/commands/carvel_upload.go | Threads new bake options and improves Kilnfile/Kilnfile.lock loading behavior. |
| internal/commands/carvel_rebake.go | Switches to name-based lockfile lookup and updated baker signatures/options. |
| internal/commands/carvel_rebake_test.go | Updates tests for new baker method signatures. |
| internal/commands/carvel_publish.go | Uses name-based lockfile lookup and passes bake options through publish path. |
| internal/commands/carvel_publish_test.go | Updates tests for new baker method signatures. |
| internal/commands/carvel_helpers.go | Adds name-based release download and lockfile upsert/preservation logic. |
| internal/commands/carvel_helpers_test.go | Adds coverage to ensure lockfile writes preserve existing entries. |
| internal/commands/carvel_bake.go | Adds --releases-directory, --skip-fetch, and --from-lockfile flow + lockfile parse handling. |
| internal/commands/carvel_bake_test.go | Adds regression coverage for “lockfile present but not opted-in” and malformed lockfile failures. |
| internal/carvel/models/metadata.go | Extends metadata schema with additional_releases and hook declarations. |
| internal/carvel/models/job.go | Broadens job properties type to support non-package-install job payloads. |
| internal/carvel/baker.go | Implements hook synthesis, additional release fetch/caching logic, and new bake option plumbing. |
| internal/carvel/baker_test.go | Adds/updates tests for additional releases, hook synthesis, and properties passthrough. |
| internal/acceptance/carvel/carvel_workflow_test.go | Adjusts acceptance workflow assertions related to lockfile/CI bake behavior. |
Suppressed comments (1)
internal/commands/carvel_helpers.go:85
writeStandardKilnfileLocksilently ignores all read errors fromos.ReadFile(not just “file does not exist”). This can cause data loss by overwriting a lockfile that was unreadable due to permissions/IO errors. Only ignoreIsNotExist; return an error for other failures.
if data, err := os.ReadFile(lockfilePath); err == nil {
if err := yaml.Unmarshal(data, &lock); err != nil {
return fmt.Errorf("failed to parse existing Kilnfile.lock: %w", err)
}
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- pkg/cargo/kilnfile.go: UpdateBOSHReleaseTarballLockWithName now forces
lock.Name to match the name argument (and rejects an empty name),
instead of trusting the caller's lock.Name — a mismatch could silently
insert/rename the wrong entry.
- internal/commands/carvel_helpers.go: downloadCarvelRelease reuses
cargo.KilnfileLock.FindBOSHReleaseWithName instead of re-implementing
the same linear scan. writeStandardKilnfileLock only tolerates a
missing lockfile (os.IsNotExist) now — a permissions/IO error used to
be silently treated the same as "no lockfile", risking overwriting one
we simply failed to read. It also reuses
UpdateBOSHReleaseTarballLockWithName instead of duplicating the upsert
loop.
- internal/commands/carvel_upload.go: no longer misattributes a
Kilnfile-side load failure (e.g. an unresolved variable(...) call) to
Kilnfile.lock — checks the Kilnfile in isolation first.
- internal/commands/carvel_bake.go: corrected a log message that said
"No Kilnfile/Kilnfile.lock found" when only the lockfile's absence is
actually confirmed.
- internal/carvel/baker.go:
- generateRuntimeConfigs's hook validation error is now mode-specific
(pre-install/post-install), matching generateBoshReleaseDir's error,
via a shared hookModeGroups() helper.
- The synthesized hook script no longer execs the raw base.yml command
string unquoted. shellQuoteCommand splits it into words and
single-quotes each one before rejoining, so shell metacharacters in a
hook command can't be interpreted by the generated script.
- internal/acceptance/carvel/carvel_workflow_test.go: Step 3 was missing
--from-lockfile, so it silently baked from source instead of
exercising the Artifactory-download path it's supposed to test; its
GetCount assertion was commented out (referencing an undeclared
variable) rather than fixed. Added --from-lockfile and restored a
working assertion.
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 19 out of 21 changed files in this pull request and generated no new comments.
Suppressed comments (5)
internal/carvel/baker.go:866
- Same as generateBoshReleaseDir: runtime-config hook validation allows whitespace-only name/command, which will later produce invalid job references or broken hook scripts. Use TrimSpace() in the validation guard.
for _, group := range b.hookModeGroups() {
for _, hook := range group.hooks {
if hook.Name == "" || hook.Command == "" {
return fmt.Errorf("%s hook declaration missing name or command", group.mode)
}
addonJobs = append(addonJobs, models.Job{Name: b.hookJobName(hook.Name), Release: b.metadata.Name})
internal/carvel/baker.go:541
- Hook validation treats whitespace-only name/command as valid (e.g.
name: " "), which will generate an invalid BOSH job or anexecline with no command. Trim whitespace before validating so these fail fast with the intended error.
This issue also appears on line 861 of the same file.
for _, group := range b.hookModeGroups() {
for _, hook := range group.hooks {
if hook.Name == "" || hook.Command == "" {
return fmt.Errorf("%s hook declaration missing name or command", group.mode)
}
internal/carvel/baker.go:976
- This progress message claims the local tarball has a “correct SHA1”, but the code also treats
lockEntry.SHA1 == ""as a cache hit (no verification). Adjust the message so it stays accurate when the lockfile doesn’t include a SHA1.
actualSHA1 := hex.EncodeToString(h.Sum(nil))
if lockEntry.SHA1 == "" || actualSHA1 == lockEntry.SHA1 {
b.progress(fmt.Sprintf(" Release %s %s already exists locally with correct SHA1 — skipping fetch", lockEntry.Name, lockEntry.Version))
f.Close()
internal/commands/carvel_rebake.go:138
- carvel rebake hard-codes
ReleasesDirectory: "releases", which is interpreted relative to the current working directory. That can unexpectedly download additional_releases into whatever directory the user ran the command from and leave artifacts behind. Prefer making it relative to the tile source directory (or a temp dir) so rebake is self-contained.
err = b.BakeFromLockfile(sourcePath, kilnfile, kilnfileLock, releaseLock, localTarball, carvel.BakeOptions{
SkipFetch: false,
ReleasesDirectory: "releases",
})
} else {
c.outLogger.Printf("Re-baking Carvel tile from %s", sourcePath)
err = b.Bake(sourcePath, kilnfile, kilnfileLock, carvel.BakeOptions{
SkipFetch: false,
ReleasesDirectory: "releases",
})
internal/commands/carvel_bake.go:83
- When LoadKilnfiles fails and Kilnfile.lock is absent, the code assumes the lockfile is the problem and proceeds. If a Kilnfile exists but is malformed/unparseable, this path will silently ignore the Kilnfile error and bake anyway (until additional_releases are needed). Consider explicitly validating Kilnfile when it exists so genuine Kilnfile parse errors aren’t swallowed.
kf, kl, loadErr := c.Options.LoadKilnfiles(nil, nil)
if loadErr == nil {
kilnfile = kf
kilnfileLock = kl
} else if lockfilePresent {
return fmt.Errorf("failed to load Kilnfiles: %w", loadErr)
} else if c.Options.FromLockfile {
return fmt.Errorf("failed to load Kilnfiles (required for --from-lockfile): %w", loadErr)
} else {
// lockfilePresent is false here, so the lockfile is confirmed absent
// — that's what actually blocks additional_releases/--from-lockfile,
// regardless of whether a Kilnfile exists on its own (LoadKilnfiles
// always requires both, so we can't tell from loadErr alone).
c.outLogger.Printf("No Kilnfile.lock found — proceeding without additional_releases support")
}
Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 19 out of 21 changed files in this pull request and generated 1 comment.
Suppressed comments (4)
internal/carvel/baker.go:934
- Same as above: runtime-config hook validation should treat whitespace-only hook.Name/hook.Command as missing to avoid emitting an addon job referencing a synthesized hook with an empty exec command.
for _, group := range b.hookModeGroups() {
for _, hook := range group.hooks {
if hook.Name == "" || hook.Command == "" {
return fmt.Errorf("%s hook declaration missing name or command", group.mode)
}
internal/commands/carvel_helpers_test.go:68
- This test relies on chmod(0000) making the lockfile unreadable. On platforms/filesystems where chmod does not remove read permissions (e.g. Windows), the test will fail because writeStandardKilnfileLock can still read the file. Add a post-chmod read probe and skip the test if the file remains readable.
require.NoError(t, os.WriteFile(lockfilePath, data, 0644))
require.NoError(t, os.Chmod(lockfilePath, 0000))
defer func() { _ = os.Chmod(lockfilePath, 0644) }()
internal/commands/carvel_bake.go:82
- This log message says the bake is proceeding "without additional_releases support", but the bake will still attempt to process additional_releases from base.yml and then fail later with a lockfile-not-found error. Consider making the message explicit that additional_releases/--from-lockfile require a lockfile, and that the command is baking from source.
// lockfilePresent is false here, so the lockfile is confirmed absent
// — that's what actually blocks additional_releases/--from-lockfile,
// regardless of whether a Kilnfile exists on its own (LoadKilnfiles
// always requires both, so we can't tell from loadErr alone).
c.outLogger.Printf("No Kilnfile.lock found — proceeding without additional_releases support")
}
internal/carvel/baker.go:589
- Hook validation currently treats whitespace-only values as non-empty, which can generate a hook script with an empty
execcommand. Trim whitespace before validating hook.Name/hook.Command.
This issue also appears on line 930 of the same file.
for _, group := range b.hookModeGroups() {
for _, hook := range group.hooks {
if hook.Name == "" || hook.Command == "" {
return fmt.Errorf("%s hook declaration missing name or command", group.mode)
}
| Expect(err.Error()).To(ContainSubstring("missing required field 'name'")) | ||
| }) |
Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 19 out of 22 changed files in this pull request and generated no new comments.
Suppressed comments (2)
internal/commands/carvel_rebake.go:132
- In the lockfile rebake path, BakeFromLockfile is configured to download additional releases into a hard-coded relative "releases" directory. That can create/modify a ./releases directory in the caller’s working directory (and potentially dirty the repo) even though rebake already has a temp dir available. Use the existing tmpDir as the releases download/cache directory for this run.
err = b.BakeFromLockfile(sourcePath, kilnfile, kilnfileLock, releaseLock, localTarball, carvel.BakeOptions{
SkipFetch: false,
ReleasesDirectory: "releases",
})
internal/commands/update_release.go:124
- Don’t ignore the return value from UpdateBOSHReleaseTarballLockWithName. The method can fail (e.g., empty name), and ignoring it would silently write an unchanged/incorrect Kilnfile.lock. Handle the error and surface it to the user.
_ = (&kilnfileLock).UpdateBOSHReleaseTarballLockWithName(u.Options.Name, releaseLock)
err = u.Options.SaveKilnfileLock(u.filesystem, kilnfileLock)
Summary
Extends
kiln carvel bakeso Carvel tiles can declare third-party BOSH releases (additional_releases) fetched from real remote sources viaKilnfile/Kilnfile.lock— instead of requiring pre-staged local tarballs — and auto-synthesizes pre/post-install hook adapter jobs directly into the tile's own release. Also bringscarvel upload/publish/rebakeup to the same standard as classickiln bake: a cached, checksummed release in Artifactory, a reproducible.pivotalbuilt from it, and a bake record forrebaketo verify against later.Key Features
Remote fetch for
additional_releasesadditional_releases:entries are now resolved viaKilnfile/Kilnfile.lock'srelease_sources:(fetchAdditionalReleases), reusing the samecomponent.NewReleaseSourceRepo/DownloadReleaseprimitive classickiln bakealready uses — not a localtarball_path:anymore.carvel bakeflags:--releases-directory(defaultreleases),--skip-fetch(use whatever's already staged there),--from-lockfile(explicit opt-in to bake the tile's own release from a cached copy instead of regenerating it — see bug fix below for why this is now explicit).Auto-synthesis of pre/post-install hooks
pre_install_hooks:/post_install_hooks:inbase.ymlget a BOSH job auto-generated into the tile's own release (auto-prefixed with the tile's name to avoid collisions), depositing a single script atbin/hooks/{pre,post}-installthatexecs the declared command.carvel upload/publish/rebake+ bake recordscarvel uploadbuilds the tile's own release, uploads it to Artifactory, and records it inKilnfile.lock.carvel publishdownloads that cached release (by name, not by list position) and bakes the final.pivotal;--finalwrites abake_records/<version>.json(git SHA + tile SHA256 + version).carvel rebakereproduces a past release from its bake record and verifies the checksum matches.Kilnfile.lock's upsert logic (writeStandardKilnfileLock/UpdateBOSHReleaseTarballLockWithName) now merges by release name instead of overwriting the whole file — the tile's own release and itsadditional_releasesentries coexist in one lockfile.Bug Fixes / Robustness (found and fixed during review)
carvel bake's mode dispatch used to switch to "bake from cached copy" purely becauseKilnfile.lockexisted on disk — silently skipping hook/registry-data synthesis. Now gated behind explicit--from-lockfile.downloadCarvelRelease/generateRuntimeConfigs's cache/lock lookups used to assume "the tile's own release" was alwaysReleases[0]— broke the momentadditional_releasesshared the same lockfile. Now resolved by name everywhere.Kilnfile.lockused to be silently swallowed (falls back to an empty lock, surfaces a confusing unrelated error later); now hard-fails immediately, while still tolerating a genuinely absent lockfile.additional_releasescached-file lookup assumed every release source names its downloaded tarball<name>-<version>.tgz;s3/artifactorysources actually writefilepath.Base(RemotePath)(e.g.smoke-tests-4.12.14-ubuntu-jammy-1.1298.tgz), which broke--skip-fetchand the cache-hit check for those sources. Now resolves the real source type before deciding the filename convention.bakepath;publish/rebake(viaBakeFromLockfile) skipped it. Now enforced on both.update-release(the "-release/-boshrelease suffix" hint) and fixed a stale test that had silently stopped asserting on it.go vet/go testoutright.Known residual gap (documented, not fixed here)
publish/rebakerebuild the runtime-config's hook/job references from whateverbase.ymlcurrently says, independent of what's actually inside the cached release tarball built byupload. If hooks change betweenuploadandpublishwithout re-runningupload, the two can drift. Currently an operator contract ("re-runuploadif you change hooks"), not enforced in code.Technical Details
internal/carvel/models/metadata.go:AdditionalReleases,PreInstallHooks,PostInstallHooksfields onMetadata;AdditionalRelease/AdditionalJob/HookDeclarationstructs.internal/carvel/baker.go:fetchAdditionalReleases,hookJobName(shared prefix helper),additionalReleaseLocalFilename(source-type-aware cache filename resolution), hook synthesis ingenerateBoshReleaseDir, hook/additional-release wiring ingenerateRuntimeConfigs,BakeOptions(SkipFetch,ReleasesDirectory) threaded throughBake/BakeFromLockfile.internal/commands/carvel_bake.go:--releases-directory/--skip-fetch/--from-lockfileflags; Kilnfile/Kilnfile.lock load-error handling that distinguishes "missing" from "malformed".internal/commands/carvel_helpers.go:downloadCarvelRelease/writeStandardKilnfileLockresolve by release name, not list position; upsert instead of overwrite.internal/commands/carvel_upload.go/carvel_publish.go/carvel_rebake.go: updated to the newBakeOptions/name-based lookup plumbing.internal/commands/update_release.go,pkg/cargo/kilnfile.go: minor fixes described above.internal/carvel/baker_test.go,internal/commands/carvel_bake_test.go,internal/commands/carvel_helpers_test.go,pkg/cargo/kilnfile_test.go— coverage for hook synthesis, remote-fetch/cache-hit behavior (including the source-type filename mismatch), lockfile-preservation, and the restored diagnostics.