Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/runware_serverless_apps_builds.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Inspect application builds

### Synopsis

List and inspect code builds and container validations for a serverless application.
List, inspect, and delete code builds and container validations for a serverless application.

```
runware serverless apps builds [flags]
Expand All @@ -28,6 +28,7 @@ runware serverless apps builds [flags]
### SEE ALSO

* [runware serverless apps](runware_serverless_apps.md) - Manage deployed serverless applications
* [runware serverless apps builds delete](runware_serverless_apps_builds_delete.md) - Delete or cancel an application build
* [runware serverless apps builds list](runware_serverless_apps_builds_list.md) - List builds for a serverless application
* [runware serverless apps builds show](runware_serverless_apps_builds_show.md) - Show a build for a serverless application

50 changes: 50 additions & 0 deletions docs/runware_serverless_apps_builds_delete.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
## runware serverless apps builds delete

Delete or cancel an application build

### Synopsis

Cancel a queued or running build, or delete a terminal build.

Cancelling a queued or running build records it as superseded and ends its
current rollout without activating it, so any previous version keeps serving.
A terminal build can be deleted once no live rollout still needs it. Ready
builds remain while a version references them (409).

Confirmation is required unless --yes or --force is passed.

```
runware serverless apps builds delete <appId> <buildId> [flags]
```

### Examples

```
# cancel or delete a build (prompts for confirmation)
runware serverless apps builds delete my-app 33333333-3333-3333-3333-333333333333

# skip the confirmation prompt
runware serverless apps builds delete my-app 33333333-3333-3333-3333-333333333333 --yes
```

### Options

```
--force Skip the confirmation prompt
-h, --help help for delete
-y, --yes Skip the confirmation prompt
```

### Options inherited from parent commands

```
--debug Show full debug output
-F, --format string CLI output format: table, json, yaml (default "table")
--transport string Transport protocol: ws (WebSocket) or http (REST) (default "ws")
-v, --verbose Show request/response details
```

### SEE ALSO

* [runware serverless apps builds](runware_serverless_apps_builds.md) - Inspect application builds

3 changes: 2 additions & 1 deletion docs/runware_serverless_apps_versions.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Manage application versions

### Synopsis

List, inspect, and activate immutable versions of a serverless application.
List, inspect, activate, and delete immutable versions of a serverless application.

```
runware serverless apps versions [flags]
Expand All @@ -29,6 +29,7 @@ runware serverless apps versions [flags]

* [runware serverless apps](runware_serverless_apps.md) - Manage deployed serverless applications
* [runware serverless apps versions activate](runware_serverless_apps_versions_activate.md) - Activate a ready application version
* [runware serverless apps versions delete](runware_serverless_apps_versions_delete.md) - Delete an unused application version
* [runware serverless apps versions list](runware_serverless_apps_versions_list.md) - List versions of a serverless application
* [runware serverless apps versions show](runware_serverless_apps_versions_show.md) - Show a version of a serverless application

51 changes: 51 additions & 0 deletions docs/runware_serverless_apps_versions_delete.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
## runware serverless apps versions delete

Delete an unused application version

### Synopsis

Delete an unused version while retaining its immutable history.

Deleted versions are omitted from version lists, return 404 from version
reads, and cannot be activated. Returns 409 while the app is deleting, or
when the version is active, is the app's only remaining version, has a
non-stopped worker, or is targeted by a live rollout. This does not remove
the version's OCI image.

Confirmation is required unless --yes or --force is passed.

```
runware serverless apps versions delete <appId> <versionNumber> [flags]
```

### Examples

```
# delete an unused version (prompts for confirmation)
runware serverless apps versions delete my-app 2

# skip the confirmation prompt
runware serverless apps versions delete my-app 2 --yes
```

### Options

```
--force Skip the confirmation prompt
-h, --help help for delete
-y, --yes Skip the confirmation prompt
```

### Options inherited from parent commands

```
--debug Show full debug output
-F, --format string CLI output format: table, json, yaml (default "table")
--transport string Transport protocol: ws (WebSocket) or http (REST) (default "ws")
-v, --verbose Show request/response details
```

### SEE ALSO

* [runware serverless apps versions](runware_serverless_apps_versions.md) - Manage application versions

64 changes: 64 additions & 0 deletions internal/api/serverless/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -608,6 +608,37 @@ func (c *Client) GetVersion(ctx context.Context, appID string, versionNumber int
}
}

