Skip to content

Commit

Permalink
adding "cf restart-app-instance" API endpoints according to CF API Ve…
Browse files Browse the repository at this point in the history
…rsion 3.
  • Loading branch information
marsteg committed Jul 15, 2024
1 parent c93d4b6 commit fbe07fe
Show file tree
Hide file tree
Showing 16 changed files with 715 additions and 7 deletions.
57 changes: 57 additions & 0 deletions api/handlers/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"net/http"
"net/url"
"sort"
"strconv"
"time"

"code.cloudfoundry.org/korifi/api/authorization"
Expand Down Expand Up @@ -38,6 +39,7 @@ const (
AppFeaturePath = "/v3/apps/{guid}/features/{name}"
AppPackagesPath = "/v3/apps/{guid}/packages"
AppSSHEnabledPath = "/v3/apps/{guid}/ssh_enabled"
AppInstanceRestartPath = "/v3/apps/{guid}/processes/{processType}/instances/{instance}"
invalidDropletMsg = "Unable to assign current droplet. Ensure the droplet exists and belongs to this app."

AppStartedState = "STARTED"
Expand All @@ -58,6 +60,11 @@ type CFAppRepository interface {
PatchApp(context.Context, authorization.Info, repositories.PatchAppMessage) (repositories.AppRecord, error)
}

//counterfeiter:generate -o fake -fake-name PodRepository . PodRepository
type PodRepository interface {
DeletePod(context.Context, authorization.Info, string, repositories.ProcessRecord, string) error
}

type App struct {
serverURL url.URL
appRepo CFAppRepository
Expand All @@ -69,6 +76,7 @@ type App struct {
spaceRepo CFSpaceRepository
packageRepo CFPackageRepository
requestValidator RequestValidator
podRepo PodRepository
}

func NewApp(
Expand All @@ -82,6 +90,7 @@ func NewApp(
spaceRepo CFSpaceRepository,
packageRepo CFPackageRepository,
requestValidator RequestValidator,
podRepo PodRepository,
) *App {
return &App{
serverURL: serverURL,
Expand All @@ -94,6 +103,7 @@ func NewApp(
spaceRepo: spaceRepo,
packageRepo: packageRepo,
requestValidator: requestValidator,
podRepo: podRepo,
}
}

Expand Down Expand Up @@ -657,6 +667,52 @@ func (h *App) getAppFeature(r *http.Request) (*routing.Response, error) {
}
}

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

app, err := h.appRepo.GetApp(r.Context(), authInfo, appGUID)
if err != nil {
return nil, apierrors.LogAndReturn(logger, apierrors.NewNotFoundError(nil, repositories.AppResourceType), "Failed to fetch app from Kubernetes", "AppGUID", appGUID)
}
appProcesses, err := h.processRepo.ListProcesses(r.Context(), authInfo, repositories.ListProcessesMessage{
AppGUIDs: []string{appGUID},
SpaceGUID: app.SpaceGUID,
})
if err != nil {
return nil, apierrors.LogAndReturn(logger, err, "failed to list processes for app")
}

process, hasProcessType := findProcessType(appProcesses, processType)
if !hasProcessType {
return nil, apierrors.LogAndReturn(logger,
apierrors.NewNotFoundError(nil, repositories.ProcessResourceType),
"app does not have required process type",
)
}
instance, err := strconv.Atoi(instanceID)
if err != nil {
return nil, apierrors.LogAndReturn(
logger,
apierrors.AsUnprocessableEntity(err, "Invalid Instance ID. Instance ID is not a valid Integer.", apierrors.NotFoundError{}, apierrors.ForbiddenError{}),
"InstanceID", instanceID,
)
}
if process.DesiredInstances <= instance {
return nil, apierrors.LogAndReturn(logger,
apierrors.NewNotFoundError(nil, fmt.Sprintf("Instance %d of process %s", instance, processType)), "Instance not found", "AppGUID", appGUID, "InstanceID", instanceID, "Process", process)
}
err = h.podRepo.DeletePod(r.Context(), authInfo, app.Revision, process, instanceID)
if err != nil {
return nil, apierrors.LogAndReturn(logger, apierrors.ForbiddenAsNotFound(err), "Failed to restart instance", "AppGUID", appGUID, "InstanceID", instanceID, "Process", process)
}

return routing.NewResponse(http.StatusNoContent), nil
}

func (h *App) UnauthenticatedRoutes() []routing.Route {
return nil
}
Expand All @@ -683,5 +739,6 @@ func (h *App) AuthenticatedRoutes() []routing.Route {
{Method: "GET", Pattern: AppFeaturePath, Handler: h.getAppFeature},
{Method: "PATCH", Pattern: AppPath, Handler: h.update},
{Method: "GET", Pattern: AppSSHEnabledPath, Handler: h.getSSHEnabled},
{Method: "DELETE", Pattern: AppInstanceRestartPath, Handler: h.restartInstance},
}
}
88 changes: 88 additions & 0 deletions api/handlers/app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ var _ = Describe("App", func() {
domainRepo *fake.CFDomainRepository
spaceRepo *fake.CFSpaceRepository
packageRepo *fake.CFPackageRepository
podRepo *fake.PodRepository
requestValidator *fake.RequestValidator
req *http.Request

Expand All @@ -55,6 +56,7 @@ var _ = Describe("App", func() {
spaceRepo = new(fake.CFSpaceRepository)
packageRepo = new(fake.CFPackageRepository)
requestValidator = new(fake.RequestValidator)
podRepo = new(fake.PodRepository)

apiHandler := NewApp(
*serverURL,
Expand All @@ -67,13 +69,15 @@ var _ = Describe("App", func() {
spaceRepo,
packageRepo,
requestValidator,
podRepo,
)

appRecord = repositories.AppRecord{
GUID: appGUID,
Name: "test-app",
SpaceGUID: spaceGUID,
State: "STOPPED",
Revision: "0",
DropletGUID: "test-droplet-guid",
Lifecycle: repositories.Lifecycle{
Type: "buildpack",
Expand Down Expand Up @@ -1772,6 +1776,90 @@ var _ = Describe("App", func() {
})
})
})

Describe("DELETE /v3/apps/:guid/processes/:process/instances/:instance", func() {
BeforeEach(func() {
processRepo.ListProcessesReturns([]repositories.ProcessRecord{
{
GUID: "process-1-guid",
SpaceGUID: spaceGUID,
AppGUID: appGUID,
Type: "web",
DesiredInstances: 1,
},
}, nil)
req = createHttpRequest("DELETE", "/v3/apps/"+appGUID+"/processes/web/instances/0", nil)
})

It("restarts the instance", func() {
Expect(rr).To(HaveHTTPStatus(http.StatusNoContent))
Expect(podRepo.DeletePodCallCount()).To(Equal(1))
_, actualAuthInfo, actualAppRevision, actualProcess, actualInstanceID := podRepo.DeletePodArgsForCall(0)
Expect(actualAuthInfo).To(Equal(authInfo))
Expect(actualAppRevision).To(Equal("0"))
Expect(actualProcess.AppGUID).To(Equal(appGUID))
Expect(actualProcess.SpaceGUID).To(Equal(spaceGUID))
Expect(actualProcess.Type).To(Equal("web"))
Expect(actualInstanceID).To(Equal("0"))
})
When("the process does not exist", func() {
BeforeEach(func() {
req = createHttpRequest("DELETE", "/v3/apps/"+appGUID+"/processes/boom/instances/0", nil)
})
It("returns an error", func() {
expectNotFoundError("Process")
})
})
When("the instance does not exist", func() {
BeforeEach(func() {
req = createHttpRequest("DELETE", "/v3/apps/"+appGUID+"/processes/web/instances/5", nil)
})
It("returns an error", func() {
expectNotFoundError("Instance 5 of process web")
})
})
When("the app has a worker process", func() {
BeforeEach(func() {
processRepo.ListProcessesReturns([]repositories.ProcessRecord{
{
GUID: "process-1-guid",
SpaceGUID: spaceGUID,
AppGUID: appGUID,
Type: "web",
DesiredInstances: 1,
},
{
GUID: "process-2-guid",
SpaceGUID: spaceGUID,
AppGUID: appGUID,
Type: "worker",
DesiredInstances: 2,
},
}, nil)
req = createHttpRequest("DELETE", "/v3/apps/"+appGUID+"/processes/worker/instances/0", nil)
})

It("restarts the instance", func() {
Expect(rr).To(HaveHTTPStatus(http.StatusNoContent))
Expect(podRepo.DeletePodCallCount()).To(Equal(1))
_, actualAuthInfo, actualAppRevision, actualProcess, actualInstanceID := podRepo.DeletePodArgsForCall(0)
Expect(actualAuthInfo).To(Equal(authInfo))
Expect(actualAppRevision).To(Equal("0"))
Expect(actualProcess.AppGUID).To(Equal(appGUID))
Expect(actualProcess.SpaceGUID).To(Equal(spaceGUID))
Expect(actualProcess.Type).To(Equal("worker"))
Expect(actualInstanceID).To(Equal("0"))
})
})
When("The app does not exist", func() {
BeforeEach(func() {
appRepo.GetAppReturns(repositories.AppRecord{}, errors.New("App not found"))
})
It("returns an error", func() {
expectNotFoundError("App")
})
})
})
})

func createHttpRequest(method string, url string, body io.Reader) *http.Request {
Expand Down
85 changes: 85 additions & 0 deletions api/handlers/fake/cfprocess_repository.go

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

Loading

0 comments on commit fbe07fe

Please sign in to comment.