Skip to content

Commit 5f1c83e

Browse files
authored
Merge pull request #484 from GSA-TTS/add-lifecycle-to-manifest
Add lifecycle to manifest
2 parents 808c011 + cba6846 commit 5f1c83e

File tree

4 files changed

+279
-21
lines changed

4 files changed

+279
-21
lines changed

operation/manifest.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,14 @@ const (
2727
TCP AppRouteProtocol = "tcp"
2828
)
2929

30+
type AppLifecycle string
31+
32+
const (
33+
Buildpack AppLifecycle = "buildpack"
34+
Docker AppLifecycle = "docker"
35+
CNB AppLifecycle = "cnb"
36+
)
37+
3038
type Manifest struct {
3139
Version string `yaml:"version,omitempty"`
3240
Applications []*AppManifest `yaml:"applications"`
@@ -37,6 +45,7 @@ type AppManifest struct {
3745
Path string `yaml:"path,omitempty"`
3846
Buildpacks []string `yaml:"buildpacks,omitempty"`
3947
Docker *AppManifestDocker `yaml:"docker,omitempty"`
48+
Lifecycle AppLifecycle `yaml:"lifecycle,omitempty"`
4049
Env map[string]string `yaml:"env,omitempty"`
4150
RandomRoute bool `yaml:"random-route,omitempty"`
4251
NoRoute bool `yaml:"no-route,omitempty"`

operation/manifest_test.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,3 +140,103 @@ func TestManifestUnMarshalling(t *testing.T) {
140140
require.Equal(t, 1, len(m.Applications))
141141
require.Equal(t, 2, len(*m.Applications[0].Services))
142142
}
143+
144+
func TestManifestLifecycle(t *testing.T) {
145+
t.Run("Marshall with buildpack lifecycle", func(t *testing.T) {
146+
m := &Manifest{
147+
Applications: []*AppManifest{
148+
{
149+
Name: "test-app",
150+
Lifecycle: Buildpack,
151+
},
152+
},
153+
}
154+
b, err := yaml.Marshal(&m)
155+
require.NoError(t, err)
156+
require.Contains(t, string(b), "lifecycle: buildpack")
157+
})
158+
159+
t.Run("Marshall with docker lifecycle", func(t *testing.T) {
160+
m := &Manifest{
161+
Applications: []*AppManifest{
162+
{
163+
Name: "test-app",
164+
Lifecycle: Docker,
165+
},
166+
},
167+
}
168+
b, err := yaml.Marshal(&m)
169+
require.NoError(t, err)
170+
require.Contains(t, string(b), "lifecycle: docker")
171+
})
172+
173+
t.Run("Marshall with cnb lifecycle", func(t *testing.T) {
174+
m := &Manifest{
175+
Applications: []*AppManifest{
176+
{
177+
Name: "test-app",
178+
Lifecycle: CNB,
179+
},
180+
},
181+
}
182+
b, err := yaml.Marshal(&m)
183+
require.NoError(t, err)
184+
require.Contains(t, string(b), "lifecycle: cnb")
185+
})
186+
187+
t.Run("Marshall without lifecycle (omitempty)", func(t *testing.T) {
188+
m := &Manifest{
189+
Applications: []*AppManifest{
190+
{
191+
Name: "test-app",
192+
},
193+
},
194+
}
195+
b, err := yaml.Marshal(&m)
196+
require.NoError(t, err)
197+
require.NotContains(t, string(b), "lifecycle:")
198+
})
199+
200+
t.Run("Unmarshall with buildpack lifecycle", func(t *testing.T) {
201+
yamlData := `applications:
202+
- name: test-app
203+
lifecycle: buildpack`
204+
205+
var m Manifest
206+
err := yaml.Unmarshal([]byte(yamlData), &m)
207+
require.NoError(t, err)
208+
require.Equal(t, Buildpack, m.Applications[0].Lifecycle)
209+
})
210+
211+
t.Run("Unmarshall with docker lifecycle", func(t *testing.T) {
212+
yamlData := `applications:
213+
- name: test-app
214+
lifecycle: docker`
215+
216+
var m Manifest
217+
err := yaml.Unmarshal([]byte(yamlData), &m)
218+
require.NoError(t, err)
219+
require.Equal(t, Docker, m.Applications[0].Lifecycle)
220+
})
221+
222+
t.Run("Unmarshall with cnb lifecycle", func(t *testing.T) {
223+
yamlData := `applications:
224+
- name: test-app
225+
lifecycle: cnb`
226+
227+
var m Manifest
228+
err := yaml.Unmarshal([]byte(yamlData), &m)
229+
require.NoError(t, err)
230+
require.Equal(t, CNB, m.Applications[0].Lifecycle)
231+
})
232+
233+
t.Run("Unmarshall without lifecycle (empty)", func(t *testing.T) {
234+
yamlData := `applications:
235+
- name: test-app`
236+
237+
var m Manifest
238+
err := yaml.Unmarshal([]byte(yamlData), &m)
239+
require.NoError(t, err)
240+
require.Equal(t, AppLifecycle(""), m.Applications[0].Lifecycle)
241+
})
242+
}

operation/push.go

Lines changed: 76 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -136,10 +136,24 @@ func (p *AppPushOperation) pushRollingApp(ctx context.Context, space *resource.S
136136
}
137137

138138
var pkg *resource.Package
139-
if manifest.Docker != nil {
140-
pkg, err = p.uploadDockerPackage(ctx, originalApp, manifest.Docker)
139+
// Check if lifecycle is explicitly set in manifest
140+
if manifest.Lifecycle != "" {
141+
switch manifest.Lifecycle {
142+
case Docker:
143+
pkg, err = p.uploadDockerPackage(ctx, originalApp, manifest.Docker)
144+
case Buildpack, CNB:
145+
pkg, err = p.uploadBitsPackage(ctx, originalApp, zipFile)
146+
default:
147+
// Default to buildpack for unknown lifecycle types
148+
pkg, err = p.uploadBitsPackage(ctx, originalApp, zipFile)
149+
}
141150
} else {
142-
pkg, err = p.uploadBitsPackage(ctx, originalApp, zipFile)
151+
// Fall back to old logic when lifecycle is not set
152+
if manifest.Docker != nil {
153+
pkg, err = p.uploadDockerPackage(ctx, originalApp, manifest.Docker)
154+
} else {
155+
pkg, err = p.uploadBitsPackage(ctx, originalApp, zipFile)
156+
}
143157
}
144158
if err != nil {
145159
return nil, err
@@ -268,10 +282,24 @@ func (p *AppPushOperation) pushApp(ctx context.Context, space *resource.Space, m
268282
}
269283

270284
var pkg *resource.Package
271-
if app.Lifecycle.Type == resource.LifecycleDocker.String() {
272-
pkg, err = p.uploadDockerPackage(ctx, app, manifest.Docker)
285+
// Check if lifecycle is explicitly set in manifest
286+
if manifest.Lifecycle != "" {
287+
switch manifest.Lifecycle {
288+
case Docker:
289+
pkg, err = p.uploadDockerPackage(ctx, app, manifest.Docker)
290+
case Buildpack, CNB:
291+
pkg, err = p.uploadBitsPackage(ctx, app, zipFile)
292+
default:
293+
// Default to buildpack for unknown lifecycle types
294+
pkg, err = p.uploadBitsPackage(ctx, app, zipFile)
295+
}
273296
} else {
274-
pkg, err = p.uploadBitsPackage(ctx, app, zipFile)
297+
// Fall back to old logic when lifecycle is not set
298+
if app.Lifecycle.Type == resource.LifecycleDocker.String() {
299+
pkg, err = p.uploadDockerPackage(ctx, app, manifest.Docker)
300+
} else {
301+
pkg, err = p.uploadBitsPackage(ctx, app, zipFile)
302+
}
275303
}
276304
if err != nil {
277305
return nil, err
@@ -350,18 +378,50 @@ func (p *AppPushOperation) uploadBitsPackage(ctx context.Context, app *resource.
350378

351379
func (p *AppPushOperation) buildDroplet(ctx context.Context, pkg *resource.Package, manifest *AppManifest) (*resource.Droplet, error) {
352380
newBuild := resource.NewBuildCreate(pkg.GUID)
353-
if pkg.Type == resource.LifecycleDocker.String() {
354-
newBuild.Lifecycle = &resource.Lifecycle{
355-
Type: pkg.Type,
356-
Data: &resource.DockerLifecycle{}, // Empty docker lifecycle data
381+
382+
// Check if lifecycle is explicitly set in manifest first
383+
if manifest.Lifecycle != "" {
384+
switch manifest.Lifecycle {
385+
case Docker:
386+
newBuild.Lifecycle = &resource.Lifecycle{
387+
Type: string(Docker),
388+
Data: &resource.DockerLifecycle{}, // Empty docker lifecycle data
389+
}
390+
case CNB:
391+
newBuild.Lifecycle = &resource.Lifecycle{
392+
Type: string(CNB),
393+
Data: &resource.CNBLifecycle{
394+
Buildpacks: manifest.Buildpacks,
395+
Stack: manifest.Stack,
396+
},
397+
}
398+
case Buildpack:
399+
fallthrough
400+
default:
401+
// Default to buildpack lifecycle for unknown types or explicit buildpack
402+
newBuild.Lifecycle = &resource.Lifecycle{
403+
Type: resource.LifecycleBuildpack.String(),
404+
Data: &resource.BuildpackLifecycle{
405+
Buildpacks: manifest.Buildpacks,
406+
Stack: manifest.Stack,
407+
},
408+
}
357409
}
358410
} else {
359-
newBuild.Lifecycle = &resource.Lifecycle{
360-
Type: resource.LifecycleBuildpack.String(),
361-
Data: &resource.BuildpackLifecycle{
362-
Buildpacks: manifest.Buildpacks,
363-
Stack: manifest.Stack,
364-
},
411+
// Fall back to old logic based on package type when lifecycle is not set
412+
if pkg.Type == resource.LifecycleDocker.String() {
413+
newBuild.Lifecycle = &resource.Lifecycle{
414+
Type: pkg.Type,
415+
Data: &resource.DockerLifecycle{}, // Empty docker lifecycle data
416+
}
417+
} else {
418+
newBuild.Lifecycle = &resource.Lifecycle{
419+
Type: resource.LifecycleBuildpack.String(),
420+
Data: &resource.BuildpackLifecycle{
421+
Buildpacks: manifest.Buildpacks,
422+
Stack: manifest.Stack,
423+
},
424+
}
365425
}
366426
}
367427
build, err := p.client.Builds.Create(ctx, newBuild)

operation/push_test.go

Lines changed: 94 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ func TestDockerLifecycleBuildCreation(t *testing.T) {
196196
require.NoError(t, err)
197197

198198
pusher := NewAppPushOperation(cf, "", "")
199-
199+
200200
// Test the buildDroplet method specifically with a docker package
201201
resultDroplet, err := pusher.buildDroplet(context.Background(), dockerPkg, manifest)
202202
require.NoError(t, err, "Docker lifecycle build should not fail")
@@ -215,7 +215,7 @@ func TestDockerLifecycleStructure(t *testing.T) {
215215

216216
// Create build request directly to test lifecycle structure
217217
buildCreate := resource.NewBuildCreate(dockerPkg.GUID)
218-
218+
219219
// Apply the same logic as in buildDroplet method
220220
if dockerPkg.Type == resource.LifecycleDocker.String() {
221221
buildCreate.Lifecycle = &resource.Lifecycle{
@@ -228,7 +228,7 @@ func TestDockerLifecycleStructure(t *testing.T) {
228228
require.NotNil(t, buildCreate.Lifecycle, "Docker build should have lifecycle")
229229
require.Equal(t, "docker", buildCreate.Lifecycle.Type, "Docker build should have docker lifecycle type")
230230
require.NotNil(t, buildCreate.Lifecycle.Data, "Docker build should have lifecycle data")
231-
231+
232232
// Verify it's the correct type
233233
dockerLifecycle, ok := buildCreate.Lifecycle.Data.(*resource.DockerLifecycle)
234234
require.True(t, ok, "Docker build lifecycle data should be DockerLifecycle type")
@@ -245,11 +245,100 @@ func TestDockerLifecycleJSONMarshaling(t *testing.T) {
245245
// Test JSON marshaling to ensure it produces the expected structure
246246
// This is what the CF API expects: {"type":"docker","data":{}}
247247
expectedJSON := `{"type":"docker","data":{}}`
248-
248+
249249
// Marshal the lifecycle
250250
actualJSON, err := dockerLifecycle.MarshalJSON()
251251
require.NoError(t, err, "Docker lifecycle should marshal without error")
252-
252+
253253
// Verify the JSON structure matches expectations
254254
require.JSONEq(t, expectedJSON, string(actualJSON), "Docker lifecycle JSON should match expected format")
255255
}
256+
257+
func TestPushOperationLifecycleLogic(t *testing.T) {
258+
tests := []struct {
259+
name string
260+
manifestLifecycle AppLifecycle
261+
docker *AppManifestDocker
262+
expectedPackage string // "docker" or "bits"
263+
expectedLifecycle string // "docker", "buildpack", or "cnb"
264+
}{
265+
{
266+
name: "Explicit docker lifecycle",
267+
manifestLifecycle: Docker,
268+
docker: &AppManifestDocker{Image: "nginx:latest"},
269+
expectedPackage: "docker",
270+
expectedLifecycle: "docker",
271+
},
272+
{
273+
name: "Explicit buildpack lifecycle",
274+
manifestLifecycle: Buildpack,
275+
docker: nil,
276+
expectedPackage: "bits",
277+
expectedLifecycle: "buildpack",
278+
},
279+
{
280+
name: "Explicit CNB lifecycle",
281+
manifestLifecycle: CNB,
282+
docker: nil,
283+
expectedPackage: "bits",
284+
expectedLifecycle: "cnb",
285+
},
286+
{
287+
name: "No lifecycle with docker",
288+
manifestLifecycle: "",
289+
docker: &AppManifestDocker{Image: "nginx:latest"},
290+
expectedPackage: "docker",
291+
expectedLifecycle: "fallback", // This would use app.Lifecycle.Type in actual code
292+
},
293+
{
294+
name: "No lifecycle without docker",
295+
manifestLifecycle: "",
296+
docker: nil,
297+
expectedPackage: "bits",
298+
expectedLifecycle: "fallback", // This would use app.Lifecycle.Type in actual code
299+
},
300+
}
301+
302+
for _, tt := range tests {
303+
t.Run(tt.name, func(t *testing.T) {
304+
manifest := &AppManifest{
305+
Name: "test-app",
306+
Lifecycle: tt.manifestLifecycle,
307+
Docker: tt.docker,
308+
}
309+
310+
// Test package type decision logic
311+
var shouldUseDockerpPackage bool
312+
if manifest.Lifecycle != "" {
313+
shouldUseDockerpPackage = (manifest.Lifecycle == Docker)
314+
} else {
315+
shouldUseDockerpPackage = (manifest.Docker != nil)
316+
}
317+
318+
if tt.expectedPackage == "docker" {
319+
require.True(t, shouldUseDockerpPackage, "Should use docker package")
320+
} else {
321+
require.False(t, shouldUseDockerpPackage, "Should use bits package")
322+
}
323+
324+
// Test lifecycle type decision logic (only when explicitly set)
325+
if manifest.Lifecycle != "" {
326+
var lifecycleType string
327+
switch manifest.Lifecycle {
328+
case Docker:
329+
lifecycleType = "docker"
330+
case CNB:
331+
lifecycleType = "cnb"
332+
case Buildpack:
333+
fallthrough
334+
default:
335+
lifecycleType = "buildpack"
336+
}
337+
338+
if tt.expectedLifecycle != "fallback" {
339+
require.Equal(t, tt.expectedLifecycle, lifecycleType, "Lifecycle type should match expected")
340+
}
341+
}
342+
})
343+
}
344+
}

0 commit comments

Comments
 (0)