Skip to content

Commit 4cf823c

Browse files
uzabanovgeorgethebeatle
authored andcommitted
Implement the /v3/apps/{guid}/manifest endpoint
Implementing the app manifest endpoint includes calliing the `CollectState` method which gives as full information as possible for the `ManifestApplication` type (Routes,Processes,Services,AppRecord,Droplet). The following changes are made: * Calling `GetDroplet` in `Collect state` to encapsulate the build record in the `AppState` * Implementing `ToManifest` method to `AppState` type to map records to the `Manifest` types * It turned out that the routes repository returns only the `GUID` not the full `DomainRecord`, so the domainrepo is injected in the route repository and gets the full domain record for a route. Also unneccesary look ups for the domain are removed from the route and domain handler * Smoke test from the `cf create-app-manifest` command which calls the implemented manifest endpoint Closes #4258
1 parent 8cace89 commit 4cf823c

File tree

20 files changed

+858
-237
lines changed

20 files changed

+858
-237
lines changed

api/actions/manifest/state_collector.go

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@ import (
55
"errors"
66
"fmt"
77
"maps"
8-
"path"
98
"slices"
109

1110
"code.cloudfoundry.org/korifi/api/actions/shared"
1211
"code.cloudfoundry.org/korifi/api/authorization"
1312
apierrors "code.cloudfoundry.org/korifi/api/errors"
13+
1414
"code.cloudfoundry.org/korifi/api/repositories"
1515
"code.cloudfoundry.org/korifi/api/tools/singleton"
1616
"github.com/BooleanCat/go-functional/v2/it"
@@ -23,13 +23,15 @@ type StateCollector struct {
2323
routeRepo shared.CFRouteRepository
2424
serviceInstanceRepo shared.CFServiceInstanceRepository
2525
serviceBindingRepo shared.CFServiceBindingRepository
26+
dropletRepo shared.CFDropletRepository
2627
}
2728

2829
type AppState struct {
2930
App repositories.AppRecord
3031
Processes map[string]repositories.ProcessRecord
3132
Routes map[string]repositories.RouteRecord
3233
ServiceBindings map[string]repositories.ServiceBindingRecord
34+
Droplet *repositories.DropletRecord
3335
}
3436

3537
func NewStateCollector(
@@ -39,6 +41,7 @@ func NewStateCollector(
3941
routeRepo shared.CFRouteRepository,
4042
serviceInstanceRepo shared.CFServiceInstanceRepository,
4143
serviceBindingRepo shared.CFServiceBindingRepository,
44+
dropletRepo shared.CFDropletRepository,
4245
) StateCollector {
4346
return StateCollector{
4447
appRepo: appRepo,
@@ -47,6 +50,7 @@ func NewStateCollector(
4750
routeRepo: routeRepo,
4851
serviceInstanceRepo: serviceInstanceRepo,
4952
serviceBindingRepo: serviceBindingRepo,
53+
dropletRepo: dropletRepo,
5054
}
5155
}
5256

@@ -82,11 +86,17 @@ func (s StateCollector) CollectState(ctx context.Context, authInfo authorization
8286
return AppState{}, err
8387
}
8488

89+
dropletRecord, err := s.getDroplet(ctx, authInfo, appRecord.DropletGUID)
90+
if err != nil {
91+
return AppState{}, err
92+
}
93+
8594
return AppState{
8695
App: appRecord,
8796
Processes: processesByType,
8897
Routes: routesByURL,
8998
ServiceBindings: bindingsByServiceName,
99+
Droplet: dropletRecord,
90100
}, nil
91101
}
92102

@@ -104,6 +114,18 @@ func (s StateCollector) indexProcessesByType(ctx context.Context, authInfo autho
104114
}), nil
105115
}
106116