// DeleteVersion deletes an unused version. Returns 409 while the version is
// active, is the app's only remaining version, has a non-stopped worker, is
// targeted by a live rollout, or the app is deleting.
func (c *Client) DeleteVersion(ctx context.Context, appID string, versionNumber int32) error {
if c.apiKey == "" {
return transport.ErrNoAPIKey
}

resp, err := c.inner.DeleteVersionWithResponse(ctx, appID, versionNumber)
if err != nil {
return fmt.Errorf("delete version: %w", err)
}

c.logResponse(ctx, resp.HTTPResponse, nil)

switch resp.StatusCode() {
case http.StatusNoContent:
return nil
case http.StatusUnauthorized:
return problemToError(resp.ApplicationproblemJSON401, http.StatusUnauthorized)
case http.StatusForbidden:
return problemToError(resp.ApplicationproblemJSON403, http.StatusForbidden)
case http.StatusNotFound:
return problemToError(resp.ApplicationproblemJSON404, http.StatusNotFound)
case http.StatusConflict:
return problemToError(resp.ApplicationproblemJSON409, http.StatusConflict)
default:
return problemFromBody(resp.Body, resp.StatusCode())
}
}

// ListBuilds returns a page of builds for an app.
func (c *Client) ListBuilds(ctx context.Context, appID string, params *ListBuildsParams) (Page[Build], error) {
if c.apiKey == "" {
Expand Down Expand Up @@ -668,6 +699,39 @@ func (c *Client) GetBuild(ctx context.Context, appID string, buildID uuid.UUID)
}
}

// DeleteBuild cancels a queued or running build (recorded as superseded) or
// deletes a terminal build that no live version still needs. Returns 409 while
// a live version still references a ready build.
func (c *Client) DeleteBuild(ctx context.Context, appID string, buildID uuid.UUID) error {
if c.apiKey == "" {
return transport.ErrNoAPIKey
}

resp, err := c.inner.DeleteBuildWithResponse(ctx, appID, buildID)
if err != nil {
return fmt.Errorf("delete build: %w", err)
}

c.logResponse(ctx, resp.HTTPResponse, nil)

switch resp.StatusCode() {
case http.StatusNoContent:
return nil
case http.StatusUnauthorized:
return problemToError(resp.ApplicationproblemJSON401, http.StatusUnauthorized)
case http.StatusForbidden:
return problemToError(resp.ApplicationproblemJSON403, http.StatusForbidden)
case http.StatusNotFound:
return problemToError(resp.ApplicationproblemJSON404, http.StatusNotFound)
case http.StatusConflict:
return problemToError(resp.ApplicationproblemJSON409, http.StatusConflict)
case http.StatusUnprocessableEntity:
return problemToError(resp.ApplicationproblemJSON422, http.StatusUnprocessableEntity)
default:
return problemFromBody(resp.Body, resp.StatusCode())
}
}

// ListWorkers returns a page of workers for an app.
func (c *Client) ListWorkers(ctx context.Context, appID string, params *ListWorkersParams) (Page[Worker], error) {
if c.apiKey == "" {
Expand Down
134 changes: 134 additions & 0 deletions internal/api/serverless/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -888,6 +888,73 @@ func TestGetBuild_NoAPIKey(t *testing.T) {
}
}

func TestDeleteBuild(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
want := "/v1/apps/" + testAppID + "/builds/" + testBuildID
if r.Method != http.MethodDelete || r.URL.Path != want {
t.Errorf("unexpected %s %s", r.Method, r.URL.Path)
}
w.WriteHeader(http.StatusNoContent)
}))
defer srv.Close()

c := newClient("test-key", srv.URL, slog.Default(), srv.Client())
if err := c.DeleteBuild(context.Background(), testAppID, uuid.MustParse(testBuildID)); err != nil {
t.Fatalf("DeleteBuild: %v", err)
}
}

func TestDeleteBuild_NotFound(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/problem+json")
w.WriteHeader(http.StatusNotFound)
_, _ = w.Write([]byte(`{"type":"about:blank","title":"Not Found","status":404,"detail":"No build exists"}`))
}))
defer srv.Close()

c := newClient("test-key", srv.URL, slog.Default(), srv.Client())
err := c.DeleteBuild(context.Background(), testAppID, uuid.MustParse(testBuildID))
var re *transport.RunwareError
if !errors.As(err, &re) {
t.Fatalf("expected *transport.RunwareError, got %T: %v", err, err)
}
if re.StatusCode != http.StatusNotFound {
t.Errorf("expected status 404, got %d", re.StatusCode)
}
}

