Skip to content

Commit da7e134

Browse files
committed
Spec complete implementation of kiln carvel commands
For more details: https://docs.google.com/document/d/1Hf_T721E18fFDVTrqNZE4m_udo3eSW30VoUU1Zrxl-A/edit?tab=t.0 ai-assisted=true
1 parent c6eab05 commit da7e134

15 files changed

Lines changed: 1243 additions & 15 deletions

File tree

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
.boshrelease
2-
.ezbake
2+
.carvel-tile
33

internal/carvel/TODO.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Carvel Package TODOs
2+
3+
## Refactor: Replace exec.Command("kiln bake") with internal function call
4+
5+
**File:** `baker.go` (line 46)
6+
7+
**Current behavior:** `KilnBake()` shells out to the system `kiln` binary via
8+
`exec.Command("kiln", "bake", "--skip-fetch", "--output-file", destination)`.
9+
10+
**Problem:**
11+
- The inner `kiln bake` resolves to whatever binary is on PATH, which may be a
12+
different version than the running `kiln carvel bake`.
13+
- Integration tests must manipulate PATH to ensure the correct binary is used.
14+
- Spawning a subprocess for logic that exists in the same codebase is unnecessary
15+
overhead.
16+
- Error propagation across the process boundary is lossy.
17+
18+
**Desired behavior:** `KilnBake()` should call the internal bake logic directly
19+
(e.g. instantiate and invoke `commands.Bake` or the underlying `BakeService`)
20+
instead of shelling out. This guarantees version consistency, improves
21+
testability, and removes the PATH dependency.
22+
23+
**Complexity note:** The `Bake` command has a non-trivial setup (BakeService,
24+
fetchers, template evaluators, checksummers). The wiring will need to be
25+
extracted into a reusable helper or the relevant subset of bake logic factored
26+
out for in-process use.

internal/carvel/baker.go

