fix(deploy): report removed services as removed (#7)
Closes #2 ## Summary `gordon apps deploy` and `gordon apps remove` report a retired service as `removed` instead of `deployed`. - New `domain.AppServiceRemoved` result. - The admin deploy response maps succeeded `service.<name>.remove` steps to `removed` and leaves their `effective_revision` empty, since the input revision no longer declares the service. - `ComputeOutcome` counts `removed` as success; the `apps remove` lifecycle path uses the new constant. - The CLI prints `runner: removed` without a trailing revision. - Docs mention the `removed` result for deploy and remove. ## Notes `result: "removed"` is a new value for JSON clients. `stop` still reports `deployed` (out of scope). ## Checks - `go test ./...` - `golangci-lint run ./...` Reviewed-on: https://git.bnema.dev/bnema/gordon/pulls/7
- commit
- 78f9cbcaa14b0b0b43ec7cbeea10ecb3bfe7cc61
- author
- bnema · · 2026-10-02 13:23 UTC
- parent
- 598d46d
- signature
- unsigned
10 files · +32 -9
M docs/cli/apps.md +4 -1
| @@ -272,6 +272,8 @@ new image behind the same tag (for example `latest`) has a new digest and is | |||
| 272 | 272 | deployed. A service already running with all of these unchanged keeps its | |
| 273 | 273 | container and is reported `unchanged`. Services with host binds or devices | |
| 274 | 274 | are always replaced, so bind and device policy changes apply on deploy. | |
| 275 | + | A service the revision no longer declares is retired during deploy and | |
| 276 | + | reported `removed`; its volumes and secrets are retained. | |
| 275 | 277 | ||
| 276 | 278 | Fail-fast across services: | |
| 277 | 279 | the first failure stops the deploy, successful services are preserved, | |
| @@ -331,7 +333,8 @@ gordon apps remove APP [--json] | |||
| 331 | 333 | ||
| 332 | 334 | Withdraws workloads. Volumes and secrets are retained as owned orphans | |
| 333 | 335 | under the old internal UUID; removing frees the name but never implicitly | |
| 334 | - | attaches retained resources to a new app reusing the name. | |
| 336 | + | attaches retained resources to a new app reusing the name. Each withdrawn | |
| 337 | + | service is reported `removed`. | |
| 335 | 338 | ||
| 336 | 339 | There is deliberately no `purge`: destructive volume deletion requires a | |
| 337 | 340 | separately accepted destructive-action contract. | |
| @@ -272,6 +272,8 @@ new image behind the same tag (for example `latest`) has a new digest and is | |||||
| 272 | deployed. A service already running with all of these unchanged keeps its | 272 | deployed. A service already running with all of these unchanged keeps its | ||
| 273 | container and is reported `unchanged`. Services with host binds or devices | 273 | container and is reported `unchanged`. Services with host binds or devices | ||
| 274 | are always replaced, so bind and device policy changes apply on deploy. | 274 | are always replaced, so bind and device policy changes apply on deploy. | ||
| 275 | + | A service the revision no longer declares is retired during deploy and | |||
| 276 | + | reported `removed`; its volumes and secrets are retained. | |||
| 275 | 277 | ||||
| 276 | Fail-fast across services: | 278 | Fail-fast across services: | ||
| 277 | the first failure stops the deploy, successful services are preserved, | 279 | the first failure stops the deploy, successful services are preserved, | ||
| @@ -331,7 +333,8 @@ gordon apps remove APP [--json] | |||||
| 331 | 333 | ||||
| 332 | Withdraws workloads. Volumes and secrets are retained as owned orphans | 334 | Withdraws workloads. Volumes and secrets are retained as owned orphans | ||
| 333 | under the old internal UUID; removing frees the name but never implicitly | 335 | under the old internal UUID; removing frees the name but never implicitly | ||
| 334 | - | attaches retained resources to a new app reusing the name. | 336 | + | attaches retained resources to a new app reusing the name. Each withdrawn |
| 337 | + | service is reported `removed`. | |||
| 335 | 338 | ||||
| 336 | There is deliberately no `purge`: destructive volume deletion requires a | 339 | There is deliberately no `purge`: destructive volume deletion requires a | ||
| 337 | separately accepted destructive-action contract. | 340 | separately accepted destructive-action contract. | ||
M internal/adapters/in/cli/apps.go +1 -1
| @@ -1000,7 +1000,7 @@ func renderDeployServices(out io.Writer, resp *dto.AppDeployResponse) error { | |||
| 1000 | 1000 | sort.Strings(names) | |
| 1001 | 1001 | for _, name := range names { | |
| 1002 | 1002 | svc := resp.Services[name] | |
| 1003 | - | detail := svc.Result + " " + svc.EffectiveRevision | |
| 1003 | + | detail := strings.TrimSpace(svc.Result + " " + svc.EffectiveRevision) | |
| 1004 | 1004 | if svc.Result == domain.AppServiceUnchanged { | |
| 1005 | 1005 | detail += " (already running this image, config, and secrets)" | |
| 1006 | 1006 | } | |
| @@ -1000,7 +1000,7 @@ func renderDeployServices(out io.Writer, resp *dto.AppDeployResponse) error { | |||||
| 1000 | sort.Strings(names) | 1000 | sort.Strings(names) | ||
| 1001 | for _, name := range names { | 1001 | for _, name := range names { | ||
| 1002 | svc := resp.Services[name] | 1002 | svc := resp.Services[name] | ||
| 1003 | - | detail := svc.Result + " " + svc.EffectiveRevision | 1003 | + | detail := strings.TrimSpace(svc.Result + " " + svc.EffectiveRevision) |
| 1004 | if svc.Result == domain.AppServiceUnchanged { | 1004 | if svc.Result == domain.AppServiceUnchanged { | ||
| 1005 | detail += " (already running this image, config, and secrets)" | 1005 | detail += " (already running this image, config, and secrets)" | ||
| 1006 | } | 1006 | } | ||
M internal/adapters/in/cli/apps_test.go +5 -2
| @@ -24,6 +24,7 @@ import ( | |||
| 24 | 24 | "github.com/bnema/gordon/internal/adapters/dto" | |
| 25 | 25 | climocks "github.com/bnema/gordon/internal/adapters/in/cli/mocks" | |
| 26 | 26 | "github.com/bnema/gordon/internal/adapters/in/cli/remote" | |
| 27 | + | "github.com/bnema/gordon/internal/domain" | |
| 27 | 28 | ) | |
| 28 | 29 | ||
| 29 | 30 | // appPlane returns the generated control plane mock for one test. | |
| @@ -277,8 +278,9 @@ func TestRenderAppDeployResponse_SortedServices(t *testing.T) { | |||
| 277 | 278 | resp := &dto.AppDeployResponse{ | |
| 278 | 279 | Op: "op-1", App: "blog", Outcome: "partial", | |
| 279 | 280 | Services: map[string]dto.AppServiceResultDTO{ | |
| 280 | - | "web": {Result: "deployed", EffectiveRevision: "rev-b"}, | |
| 281 | - | "db": {Result: "failed", EffectiveRevision: "rev-a", Error: "nope", RestartUnsafe: true}, | |
| 281 | + | "web": {Result: "deployed", EffectiveRevision: "rev-b"}, | |
| 282 | + | "db": {Result: "failed", EffectiveRevision: "rev-a", Error: "nope", RestartUnsafe: true}, | |
| 283 | + | "runner": {Result: domain.AppServiceRemoved}, | |
| 282 | 284 | }, | |
| 283 | 285 | CleanupWarnings: []dto.AppCleanupWarningDTO{{Service: "web", Leftover: "ctr-old", Detail: "retire failed"}}, | |
| 284 | 286 | Effective: &dto.AppEffectiveDTO{Services: map[string]string{"web": "rev-b", "db": "rev-a"}}, | |
| @@ -289,6 +291,7 @@ func TestRenderAppDeployResponse_SortedServices(t *testing.T) { | |||
| 289 | 291 | text := out.String() | |
| 290 | 292 | assert.Less(t, strings.Index(text, "db:"), strings.Index(text, "web:")) | |
| 291 | 293 | assert.Contains(t, text, "restart_unsafe") | |
| 294 | + | assert.Contains(t, text, "runner: removed\n") | |
| 292 | 295 | assert.Contains(t, text, "cleanup:") | |
| 293 | 296 | assert.Contains(t, text, "blog-db") | |
| 294 | 297 | } | |
| @@ -24,6 +24,7 @@ import ( | |||||
| 24 | "github.com/bnema/gordon/internal/adapters/dto" | 24 | "github.com/bnema/gordon/internal/adapters/dto" | ||
| 25 | climocks "github.com/bnema/gordon/internal/adapters/in/cli/mocks" | 25 | climocks "github.com/bnema/gordon/internal/adapters/in/cli/mocks" | ||
| 26 | "github.com/bnema/gordon/internal/adapters/in/cli/remote" | 26 | "github.com/bnema/gordon/internal/adapters/in/cli/remote" | ||
| 27 | + | "github.com/bnema/gordon/internal/domain" | |||
| 27 | ) | 28 | ) | ||
| 28 | 29 | ||||
| 29 | // appPlane returns the generated control plane mock for one test. | 30 | // appPlane returns the generated control plane mock for one test. | ||
| @@ -277,8 +278,9 @@ func TestRenderAppDeployResponse_SortedServices(t *testing.T) { | |||||
| 277 | resp := &dto.AppDeployResponse{ | 278 | resp := &dto.AppDeployResponse{ | ||
| 278 | Op: "op-1", App: "blog", Outcome: "partial", | 279 | Op: "op-1", App: "blog", Outcome: "partial", | ||
| 279 | Services: map[string]dto.AppServiceResultDTO{ | 280 | Services: map[string]dto.AppServiceResultDTO{ | ||
| 280 | - | "web": {Result: "deployed", EffectiveRevision: "rev-b"}, | 281 | + | "web": {Result: "deployed", EffectiveRevision: "rev-b"}, |
| 281 | - | "db": {Result: "failed", EffectiveRevision: "rev-a", Error: "nope", RestartUnsafe: true}, | 282 | + | "db": {Result: "failed", EffectiveRevision: "rev-a", Error: "nope", RestartUnsafe: true}, |
| 283 | + | "runner": {Result: domain.AppServiceRemoved}, | |||
| 282 | }, | 284 | }, | ||
| 283 | CleanupWarnings: []dto.AppCleanupWarningDTO{{Service: "web", Leftover: "ctr-old", Detail: "retire failed"}}, | 285 | CleanupWarnings: []dto.AppCleanupWarningDTO{{Service: "web", Leftover: "ctr-old", Detail: "retire failed"}}, | ||
| 284 | Effective: &dto.AppEffectiveDTO{Services: map[string]string{"web": "rev-b", "db": "rev-a"}}, | 286 | Effective: &dto.AppEffectiveDTO{Services: map[string]string{"web": "rev-b", "db": "rev-a"}}, | ||
| @@ -289,6 +291,7 @@ func TestRenderAppDeployResponse_SortedServices(t *testing.T) { | |||||
| 289 | text := out.String() | 291 | text := out.String() | ||
| 290 | assert.Less(t, strings.Index(text, "db:"), strings.Index(text, "web:")) | 292 | assert.Less(t, strings.Index(text, "db:"), strings.Index(text, "web:")) | ||
| 291 | assert.Contains(t, text, "restart_unsafe") | 293 | assert.Contains(t, text, "restart_unsafe") | ||
| 294 | + | assert.Contains(t, text, "runner: removed\n") | |||
| 292 | assert.Contains(t, text, "cleanup:") | 295 | assert.Contains(t, text, "cleanup:") | ||
| 293 | assert.Contains(t, text, "blog-db") | 296 | assert.Contains(t, text, "blog-db") | ||
| 294 | } | 297 | } | ||
M internal/adapters/in/http/admin/handler_apps.go +7 -2
| @@ -602,11 +602,14 @@ func toAppDeployResponse(app string, op *domain.AppOperation, includeDiagnostics | |||
| 602 | 602 | if !ok { | |
| 603 | 603 | continue | |
| 604 | 604 | } | |
| 605 | - | service, _, _ = strings.Cut(service, ".") | |
| 605 | + | service, action, _ := strings.Cut(service, ".") | |
| 606 | 606 | entry := resp.Services[service] | |
| 607 | 607 | switch step.State { | |
| 608 | 608 | case domain.AppStepSucceeded: | |
| 609 | 609 | entry.Result = "deployed" | |
| 610 | + | if action == "remove" { | |
| 611 | + | entry.Result = domain.AppServiceRemoved | |
| 612 | + | } | |
| 610 | 613 | if strings.HasPrefix(step.Detail, domain.AppServiceUnchanged+":") { | |
| 611 | 614 | entry.Result = domain.AppServiceUnchanged | |
| 612 | 615 | } | |
| @@ -618,7 +621,9 @@ func toAppDeployResponse(app string, op *domain.AppOperation, includeDiagnostics | |||
| 618 | 621 | } | |
| 619 | 622 | entry.Before = step.Before | |
| 620 | 623 | entry.After = step.After | |
| 621 | - | if entry.EffectiveRevision == "" { | |
| 624 | + | // A removed service has no effective revision: the input revision | |
| 625 | + | // no longer declares it. | |
| 626 | + | if entry.EffectiveRevision == "" && entry.Result != domain.AppServiceRemoved { | |
| 622 | 627 | entry.EffectiveRevision = op.InputRevision | |
| 623 | 628 | } | |
| 624 | 629 | resp.Services[service] = entry | |
| @@ -602,11 +602,14 @@ func toAppDeployResponse(app string, op *domain.AppOperation, includeDiagnostics | |||||
| 602 | if !ok { | 602 | if !ok { | ||
| 603 | continue | 603 | continue | ||
| 604 | } | 604 | } | ||
| 605 | - | service, _, _ = strings.Cut(service, ".") | 605 | + | service, action, _ := strings.Cut(service, ".") |
| 606 | entry := resp.Services[service] | 606 | entry := resp.Services[service] | ||
| 607 | switch step.State { | 607 | switch step.State { | ||
| 608 | case domain.AppStepSucceeded: | 608 | case domain.AppStepSucceeded: | ||
| 609 | entry.Result = "deployed" | 609 | entry.Result = "deployed" | ||
| 610 | + | if action == "remove" { | |||
| 611 | + | entry.Result = domain.AppServiceRemoved | |||
| 612 | + | } | |||
| 610 | if strings.HasPrefix(step.Detail, domain.AppServiceUnchanged+":") { | 613 | if strings.HasPrefix(step.Detail, domain.AppServiceUnchanged+":") { | ||
| 611 | entry.Result = domain.AppServiceUnchanged | 614 | entry.Result = domain.AppServiceUnchanged | ||
| 612 | } | 615 | } | ||
| @@ -618,7 +621,9 @@ func toAppDeployResponse(app string, op *domain.AppOperation, includeDiagnostics | |||||
| 618 | } | 621 | } | ||
| 619 | entry.Before = step.Before | 622 | entry.Before = step.Before | ||
| 620 | entry.After = step.After | 623 | entry.After = step.After | ||
| 621 | - | if entry.EffectiveRevision == "" { | 624 | + | // A removed service has no effective revision: the input revision |
| 625 | + | // no longer declares it. | |||
| 626 | + | if entry.EffectiveRevision == "" && entry.Result != domain.AppServiceRemoved { | |||
| 622 | entry.EffectiveRevision = op.InputRevision | 627 | entry.EffectiveRevision = op.InputRevision | ||
| 623 | } | 628 | } | ||
| 624 | resp.Services[service] = entry | 629 | resp.Services[service] = entry | ||
M internal/adapters/in/http/admin/handler_apps_test.go +4 -0
| @@ -128,6 +128,7 @@ func TestHandler_AppDeploy_MapsOp(t *testing.T) { | |||
| 128 | 128 | Steps: []domain.AppOperationStep{ | |
| 129 | 129 | {ID: "preflight", State: domain.AppStepSucceeded}, | |
| 130 | 130 | {ID: "service.web.replace", State: domain.AppStepSucceeded, Before: "c-old", After: "c-new"}, | |
| 131 | + | {ID: "service.runner.remove", State: domain.AppStepSucceeded, Before: "c-runner"}, | |
| 131 | 132 | }, | |
| 132 | 133 | } | |
| 133 | 134 | appSvc.EXPECT().Deploy(mock.Anything, "blog", "", "", false, "test-operation-key").Return(op, nil).Once() | |
| @@ -147,6 +148,9 @@ func TestHandler_AppDeploy_MapsOp(t *testing.T) { | |||
| 147 | 148 | require.Contains(t, resp.Services, "web") | |
| 148 | 149 | assert.Equal(t, "deployed", resp.Services["web"].Result) | |
| 149 | 150 | assert.Equal(t, "c-new", resp.Services["web"].After) | |
| 151 | + | require.Contains(t, resp.Services, "runner") | |
| 152 | + | assert.Equal(t, domain.AppServiceRemoved, resp.Services["runner"].Result) | |
| 153 | + | assert.Empty(t, resp.Services["runner"].EffectiveRevision, "a removed service has no effective revision") | |
| 150 | 154 | // Effective and retained state comes from the app read model, never | |
| 151 | 155 | // from the journal outcome. | |
| 152 | 156 | require.NotNil(t, resp.Effective) | |
| @@ -128,6 +128,7 @@ func TestHandler_AppDeploy_MapsOp(t *testing.T) { | |||||
| 128 | Steps: []domain.AppOperationStep{ | 128 | Steps: []domain.AppOperationStep{ | ||
| 129 | {ID: "preflight", State: domain.AppStepSucceeded}, | 129 | {ID: "preflight", State: domain.AppStepSucceeded}, | ||
| 130 | {ID: "service.web.replace", State: domain.AppStepSucceeded, Before: "c-old", After: "c-new"}, | 130 | {ID: "service.web.replace", State: domain.AppStepSucceeded, Before: "c-old", After: "c-new"}, | ||
| 131 | + | {ID: "service.runner.remove", State: domain.AppStepSucceeded, Before: "c-runner"}, | |||
| 131 | }, | 132 | }, | ||
| 132 | } | 133 | } | ||
| 133 | appSvc.EXPECT().Deploy(mock.Anything, "blog", "", "", false, "test-operation-key").Return(op, nil).Once() | 134 | appSvc.EXPECT().Deploy(mock.Anything, "blog", "", "", false, "test-operation-key").Return(op, nil).Once() | ||
| @@ -147,6 +148,9 @@ func TestHandler_AppDeploy_MapsOp(t *testing.T) { | |||||
| 147 | require.Contains(t, resp.Services, "web") | 148 | require.Contains(t, resp.Services, "web") | ||
| 148 | assert.Equal(t, "deployed", resp.Services["web"].Result) | 149 | assert.Equal(t, "deployed", resp.Services["web"].Result) | ||
| 149 | assert.Equal(t, "c-new", resp.Services["web"].After) | 150 | assert.Equal(t, "c-new", resp.Services["web"].After) | ||
| 151 | + | require.Contains(t, resp.Services, "runner") | |||
| 152 | + | assert.Equal(t, domain.AppServiceRemoved, resp.Services["runner"].Result) | |||
| 153 | + | assert.Empty(t, resp.Services["runner"].EffectiveRevision, "a removed service has no effective revision") | |||
| 150 | // Effective and retained state comes from the app read model, never | 154 | // Effective and retained state comes from the app read model, never | ||
| 151 | // from the journal outcome. | 155 | // from the journal outcome. | ||
| 152 | require.NotNil(t, resp.Effective) | 156 | require.NotNil(t, resp.Effective) | ||
M internal/domain/app_state.go +4 -0
| @@ -35,6 +35,10 @@ const ( | |||
| 35 | 35 | // A succeeded step whose Detail starts with it reports this result. | |
| 36 | 36 | const AppServiceUnchanged = "unchanged" | |
| 37 | 37 | ||
| 38 | + | // AppServiceRemoved is the result of a step that retired a service and its | |
| 39 | + | // container. A succeeded "service.<name>.remove" step reports this result. | |
| 40 | + | const AppServiceRemoved = "removed" | |
| 41 | + | ||
| 38 | 42 | // App operation outcomes over terminal per-service results. | |
| 39 | 43 | const ( | |
| 40 | 44 | AppOutcomeSuccess = "success" | |
| @@ -35,6 +35,10 @@ const ( | |||||
| 35 | // A succeeded step whose Detail starts with it reports this result. | 35 | // A succeeded step whose Detail starts with it reports this result. | ||
| 36 | const AppServiceUnchanged = "unchanged" | 36 | const AppServiceUnchanged = "unchanged" | ||
| 37 | 37 | ||||
| 38 | + | // AppServiceRemoved is the result of a step that retired a service and its | |||
| 39 | + | // container. A succeeded "service.<name>.remove" step reports this result. | |||
| 40 | + | const AppServiceRemoved = "removed" | |||
| 41 | + | ||||
| 38 | // App operation outcomes over terminal per-service results. | 42 | // App operation outcomes over terminal per-service results. | ||
| 39 | const ( | 43 | const ( | ||
| 40 | AppOutcomeSuccess = "success" | 44 | AppOutcomeSuccess = "success" | ||
M internal/usecase/deployment/lifecycle.go +2 -2
| @@ -505,9 +505,9 @@ func (s *Service) removeServiceContainers(ctx context.Context, app, opID string, | |||
| 505 | 505 | if !retired.Gone { | |
| 506 | 506 | return nil, fmt.Errorf("deployment: remove %q/%q: container %s not confirmed gone: %s", app, name, container, cleanupDetail(retired)) | |
| 507 | 507 | } | |
| 508 | - | result.Services[name] = ServiceResult{Result: "deployed", Before: container, After: "", CleanupWarnings: retired.Warnings} | |
| 508 | + | result.Services[name] = ServiceResult{Result: domain.AppServiceRemoved, Before: container, After: "", CleanupWarnings: retired.Warnings} | |
| 509 | 509 | } else { | |
| 510 | - | result.Services[name] = ServiceResult{Result: "deployed", Before: container, After: ""} | |
| 510 | + | result.Services[name] = ServiceResult{Result: domain.AppServiceRemoved, Before: container, After: ""} | |
| 511 | 511 | } | |
| 512 | 512 | step.State = domain.AppStepSucceeded | |
| 513 | 513 | steps = append(steps, step) | |
| @@ -505,9 +505,9 @@ func (s *Service) removeServiceContainers(ctx context.Context, app, opID string, | |||||
| 505 | if !retired.Gone { | 505 | if !retired.Gone { | ||
| 506 | return nil, fmt.Errorf("deployment: remove %q/%q: container %s not confirmed gone: %s", app, name, container, cleanupDetail(retired)) | 506 | return nil, fmt.Errorf("deployment: remove %q/%q: container %s not confirmed gone: %s", app, name, container, cleanupDetail(retired)) | ||
| 507 | } | 507 | } | ||
| 508 | - | result.Services[name] = ServiceResult{Result: "deployed", Before: container, After: "", CleanupWarnings: retired.Warnings} | 508 | + | result.Services[name] = ServiceResult{Result: domain.AppServiceRemoved, Before: container, After: "", CleanupWarnings: retired.Warnings} |
| 509 | } else { | 509 | } else { | ||
| 510 | - | result.Services[name] = ServiceResult{Result: "deployed", Before: container, After: ""} | 510 | + | result.Services[name] = ServiceResult{Result: domain.AppServiceRemoved, Before: container, After: ""} |
| 511 | } | 511 | } | ||
| 512 | step.State = domain.AppStepSucceeded | 512 | step.State = domain.AppStepSucceeded | ||
| 513 | steps = append(steps, step) | 513 | steps = append(steps, step) | ||
M internal/usecase/deployment/recovery_integration_test.go +1 -0
| @@ -225,6 +225,7 @@ func TestRemove_InhibitsBeforeRuntimeWithdrawal(t *testing.T) { | |||
| 225 | 225 | result, err := svc.Remove(ctx, "blog", "") | |
| 226 | 226 | require.NoError(t, err) | |
| 227 | 227 | require.NotNil(t, result) | |
| 228 | + | assert.Equal(t, domain.AppServiceRemoved, result.Services["web"].Result) | |
| 228 | 229 | assert.Equal(t, []string{"intent-stopped", "inhibited", "stopped", "removed", "retired"}, order) | |
| 229 | 230 | runtime.AssertNotCalled(t, "RemoveVolume", mock.Anything, mock.Anything, mock.Anything) | |
| 230 | 231 | } | |
| @@ -225,6 +225,7 @@ func TestRemove_InhibitsBeforeRuntimeWithdrawal(t *testing.T) { | |||||
| 225 | result, err := svc.Remove(ctx, "blog", "") | 225 | result, err := svc.Remove(ctx, "blog", "") | ||
| 226 | require.NoError(t, err) | 226 | require.NoError(t, err) | ||
| 227 | require.NotNil(t, result) | 227 | require.NotNil(t, result) | ||
| 228 | + | assert.Equal(t, domain.AppServiceRemoved, result.Services["web"].Result) | |||
| 228 | assert.Equal(t, []string{"intent-stopped", "inhibited", "stopped", "removed", "retired"}, order) | 229 | assert.Equal(t, []string{"intent-stopped", "inhibited", "stopped", "removed", "retired"}, order) | ||
| 229 | runtime.AssertNotCalled(t, "RemoveVolume", mock.Anything, mock.Anything, mock.Anything) | 230 | runtime.AssertNotCalled(t, "RemoveVolume", mock.Anything, mock.Anything, mock.Anything) | ||
| 230 | } | 231 | } | ||
M internal/usecase/deployment/service.go +1 -1
| @@ -936,7 +936,7 @@ func ComputeOutcome(results map[string]ServiceResult) string { | |||
| 936 | 936 | failed := 0 | |
| 937 | 937 | for _, result := range results { | |
| 938 | 938 | switch result.Result { | |
| 939 | - | case "deployed", ServiceResultUnchanged: | |
| 939 | + | case "deployed", ServiceResultUnchanged, domain.AppServiceRemoved: | |
| 940 | 940 | deployed++ | |
| 941 | 941 | case "failed": | |
| 942 | 942 | failed++ | |
| @@ -936,7 +936,7 @@ func ComputeOutcome(results map[string]ServiceResult) string { | |||||
| 936 | failed := 0 | 936 | failed := 0 | ||
| 937 | for _, result := range results { | 937 | for _, result := range results { | ||
| 938 | switch result.Result { | 938 | switch result.Result { | ||
| 939 | - | case "deployed", ServiceResultUnchanged: | 939 | + | case "deployed", ServiceResultUnchanged, domain.AppServiceRemoved: |
| 940 | deployed++ | 940 | deployed++ | ||
| 941 | case "failed": | 941 | case "failed": | ||
| 942 | failed++ | 942 | failed++ | ||
M internal/usecase/deployment/service_test.go +3 -0
| @@ -63,6 +63,9 @@ func preflightService( | |||
| 63 | 63 | func TestComputeOutcome_TerminalResults(t *testing.T) { | |
| 64 | 64 | deployed := deployment.ServiceResult{Result: "deployed"} | |
| 65 | 65 | failed := deployment.ServiceResult{Result: "failed", Error: "boom"} | |
| 66 | + | removed := deployment.ServiceResult{Result: domain.AppServiceRemoved} | |
| 67 | + | assert.Equal(t, "success", deployment.ComputeOutcome(map[string]deployment.ServiceResult{"a": removed})) | |
| 68 | + | assert.Equal(t, "partial", deployment.ComputeOutcome(map[string]deployment.ServiceResult{"a": removed, "b": failed})) | |
| 66 | 69 | assert.Equal(t, "success", deployment.ComputeOutcome(map[string]deployment.ServiceResult{"a": deployed})) | |
| 67 | 70 | assert.Equal(t, "failed", deployment.ComputeOutcome(map[string]deployment.ServiceResult{"a": failed})) | |
| 68 | 71 | assert.Equal(t, "partial", deployment.ComputeOutcome(map[string]deployment.ServiceResult{"a": deployed, "b": failed})) | |
| @@ -63,6 +63,9 @@ func preflightService( | |||||
| 63 | func TestComputeOutcome_TerminalResults(t *testing.T) { | 63 | func TestComputeOutcome_TerminalResults(t *testing.T) { | ||
| 64 | deployed := deployment.ServiceResult{Result: "deployed"} | 64 | deployed := deployment.ServiceResult{Result: "deployed"} | ||
| 65 | failed := deployment.ServiceResult{Result: "failed", Error: "boom"} | 65 | failed := deployment.ServiceResult{Result: "failed", Error: "boom"} | ||
| 66 | + | removed := deployment.ServiceResult{Result: domain.AppServiceRemoved} | |||
| 67 | + | assert.Equal(t, "success", deployment.ComputeOutcome(map[string]deployment.ServiceResult{"a": removed})) | |||
| 68 | + | assert.Equal(t, "partial", deployment.ComputeOutcome(map[string]deployment.ServiceResult{"a": removed, "b": failed})) | |||
| 66 | assert.Equal(t, "success", deployment.ComputeOutcome(map[string]deployment.ServiceResult{"a": deployed})) | 69 | assert.Equal(t, "success", deployment.ComputeOutcome(map[string]deployment.ServiceResult{"a": deployed})) | ||
| 67 | assert.Equal(t, "failed", deployment.ComputeOutcome(map[string]deployment.ServiceResult{"a": failed})) | 70 | assert.Equal(t, "failed", deployment.ComputeOutcome(map[string]deployment.ServiceResult{"a": failed})) | ||
| 68 | assert.Equal(t, "partial", deployment.ComputeOutcome(map[string]deployment.ServiceResult{"a": deployed, "b": failed})) | 71 | assert.Equal(t, "partial", deployment.ComputeOutcome(map[string]deployment.ServiceResult{"a": deployed, "b": failed})) | ||