func TestDeleteBuild_Conflict(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/problem+json")
w.WriteHeader(http.StatusConflict)
_, _ = w.Write([]byte(`{"type":"about:blank","title":"Conflict","status":409,"detail":"Build is still referenced by a live version"}`))
}))
defer srv.Close()

c := newClient("test-key", srv.URL, slog.Default(), srv.Client())
err := c.DeleteBuild(context.Background(), testAppID, uuid.MustParse(testBuildID))
var re *transport.RunwareError
if !errors.As(err, &re) {
t.Fatalf("expected *transport.RunwareError, got %T: %v", err, err)
}
if re.Code != transport.CodeValidation {
t.Errorf("expected CodeValidation, got %v", re.Code)
}
if re.StatusCode != http.StatusConflict {
t.Errorf("expected status 409, got %d", re.StatusCode)
}
if re.Message != "Build is still referenced by a live version" {
t.Errorf("unexpected message: %q", re.Message)
}
}

func TestDeleteBuild_NoAPIKey(t *testing.T) {
c := NewClient("", "https://example.invalid", slog.Default())
if err := c.DeleteBuild(context.Background(), testAppID, uuid.MustParse(testBuildID)); !errors.Is(err, transport.ErrNoAPIKey) {
t.Fatalf("expected ErrNoAPIKey, got %v", err)
}
}

func TestListVersions(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
want := "/v1/apps/" + testAppID + "/versions"
Expand Down Expand Up @@ -971,6 +1038,73 @@ func TestGetVersion_NoAPIKey(t *testing.T) {
}
}

func TestDeleteVersion(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
want := fmt.Sprintf("/v1/apps/%s/versions/%d", testAppID, testVersionNumber)
if r.Method != http.MethodDelete || r.URL.Path != want {
t.Errorf("unexpected %s %s", r.Method, r.URL.Path)
}
w.WriteHeader(http.StatusNoContent)
}))
defer srv.Close()

c := newClient("test-key", srv.URL, slog.Default(), srv.Client())
if err := c.DeleteVersion(context.Background(), testAppID, testVersionNumber); err != nil {
t.Fatalf("DeleteVersion: %v", err)
}
}

func TestDeleteVersion_NotFound(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/problem+json")
w.WriteHeader(http.StatusNotFound)
_, _ = w.Write([]byte(`{"type":"about:blank","title":"Not Found","status":404,"detail":"No version exists"}`))
}))
defer srv.Close()

c := newClient("test-key", srv.URL, slog.Default(), srv.Client())
err := c.DeleteVersion(context.Background(), testAppID, 99999)
var re *transport.RunwareError
if !errors.As(err, &re) {
t.Fatalf("expected *transport.RunwareError, got %T: %v", err, err)
}
if re.StatusCode != http.StatusNotFound {
t.Errorf("expected status 404, got %d", re.StatusCode)
}
}

func TestDeleteVersion_Conflict(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/problem+json")
w.WriteHeader(http.StatusConflict)
_, _ = w.Write([]byte(`{"type":"about:blank","title":"Conflict","status":409,"detail":"Version is active"}`))
}))
defer srv.Close()

c := newClient("test-key", srv.URL, slog.Default(), srv.Client())
err := c.DeleteVersion(context.Background(), testAppID, testVersionNumber)
var re *transport.RunwareError
if !errors.As(err, &re) {
t.Fatalf("expected *transport.RunwareError, got %T: %v", err, err)
}
if re.Code != transport.CodeValidation {
t.Errorf("expected CodeValidation, got %v", re.Code)
}
if re.StatusCode != http.StatusConflict {
t.Errorf("expected status 409, got %d", re.StatusCode)
}
if re.Message != "Version is active" {
t.Errorf("unexpected message: %q", re.Message)
}
}

func TestDeleteVersion_NoAPIKey(t *testing.T) {
c := NewClient("", "https://example.invalid", slog.Default())
if err := c.DeleteVersion(context.Background(), testAppID, testVersionNumber); !errors.Is(err, transport.ErrNoAPIKey) {
t.Fatalf("expected ErrNoAPIKey, got %v", err)
}
}

func TestDeployVersion(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
want := "/v1/apps/" + testAppID + "/deploy"
Expand Down
Loading