diff --git a/pkg/app/api/grpcapi/api.go b/pkg/app/api/grpcapi/api.go index 5e75b44c9a..5c908be499 100644 --- a/pkg/app/api/grpcapi/api.go +++ b/pkg/app/api/grpcapi/api.go @@ -135,6 +135,7 @@ func (a *API) SyncApplication(ctx context.Context, req *apiservice.SyncApplicati Commander: key.Id, SyncApplication: &model.Command_SyncApplication{ ApplicationId: app.Id, + SyncStrategy: model.SyncStrategy_AUTO, }, } if err := addCommand(ctx, a.commandStore, &cmd, a.logger); err != nil { diff --git a/pkg/app/api/grpcapi/web_api.go b/pkg/app/api/grpcapi/web_api.go index 644ea7c2b4..0051f63557 100644 --- a/pkg/app/api/grpcapi/web_api.go +++ b/pkg/app/api/grpcapi/web_api.go @@ -602,6 +602,7 @@ func (a *WebAPI) SyncApplication(ctx context.Context, req *webservice.SyncApplic Commander: claims.Subject, SyncApplication: &model.Command_SyncApplication{ ApplicationId: app.Id, + SyncStrategy: req.SyncStrategy, }, } if err := addCommand(ctx, a.commandStore, &cmd, a.logger); err != nil { diff --git a/pkg/app/api/service/webservice/service.proto b/pkg/app/api/service/webservice/service.proto index 5eee87ecf1..9ce77af292 100644 --- a/pkg/app/api/service/webservice/service.proto +++ b/pkg/app/api/service/webservice/service.proto @@ -234,6 +234,7 @@ message ListApplicationsResponse { message SyncApplicationRequest { string application_id = 1 [(validate.rules).string.min_len = 1]; + model.SyncStrategy sync_strategy = 2; } message SyncApplicationResponse { diff --git a/pkg/app/piped/planner/cloudrun/cloudrun.go b/pkg/app/piped/planner/cloudrun/cloudrun.go index ff824d0398..315c02f6c1 100644 --- a/pkg/app/piped/planner/cloudrun/cloudrun.go +++ b/pkg/app/piped/planner/cloudrun/cloudrun.go @@ -62,11 +62,28 @@ func (p *Planner) Plan(ctx context.Context, in planner.Input) (out planner.Outpu in.Logger.Warn("unable to determine target version", zap.Error(e)) } + // If the deployment was triggered by forcing via web UI, + // we rely on the user's decision. + switch in.Deployment.Trigger.SyncStrategy { + case model.SyncStrategy_QUICK_SYNC: + out.Stages = buildQuickSyncPipeline(cfg.Input.AutoRollback, time.Now()) + out.Summary = fmt.Sprintf("Quick sync to deploy image %s and configure all traffic to it (forced via web)", out.Version) + return + case model.SyncStrategy_PIPELINE: + if cfg.Pipeline == nil { + err = fmt.Errorf("unable to force sync with pipeline because no pipeline was specified") + return + } + out.Stages = buildProgressivePipeline(cfg.Pipeline, cfg.Input.AutoRollback, time.Now()) + out.Summary = fmt.Sprintf("Sync with pipeline to deploy image %s (forced via web)", out.Version) + return + } + // This is the first time to deploy this application or it was unable to retrieve that value. // We just do the quick sync. if in.MostRecentSuccessfulCommitHash == "" { out.Stages = buildQuickSyncPipeline(cfg.Input.AutoRollback, time.Now()) - out.Summary = fmt.Sprintf("Quick sync to deploy image %s and configure all traffic to it because it seems this is the first deployment", out.Version) + out.Summary = fmt.Sprintf("Quick sync to deploy image %s and configure all traffic to it (it seems this is the first deployment)", out.Version) return } @@ -82,13 +99,13 @@ func (p *Planner) Plan(ctx context.Context, in planner.Input) (out planner.Outpu if err == nil { if lastVersion, e := p.determineVersion(ds.AppDir, cfg.Input.ServiceManifestFile); e == nil { out.Stages = buildProgressivePipeline(cfg.Pipeline, cfg.Input.AutoRollback, time.Now()) - out.Summary = fmt.Sprintf("Sync progressively to update image from %s to %s", lastVersion, out.Version) + out.Summary = fmt.Sprintf("Sync with pipeline to update image from %s to %s", lastVersion, out.Version) return } } out.Stages = buildProgressivePipeline(cfg.Pipeline, cfg.Input.AutoRollback, time.Now()) - out.Summary = "Sync progressively with the specified pipeline" + out.Summary = "Sync with the specified pipeline" return } diff --git a/pkg/app/piped/planner/kubernetes/kubernetes.go b/pkg/app/piped/planner/kubernetes/kubernetes.go index f239227ab3..f71c961f5e 100644 --- a/pkg/app/piped/planner/kubernetes/kubernetes.go +++ b/pkg/app/piped/planner/kubernetes/kubernetes.go @@ -88,10 +88,27 @@ func (p *Planner) Plan(ctx context.Context, in planner.Input) (out planner.Outpu out.Version = version } + // If the deployment was triggered by forcing via web UI, + // we rely on the user's decision. + switch in.Deployment.Trigger.SyncStrategy { + case model.SyncStrategy_QUICK_SYNC: + out.Stages = buildQuickSyncPipeline(cfg.Input.AutoRollback, time.Now()) + out.Summary = fmt.Sprintf("Quick sync by applying all manifests (forced via web)", out.Version) + return + case model.SyncStrategy_PIPELINE: + if cfg.Pipeline == nil { + err = fmt.Errorf("unable to force sync with pipeline because no pipeline was specified") + return + } + out.Stages = buildProgressivePipeline(cfg.Pipeline, cfg.Input.AutoRollback, time.Now()) + out.Summary = "Sync with the specified pipeline (forced via web)" + return + } + // If the progressive pipeline was not configured // we have only one choise to do is applying all manifestt. if cfg.Pipeline == nil || len(cfg.Pipeline.Stages) == 0 { - out.Stages = buildPipeline(cfg.Input.AutoRollback, time.Now()) + out.Stages = buildQuickSyncPipeline(cfg.Input.AutoRollback, time.Now()) out.Summary = "Quick sync by applying all manifests (no pipeline was configured)" return } @@ -120,7 +137,7 @@ func (p *Planner) Plan(ctx context.Context, in planner.Input) (out planner.Outpu return out, err } if syncRegex.MatchString(in.Deployment.Trigger.Commit.Message) { - out.Stages = buildPipeline(cfg.Input.AutoRollback, time.Now()) + out.Stages = buildQuickSyncPipeline(cfg.Input.AutoRollback, time.Now()) out.Summary = fmt.Sprintf("Quick sync by applying all manifests because the commit message was matching %q", s) return out, err } @@ -130,7 +147,7 @@ func (p *Planner) Plan(ctx context.Context, in planner.Input) (out planner.Outpu // or it was unable to retrieve that value. // We just apply all manifests. if in.MostRecentSuccessfulCommitHash == "" { - out.Stages = buildPipeline(cfg.Input.AutoRollback, time.Now()) + out.Stages = buildQuickSyncPipeline(cfg.Input.AutoRollback, time.Now()) out.Summary = "Quick sync by applying all manifests because it seems this is the first deployment" return } @@ -163,7 +180,7 @@ func (p *Planner) Plan(ctx context.Context, in planner.Input) (out planner.Outpu return } - out.Stages = buildPipeline(cfg.Input.AutoRollback, time.Now()) + out.Stages = buildQuickSyncPipeline(cfg.Input.AutoRollback, time.Now()) return } diff --git a/pkg/app/piped/planner/kubernetes/pipeline.go b/pkg/app/piped/planner/kubernetes/pipeline.go index 8e65bdb075..2dbbd7bacf 100644 --- a/pkg/app/piped/planner/kubernetes/pipeline.go +++ b/pkg/app/piped/planner/kubernetes/pipeline.go @@ -23,7 +23,7 @@ import ( "github.com/pipe-cd/pipe/pkg/model" ) -func buildPipeline(autoRollback bool, now time.Time) []*model.PipelineStage { +func buildQuickSyncPipeline(autoRollback bool, now time.Time) []*model.PipelineStage { var ( preStageID = "" stage, _ = planner.GetPredefinedStage(planner.PredefinedStageK8sSync) diff --git a/pkg/app/piped/planner/kubernetes/pipeline_test.go b/pkg/app/piped/planner/kubernetes/pipeline_test.go index de639a0c5d..61546fb1a1 100644 --- a/pkg/app/piped/planner/kubernetes/pipeline_test.go +++ b/pkg/app/piped/planner/kubernetes/pipeline_test.go @@ -10,7 +10,7 @@ import ( "github.com/pipe-cd/pipe/pkg/model" ) -func TestBuildPipeline(t *testing.T) { +func TestBuildQuickSyncPipeline(t *testing.T) { tests := []struct { name string wantAutoRollback bool @@ -26,7 +26,7 @@ func TestBuildPipeline(t *testing.T) { } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - gotStages := buildPipeline(tc.wantAutoRollback, time.Now()) + gotStages := buildQuickSyncPipeline(tc.wantAutoRollback, time.Now()) var gotAutoRollback bool for _, stage := range gotStages { if stage.Name == string(model.StageRollback) { diff --git a/pkg/app/piped/planner/terraform/pipeline.go b/pkg/app/piped/planner/terraform/pipeline.go index 372409f2b2..69ed82a215 100644 --- a/pkg/app/piped/planner/terraform/pipeline.go +++ b/pkg/app/piped/planner/terraform/pipeline.go @@ -23,7 +23,7 @@ import ( "github.com/pipe-cd/pipe/pkg/model" ) -func builQuickSyncPipeline(autoRollback bool, now time.Time) []*model.PipelineStage { +func buildQuickSyncPipeline(autoRollback bool, now time.Time) []*model.PipelineStage { var ( s, _ = planner.GetPredefinedStage(planner.PredefinedStageTerraformSync) out = make([]*model.PipelineStage, 0, 2) diff --git a/pkg/app/piped/planner/terraform/terraform.go b/pkg/app/piped/planner/terraform/terraform.go index ddd3719b90..ccfbbda3b6 100644 --- a/pkg/app/piped/planner/terraform/terraform.go +++ b/pkg/app/piped/planner/terraform/terraform.go @@ -51,11 +51,28 @@ func (p *Planner) Plan(ctx context.Context, in planner.Input) (out planner.Outpu return } + // If the deployment was triggered by forcing via web UI, + // we rely on the user's decision. + switch in.Deployment.Trigger.SyncStrategy { + case model.SyncStrategy_QUICK_SYNC: + out.Stages = buildQuickSyncPipeline(cfg.Input.AutoRollback, time.Now()) + out.Summary = "Quick sync by automatically applying any detected changes because no pipeline was configured (forced via web)" + return + case model.SyncStrategy_PIPELINE: + if cfg.Pipeline == nil { + err = fmt.Errorf("unable to force sync with pipeline because no pipeline was specified") + return + } + out.Stages = buildProgressivePipeline(cfg.Pipeline, cfg.Input.AutoRollback, time.Now()) + out.Summary = "Sync with the specified progressive pipeline (forced via web)" + return + } + now := time.Now() out.Version = "N/A" if cfg.Pipeline == nil || len(cfg.Pipeline.Stages) == 0 { - out.Stages = builQuickSyncPipeline(cfg.Input.AutoRollback, now) + out.Stages = buildQuickSyncPipeline(cfg.Input.AutoRollback, now) out.Summary = "Quick sync by automatically applying any detected changes because no pipeline was configured" return } diff --git a/pkg/app/piped/trigger/deployment.go b/pkg/app/piped/trigger/deployment.go index 40f12f7f5b..dd5302e3f1 100644 --- a/pkg/app/piped/trigger/deployment.go +++ b/pkg/app/piped/trigger/deployment.go @@ -27,8 +27,15 @@ import ( "github.com/pipe-cd/pipe/pkg/model" ) -func (t *Trigger) triggerDeployment(ctx context.Context, app *model.Application, branch string, commit git.Commit, commander string) (deployment *model.Deployment, err error) { - deployment, err = buildDeployment(app, branch, commit, commander, time.Now()) +func (t *Trigger) triggerDeployment( + ctx context.Context, + app *model.Application, + branch string, + commit git.Commit, + commander string, + syncStrategy model.SyncStrategy, +) (deployment *model.Deployment, err error) { + deployment, err = buildDeployment(app, branch, commit, commander, syncStrategy, time.Now()) if err != nil { return } @@ -97,7 +104,14 @@ func (t *Trigger) reportMostRecentlyTriggeredDeployment(ctx context.Context, d * return err } -func buildDeployment(app *model.Application, branch string, commit git.Commit, commander string, now time.Time) (*model.Deployment, error) { +func buildDeployment( + app *model.Application, + branch string, + commit git.Commit, + commander string, + syncStrategy model.SyncStrategy, + now time.Time, +) (*model.Deployment, error) { commitURL := "" if r := app.GitPath.Repo; r != nil { var err error @@ -124,8 +138,9 @@ func buildDeployment(app *model.Application, branch string, commit git.Commit, c Url: commitURL, CreatedAt: int64(commit.CreatedAt), }, - Commander: commander, - Timestamp: now.Unix(), + Commander: commander, + Timestamp: now.Unix(), + SyncStrategy: syncStrategy, }, GitPath: app.GitPath, CloudProvider: app.CloudProvider, diff --git a/pkg/app/piped/trigger/trigger.go b/pkg/app/piped/trigger/trigger.go index d31e5cfc24..e8a06f4e55 100644 --- a/pkg/app/piped/trigger/trigger.go +++ b/pkg/app/piped/trigger/trigger.go @@ -173,7 +173,7 @@ func (t *Trigger) checkCommand(ctx context.Context) error { ) continue } - d, err := t.syncApplication(ctx, app, cmd.Commander) + d, err := t.syncApplication(ctx, app, cmd.Commander, syncCmd.SyncStrategy) if err != nil { t.logger.Error("failed to sync application", zap.String("app-id", app.Id), @@ -195,7 +195,7 @@ func (t *Trigger) checkCommand(ctx context.Context) error { return nil } -func (t *Trigger) syncApplication(ctx context.Context, app *model.Application, commander string) (*model.Deployment, error) { +func (t *Trigger) syncApplication(ctx context.Context, app *model.Application, commander string, syncStrategy model.SyncStrategy) (*model.Deployment, error) { _, branch, headCommit, err := t.updateRepoToLatest(ctx, app.GitPath.Repo.Id) if err != nil { return nil, err @@ -205,7 +205,7 @@ func (t *Trigger) syncApplication(ctx context.Context, app *model.Application, c t.logger.Info(fmt.Sprintf("application %s will be synced because of a sync command", app.Id), zap.String("head-commit", headCommit.Hash), ) - d, err := t.triggerDeployment(ctx, app, branch, headCommit, commander) + d, err := t.triggerDeployment(ctx, app, branch, headCommit, commander, syncStrategy) if err != nil { return nil, err } @@ -279,7 +279,7 @@ func (t *Trigger) checkApplication(ctx context.Context, app *model.Application, logger.Info("application should be synced because of the new commit", zap.String("most-recently-triggered-commit", preCommitHash), ) - if _, err := t.triggerDeployment(ctx, app, branch, headCommit, ""); err != nil { + if _, err := t.triggerDeployment(ctx, app, branch, headCommit, "", model.SyncStrategy_AUTO); err != nil { return err } t.mostRecentlyTriggeredCommits[app.Id] = headCommit.Hash diff --git a/pkg/app/web/src/__fixtures__/dummy-command.ts b/pkg/app/web/src/__fixtures__/dummy-command.ts index 8fa7ecefae..9a539d435f 100644 --- a/pkg/app/web/src/__fixtures__/dummy-command.ts +++ b/pkg/app/web/src/__fixtures__/dummy-command.ts @@ -1,3 +1,4 @@ +import { SyncStrategy } from "pipe/pkg/app/web/model/deployment_pb"; import { Command, CommandModel, CommandStatus } from "../modules/commands"; import { dummyDeployment } from "./dummy-deployment"; @@ -14,6 +15,7 @@ export const dummyCommand: Command = { type: CommandModel.Type.SYNC_APPLICATION, syncApplication: { applicationId: "app-1", + syncStrategy: SyncStrategy.AUTO, }, createdAt: 0, updatedAt: 0, diff --git a/pkg/app/web/src/__fixtures__/dummy-deployment.ts b/pkg/app/web/src/__fixtures__/dummy-deployment.ts index 41824a166c..f269da1b5b 100644 --- a/pkg/app/web/src/__fixtures__/dummy-deployment.ts +++ b/pkg/app/web/src/__fixtures__/dummy-deployment.ts @@ -4,6 +4,7 @@ import { dummyApplication } from "./dummy-application"; import { dummyEnv } from "./dummy-environment"; import { dummyPiped } from "./dummy-piped"; import { dummyStage } from "./dummy-stage"; +import { SyncStrategy } from "pipe/pkg/app/web/model/deployment_pb"; export const dummyDeployment: Deployment = { id: "deployment-1", @@ -27,6 +28,7 @@ export const dummyDeployment: Deployment = { pullRequest: 123, url: "", }, + syncStrategy: SyncStrategy.AUTO, }, updatedAt: 1, version: "0.0.0", diff --git a/pkg/app/web/src/api/applications.ts b/pkg/app/web/src/api/applications.ts index d772c7a948..b9a2d1469e 100644 --- a/pkg/app/web/src/api/applications.ts +++ b/pkg/app/web/src/api/applications.ts @@ -96,11 +96,13 @@ export const addApplication = async ({ export const syncApplication = async ({ applicationId, + syncStrategy, }: SyncApplicationRequest.AsObject): Promise< SyncApplicationResponse.AsObject > => { const req = new SyncApplicationRequest(); req.setApplicationId(applicationId); + req.setSyncStrategy(syncStrategy); return apiRequest(req, apiClient.syncApplication); }; diff --git a/pkg/app/web/src/components/pipeline.stories.tsx b/pkg/app/web/src/components/pipeline.stories.tsx index f759dc5959..f23d7e32b7 100644 --- a/pkg/app/web/src/components/pipeline.stories.tsx +++ b/pkg/app/web/src/components/pipeline.stories.tsx @@ -4,6 +4,7 @@ import { Pipeline } from "./pipeline"; import { Deployment, Stage } from "../modules/deployments"; import { dummyStage } from "../__fixtures__/dummy-stage"; import { METADATA_APPROVED_BY } from "../constants/metadata-keys"; +import { SyncStrategy } from "pipe/pkg/app/web/model/deployment_pb"; const stage = (props?: Partial): Stage => ({ ...dummyStage, @@ -44,6 +45,7 @@ const fakeDeployment: Deployment = { }, commander: "cakecatz", timestamp: 1592201366, + syncStrategy: SyncStrategy.AUTO, }, runningCommitHash: "3808585b46f1e90196d7ffe8dd04c807a251febc", summary: "This deployment is debug", diff --git a/pkg/app/web/src/modules/applications.ts b/pkg/app/web/src/modules/applications.ts index 8b274d0c97..72df1d0869 100644 --- a/pkg/app/web/src/modules/applications.ts +++ b/pkg/app/web/src/modules/applications.ts @@ -13,6 +13,7 @@ import { ApplicationGitRepository, ApplicationKind, } from "pipe/pkg/app/web/model/common_pb"; +import { SyncStrategy } from "pipe/pkg/app/web/model/deployment_pb"; import { fetchCommand, CommandStatus, CommandModel } from "./commands"; import { AppState } from "."; @@ -53,7 +54,8 @@ export const syncApplication = createAsyncThunk< { applicationId: string } >("applications/sync", async ({ applicationId }, thunkAPI) => { const { commandId } = await applicationsAPI.syncApplication({ - applicationId, + applicationId: applicationId, + syncStrategy: SyncStrategy.AUTO, }); await thunkAPI.dispatch(fetchCommand(commandId)); diff --git a/pkg/config/BUILD.bazel b/pkg/config/BUILD.bazel index 65cf47f73c..fc0a64a903 100644 --- a/pkg/config/BUILD.bazel +++ b/pkg/config/BUILD.bazel @@ -48,7 +48,6 @@ go_test( deps = [ "//pkg/model:go_default_library", "@com_github_golang_protobuf//proto:go_default_library", - "@com_github_magiconair_properties//assert:go_default_library", "@com_github_stretchr_testify//assert:go_default_library", "@com_github_stretchr_testify//require:go_default_library", ], diff --git a/pkg/model/command.proto b/pkg/model/command.proto index be814f6359..0d7d82a656 100644 --- a/pkg/model/command.proto +++ b/pkg/model/command.proto @@ -18,6 +18,7 @@ package pipe.model; option go_package = "github.com/pipe-cd/pipe/pkg/model"; import "validate/validate.proto"; +import "pkg/model/deployment.proto"; enum CommandStatus { COMMAND_NOT_HANDLED_YET = 0; @@ -36,6 +37,7 @@ message Command { message SyncApplication { string application_id = 1 [(validate.rules).string.min_len = 1]; + model.SyncStrategy sync_strategy = 2; } message UpdateApplicationConfig { diff --git a/pkg/model/deployment.proto b/pkg/model/deployment.proto index 38cab9f7b6..2c132693f6 100644 --- a/pkg/model/deployment.proto +++ b/pkg/model/deployment.proto @@ -88,11 +88,18 @@ message Deployment { int64 updated_at = 102 [(validate.rules).int64.gte = 0]; } +enum SyncStrategy { + AUTO = 0; + QUICK_SYNC = 1; + PIPELINE = 2; +} + message DeploymentTrigger { Commit commit = 1 [(validate.rules).message.required = true]; // Who triggered this deployment via web page. string commander= 2; int64 timestamp = 3 [(validate.rules).int64.gt = 0]; + SyncStrategy sync_strategy = 4; } message PipelineStage {