Skip to content

Commit

Permalink
Support docker lifecycle when creating apps
Browse files Browse the repository at this point in the history
* Allow `docker` lifecycle type in the `Lifecycle` object
* Omit empty `LifecycleData` attributes in the app/droplet response
* LifecycleData is required to be an empty object if the lifecycle type
  is `docker`

Issue: #2790

Co-authored-by: Georgi Sabev <georgethebeatle@gmail.com>
  • Loading branch information
georgethebeatle committed Aug 23, 2023
1 parent caae5e5 commit 539aef8
Show file tree
Hide file tree
Showing 30 changed files with 485 additions and 140 deletions.
65 changes: 61 additions & 4 deletions api/handlers/app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,10 +154,6 @@ var _ = Describe("App", func() {
})

It("returns the App", func() {
Expect(appRepo.CreateAppCallCount()).To(Equal(1))
_, actualAuthInfo, _ := appRepo.CreateAppArgsForCall(0)
Expect(actualAuthInfo).To(Equal(authInfo))

Expect(rr).To(HaveHTTPStatus(http.StatusCreated))
Expect(rr).To(HaveHTTPHeaderWithValue("Content-Type", "application/json"))

Expand All @@ -168,6 +164,67 @@ var _ = Describe("App", func() {
)))
})

It("sends an AppCreate message with default lifecycle to the repository", func() {
Expect(appRepo.CreateAppCallCount()).To(Equal(1))
_, actualAuthInfo, actualCreateMessage := appRepo.CreateAppArgsForCall(0)
Expect(actualAuthInfo).To(Equal(authInfo))
Expect(actualCreateMessage).To(Equal(repositories.CreateAppMessage{
Name: appName,
SpaceGUID: spaceGUID,
State: "STOPPED",
Lifecycle: repositories.Lifecycle{
Type: "buildpack",
Data: repositories.LifecycleData{
Stack: "cflinuxfs3",
},
},
}))
})

When("the app has buildpack lifecycle", func() {
BeforeEach(func() {
payload.Lifecycle = &payloads.Lifecycle{
Type: "buildpack",
Data: &payloads.LifecycleData{
Buildpacks: []string{"bp1"},
Stack: "my-stack",
},
}
})

It("sends an AppCreate message with buildpack lifecycle", func() {
Expect(appRepo.CreateAppCallCount()).To(Equal(1))
_, _, actualCreateMessage := appRepo.CreateAppArgsForCall(0)
Expect(actualCreateMessage.Lifecycle).To(Equal(
repositories.Lifecycle{
Type: "buildpack",
Data: repositories.LifecycleData{
Stack: "my-stack",
Buildpacks: []string{"bp1"},
},
}))
})
})

When("the app has docker lifecycle", func() {
BeforeEach(func() {
payload.Lifecycle = &payloads.Lifecycle{
Type: "docker",
Data: &payloads.LifecycleData{},
}
})

It("sends an AppCreate message with docker lifecycle and empty data", func() {
Expect(appRepo.CreateAppCallCount()).To(Equal(1))
_, _, actualCreateMessage := appRepo.CreateAppArgsForCall(0)
Expect(actualCreateMessage.Lifecycle).To(Equal(
repositories.Lifecycle{
Type: "docker",
Data: repositories.LifecycleData{},
}))
})
})

