Skip to content
This repository has been archived by the owner on Nov 3, 2021. It is now read-only.

Commit

Permalink
Added POST /v3/apps/:guid/actions/start
Browse files Browse the repository at this point in the history
- updated README

Co-authored-by: Andrew Wittrock <awittrock@vmware.com>
  • Loading branch information
gnovv and Birdrock committed Oct 12, 2021
1 parent d6fe49b commit 167a732
Show file tree
Hide file tree
Showing 6 changed files with 416 additions and 2 deletions.
62 changes: 61 additions & 1 deletion apis/app_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,10 @@ const (
AppGetEndpoint = "/v3/apps/{guid}"
AppListEndpoint = "/v3/apps"
AppSetCurrentDropletEndpoint = "/v3/apps/{guid}/relationships/current_droplet"
AppStartEndpoint = "/v3/apps/{guid}/actions/start"
invalidDropletMsg = "Unable to assign current droplet. Ensure the droplet exists and belongs to this app."

invalidDropletMsg = "Unable to assign current droplet. Ensure the droplet exists and belongs to this app."
AppStartedState = "STARTED"
)

//counterfeiter:generate -o fake -fake-name CFAppRepository . CFAppRepository
Expand All @@ -39,6 +41,7 @@ type CFAppRepository interface {
CreateAppEnvironmentVariables(context.Context, client.Client, repositories.AppEnvVarsRecord) (repositories.AppEnvVarsRecord, error)
CreateApp(context.Context, client.Client, repositories.AppRecord) (repositories.AppRecord, error)
SetCurrentDroplet(context.Context, client.Client, repositories.SetCurrentDropletMessage) (repositories.CurrentDropletRecord, error)
SetAppDesiredState(context.Context, client.Client, repositories.SetAppDesiredStateMessage) (repositories.AppRecord, error)
}

type AppHandler struct {
Expand Down Expand Up @@ -297,9 +300,66 @@ func (h *AppHandler) appSetCurrentDropletHandler(w http.ResponseWriter, r *http.
w.Write(responseBody)
}

func (h *AppHandler) appStartHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
w.Header().Set("Content-Type", "application/json")

vars := mux.Vars(r)
appGUID := vars["guid"]

// TODO: Instantiate config based on bearer token
// Spike code from EMEA folks around this: https://github.com/cloudfoundry/cf-crd-explorations/blob/136417fbff507eb13c92cd67e6fed6b061071941/cfshim/handlers/app_handler.go#L78
client, err := h.buildClient(h.k8sConfig)
if err != nil {
h.logger.Error(err, "Unable to create Kubernetes client", "AppGUID", appGUID)
writeUnknownErrorResponse(w)
return
}

app, err := h.appRepo.FetchApp(ctx, client, appGUID)
if err != nil {
switch err.(type) {
case repositories.NotFoundError:
h.logger.Info("App not found", "AppGUID", appGUID)
writeNotFoundErrorResponse(w, "App")
return
default:
h.logger.Error(err, "Failed to fetch app from Kubernetes", "AppGUID", appGUID)
writeUnknownErrorResponse(w)
return
}
}
if app.DropletGUID == "" {
h.logger.Info("App droplet not set before start", "AppGUID", appGUID)
writeUnprocessableEntityError(w, "Assign a droplet before starting this app.")
return
}

app, err = h.appRepo.SetAppDesiredState(ctx, client, repositories.SetAppDesiredStateMessage{
AppGUID: app.GUID,
SpaceGUID: app.SpaceGUID,
Value: AppStartedState,
})
if err != nil {
h.logger.Error(err, "Failed to update app in Kubernetes", "AppGUID", appGUID)
writeUnknownErrorResponse(w)
return
}

responseBody, err := json.Marshal(presenter.ForApp(app, h.serverURL))
if err != nil {
h.logger.Error(err, "Failed to render response", "AppGUID", appGUID)
writeUnknownErrorResponse(w)
return
}

w.Write(responseBody)
}

func (h *AppHandler) RegisterRoutes(router *mux.Router) {
router.Path(AppGetEndpoint).Methods("GET").HandlerFunc(h.appGetHandler)
router.Path(AppListEndpoint).Methods("GET").HandlerFunc(h.appListHandler)
router.Path(AppCreateEndpoint).Methods("POST").HandlerFunc(h.appCreateHandler)
router.Path(AppSetCurrentDropletEndpoint).Methods("PATCH").HandlerFunc(h.appSetCurrentDropletHandler)
router.Path(AppStartEndpoint).Methods("POST").HandlerFunc(h.appStartHandler)
}
156 changes: 156 additions & 0 deletions apis/app_handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (

const (
appGUID = "test-app-guid"
appName = "test-app"
spaceGUID = "test-space-guid"
testAppHandlerLoggerName = "TestAppHandler"
)
Expand Down Expand Up @@ -955,6 +956,161 @@ var _ = Describe("AppHandler", func() {
itRespondsWithUnknownError(getRR)
})
})