117+
func (s StateCollector) getDroplet(ctx context.Context, authInfo authorization.Info, dropletGUID string) (*repositories.DropletRecord, error) {
118+
droplet, err := s.dropletRepo.GetDroplet(ctx, authInfo, dropletGUID)
119+
if err != nil {
120+
if errors.As(err, new(apierrors.NotFoundError)) {
121+
return nil, nil
122+
}
123+
return nil, err
124+
}
125+
126+
return &droplet, nil
127+
}
128+
107129
func (s StateCollector) indexRoutesByURL(ctx context.Context, authInfo authorization.Info, appGUID, spaceGUID string) (map[string]repositories.RouteRecord, error) {
108130
routes, err := s.routeRepo.ListRoutes(ctx, authInfo, repositories.ListRoutesMessage{
109131
AppGUIDs: []string{appGUID},
@@ -113,9 +135,7 @@ func (s StateCollector) indexRoutesByURL(ctx context.Context, authInfo authoriza
113135
return nil, err
114136
}
115137

116-
return index(routes.Records, func(r repositories.RouteRecord) string {
117-
return path.Join(fmt.Sprintf("%s.%s", r.Host, r.Domain.Name), r.Path)
118-
}), nil
138+
return index(routes.Records, routeURL), nil
119139
}
120140

121141
func (s StateCollector) indexBindingsByServiceName(ctx context.Context, authInfo authorization.Info, appGUID string) (map[string]repositories.ServiceBindingRecord, error) {
@@ -169,3 +189,11 @@ func tryIndex[T any](records []T, keyFunc func(T) (string, error)) (map[string]T
169189
recordsIter,
170190
)), nil
171191
}
192+
193+
func routeURL(route repositories.RouteRecord) string {
194+
if route.Host == "" {
195+
return fmt.Sprintf("%s%s", route.Domain.Name, route.Path)
196+
}
197+
198+
return fmt.Sprintf("%s.%s%s", route.Host, route.Domain.Name, route.Path)
199+
}

api/actions/manifest/state_collector_test.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import (
1212
"code.cloudfoundry.org/korifi/api/authorization"
1313
"code.cloudfoundry.org/korifi/api/repositories"
1414
"code.cloudfoundry.org/korifi/api/repositories/k8sklient/descriptors"
15+
16+
apierrors "code.cloudfoundry.org/korifi/api/errors"
1517
)
1618

1719
var _ = Describe("StateCollector", func() {
@@ -22,6 +24,7 @@ var _ = Describe("StateCollector", func() {
2224
routeRepo *fake.CFRouteRepository
2325
serviceInstanceRepo *fake.CFServiceInstanceRepository
2426
serviceBindingRepo *fake.CFServiceBindingRepository
27+
dropletRepo *fake.CFDropletRepository
2528
stateCollector manifest.StateCollector
2629
appState manifest.AppState
2730
collectStateErr error
@@ -34,13 +37,15 @@ var _ = Describe("StateCollector", func() {
3437
routeRepo = new(fake.CFRouteRepository)
3538
serviceInstanceRepo = new(fake.CFServiceInstanceRepository)
3639
serviceBindingRepo = new(fake.CFServiceBindingRepository)
40+
dropletRepo = new(fake.CFDropletRepository)
3741
stateCollector = manifest.NewStateCollector(
3842
appRepo,
3943
domainRepo,
4044
processRepo,
4145
routeRepo,
4246
serviceInstanceRepo,
4347
serviceBindingRepo,
48+
dropletRepo,
4449
)
4550
})
4651

@@ -90,6 +95,64 @@ var _ = Describe("StateCollector", func() {
9095
})
9196
})
9297

98+
Describe("droplet", func() {
99+
BeforeEach(func() {
100+
appRepo.ListAppsReturns(repositories.ListResult[repositories.AppRecord]{Records: []repositories.AppRecord{{GUID: "app-guid", DropletGUID: "droplet-guid"}}}, nil)
101+
dropletRepo.GetDropletReturns(repositories.DropletRecord{
102+
Lifecycle: repositories.Lifecycle{
103+
Type: "buildpack",
104+
Data: repositories.LifecycleData{
105+
Buildpacks: []string{"bp1"},
106+
},
107+
},
108+
AppGUID: "app-guid",
109+
Image: "docker-image",
110+
}, nil)
111+
})
112+
113+
It("gets the droplet", func() {
114+
Expect(collectStateErr).NotTo(HaveOccurred())
115+
Expect(dropletRepo.GetDropletCallCount()).To(Equal(1))
116+
_, _, dropletGUID := dropletRepo.GetDropletArgsForCall(0)
117+
Expect(dropletGUID).To(Equal("droplet-guid"))
118+
})
119+
120+
It("returns the droplet", func() {
121+
Expect(collectStateErr).NotTo(HaveOccurred())
122+
Expect(appState.Droplet).To(Equal(&repositories.DropletRecord{
123+
Lifecycle: repositories.Lifecycle{
124+
Type: "buildpack",
125+
Data: repositories.LifecycleData{
126+
Buildpacks: []string{"bp1"},
127+
},
128+
},
129+
AppGUID: "app-guid",
130+
Image: "docker-image",
131+
}))
132+
})
133+
134+
When("droplet is not found", func() {
135+
BeforeEach(func() {
136+
dropletRepo.GetDropletReturns(repositories.DropletRecord{}, apierrors.NewNotFoundError(nil, repositories.DropletResourceType, "droplet-guid"))
137+
})
138+
139+
It("returns nil", func() {
140+
Expect(collectStateErr).NotTo(HaveOccurred())
141+
Expect(appState.Droplet).To(BeNil())
142+
})
143+
})
144+
145+
When("getting droplet fails", func() {
146+
BeforeEach(func() {
147+
dropletRepo.GetDropletReturns(repositories.DropletRecord{}, errors.New("get-droplet-error"))
148+
})
149+
150+
It("returns the error", func() {
151+
Expect(collectStateErr).To(MatchError("get-droplet-error"))
152+
})
153+
})
154+
})
155+
93156
Describe("processes", func() {
94157
BeforeEach(func() {
95158
appRepo.ListAppsReturns(repositories.ListResult[repositories.AppRecord]{Records: []repositories.AppRecord{{GUID: "app-guid"}}}, nil)

api/actions/shared/fake/cfdroplet_repository.go

Lines changed: 121 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api/actions/shared/shared.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,8 @@ type CFServiceBindingRepository interface {
5353
type CFServiceInstanceRepository interface {
5454
ListServiceInstances(context.Context, authorization.Info, repositories.ListServiceInstanceMessage) (repositories.ListResult[repositories.ServiceInstanceRecord], error)
5555
}
56+
57+
//counterfeiter:generate -o fake -fake-name CFDropletRepository . CFDropletRepository
58+
type CFDropletRepository interface {
59+
GetDroplet(context.Context, authorization.Info, string) (repositories.DropletRecord, error)
60+
}

0 commit comments

Comments
 (0)