It("creates the `web` process", func() {
Expect(processRepo.CreateProcessCallCount()).To(Equal(1))
_, actualAuthInfo, actualMsg := processRepo.CreateProcessArgsForCall(0)
Expand Down
1 change: 1 addition & 0 deletions api/payloads/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ func (p AppCreate) ToAppCreateMessage() repositories.CreateAppMessage {
},
}
if p.Lifecycle != nil {
lifecycleBlock.Type = p.Lifecycle.Type
lifecycleBlock.Data.Stack = p.Lifecycle.Data.Stack
lifecycleBlock.Data.Buildpacks = p.Lifecycle.Data.Buildpacks
}
Expand Down
214 changes: 154 additions & 60 deletions api/payloads/app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,96 +65,190 @@ var _ = Describe("App payload validation", func() {
var validatorErr error

Describe("AppCreate", func() {
var (
payload payloads.AppCreate
decodedPayload *payloads.AppCreate
)
Describe("Decoding", func() {
var (
payload payloads.AppCreate
decodedPayload *payloads.AppCreate
)

BeforeEach(func() {
payload = payloads.AppCreate{
Name: "my-app",
Relationships: &payloads.AppRelationships{
Space: &payloads.Relationship{
Data: &payloads.RelationshipData{
GUID: "app-guid",
BeforeEach(func() {
payload = payloads.AppCreate{
Name: "my-app",
Relationships: &payloads.AppRelationships{
Space: &payloads.Relationship{
Data: &payloads.RelationshipData{
GUID: "app-guid",
},
},
},
},
}

decodedPayload = new(payloads.AppCreate)
})

JustBeforeEach(func() {
validatorErr = validator.DecodeAndValidateJSONPayload(createJSONRequest(payload), decodedPayload)
})
}

It("succeeds", func() {
Expect(validatorErr).NotTo(HaveOccurred())
Expect(decodedPayload).To(gstruct.PointTo(Equal(payload)))
})
decodedPayload = new(payloads.AppCreate)
})

When("name is not set", func() {
BeforeEach(func() {
payload.Name = ""
JustBeforeEach(func() {
validatorErr = validator.DecodeAndValidateJSONPayload(createJSONRequest(payload), decodedPayload)
})

It("returns an error", func() {
expectUnprocessableEntityError(validatorErr, "name cannot be blank")
It("succeeds", func() {
Expect(validatorErr).NotTo(HaveOccurred())
Expect(decodedPayload).To(gstruct.PointTo(Equal(payload)))
})
})

When("name is invalid", func() {
BeforeEach(func() {
payload.Name = "!@#"
When("name is not set", func() {
BeforeEach(func() {
payload.Name = ""
})

It("returns an error", func() {
expectUnprocessableEntityError(validatorErr, "name cannot be blank")
})
})

It("returns an error", func() {
expectUnprocessableEntityError(validatorErr, "name must consist only of letters, numbers, underscores and dashes")
When("name is invalid", func() {
BeforeEach(func() {
payload.Name = "!@#"
})

It("returns an error", func() {
expectUnprocessableEntityError(validatorErr, "name must consist only of letters, numbers, underscores and dashes")
})
})
})

When("lifecycle is invalid", func() {
BeforeEach(func() {
payload.Lifecycle = &payloads.Lifecycle{}
When("lifecycle is invalid", func() {
BeforeEach(func() {
payload.Lifecycle = &payloads.Lifecycle{}
})

It("returns an unprocessable entity error", func() {
expectUnprocessableEntityError(validatorErr, "lifecycle.type cannot be blank")
})
})

It("returns an unprocessable entity error", func() {
expectUnprocessableEntityError(validatorErr, "lifecycle.type cannot be blank")
When("relationships are not set", func() {
BeforeEach(func() {
payload.Relationships = nil
})

It("returns an unprocessable entity error", func() {
expectUnprocessableEntityError(validatorErr, "relationships is required")
})
})
})

When("relationships are not set", func() {
BeforeEach(func() {
payload.Relationships = nil
When("relationships space is not set", func() {
BeforeEach(func() {
payload.Relationships.Space = nil
})

It("returns an unprocessable entity error", func() {
expectUnprocessableEntityError(validatorErr, "relationships.space is required")
})
})

It("returns an unprocessable entity error", func() {
expectUnprocessableEntityError(validatorErr, "relationships is required")
When("metadata is invalid", func() {
BeforeEach(func() {
payload.Metadata = payloads.Metadata{
Labels: map[string]string{
"foo.cloudfoundry.org/bar": "jim",
},
}
})

It("returns an appropriate error", func() {
expectUnprocessableEntityError(validatorErr, "label/annotation key cannot use the cloudfoundry.org domain")
})
})
})

When("relationships space is not set", func() {
Describe("ToAppCreateMessage", func() {
var (
payload payloads.AppCreate
repoMessage repositories.CreateAppMessage
)

BeforeEach(func() {
payload.Relationships.Space = nil
payload = payloads.AppCreate{
Name: "app-name",
EnvironmentVariables: map[string]string{
"foo": "bar",
},
Relationships: &payloads.AppRelationships{
Space: &payloads.Relationship{
Data: &payloads.RelationshipData{
GUID: "space-guid",
},
},
},
Metadata: payloads.Metadata{
Labels: map[string]string{
"l1": "v1",
},
},
}
})

It("returns an unprocessable entity error", func() {
expectUnprocessableEntityError(validatorErr, "relationships.space is required")
JustBeforeEach(func() {
repoMessage = payload.ToAppCreateMessage()
})
})

When("metadata is invalid", func() {
BeforeEach(func() {
payload.Metadata = payloads.Metadata{
Labels: map[string]string{
"foo.cloudfoundry.org/bar": "jim",
It("creates an app create message with default lifecycle", func() {
Expect(repoMessage).To(Equal(repositories.CreateAppMessage{
Name: "app-name",
SpaceGUID: "space-guid",
State: repositories.StoppedState,
Lifecycle: repositories.Lifecycle{
Type: "buildpack",
Data: repositories.LifecycleData{
Stack: "cflinuxfs3",
},
},
}
EnvironmentVariables: map[string]string{
"foo": "bar",
},
Metadata: repositories.Metadata{
Labels: map[string]string{
"l1": "v1",
},
},
}))
})

It("returns an appropriate error", func() {
expectUnprocessableEntityError(validatorErr, "label/annotation key cannot use the cloudfoundry.org domain")
When("the lifecycle is buildpack", func() {
BeforeEach(func() {
payload.Lifecycle = &payloads.Lifecycle{
Type: "buildpack",
Data: &payloads.LifecycleData{
Buildpacks: []string{"my-bp"},
Stack: "my-stack",
},
}
})

It("sets the lifecycle to the repo message", func() {
Expect(repoMessage.Lifecycle).To(Equal(repositories.Lifecycle{
Type: "buildpack",
Data: repositories.LifecycleData{
Buildpacks: []string{"my-bp"},
Stack: "my-stack",
},
}))
})
})

When("the lifecycle is docker", func() {
BeforeEach(func() {
payload.Lifecycle = &payloads.Lifecycle{
Type: "docker",
Data: &payloads.LifecycleData{},
}
})

It("sets the lifecycle to the repo message", func() {
Expect(repoMessage.Lifecycle).To(Equal(repositories.Lifecycle{
Type: "docker",
Data: repositories.LifecycleData{},
}))
})
})
})
})
Expand Down
Loading

0 comments on commit 539aef8

Please sign in to comment.