Skip to content

Commit

Permalink
Add initial GET /v3/apps/:guid/processes/:type/stats endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
gogolok committed May 29, 2024
1 parent 5049210 commit ff5a43a
Show file tree
Hide file tree
Showing 4 changed files with 136 additions and 0 deletions.
31 changes: 31 additions & 0 deletions api/handlers/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const (
AppCurrentDropletPath = "/v3/apps/{guid}/droplets/current"
AppProcessesPath = "/v3/apps/{guid}/processes"
AppProcessByTypePath = "/v3/apps/{guid}/processes/{type}"
AppProcessStatsByTypePath = "/v3/apps/{guid}/processes/{type}/stats"
AppProcessScalePath = "/v3/apps/{guid}/processes/{processType}/actions/scale"
AppRoutesPath = "/v3/apps/{guid}/routes"
AppStartPath = "/v3/apps/{guid}/actions/start"
Expand Down Expand Up @@ -62,6 +63,7 @@ type App struct {
appRepo CFAppRepository
dropletRepo CFDropletRepository
processRepo CFProcessRepository
processStats ProcessStats
routeRepo CFRouteRepository
domainRepo CFDomainRepository
spaceRepo CFSpaceRepository
Expand All @@ -74,6 +76,7 @@ func NewApp(
appRepo CFAppRepository,
dropletRepo CFDropletRepository,
processRepo CFProcessRepository,
processStatsFetcher ProcessStats,
routeRepo CFRouteRepository,
domainRepo CFDomainRepository,
spaceRepo CFSpaceRepository,
Expand All @@ -85,6 +88,7 @@ func NewApp(
appRepo: appRepo,
dropletRepo: dropletRepo,
processRepo: processRepo,
processStats: processStatsFetcher,
routeRepo: routeRepo,
domainRepo: domainRepo,
spaceRepo: spaceRepo,
Expand Down Expand Up @@ -554,6 +558,32 @@ func (h *App) getProcess(r *http.Request) (*routing.Response, error) {
return routing.NewResponse(http.StatusOK).WithBody(presenter.ForProcess(process, h.serverURL)), nil
}

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

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)
}

process, err := h.processRepo.GetProcessByAppTypeAndSpace(r.Context(), authInfo, appGUID, processType, app.SpaceGUID)
if err != nil {
return nil, apierrors.LogAndReturn(logger, err, "Failed to fetch process from Kubernetes", "AppGUID", appGUID)
}

processGUID := process.GUID

records, err := h.processStats.FetchStats(r.Context(), authInfo, processGUID)
if err != nil {
return nil, apierrors.LogAndReturn(logger, apierrors.ForbiddenAsNotFound(err), "Failed to get process stats from Kubernetes", "ProcessGUID", processGUID)
}

return routing.NewResponse(http.StatusOK).WithBody(presenter.ForProcessStats(records)), nil
}

func (h *App) getPackages(r *http.Request) (*routing.Response, error) {
authInfo, _ := authorization.InfoFromContext(r.Context())
logger := logr.FromContextOrDiscard(r.Context()).WithName("handlers.app.get-packages")
Expand Down Expand Up @@ -644,6 +674,7 @@ func (h *App) AuthenticatedRoutes() []routing.Route {
{Method: "POST", Pattern: AppProcessScalePath, Handler: h.scaleProcess},
{Method: "GET", Pattern: AppProcessesPath, Handler: h.getProcesses},
{Method: "GET", Pattern: AppProcessByTypePath, Handler: h.getProcess},
{Method: "GET", Pattern: AppProcessStatsByTypePath, Handler: h.getProcessStats},
{Method: "GET", Pattern: AppRoutesPath, Handler: h.getRoutes},
{Method: "DELETE", Pattern: AppPath, Handler: h.delete},
{Method: "PATCH", Pattern: AppEnvVarsPath, Handler: h.updateEnvVars},
Expand Down
70 changes: 70 additions & 0 deletions api/handlers/app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"strings"
"time"

"code.cloudfoundry.org/korifi/api/actions"
apierrors "code.cloudfoundry.org/korifi/api/errors"
. "code.cloudfoundry.org/korifi/api/handlers"
"code.cloudfoundry.org/korifi/api/handlers/fake"
Expand All @@ -33,6 +34,7 @@ var _ = Describe("App", func() {
appRepo *fake.CFAppRepository
dropletRepo *fake.CFDropletRepository
processRepo *fake.CFProcessRepository
processStats *fake.ProcessStats
routeRepo *fake.CFRouteRepository
domainRepo *fake.CFDomainRepository
spaceRepo *fake.CFSpaceRepository
Expand All @@ -47,6 +49,7 @@ var _ = Describe("App", func() {
appRepo = new(fake.CFAppRepository)
dropletRepo = new(fake.CFDropletRepository)
processRepo = new(fake.CFProcessRepository)
processStats = new(fake.ProcessStats)
routeRepo = new(fake.CFRouteRepository)
domainRepo = new(fake.CFDomainRepository)
spaceRepo = new(fake.CFSpaceRepository)
Expand All @@ -58,6 +61,7 @@ var _ = Describe("App", func() {
appRepo,
dropletRepo,
processRepo,
processStats,
routeRepo,
domainRepo,
spaceRepo,
Expand Down Expand Up @@ -987,6 +991,72 @@ var _ = Describe("App", func() {
})
})

Describe("GET /v3/apps/:guid/processes/{type}/stats", func() {
BeforeEach(func() {
processStats.FetchStatsReturns([]actions.PodStatsRecord{
{
Type: "web",
Index: 0,
MemQuota: tools.PtrTo(int64(1024)),
},
{
Type: "web",
Index: 1,
MemQuota: tools.PtrTo(int64(512)),
},
}, nil)

req = createHttpRequest("GET", "/v3/apps/"+appGUID+"/processes/web/stats", nil)
})

It("returns the process stats", func() {
Expect(processStats.FetchStatsCallCount()).To(Equal(1))
_, actualAuthInfo, _ := processStats.FetchStatsArgsForCall(0)
Expect(actualAuthInfo).To(Equal(authInfo))

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

Expect(rr).To(HaveHTTPBody(SatisfyAll(
MatchJSONPath("$.resources", HaveLen(2)),
MatchJSONPath("$.resources[0].type", "web"),
MatchJSONPath("$.resources[0].index", BeEquivalentTo(0)),
MatchJSONPath("$.resources[1].type", "web"),
MatchJSONPath("$.resources[1].mem_quota", BeEquivalentTo(512)),
)))
})

When("getting the app fails", func() {
BeforeEach(func() {
appRepo.GetAppReturns(repositories.AppRecord{}, errors.New("get-app"))
})

It("returns an error", func() {
expectUnknownError()
})
})

When("there is an error fetching the process", func() {
BeforeEach(func() {
processRepo.GetProcessByAppTypeAndSpaceReturns(repositories.ProcessRecord{}, errors.New("some-error"))
})

It("return a process unknown error", func() {
expectUnknownError()
})
})

When("fetching the process stats errors", func() {
BeforeEach(func() {
processStats.FetchStatsReturns(nil, errors.New("boom"))
})

It("returns an error", func() {
expectUnknownError()
})
})
})

Describe("the POST /v3/apps/:guid/process/:processType/actions/scale endpoint", func() {
var payload *payloads.ProcessScale

Expand Down
1 change: 1 addition & 0 deletions api/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,7 @@ func main() {
appRepo,
dropletRepo,
processRepo,
processStats,
routeRepo,
domainRepo,
spaceRepo,
Expand Down
34 changes: 34 additions & 0 deletions tests/e2e/apps_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,40 @@ var _ = Describe("Apps", func() {
})
})

Describe("Get app process stats by type", func() {
var processStats resourceList[statsResource]

JustBeforeEach(func() {
Eventually(func(g Gomega) {
var err error
resp, err = adminClient.R().
SetResult(&processStats).
Get("/v3/apps/" + appGUID + "/processes/web/stats")
g.Expect(err).NotTo(HaveOccurred())

// no 'g.' here - we require all calls to return 200
Expect(resp).To(HaveRestyStatusCode(http.StatusOK))
g.Expect(processStats.Resources).ToNot(BeEmpty())
g.Expect(processStats.Resources[0].Usage).ToNot(BeZero())
}).Should(Succeed())
})

BeforeEach(func() {
appGUID, _ = pushTestApp(space1GUID, defaultAppBitsFile)
})

It("succeeds", func() {
Expect(resp).To(HaveRestyStatusCode(http.StatusOK))
Expect(processStats.Resources).To(HaveLen(1))

Expect(processStats.Resources[0].Usage).To(MatchFields(IgnoreExtras, Fields{
"Mem": Not(BeNil()),
"CPU": Not(BeNil()),
"Time": Not(BeNil()),
}))
})
})

Describe("List app packages", func() {
var (
result resourceList[typedResource]
Expand Down

0 comments on commit ff5a43a

Please sign in to comment.