Skip to content

Commit

Permalink
Adding v3/apps/guid/droplets endpoint; Fixes cloudfoundry#3552
Browse files Browse the repository at this point in the history
  • Loading branch information
benjaminguttmann-avtq authored and georgethebeatle committed Nov 5, 2024
1 parent df9925d commit fa80f0b
Show file tree
Hide file tree
Showing 7 changed files with 281 additions and 0 deletions.
20 changes: 20 additions & 0 deletions api/handlers/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const (
AppsPath = "/v3/apps"
AppPath = "/v3/apps/{guid}"
AppCurrentDropletRelationshipPath = "/v3/apps/{guid}/relationships/current_droplet"
AppDropletsPath = "/v3/apps/{guid}/droplets"
AppCurrentDropletPath = "/v3/apps/{guid}/droplets/current"
AppProcessesPath = "/v3/apps/{guid}/processes"
AppProcessByTypePath = "/v3/apps/{guid}/processes/{type}"
Expand Down Expand Up @@ -217,6 +218,24 @@ func (h *App) setCurrentDroplet(r *http.Request) (*routing.Response, error) {
return routing.NewResponse(http.StatusOK).WithBody(presenter.ForCurrentDroplet(currentDroplet, h.serverURL)), nil
}

func (h *App) getDroplets(r *http.Request) (*routing.Response, error) {
authInfo, _ := authorization.InfoFromContext(r.Context())
logger := logr.FromContextOrDiscard(r.Context()).WithName("handlers.app.get-droplets")
appGUID := routing.URLParam(r, "guid")

app, err := h.appRepo.GetApp(r.Context(), authInfo, appGUID)
if err != nil {
return nil, apierrors.LogAndReturn(logger, apierrors.ForbiddenAsNotFound(err), "Failed to fetch app from Kubernetes", "AppGUID", appGUID)
}

droplets, err := h.dropletRepo.GetDroplets(r.Context(), authInfo, appGUID)
if err != nil {
return nil, apierrors.LogAndReturn(logger, apierrors.DropletForbiddenAsNotFound(err), "Failed to fetch droplet from Kubernetes", "dropletGUID", app.DropletGUID)
}

return routing.NewResponse(http.StatusOK).WithBody(presenter.ForList(presenter.ForDroplet, droplets, h.serverURL, *r.URL)), nil
}

func (h *App) getCurrentDroplet(r *http.Request) (*routing.Response, error) {
authInfo, _ := authorization.InfoFromContext(r.Context())
logger := logr.FromContextOrDiscard(r.Context()).WithName("handlers.app.get-current-droplet")
Expand Down Expand Up @@ -707,6 +726,7 @@ func (h *App) AuthenticatedRoutes() []routing.Route {
{Method: "GET", Pattern: AppsPath, Handler: h.list},
{Method: "POST", Pattern: AppsPath, Handler: h.create},
{Method: "PATCH", Pattern: AppCurrentDropletRelationshipPath, Handler: h.setCurrentDroplet},
{Method: "GET", Pattern: AppDropletsPath, Handler: h.getDroplets},
{Method: "GET", Pattern: AppCurrentDropletPath, Handler: h.getCurrentDroplet},
{Method: "POST", Pattern: AppStartPath, Handler: h.start},
{Method: "POST", Pattern: AppStopPath, Handler: h.stop},
Expand Down
41 changes: 41 additions & 0 deletions api/handlers/app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1306,6 +1306,47 @@ var _ = Describe("App", func() {
})
})

Describe("GET /v3/apps/:guid/droplets", func() {
BeforeEach(func() {
dropletRepo.GetDropletsReturns([]repositories.DropletRecord{{
GUID: dropletGUID,
State: "STAGED",
AppGUID: appGUID,
}}, nil)

req = createHttpRequest("GET", "/v3/apps/"+appGUID+"/droplets", nil)
})

It("returns the list of droplets", func() {
Expect(dropletRepo.GetDropletsCallCount()).To(Equal(1))
_, actualAuthInfo, actualAppGUID := dropletRepo.GetDropletsArgsForCall(0)
Expect(actualAuthInfo).To(Equal(authInfo))
Expect(actualAppGUID).To(Equal(appGUID))

Expect(rr).To(HaveHTTPStatus(http.StatusOK))
Expect(rr).To(HaveHTTPHeaderWithValue("Content-Type", "application/json"))
Expect(rr).To(HaveHTTPBody(SatisfyAll(
MatchJSONPath("$.pagination.total_results", BeEquivalentTo(1)),
MatchJSONPath("$.pagination.first.href", "https://api.example.org/v3/apps/"+appGUID+"/droplets"),
MatchJSONPath("$.resources", HaveLen(1)),
MatchJSONPath("$.resources[0].guid", Equal(dropletGUID)),
MatchJSONPath("$.resources[0].relationships.app.data.guid", Equal(appGUID)),
MatchJSONPath("$.resources[0].state", Equal("STAGED")),
)))
})

When("the app is not accessible", func() {
BeforeEach(func() {
toReturnErr := apierrors.NewForbiddenError(nil, repositories.AppResourceType)
appRepo.GetAppReturns(repositories.AppRecord{}, toReturnErr)
})

It("returns an error", func() {
expectNotFoundError("App")
})
})
})

Describe("GET /v3/apps/:guid/actions/restart", func() {
BeforeEach(func() {
updatedAppRecord := appRecord
Expand Down
1 change: 1 addition & 0 deletions api/handlers/droplet.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const (
//counterfeiter:generate -o fake -fake-name CFDropletRepository . CFDropletRepository
type CFDropletRepository interface {
GetDroplet(context.Context, authorization.Info, string) (repositories.DropletRecord, error)
GetDroplets(context.Context, authorization.Info, string) ([]repositories.DropletRecord, error)
ListDroplets(context.Context, authorization.Info, repositories.ListDropletsMessage) ([]repositories.DropletRecord, error)
UpdateDroplet(context.Context, authorization.Info, repositories.UpdateDropletMessage) (repositories.DropletRecord, error)
}
Expand Down
83 changes: 83 additions & 0 deletions api/handlers/fake/cfdroplet_repository.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

34 changes: 34 additions & 0 deletions api/repositories/droplet_repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,40 @@ func (r *DropletRepo) GetDroplet(ctx context.Context, authInfo authorization.Inf
return cfBuildToDroplet(build)
}

func (r *DropletRepo) GetDroplets(ctx context.Context, authInfo authorization.Info, appGuid string) ([]DropletRecord, error) {
buildList := &korifiv1alpha1.CFBuildList{}

namespaces, err := r.namespacePermissions.GetAuthorizedSpaceNamespaces(ctx, authInfo)
if err != nil {
return nil, fmt.Errorf("failed to list namespaces for spaces with user role bindings: %w", err)
}

userClient, err := r.userClientFactory.BuildClient(authInfo)
if err != nil {
return []DropletRecord{}, fmt.Errorf("failed to build user client: %w", err)
}

var allBuilds []korifiv1alpha1.CFBuild
for ns := range namespaces {
err := userClient.List(ctx, buildList, client.InNamespace(ns))
if k8serrors.IsForbidden(err) {
continue
}
if err != nil {
return []DropletRecord{}, apierrors.FromK8sError(err, BuildResourceType)
}
allBuilds = append(allBuilds, buildList.Items...)
}

var filteredDropletRecords []DropletRecord
for _, build := range allBuilds {
if build.Spec.AppRef.Name == appGuid {
filteredDropletRecords = append(filteredDropletRecords, cfBuildToDropletRecord(build))
}
}
return filteredDropletRecords, nil
}

func (r *DropletRepo) getBuildAssociatedWithDroplet(ctx context.Context, authInfo authorization.Info, dropletGUID string) (*korifiv1alpha1.CFBuild, client.WithWatch, error) {
// A droplet is a subset of a build
ns, err := r.namespaceRetriever.NamespaceFor(ctx, dropletGUID, DropletResourceType)
Expand Down
82 changes: 82 additions & 0 deletions api/repositories/droplet_repository_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,88 @@ var _ = Describe("DropletRepository", func() {
})
})

Describe("GetDroplets", func() {
var (
dropletRecords []repositories.DropletRecord
// appGUID string
listErr error
)

BeforeEach(func() {
meta.SetStatusCondition(&build.Status.Conditions, metav1.Condition{
Type: "Staging",
Status: metav1.ConditionFalse,
Reason: "kpack",
Message: "kpack",
})
meta.SetStatusCondition(&build.Status.Conditions, metav1.Condition{
Type: "Succeeded",
Status: metav1.ConditionTrue,
Reason: "Unknown",
Message: "Unknown",
})
build.Status.Droplet = &korifiv1alpha1.BuildDropletStatus{
Stack: dropletStack,
Registry: korifiv1alpha1.Registry{
Image: registryImage,
ImagePullSecrets: []corev1.LocalObjectReference{
{
Name: registryImageSecret,
},
},
},
ProcessTypes: []korifiv1alpha1.ProcessType{
{
Type: "rake",
Command: "bundle exec rake",
},
{
Type: "web",
Command: "bundle exec rackup config.ru -p $PORT",
},
},
Ports: []int32{8080, 443},
}
// Update Build Status based on changes made to local copy
Expect(k8sClient.Status().Update(testCtx, build)).To(Succeed())
})

JustBeforeEach(func() {
dropletRecords, listErr = dropletRepo.GetDroplets(testCtx, authInfo, appGUID)
})

When("the user is not authorized to get the droplets", func() {
It("returns an empty list to users who lack access", func() {
Expect(listErr).NotTo(HaveOccurred())
Expect(dropletRecords).To(BeEmpty())
})
})

When("the user is a space manager", func() {
BeforeEach(func() {
createRoleBinding(testCtx, userName, spaceDeveloperRole.Name, space.Name)
})

It("returns a list of droplet records with the appGUID relationship set on them", func() {
Expect(listErr).NotTo(HaveOccurred())
Expect(dropletRecords).To(HaveLen(1))
Expect(dropletRecords[0].Relationships()).To(Equal(map[string]string{"app": build.Spec.AppRef.Name}))
})

When("a space exists with a rolebinding for the user, but without permission to list droplets", func() {
BeforeEach(func() {
anotherSpace := createSpaceWithCleanup(testCtx, org.Name, "space-without-droplet-space-perm")
createRoleBinding(testCtx, userName, rootNamespaceUserRole.Name, anotherSpace.Name)
})

It("returns the droplet", func() {
Expect(listErr).NotTo(HaveOccurred())
Expect(dropletRecords).To(HaveLen(1))
})
})
})
})

Describe("ListDroplets", func() {
var (
dropletRecords []repositories.DropletRecord
Expand Down
20 changes: 20 additions & 0 deletions tests/e2e/apps_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,26 @@ var _ = Describe("Apps", func() {
})
})

Describe("Get app droplets", func() {
BeforeEach(func() {
setCurrentDroplet(appGUID, buildGUID)
})

var resultList resourceList[resource]

JustBeforeEach(func() {
var err error
resp, err = adminClient.R().SetResult(&resultList).Get("/v3/apps/" + appGUID + "/droplets")
Expect(err).NotTo(HaveOccurred())
})

It("succeeds", func() {
Expect(resp).To(HaveRestyStatusCode(http.StatusOK))
Expect(resultList.Resources[0].GUID).To(Equal(buildGUID))

})
})

Describe("Start an app", func() {
var result appResource

Expand Down

0 comments on commit fa80f0b

Please sign in to comment.