Describe("the POST /v3/apps/:guid/actions/start endpoint", func() {

BeforeEach(func() {
fetchAppRecord := repositories.AppRecord{
Name: appName,
GUID: appGUID,
SpaceGUID: spaceGUID,
DropletGUID: "some-droplet-guid",
State: "STOPPED",
Lifecycle: repositories.Lifecycle{
Type: "buildpack",
Data: repositories.LifecycleData{
Buildpacks: []string{},
Stack: "",
},
},
}
appRepo.FetchAppReturns(fetchAppRecord, nil)
setAppDesiredStateRecord := fetchAppRecord
setAppDesiredStateRecord.State = "STARTED"
appRepo.SetAppDesiredStateReturns(setAppDesiredStateRecord, nil)

var err error
req, err = http.NewRequest("POST", "/v3/apps/"+appGUID+"/actions/start", nil)
Expect(err).NotTo(HaveOccurred())
})

When("on the happy path", func() {
It("returns status 200 OK", func() {
Expect(rr.Code).To(Equal(http.StatusOK), "Matching HTTP response code:")
})

It("returns the App in the response with a state of STARTED", func() {
contentTypeHeader := rr.Header().Get("Content-Type")
Expect(contentTypeHeader).To(Equal(jsonHeader), "Matching Content-Type header:")

Expect(rr.Body.String()).To(MatchJSON(fmt.Sprintf(`{
"guid": "%[2]s",
"created_at": "",
"updated_at": "",
"name": "%[4]s",
"state": "STARTED",
"lifecycle": {
"type": "buildpack",
"data": {
"buildpacks": [],
"stack": ""
}
},
"relationships": {
"space": {
"data": {
"guid": "%[3]s"
}
}
},
"metadata": {
"labels": {},
"annotations": {}
},
"links": {
"self": {
"href": "https://api.example.org/v3/apps/%[2]s"
},
"environment_variables": {
"href": "https://api.example.org/v3/apps/%[2]s/environment_variables"
},
"space": {
"href": "https://api.example.org/v3/spaces/%[3]s"
},
"processes": {
"href": "https://api.example.org/v3/apps/%[2]s/processes"
},
"packages": {
"href": "https://api.example.org/v3/apps/%[2]s/packages"
},
"current_droplet": {
"href": "https://api.example.org/v3/apps/%[2]s/droplets/current"
},
"droplets": {
"href": "https://api.example.org/v3/apps/%[2]s/droplets"
},
"tasks": {
"href": "https://api.example.org/v3/apps/%[2]s/tasks"
},
"start": {
"href": "https://api.example.org/v3/apps/%[2]s/actions/start",
"method": "POST"
},
"stop": {
"href": "https://api.example.org/v3/apps/%[2]s/actions/stop",
"method": "POST"
},
"revisions": {
"href": "https://api.example.org/v3/apps/%[2]s/revisions"
},
"deployed_revisions": {
"href": "https://api.example.org/v3/apps/%[2]s/revisions/deployed"
},
"features": {
"href": "https://api.example.org/v3/apps/%[2]s/features"
}
}
}`, defaultServerURL, appGUID, spaceGUID, appName)), "Response body matches response:")
})
})

When("the app cannot be found", func() {
BeforeEach(func() {
appRepo.FetchAppReturns(repositories.AppRecord{}, repositories.NotFoundError{})
})

// TODO: should we return code 100004 instead?
itRespondsWithNotFound("App not found", getRR)
})

When("there is some other error fetching the app", func() {
BeforeEach(func() {
appRepo.FetchAppReturns(repositories.AppRecord{}, errors.New("unknown!"))
})

itRespondsWithUnknownError(getRR)
})

When("the app has no droplet", func() {
BeforeEach(func() {
fetchAppRecord := repositories.AppRecord{
Name: appName,
GUID: appGUID,
SpaceGUID: spaceGUID,
DropletGUID: "",
State: "STOPPED",
Lifecycle: repositories.Lifecycle{
Type: "buildpack",
Data: repositories.LifecycleData{
Buildpacks: []string{},
Stack: "",
},
},
}
appRepo.FetchAppReturns(fetchAppRecord, nil)
})

itRespondsWithUnprocessableEntity(`Assign a droplet before starting this app.`, getRR)
})

When("there is some other error updating app desiredState", func() {
BeforeEach(func() {
appRepo.SetAppDesiredStateReturns(repositories.AppRecord{}, errors.New("unknown!"))
})

itRespondsWithUnknownError(getRR)
})
})
})

func initializeCreateAppRequestBody(appName, spaceGUID string, envVars, labels, annotations map[string]string) string {
Expand Down
83 changes: 83 additions & 0 deletions apis/fake/cfapp_repository.go

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

7 changes: 7 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ Docs: https://v3-apidocs.cloudfoundry.org/version/3.107.0/index.html#apps
| Get App | GET /v3/apps/\<guid> |
| Create App | POST /v3/apps |
| Set App's Current Droplet | PATCH /v3/apps/\<guid>/relationships/current_droplet |
| Start App | POST /v3/apps/\<guid>/actions/start |

#### [Creating Apps](https://v3-apidocs.cloudfoundry.org/version/3.100.0/index.html#the-app-object)
Note : `namespace` needs to exist before creating the app.
Expand All @@ -54,6 +55,12 @@ curl "http://localhost:9000/v3/apps/<app-guid>/relationships/current_droplet" \
-d '{"data":{"guid":"<droplet-guid>"}}'
```

#### [Start an app](https://v3-apidocs.cloudfoundry.org/version/3.100.0/index.html#start-an-app)
```bash
curl "http://localhost:9000/v3/apps/<app-guid>/actions/start" \
-X POST
```

### Packages

| Resource | Endpoint |
Expand Down
Loading

0 comments on commit 167a732

Please sign in to comment.