diff --git a/api/models/app.go b/api/models/app.go index 30f7f48c..b0fe7070 100644 --- a/api/models/app.go +++ b/api/models/app.go @@ -8,16 +8,17 @@ import ( type Apps []*App var ( - ErrAppsCreate = errors.New("Could not create app") - ErrAppsUpdate = errors.New("Could not update app") - ErrAppsRemoving = errors.New("Could not remove app from datastore") - ErrAppsGet = errors.New("Could not get app from datastore") - ErrAppsList = errors.New("Could not list apps from datastore") - ErrAppsAlreadyExists = errors.New("App already exists") - ErrAppsNotFound = errors.New("App not found") - ErrAppsNothingToUpdate = errors.New("Nothing to update") - ErrAppsMissingNew = errors.New("Missing new application") - ErrUsableImage = errors.New("Image not found") + ErrAppsAlreadyExists = errors.New("App already exists") + ErrAppsCreate = errors.New("Could not create app") + ErrAppsGet = errors.New("Could not get app from datastore") + ErrAppsList = errors.New("Could not list apps from datastore") + ErrAppsMissingNew = errors.New("Missing new application") + ErrAppsNotFound = errors.New("App not found") + ErrAppsNothingToUpdate = errors.New("Nothing to update") + ErrAppsRemoving = errors.New("Could not remove app from datastore") + ErrDeleteAppsWithRoutes = errors.New("Cannot remove apps with routes") + ErrAppsUpdate = errors.New("Could not update app") + ErrUsableImage = errors.New("Image not found") ) type App struct { diff --git a/api/server/apps_delete.go b/api/server/apps_delete.go index 302a38e0..9f591fc8 100644 --- a/api/server/apps_delete.go +++ b/api/server/apps_delete.go @@ -14,13 +14,25 @@ func handleAppDelete(c *gin.Context) { log := common.Logger(ctx) appName := c.Param("app") - err := Api.Datastore.RemoveApp(appName) + routes, err := Api.Datastore.GetRoutesByApp(appName, &models.RouteFilter{}) if err != nil { log.WithError(err).Debug(models.ErrAppsRemoving) c.JSON(http.StatusInternalServerError, simpleError(models.ErrAppsRemoving)) return } + if len(routes) > 0 { + log.WithError(err).Debug(models.ErrDeleteAppsWithRoutes) + c.JSON(http.StatusBadRequest, simpleError(models.ErrDeleteAppsWithRoutes)) + return + } + + if err := Api.Datastore.RemoveApp(appName); err != nil { + log.WithError(err).Debug(models.ErrAppsRemoving) + c.JSON(http.StatusInternalServerError, simpleError(models.ErrAppsRemoving)) + return + } + c.JSON(http.StatusOK, gin.H{"message": "App deleted"}) } diff --git a/api/server/server_test.go b/api/server/server_test.go index 30db340c..55070cf3 100644 --- a/api/server/server_test.go +++ b/api/server/server_test.go @@ -97,33 +97,36 @@ func TestFullStack(t *testing.T) { router := testRouter(ds, &mqs.Mock{}, testRunner(t), tasks) - for i, test := range []struct { + for _, test := range []struct { + name string method string path string body string expectedCode int }{ - {"POST", "/v1/apps", `{ "app": { "name": "myapp" } }`, http.StatusCreated}, - {"GET", "/v1/apps", ``, http.StatusOK}, - {"GET", "/v1/apps/myapp", ``, http.StatusOK}, - {"POST", "/v1/apps/myapp/routes", `{ "route": { "name": "myroute", "path": "/myroute", "image": "iron/hello" } }`, http.StatusCreated}, - {"POST", "/v1/apps/myapp/routes", `{ "route": { "name": "myroute2", "path": "/myroute2", "image": "iron/error" } }`, http.StatusCreated}, - {"GET", "/v1/apps/myapp/routes/myroute", ``, http.StatusOK}, - {"GET", "/v1/apps/myapp/routes/myroute2", ``, http.StatusOK}, - {"GET", "/v1/apps/myapp/routes", ``, http.StatusOK}, - {"POST", "/r/myapp/myroute", `{ "name": "Teste" }`, http.StatusOK}, - {"POST", "/r/myapp/myroute2", `{ "name": "Teste" }`, http.StatusInternalServerError}, - {"DELETE", "/v1/apps/myapp/routes/myroute", ``, http.StatusOK}, - {"DELETE", "/v1/apps/myapp", ``, http.StatusOK}, - {"GET", "/v1/apps/myapp", ``, http.StatusNotFound}, - {"GET", "/v1/apps/myapp/routes/myroute", ``, http.StatusInternalServerError}, + {"create my app", "POST", "/v1/apps", `{ "app": { "name": "myapp" } }`, http.StatusCreated}, + {"list apps", "GET", "/v1/apps", ``, http.StatusOK}, + {"get app", "GET", "/v1/apps/myapp", ``, http.StatusOK}, + {"add myroute", "POST", "/v1/apps/myapp/routes", `{ "route": { "name": "myroute", "path": "/myroute", "image": "iron/hello" } }`, http.StatusCreated}, + {"add myroute2", "POST", "/v1/apps/myapp/routes", `{ "route": { "name": "myroute2", "path": "/myroute2", "image": "iron/error" } }`, http.StatusCreated}, + {"get myroute", "GET", "/v1/apps/myapp/routes/myroute", ``, http.StatusOK}, + {"get myroute2", "GET", "/v1/apps/myapp/routes/myroute2", ``, http.StatusOK}, + {"get all routes", "GET", "/v1/apps/myapp/routes", ``, http.StatusOK}, + {"execute myroute", "POST", "/r/myapp/myroute", `{ "name": "Teste" }`, http.StatusOK}, + {"execute myroute2", "POST", "/r/myapp/myroute2", `{ "name": "Teste" }`, http.StatusInternalServerError}, + {"delete myroute", "DELETE", "/v1/apps/myapp/routes/myroute", ``, http.StatusOK}, + {"delete app (fail)", "DELETE", "/v1/apps/myapp", ``, http.StatusBadRequest}, + {"delete myroute2", "DELETE", "/v1/apps/myapp/routes/myroute2", ``, http.StatusOK}, + {"delete app (success)", "DELETE", "/v1/apps/myapp", ``, http.StatusOK}, + {"get deleted app", "GET", "/v1/apps/myapp", ``, http.StatusNotFound}, + {"get delete route on deleted app", "GET", "/v1/apps/myapp/routes/myroute", ``, http.StatusInternalServerError}, } { _, rec := routerRequest(t, router, test.method, test.path, bytes.NewBuffer([]byte(test.body))) if rec.Code != test.expectedCode { t.Log(buf.String()) - t.Errorf("Test %d: Expected status code to be %d but was %d", - i, test.expectedCode, rec.Code) + t.Errorf("Test \"%s\": Expected status code to be %d but was %d", + test.name, test.expectedCode, rec.Code) } } } diff --git a/docs/swagger.yml b/docs/swagger.yml index 7e741de5..da2413f8 100644 --- a/docs/swagger.yml +++ b/docs/swagger.yml @@ -6,7 +6,7 @@ swagger: '2.0' info: title: IronFunctions description: The open source serverless platform. - version: "0.1.16" + version: "0.1.17" # the domain of the service host: "127.0.0.1:8080" # array of all schemes that your API supports @@ -67,6 +67,20 @@ paths: /apps/{app}: + delete: + summary: "Delete an app." + description: "Delete an app." + tags: + - Apps + parameters: + - name: app + in: path + description: Name of the app. + required: true + type: string + responses: + 200: + description: Apps successfully deleted. get: summary: "Get information for a app." description: "This gives more details about a app, such as statistics." @@ -271,7 +285,7 @@ paths: type: string responses: 200: - description: Route successfully deleted. Deletion succeeds even on routes that do not exist. + description: Route successfully deleted. /tasks: get: diff --git a/fn/apps.go b/fn/apps.go index a60f0a78..eba5043d 100644 --- a/fn/apps.go +++ b/fn/apps.go @@ -4,6 +4,7 @@ import ( "encoding/json" "errors" "fmt" + "net/http" "os" "text/tabwriter" @@ -79,6 +80,11 @@ func apps() cli.Command { }, }, }, + { + Name: "delete", + Usage: "delete an app", + Action: a.delete, + }, }, } } @@ -249,3 +255,26 @@ func (a *appsCmd) storeApp(appName string, config map[string]string) error { } return nil } + +func (a *appsCmd) delete(c *cli.Context) error { + appName := c.Args().First() + if appName == "" { + return errors.New("error: deleting an app takes one argument, an app name") + } + + if err := resetBasePath(a.Configuration); err != nil { + return fmt.Errorf("error setting endpoint: %v", err) + } + + resp, err := a.AppsAppDelete(appName) + if err != nil { + return fmt.Errorf("error deleting app: %v", err) + } + + if resp.StatusCode == http.StatusBadRequest { + return errors.New("could not delete this application - pending routes") + } + + fmt.Println(appName, "deleted") + return nil +}