Lines changed: 145 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,33 +21,44 @@ import (
2121
// and kiln-compatible tile structure that can be baked into a .pivotal file.
2222
type Baker interface {
2323
Bake(source string) error
24+
BakeFromLockfile(source string, lockfilePath string) error
2425
KilnBake(destination string) error
2526
GetName() string
2627
GetVersion() (string, error)
28+
GetReleaseTarball() (string, error)
2729
SetWriter(w io.Writer)
30+
SetProgressWriter(w io.Writer)
2831
}
2932

3033
// NewBaker creates a new Baker for transforming imgpkg bundles into BOSH releases.
3134
func NewBaker() Baker {
3235
return &baker{
33-
writer: io.Discard,
36+
writer: io.Discard,
37+
progressWriter: io.Discard,
3438
}
3539
}
3640

3741
type baker struct {
3842
metadata models.Metadata
3943
source, destination string
4044
writer io.Writer
45+
progressWriter io.Writer
4146
}
4247

4348
func (b *baker) KilnBake(destination string) error {
49+
if err := b.ensureGitRepo(); err != nil {
50+
return fmt.Errorf("failed to initialize git repo for kiln bake: %w", err)
51+
}
52+
53+
b.progress("Assembling final .pivotal file...")
4454
cmd := exec.Command("kiln",
4555
"bake",
4656
"--skip-fetch",
4757
"--output-file", destination,
4858
)
4959
cmd.Dir = b.destination
5060
out, err := cmd.CombinedOutput()
61+
b.log(string(out))
5162
if err != nil {
5263
b.log("failed to invoke kiln: " + string(out))
5364
return err
@@ -56,10 +67,29 @@ func (b *baker) KilnBake(destination string) error {
5667
return nil
5768
}
5869

70+
// ensureGitRepo initializes a git repo with an empty commit in the
71+
// generated tile directory so that `kiln bake` (which runs git status
72+
// and git rev-parse HEAD) can operate on it without failing.
73+
func (b *baker) ensureGitRepo() error {
74+
commands := []*exec.Cmd{
75+
exec.Command("git", "init"),
76+
exec.Command("git", "commit", "--allow-empty", "-m", "carvel tile build"),
77+
}
78+
for _, cmd := range commands {
79+
cmd.Dir = b.destination
80+
out, err := cmd.CombinedOutput()
81+
if err != nil {
82+
return fmt.Errorf("command %q failed: %s: %w", cmd.String(), string(out), err)
83+
}
84+
}
85+
return nil
86+
}
87+
5988
func (b *baker) Bake(source string) error {
6089
b.source = source
61-
b.destination = path.Join(source, ".ezbake")
90+
b.destination = path.Join(source, ".carvel-tile")
6291

92+
b.progress("Reading tile metadata from " + path.Join(source, "base.yml"))
6393
yamlPath := path.Join(source, "base.yml")
6494
yamlData, err := os.ReadFile(yamlPath)
6595
if err != nil {
@@ -71,10 +101,11 @@ func (b *baker) Bake(source string) error {
71101
return err
72102
}
73103

74-
_, err = b.GetVersion()
104+
ver, err := b.GetVersion()
75105
if err != nil {
76106
return err
77107
}
108+
b.progress(fmt.Sprintf("Tile: %s version %s (metadata_version %s)", b.metadata.Name, ver, b.metadata.MetadataVersion))
78109

79110
metadataVersion, err := version.NewVersion(b.metadata.MetadataVersion)
80111
if err != nil {
@@ -85,12 +116,14 @@ func (b *baker) Bake(source string) error {
85116
return errors.New("tile metadata_version too old for kubernetes support (must be >=3.2.0)")
86117
}
87118

119+
b.progress("Generating BOSH release structure...")
88120
err = b.generateBoshReleaseDir()
89121
if err != nil {
90122
b.log(err.Error())
91123
return err
92124
}
93125

126+
b.progress("Generating tile layout in " + b.destination)
94127
err = b.generateOutputTile()
95128
if err != nil {
96129
b.log(err.Error())
@@ -100,6 +133,100 @@ func (b *baker) Bake(source string) error {
100133
return nil
101134
}
102135

136+
func (b *baker) BakeFromLockfile(source string, lockfilePath string) error {
137+
b.source = source
138+
b.destination = path.Join(source, ".carvel-tile")
139+
140+
b.progress("Reading tile metadata from " + path.Join(source, "base.yml"))
141+
yamlPath := path.Join(source, "base.yml")
142+
yamlData, err := os.ReadFile(yamlPath)
143+
if err != nil {
144+
return err
145+
}
146+
147+
err = yaml.Unmarshal(yamlData, &b.metadata)
148+
if err != nil {
149+
return err
150+
}
151+
152+
ver, err := b.GetVersion()
153+
if err != nil {
154+
return err
155+
}
156+
b.progress(fmt.Sprintf("Tile: %s version %s (metadata_version %s)", b.metadata.Name, ver, b.metadata.MetadataVersion))
157+
158+
b.progress("Reading lockfile from " + lockfilePath)
159+
lf, err := models.ReadCarvelLockfile(lockfilePath)
160+
if err != nil {
161+
return fmt.Errorf("failed to read lockfile: %w", err)
162+
}
163+
164+
if lf.Release.Name != b.metadata.Name {
165+
return fmt.Errorf("lockfile release name %q does not match tile name %q", lf.Release.Name, b.metadata.Name)
166+
}
167+
168+
err = os.RemoveAll(b.destination)
169+
if err != nil {
170+
return err
171+
}
172+
err = os.MkdirAll(b.destination, 0755)
173+
if err != nil {
174+
return err
175+
}
176+
177+
b.progress("Generating tile layout in " + b.destination)
178+
err = b.generateBaseYaml()
179+
if err != nil {
180+
return err
181+
}
182+
err = b.copyFiles()
183+
if err != nil {
184+
return err
185+
}
186+
err = b.generateJobFiles()
187+
if err != nil {
188+
return err
189+
}
190+
err = b.generateInstanceGroupFiles()
191+
if err != nil {
192+
return err
193+
}
194+
err = b.generateRuntimeConfigs()
195+
if err != nil {
196+
return err
197+
}
198+
199+
releasesDir := path.Join(b.destination, "releases")
200+
err = os.MkdirAll(releasesDir, 0755)
201+
if err != nil {
202+
return err
203+
}
204+
205+
cachedTarball := lf.Release.RemotePath
206+
destTarball := path.Join(releasesDir, b.metadata.Name+"-"+ver+".tgz")
207+
208+
b.progress("Copying cached BOSH release from " + cachedTarball)
209+
b.log("copying cached BOSH release from " + cachedTarball)
210+
err = copyFileContents(cachedTarball, destTarball)
211+
if err != nil {
212+
return fmt.Errorf("failed to copy cached release tarball: %w", err)
213+
}
214+
215+
return nil
216+
}
217+
218+
func (b *baker) GetReleaseTarball() (string, error) {
219+
ver, err := b.GetVersion()
220+
if err != nil {
221+
return "", err
222+
}
223+
tarball := path.Join(b.destination, "releases", b.metadata.Name+"-"+ver+".tgz")
224+
if _, err := os.Stat(tarball); err != nil {
225+
return "", fmt.Errorf("release tarball not found at %s: %w", tarball, err)
226+
}
227+
return tarball, nil
228+
}
229+
103230
func (b *baker) GetName() string {
104231
return b.metadata.Name
105232
}
@@ -122,18 +249,26 @@ func (b *baker) SetWriter(w io.Writer) {
122249
b.writer = w
123250
}
124251

252+
func (b *baker) SetProgressWriter(w io.Writer) {
253+
b.progressWriter = w
254+
}
255+
125256
func (b *baker) log(message string) {
126257
_, _ = fmt.Fprintln(b.writer, message)
127258
}
128259

260+
func (b *baker) progress(message string) {
261+
_, _ = fmt.Fprintln(b.progressWriter, message)
262+
}
263+
129264
func (b *baker) generateBoshReleaseDir() error {
130265
dirName := path.Join(b.source, ".boshrelease")
131-
// first clean out any previous bosh release directory
132266
err := os.RemoveAll(dirName)
133267
if err != nil {
134268
return err
135269
}
136270

271+
b.progress(" Initializing BOSH release")
137272
commands := []*exec.Cmd{
138273
exec.Command("bosh", "init-release", "--dir="+dirName),
139274
exec.Command("bosh", "add-blob", "--dir="+dirName, path.Join(b.source, "bundle.tar"), "imgpkg/bundle.tar"),
@@ -173,12 +308,13 @@ files:
173308
registryDataTemplates := ""
174309
registryDataProperties := ""
175310

176-
// we need one PackageInstall YAML manifest for each entry in the metadata.
311+
b.progress(" Configuring package installs")
177312
for _, entry := range b.metadata.PackageInstalls {
178313
entry = strings.Trim(entry, "$() ")
179314
entry = strings.TrimPrefix(entry, "package")
180315
entry = strings.Trim(entry, `"' `)
181316

317+
b.progress(" - " + entry)
182318
b.log("looking for package install: " + entry)
183319

184320
// find this entry in the packageinstalls directory
@@ -320,8 +456,6 @@ spec:
320456
}
321457

322458
func (b *baker) generateOutputTile() error {
323-
// first clean out any previous tile directory
324-
// Note: this directory should only ever contain generated files, which we are about to regenerate.
325459
err := os.RemoveAll(b.destination)
326460
if err != nil {
327461
return err
@@ -332,11 +466,13 @@ func (b *baker) generateOutputTile() error {
332466
return err
333467
}
334468

469+
b.progress(" Generating base.yml")
335470
err = b.generateBaseYaml()
336471
if err != nil {
337472
return err
338473
}
339474

475+
b.progress(" Copying forms, properties, and static assets")
340476
err = b.copyFiles()
341477
if err != nil {
342478
return err
@@ -352,11 +488,13 @@ func (b *baker) generateOutputTile() error {
352488
return err
353489
}
354490

491+
b.progress(" Generating runtime configs")
355492
err = b.generateRuntimeConfigs()
356493
if err != nil {
357494
return err
358495
}
359496

497+
b.progress(" Creating BOSH release tarball (this may take a while)...")
360498
err = b.createBoshRelease()
361499
if err != nil {
362500
return err

0 commit comments

Comments
 (0)