diff --git a/pkg/app/pipedv1/controller/planner.go b/pkg/app/pipedv1/controller/planner.go index 13efadba7a..a6019970e5 100644 --- a/pkg/app/pipedv1/controller/planner.go +++ b/pkg/app/pipedv1/controller/planner.go @@ -18,8 +18,6 @@ import ( "context" "encoding/json" "fmt" - "io" - "path/filepath" "sort" "time" @@ -30,10 +28,9 @@ import ( "go.uber.org/zap" "github.com/pipe-cd/pipecd/pkg/app/pipedv1/controller/controllermetrics" - "github.com/pipe-cd/pipecd/pkg/app/pipedv1/deploysource" "github.com/pipe-cd/pipecd/pkg/app/pipedv1/metadatastore" "github.com/pipe-cd/pipecd/pkg/app/server/service/pipedservice" - "github.com/pipe-cd/pipecd/pkg/config" + "github.com/pipe-cd/pipecd/pkg/configv1" "github.com/pipe-cd/pipecd/pkg/model" pluginapi "github.com/pipe-cd/pipecd/pkg/plugin/api/v1alpha1" "github.com/pipe-cd/pipecd/pkg/plugin/api/v1alpha1/deployment" @@ -192,27 +189,30 @@ func (p *planner) Run(ctx context.Context) error { controllermetrics.UpdateDeploymentStatus(p.deployment, p.doneDeploymentStatus) }() - repoCfg := config.PipedRepository{ - RepoID: p.deployment.GitPath.Repo.Id, - Remote: p.deployment.GitPath.Repo.Remote, - Branch: p.deployment.GitPath.Repo.Branch, - } + // TODO: Prepare running deploy source and target deploy source. + var runningDS, targetDS *model.DeploymentSource - // Prepare target deploy source. - targetDSP := deploysource.NewProvider( - filepath.Join(p.workingDir, "deploysource"), - deploysource.NewGitSourceCloner(p.gitClient, repoCfg, "target", p.deployment.Trigger.Commit.Hash), - *p.deployment.GitPath, - nil, // TODO: Revise this secret decryter, is this need? - ) + // repoCfg := config.PipedRepository{ + // RepoID: p.deployment.GitPath.Repo.Id, + // Remote: p.deployment.GitPath.Repo.Remote, + // Branch: p.deployment.GitPath.Repo.Branch, + // } - targetDS, err := targetDSP.Get(ctx, io.Discard) - if err != nil { - return fmt.Errorf("error while preparing deploy source data (%v)", err) - } + // Prepare target deploy source. + // targetDSP := deploysource.NewProvider( + // filepath.Join(p.workingDir, "deploysource"), + // deploysource.NewGitSourceCloner(p.gitClient, repoCfg, "target", p.deployment.Trigger.Commit.Hash), + // *p.deployment.GitPath, + // nil, // TODO: Revise this secret decryter, is this need? + // ) + + // targetDS, err := targetDSP.Get(ctx, io.Discard) + // if err != nil { + // return fmt.Errorf("error while preparing deploy source data (%v)", err) + // } // TODO: Pass running DS as well if need? - out, err := p.buildPlan(ctx, targetDS) + out, err := p.buildPlan(ctx, runningDS, targetDS) // If the deployment was already cancelled, we ignore the plan result. select { @@ -243,13 +243,15 @@ func (p *planner) Run(ctx context.Context) error { // - CommitMatcher ensure pipeline/quick sync based on the commit message // - Force quick sync if there is no previous deployment (aka. this is the first deploy) // - Based on PlannerService.DetermineStrategy returned by plugins -func (p *planner) buildPlan(ctx context.Context, targetDS *deploysource.DeploySource) (*plannerOutput, error) { +func (p *planner) buildPlan(ctx context.Context, runningDS, targetDS *model.DeploymentSource) (*plannerOutput, error) { out := &plannerOutput{} input := &deployment.PlanPluginInput{ - Deployment: p.deployment, + Deployment: p.deployment, + RunningDeploymentSource: runningDS, + TargetDeploymentSource: targetDS, // TODO: Add more planner input fields. - // NOTE: As discussed we pass targetDS & runningDS here. + // we need passing PluginConfig } // Build deployment target versions. @@ -270,20 +272,25 @@ func (p *planner) buildPlan(ctx context.Context, targetDS *deploysource.DeploySo } } - cfg := targetDS.GenericApplicationConfig + cfg, err := config.DecodeYAML(targetDS.GetApplicationConfig()) + if err != nil { + p.logger.Error("unable to parse application config", zap.Error(err)) + return nil, err + } + spec := cfg.ApplicationSpec // In case the strategy has been decided by trigger. // For example: user triggered the deployment via web console. switch p.deployment.Trigger.SyncStrategy { case model.SyncStrategy_QUICK_SYNC: - if stages, err := p.buildQuickSyncStages(ctx, cfg); err == nil { + if stages, err := p.buildQuickSyncStages(ctx, spec); err == nil { out.SyncStrategy = model.SyncStrategy_QUICK_SYNC out.Summary = p.deployment.Trigger.StrategySummary out.Stages = stages return out, nil } case model.SyncStrategy_PIPELINE: - if stages, err := p.buildPipelineSyncStages(ctx, cfg); err == nil { + if stages, err := p.buildPipelineSyncStages(ctx, spec); err == nil { out.SyncStrategy = model.SyncStrategy_PIPELINE out.Summary = p.deployment.Trigger.StrategySummary out.Stages = stages @@ -292,8 +299,8 @@ func (p *planner) buildPlan(ctx context.Context, targetDS *deploysource.DeploySo } // When no pipeline was configured, do the quick sync. - if cfg.Pipeline == nil || len(cfg.Pipeline.Stages) == 0 { - if stages, err := p.buildQuickSyncStages(ctx, cfg); err == nil { + if spec.Pipeline == nil || len(spec.Pipeline.Stages) == 0 { + if stages, err := p.buildQuickSyncStages(ctx, spec); err == nil { out.SyncStrategy = model.SyncStrategy_QUICK_SYNC out.Summary = "Quick sync due to the pipeline was not configured" out.Stages = stages @@ -302,8 +309,8 @@ func (p *planner) buildPlan(ctx context.Context, targetDS *deploysource.DeploySo } // Force to use pipeline when the `spec.planner.alwaysUsePipeline` was configured. - if cfg.Planner.AlwaysUsePipeline { - if stages, err := p.buildPipelineSyncStages(ctx, cfg); err == nil { + if spec.Planner.AlwaysUsePipeline { + if stages, err := p.buildPipelineSyncStages(ctx, spec); err == nil { out.SyncStrategy = model.SyncStrategy_PIPELINE out.Summary = "Sync with the specified pipeline (alwaysUsePipeline was set)" out.Stages = stages @@ -315,10 +322,10 @@ func (p *planner) buildPlan(ctx context.Context, targetDS *deploysource.DeploySo // This deployment is triggered by a commit with the intent to perform pipeline. // Commit Matcher will be ignored when triggered by a command. - if pattern := cfg.CommitMatcher.Pipeline; pattern != "" && p.deployment.Trigger.Commander == "" { + if pattern := spec.CommitMatcher.Pipeline; pattern != "" && p.deployment.Trigger.Commander == "" { if pipelineRegex, err := regexPool.Get(pattern); err == nil && pipelineRegex.MatchString(p.deployment.Trigger.Commit.Message) { - if stages, err := p.buildPipelineSyncStages(ctx, cfg); err == nil { + if stages, err := p.buildPipelineSyncStages(ctx, spec); err == nil { out.SyncStrategy = model.SyncStrategy_PIPELINE out.Summary = fmt.Sprintf("Sync progressively because the commit message was matching %q", pattern) out.Stages = stages @@ -329,10 +336,10 @@ func (p *planner) buildPlan(ctx context.Context, targetDS *deploysource.DeploySo // This deployment is triggered by a commit with the intent to synchronize. // Commit Matcher will be ignored when triggered by a command. - if pattern := cfg.CommitMatcher.QuickSync; pattern != "" && p.deployment.Trigger.Commander == "" { + if pattern := spec.CommitMatcher.QuickSync; pattern != "" && p.deployment.Trigger.Commander == "" { if syncRegex, err := regexPool.Get(pattern); err == nil && syncRegex.MatchString(p.deployment.Trigger.Commit.Message) { - if stages, err := p.buildQuickSyncStages(ctx, cfg); err == nil { + if stages, err := p.buildQuickSyncStages(ctx, spec); err == nil { out.SyncStrategy = model.SyncStrategy_QUICK_SYNC out.Summary = fmt.Sprintf("Quick sync because the commit message was matching %q", pattern) out.Stages = stages @@ -343,7 +350,7 @@ func (p *planner) buildPlan(ctx context.Context, targetDS *deploysource.DeploySo // Quick sync if this is the first time to deploy this application or it was unable to retrieve running commit hash. if p.lastSuccessfulCommitHash == "" { - if stages, err := p.buildQuickSyncStages(ctx, cfg); err == nil { + if stages, err := p.buildQuickSyncStages(ctx, spec); err == nil { out.SyncStrategy = model.SyncStrategy_QUICK_SYNC out.Summary = "Quick sync, it seems this is the first deployment of the application" out.Stages = stages @@ -372,14 +379,14 @@ func (p *planner) buildPlan(ctx context.Context, targetDS *deploysource.DeploySo switch strategy { case model.SyncStrategy_QUICK_SYNC: - if stages, err := p.buildQuickSyncStages(ctx, cfg); err == nil { + if stages, err := p.buildQuickSyncStages(ctx, spec); err == nil { out.SyncStrategy = model.SyncStrategy_QUICK_SYNC out.Summary = summary out.Stages = stages return out, nil } case model.SyncStrategy_PIPELINE: - if stages, err := p.buildPipelineSyncStages(ctx, cfg); err == nil { + if stages, err := p.buildPipelineSyncStages(ctx, spec); err == nil { out.SyncStrategy = model.SyncStrategy_PIPELINE out.Summary = summary out.Stages = stages diff --git a/pkg/app/pipedv1/controller/planner_test.go b/pkg/app/pipedv1/controller/planner_test.go index 9f57817686..aaad8c635d 100644 --- a/pkg/app/pipedv1/controller/planner_test.go +++ b/pkg/app/pipedv1/controller/planner_test.go @@ -16,13 +16,16 @@ package controller import ( "context" + "encoding/json" "testing" + "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.uber.org/zap" "google.golang.org/grpc" - "github.com/pipe-cd/pipecd/pkg/config" + "github.com/pipe-cd/pipecd/pkg/configv1" "github.com/pipe-cd/pipecd/pkg/model" pluginapi "github.com/pipe-cd/pipecd/pkg/plugin/api/v1alpha1" "github.com/pipe-cd/pipecd/pkg/plugin/api/v1alpha1/deployment" @@ -30,6 +33,7 @@ import ( type fakePlugin struct { pluginapi.PluginClient + syncStrategy *deployment.DetermineStrategyResponse quickStages []*model.PipelineStage pipelineStages []*model.PipelineStage rollbackStages []*model.PipelineStage @@ -70,10 +74,12 @@ func (p *fakePlugin) BuildPipelineSyncStages(ctx context.Context, req *deploymen }, nil } func (p *fakePlugin) DetermineStrategy(ctx context.Context, req *deployment.DetermineStrategyRequest, opts ...grpc.CallOption) (*deployment.DetermineStrategyResponse, error) { - return nil, nil + return p.syncStrategy, nil } func (p *fakePlugin) DetermineVersions(ctx context.Context, req *deployment.DetermineVersionsRequest, opts ...grpc.CallOption) (*deployment.DetermineVersionsResponse, error) { - return nil, nil + return &deployment.DetermineVersionsResponse{ + Versions: []*model.ArtifactVersion{}, + }, nil } func (p *fakePlugin) FetchDefinedStages(ctx context.Context, req *deployment.FetchDefinedStagesRequest, opts ...grpc.CallOption) (*deployment.FetchDefinedStagesResponse, error) { stages := make([]string, 0, len(p.quickStages)+len(p.pipelineStages)+len(p.rollbackStages)) @@ -574,3 +580,365 @@ func TestBuildPipelineSyncStages(t *testing.T) { }) } } + +func TestPlanner_BuildPlan(t *testing.T) { + t.Parallel() + + testcases := []struct { + name string + isFirstDeploy bool + plugins []pluginapi.PluginClient + cfg *config.GenericApplicationSpec + deployment *model.Deployment + wantErr bool + expectedOutput *plannerOutput + }{ + { + name: "quick sync strategy triggered by web console", + isFirstDeploy: false, + plugins: []pluginapi.PluginClient{ + &fakePlugin{ + quickStages: []*model.PipelineStage{ + { + Id: "plugin-1-stage-1", + Visible: true, + }, + }, + }, + }, + cfg: &config.GenericApplicationSpec{ + Planner: config.DeploymentPlanner{ + AutoRollback: pointerBool(true), + }, + }, + deployment: &model.Deployment{ + Trigger: &model.DeploymentTrigger{ + SyncStrategy: model.SyncStrategy_QUICK_SYNC, + StrategySummary: "Triggered by web console", + }, + }, + wantErr: false, + expectedOutput: &plannerOutput{ + SyncStrategy: model.SyncStrategy_QUICK_SYNC, + Summary: "Triggered by web console", + Stages: []*model.PipelineStage{ + { + Id: "plugin-1-stage-1", + Visible: true, + }, + }, + Versions: []*model.ArtifactVersion{ + { + Kind: model.ArtifactVersion_UNKNOWN, + Version: versionUnknown, + }, + }, + }, + }, + { + name: "pipeline sync strategy triggered by web console", + isFirstDeploy: false, + plugins: []pluginapi.PluginClient{ + &fakePlugin{ + pipelineStages: []*model.PipelineStage{ + { + Id: "plugin-1-stage-1", + Name: "plugin-1-stage-1", + Visible: true, + }, + }, + }, + }, + cfg: &config.GenericApplicationSpec{ + Planner: config.DeploymentPlanner{ + AutoRollback: pointerBool(true), + }, + Pipeline: &config.DeploymentPipeline{ + Stages: []config.PipelineStage{ + { + ID: "plugin-1-stage-1", + Name: "plugin-1-stage-1", + }, + }, + }, + }, + deployment: &model.Deployment{ + Trigger: &model.DeploymentTrigger{ + SyncStrategy: model.SyncStrategy_PIPELINE, + StrategySummary: "Triggered by web console", + }, + }, + wantErr: false, + expectedOutput: &plannerOutput{ + SyncStrategy: model.SyncStrategy_PIPELINE, + Summary: "Triggered by web console", + Stages: []*model.PipelineStage{ + { + Id: "plugin-1-stage-1", + Name: "plugin-1-stage-1", + Index: 0, + Visible: true, + }, + }, + Versions: []*model.ArtifactVersion{ + { + Kind: model.ArtifactVersion_UNKNOWN, + Version: versionUnknown, + }, + }, + }, + }, + { + name: "quick sync due to no pipeline configured", + isFirstDeploy: false, + plugins: []pluginapi.PluginClient{ + &fakePlugin{ + quickStages: []*model.PipelineStage{ + { + Id: "plugin-1-stage-1", + Visible: true, + }, + }, + }, + }, + cfg: &config.GenericApplicationSpec{ + Planner: config.DeploymentPlanner{ + AutoRollback: pointerBool(true), + }, + }, + deployment: &model.Deployment{ + Trigger: &model.DeploymentTrigger{}, + }, + wantErr: false, + expectedOutput: &plannerOutput{ + SyncStrategy: model.SyncStrategy_QUICK_SYNC, + Summary: "Quick sync due to the pipeline was not configured", + Stages: []*model.PipelineStage{ + { + Id: "plugin-1-stage-1", + Visible: true, + }, + }, + Versions: []*model.ArtifactVersion{ + { + Kind: model.ArtifactVersion_UNKNOWN, + Version: versionUnknown, + }, + }, + }, + }, + { + name: "pipeline sync due to alwaysUsePipeline", + isFirstDeploy: false, + plugins: []pluginapi.PluginClient{ + &fakePlugin{ + pipelineStages: []*model.PipelineStage{ + { + Id: "plugin-1-stage-1", + Name: "plugin-1-stage-1", + Visible: true, + }, + }, + }, + }, + cfg: &config.GenericApplicationSpec{ + Planner: config.DeploymentPlanner{ + AlwaysUsePipeline: true, + AutoRollback: pointerBool(true), + }, + Pipeline: &config.DeploymentPipeline{ + Stages: []config.PipelineStage{ + { + ID: "plugin-1-stage-1", + Name: "plugin-1-stage-1", + }, + }, + }, + }, + deployment: &model.Deployment{ + Trigger: &model.DeploymentTrigger{}, + }, + wantErr: false, + expectedOutput: &plannerOutput{ + SyncStrategy: model.SyncStrategy_PIPELINE, + Summary: "Sync with the specified pipeline (alwaysUsePipeline was set)", + Stages: []*model.PipelineStage{ + { + Id: "plugin-1-stage-1", + Name: "plugin-1-stage-1", + Index: 0, + Visible: true, + }, + }, + Versions: []*model.ArtifactVersion{ + { + Kind: model.ArtifactVersion_UNKNOWN, + Version: versionUnknown, + }, + }, + }, + }, + { + name: "quick sync due to first deployment", + isFirstDeploy: true, + plugins: []pluginapi.PluginClient{ + &fakePlugin{ + quickStages: []*model.PipelineStage{ + { + Id: "plugin-1-stage-1", + Visible: true, + }, + }, + }, + }, + cfg: &config.GenericApplicationSpec{ + Planner: config.DeploymentPlanner{ + AutoRollback: pointerBool(true), + }, + Pipeline: &config.DeploymentPipeline{ + Stages: []config.PipelineStage{ + { + ID: "plugin-1-stage-1", + Name: "plugin-1-stage-1", + }, + }, + }, + }, + deployment: &model.Deployment{ + Trigger: &model.DeploymentTrigger{}, + }, + wantErr: false, + expectedOutput: &plannerOutput{ + SyncStrategy: model.SyncStrategy_QUICK_SYNC, + Summary: "Quick sync, it seems this is the first deployment of the application", + Stages: []*model.PipelineStage{ + { + Id: "plugin-1-stage-1", + Visible: true, + }, + }, + Versions: []*model.ArtifactVersion{ + { + Kind: model.ArtifactVersion_UNKNOWN, + Version: versionUnknown, + }, + }, + }, + }, + { + name: "pipeline sync determined by plugin", + isFirstDeploy: false, + plugins: []pluginapi.PluginClient{ + &fakePlugin{ + syncStrategy: &deployment.DetermineStrategyResponse{ + SyncStrategy: model.SyncStrategy_PIPELINE, + Summary: "determined by plugin", + }, + pipelineStages: []*model.PipelineStage{ + { + Id: "plugin-1-stage-1", + Name: "plugin-1-stage-1", + Visible: true, + }, + }, + quickStages: []*model.PipelineStage{ + { + Id: "plugin-1-quick-stage-1", + Visible: true, + }, + }, + }, + }, + cfg: &config.GenericApplicationSpec{ + Planner: config.DeploymentPlanner{ + AutoRollback: pointerBool(true), + }, + Pipeline: &config.DeploymentPipeline{ + Stages: []config.PipelineStage{ + { + ID: "plugin-1-stage-1", + Name: "plugin-1-stage-1", + }, + }, + }, + }, + deployment: &model.Deployment{ + Trigger: &model.DeploymentTrigger{}, + }, + wantErr: false, + expectedOutput: &plannerOutput{ + SyncStrategy: model.SyncStrategy_PIPELINE, + Summary: "determined by plugin", + Stages: []*model.PipelineStage{ + { + Id: "plugin-1-stage-1", + Name: "plugin-1-stage-1", + Index: 0, + Visible: true, + }, + }, + Versions: []*model.ArtifactVersion{ + { + Kind: model.ArtifactVersion_UNKNOWN, + Version: versionUnknown, + }, + }, + }, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + stageBasedPluginMap := make(map[string]pluginapi.PluginClient) + for _, p := range tc.plugins { + stages, _ := p.FetchDefinedStages(context.TODO(), &deployment.FetchDefinedStagesRequest{}) + for _, s := range stages.Stages { + stageBasedPluginMap[s] = p + } + } + planner := &planner{ + plugins: tc.plugins, + stageBasedPluginsMap: stageBasedPluginMap, + deployment: tc.deployment, + lastSuccessfulCommitHash: "", + lastSuccessfulConfigFilename: "", + workingDir: "", + apiClient: nil, + gitClient: nil, + notifier: nil, + logger: zap.NewNop(), + nowFunc: func() time.Time { return time.Now() }, + } + + if !tc.isFirstDeploy { + planner.lastSuccessfulCommitHash = "123" + } + + runningDS := &model.DeploymentSource{} + + type genericConfig struct { + Kind config.Kind `json:"kind"` + APIVersion string `json:"apiVersion,omitempty"` + Spec any `json:"spec"` + } + + jsonBytes, err := json.Marshal(genericConfig{ + Kind: config.KindApplication, + APIVersion: config.VersionV1Beta1, + Spec: tc.cfg, + }) + + require.NoError(t, err) + targetDS := &model.DeploymentSource{ + ApplicationConfig: jsonBytes, + } + out, err := planner.buildPlan(context.TODO(), runningDS, targetDS) + if tc.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + assert.Equal(t, tc.expectedOutput, out) + }) + } +} diff --git a/pkg/configv1/application.go b/pkg/configv1/application.go index 3492219e5a..a2c49495b1 100644 --- a/pkg/configv1/application.go +++ b/pkg/configv1/application.go @@ -122,26 +122,6 @@ type OnChain struct { } func (s *GenericApplicationSpec) Validate() error { - if s.Pipeline != nil { - for _, stage := range s.Pipeline.Stages { - if stage.AnalysisStageOptions != nil { - if err := stage.AnalysisStageOptions.Validate(); err != nil { - return err - } - } - if stage.WaitApprovalStageOptions != nil { - if err := stage.WaitApprovalStageOptions.Validate(); err != nil { - return err - } - } - if stage.CustomSyncOptions != nil { - if err := stage.CustomSyncOptions.Validate(); err != nil { - return err - } - } - } - } - if ps := s.PostSync; ps != nil { if err := ps.Validate(); err != nil { return err @@ -219,44 +199,6 @@ type DeploymentPipeline struct { // PipelineStage represents a single stage of a pipeline. // This is used as a generic struct for all stage type. type PipelineStage struct { - ID string - Name model.Stage - Desc string - Timeout Duration - With json.RawMessage - - CustomSyncOptions *CustomSyncOptions - WaitStageOptions *WaitStageOptions - WaitApprovalStageOptions *WaitApprovalStageOptions - AnalysisStageOptions *AnalysisStageOptions - ScriptRunStageOptions *ScriptRunStageOptions - - K8sPrimaryRolloutStageOptions *K8sPrimaryRolloutStageOptions - K8sCanaryRolloutStageOptions *K8sCanaryRolloutStageOptions - K8sCanaryCleanStageOptions *K8sCanaryCleanStageOptions - K8sBaselineRolloutStageOptions *K8sBaselineRolloutStageOptions - K8sBaselineCleanStageOptions *K8sBaselineCleanStageOptions - K8sTrafficRoutingStageOptions *K8sTrafficRoutingStageOptions - - TerraformSyncStageOptions *TerraformSyncStageOptions - TerraformPlanStageOptions *TerraformPlanStageOptions - TerraformApplyStageOptions *TerraformApplyStageOptions - - CloudRunSyncStageOptions *CloudRunSyncStageOptions - CloudRunPromoteStageOptions *CloudRunPromoteStageOptions - - LambdaSyncStageOptions *LambdaSyncStageOptions - LambdaCanaryRolloutStageOptions *LambdaCanaryRolloutStageOptions - LambdaPromoteStageOptions *LambdaPromoteStageOptions - - ECSSyncStageOptions *ECSSyncStageOptions - ECSCanaryRolloutStageOptions *ECSCanaryRolloutStageOptions - ECSPrimaryRolloutStageOptions *ECSPrimaryRolloutStageOptions - ECSCanaryCleanStageOptions *ECSCanaryCleanStageOptions - ECSTrafficRoutingStageOptions *ECSTrafficRoutingStageOptions -} - -type genericPipelineStage struct { ID string `json:"id"` Name model.Stage `json:"name"` Desc string `json:"desc,omitempty"` @@ -264,151 +206,6 @@ type genericPipelineStage struct { With json.RawMessage `json:"with"` } -func (s *PipelineStage) UnmarshalJSON(data []byte) error { - var err error - gs := genericPipelineStage{} - if err = json.Unmarshal(data, &gs); err != nil { - return err - } - s.ID = gs.ID - s.Name = gs.Name - s.Desc = gs.Desc - s.Timeout = gs.Timeout - s.With = gs.With - - switch s.Name { - case model.StageCustomSync: - s.CustomSyncOptions = &CustomSyncOptions{} - if len(gs.With) > 0 { - err = json.Unmarshal(gs.With, s.CustomSyncOptions) - } - case model.StageWait: - s.WaitStageOptions = &WaitStageOptions{} - if len(gs.With) > 0 { - err = json.Unmarshal(gs.With, s.WaitStageOptions) - } - case model.StageWaitApproval: - s.WaitApprovalStageOptions = &WaitApprovalStageOptions{} - if len(gs.With) > 0 { - err = json.Unmarshal(gs.With, s.WaitApprovalStageOptions) - } - case model.StageAnalysis: - s.AnalysisStageOptions = &AnalysisStageOptions{} - if len(gs.With) > 0 { - err = json.Unmarshal(gs.With, s.AnalysisStageOptions) - } - case model.StageScriptRun: - s.ScriptRunStageOptions = &ScriptRunStageOptions{} - if len(gs.With) > 0 { - err = json.Unmarshal(gs.With, s.ScriptRunStageOptions) - } - - case model.StageK8sPrimaryRollout: - s.K8sPrimaryRolloutStageOptions = &K8sPrimaryRolloutStageOptions{} - if len(gs.With) > 0 { - err = json.Unmarshal(gs.With, s.K8sPrimaryRolloutStageOptions) - } - case model.StageK8sCanaryRollout: - s.K8sCanaryRolloutStageOptions = &K8sCanaryRolloutStageOptions{} - if len(gs.With) > 0 { - err = json.Unmarshal(gs.With, s.K8sCanaryRolloutStageOptions) - } - case model.StageK8sCanaryClean: - s.K8sCanaryCleanStageOptions = &K8sCanaryCleanStageOptions{} - if len(gs.With) > 0 { - err = json.Unmarshal(gs.With, s.K8sCanaryCleanStageOptions) - } - case model.StageK8sBaselineRollout: - s.K8sBaselineRolloutStageOptions = &K8sBaselineRolloutStageOptions{} - if len(gs.With) > 0 { - err = json.Unmarshal(gs.With, s.K8sBaselineRolloutStageOptions) - } - case model.StageK8sBaselineClean: - s.K8sBaselineCleanStageOptions = &K8sBaselineCleanStageOptions{} - if len(gs.With) > 0 { - err = json.Unmarshal(gs.With, s.K8sBaselineCleanStageOptions) - } - case model.StageK8sTrafficRouting: - s.K8sTrafficRoutingStageOptions = &K8sTrafficRoutingStageOptions{} - if len(gs.With) > 0 { - err = json.Unmarshal(gs.With, s.K8sTrafficRoutingStageOptions) - } - - case model.StageTerraformSync: - s.TerraformSyncStageOptions = &TerraformSyncStageOptions{} - if len(gs.With) > 0 { - err = json.Unmarshal(gs.With, s.TerraformSyncStageOptions) - } - case model.StageTerraformPlan: - s.TerraformPlanStageOptions = &TerraformPlanStageOptions{} - if len(gs.With) > 0 { - err = json.Unmarshal(gs.With, s.TerraformPlanStageOptions) - } - case model.StageTerraformApply: - s.TerraformApplyStageOptions = &TerraformApplyStageOptions{} - if len(gs.With) > 0 { - err = json.Unmarshal(gs.With, s.TerraformApplyStageOptions) - } - - case model.StageCloudRunSync: - s.CloudRunSyncStageOptions = &CloudRunSyncStageOptions{} - if len(gs.With) > 0 { - err = json.Unmarshal(gs.With, s.CloudRunSyncStageOptions) - } - case model.StageCloudRunPromote: - s.CloudRunPromoteStageOptions = &CloudRunPromoteStageOptions{} - if len(gs.With) > 0 { - err = json.Unmarshal(gs.With, s.CloudRunPromoteStageOptions) - } - - case model.StageLambdaSync: - s.LambdaSyncStageOptions = &LambdaSyncStageOptions{} - if len(gs.With) > 0 { - err = json.Unmarshal(gs.With, s.LambdaSyncStageOptions) - } - case model.StageLambdaPromote: - s.LambdaPromoteStageOptions = &LambdaPromoteStageOptions{} - if len(gs.With) > 0 { - err = json.Unmarshal(gs.With, s.LambdaPromoteStageOptions) - } - case model.StageLambdaCanaryRollout: - s.LambdaCanaryRolloutStageOptions = &LambdaCanaryRolloutStageOptions{} - if len(gs.With) > 0 { - err = json.Unmarshal(gs.With, s.LambdaCanaryRolloutStageOptions) - } - - case model.StageECSSync: - s.ECSSyncStageOptions = &ECSSyncStageOptions{} - if len(gs.With) > 0 { - err = json.Unmarshal(gs.With, s.ECSSyncStageOptions) - } - case model.StageECSCanaryRollout: - s.ECSCanaryRolloutStageOptions = &ECSCanaryRolloutStageOptions{} - if len(gs.With) > 0 { - err = json.Unmarshal(gs.With, s.ECSCanaryRolloutStageOptions) - } - case model.StageECSPrimaryRollout: - s.ECSPrimaryRolloutStageOptions = &ECSPrimaryRolloutStageOptions{} - if len(gs.With) > 0 { - err = json.Unmarshal(gs.With, s.ECSPrimaryRolloutStageOptions) - } - case model.StageECSCanaryClean: - s.ECSCanaryCleanStageOptions = &ECSCanaryCleanStageOptions{} - if len(gs.With) > 0 { - err = json.Unmarshal(gs.With, s.ECSCanaryCleanStageOptions) - } - case model.StageECSTrafficRouting: - s.ECSTrafficRoutingStageOptions = &ECSTrafficRoutingStageOptions{} - if len(gs.With) > 0 { - err = json.Unmarshal(gs.With, s.ECSTrafficRoutingStageOptions) - } - - default: - err = fmt.Errorf("unsupported stage name: %s", s.Name) - } - return err -} - // SkipOptions contains all configurable values for skipping a stage. type SkipOptions struct { CommitMessagePrefixes []string `json:"commitMessagePrefixes,omitempty"` diff --git a/pkg/configv1/application_kubernetes_test.go b/pkg/configv1/application_kubernetes_test.go deleted file mode 100644 index 54b9f39d65..0000000000 --- a/pkg/configv1/application_kubernetes_test.go +++ /dev/null @@ -1,195 +0,0 @@ -// Copyright 2024 The PipeCD Authors. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package config - -import ( - "encoding/json" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/pipe-cd/pipecd/pkg/model" -) - -func TestKubernetesApplicationConfig(t *testing.T) { - testcases := []struct { - fileName string - expectedKind Kind - expectedAPIVersion string - expectedSpec interface{} - expectedError error - }{ - { - fileName: "testdata/application/k8s-app-bluegreen.yaml", - expectedKind: KindKubernetesApp, - expectedAPIVersion: "pipecd.dev/v1beta1", - expectedSpec: &KubernetesApplicationSpec{ - GenericApplicationSpec: GenericApplicationSpec{ - Description: "application description first string\napplication description second string\n", - Planner: DeploymentPlanner{ - AlwaysUsePipeline: true, - AutoRollback: newBoolPointer(true), - }, - Pipeline: &DeploymentPipeline{ - Stages: []PipelineStage{ - { - Name: model.StageK8sCanaryRollout, - K8sCanaryRolloutStageOptions: &K8sCanaryRolloutStageOptions{ - Replicas: Replicas{ - Number: 100, - IsPercentage: true, - }, - }, - With: json.RawMessage(`{"replicas":"100%"}`), - }, - { - Name: model.StageK8sTrafficRouting, - K8sTrafficRoutingStageOptions: &K8sTrafficRoutingStageOptions{ - Canary: Percentage{ - Number: 100, - }, - }, - With: json.RawMessage(`{"canary":100}`), - }, - { - Name: model.StageK8sPrimaryRollout, - K8sPrimaryRolloutStageOptions: &K8sPrimaryRolloutStageOptions{}, - }, - { - Name: model.StageK8sTrafficRouting, - K8sTrafficRoutingStageOptions: &K8sTrafficRoutingStageOptions{ - Primary: Percentage{ - Number: 100, - }, - }, - With: json.RawMessage(`{"primary":100}`), - }, - { - Name: model.StageK8sCanaryClean, - K8sCanaryCleanStageOptions: &K8sCanaryCleanStageOptions{}, - }, - }, - }, - Timeout: Duration(6 * time.Hour), - Trigger: Trigger{ - OnCommit: OnCommit{ - Disabled: false, - }, - OnCommand: OnCommand{ - Disabled: false, - }, - OnOutOfSync: OnOutOfSync{ - Disabled: newBoolPointer(true), - MinWindow: Duration(5 * time.Minute), - }, - OnChain: OnChain{ - Disabled: newBoolPointer(true), - }, - }, - }, - Input: KubernetesDeploymentInput{ - AutoRollback: newBoolPointer(true), - }, - TrafficRouting: &KubernetesTrafficRouting{ - Method: KubernetesTrafficRoutingMethodPodSelector, - }, - VariantLabel: KubernetesVariantLabel{ - Key: "pipecd.dev/variant", - PrimaryValue: "primary", - BaselineValue: "baseline", - CanaryValue: "canary", - }, - }, - expectedError: nil, - }, - { - fileName: "testdata/application/k8s-app-resource-route.yaml", - expectedKind: KindKubernetesApp, - expectedAPIVersion: "pipecd.dev/v1beta1", - expectedSpec: &KubernetesApplicationSpec{ - GenericApplicationSpec: GenericApplicationSpec{ - Timeout: Duration(6 * time.Hour), - Trigger: Trigger{ - OnCommit: OnCommit{ - Disabled: false, - }, - OnCommand: OnCommand{ - Disabled: false, - }, - OnOutOfSync: OnOutOfSync{ - Disabled: newBoolPointer(true), - MinWindow: Duration(5 * time.Minute), - }, - OnChain: OnChain{ - Disabled: newBoolPointer(true), - }, - }, - Planner: DeploymentPlanner{ - AutoRollback: newBoolPointer(true), - }, - }, - Input: KubernetesDeploymentInput{ - AutoRollback: newBoolPointer(true), - }, - VariantLabel: KubernetesVariantLabel{ - Key: "pipecd.dev/variant", - PrimaryValue: "primary", - BaselineValue: "baseline", - CanaryValue: "canary", - }, - ResourceRoutes: []KubernetesResourceRoute{ - { - Provider: KubernetesProviderMatcher{ - Name: "ConfigCluster", - }, - Match: &KubernetesResourceRouteMatcher{ - Kind: "Ingress", - }, - }, - { - Provider: KubernetesProviderMatcher{ - Name: "ConfigCluster", - }, - Match: &KubernetesResourceRouteMatcher{ - Kind: "Service", - Name: "Foo", - }, - }, - { - Provider: KubernetesProviderMatcher{ - Labels: map[string]string{ - "group": "workload", - }, - }, - }, - }, - }, - expectedError: nil, - }, - } - for _, tc := range testcases { - t.Run(tc.fileName, func(t *testing.T) { - cfg, err := LoadFromYAML(tc.fileName) - require.Equal(t, tc.expectedError, err) - if err == nil { - assert.Equal(t, tc.expectedKind, cfg.Kind) - assert.Equal(t, tc.expectedAPIVersion, cfg.APIVersion) - assert.Equal(t, tc.expectedSpec, cfg.spec) - } - }) - } -} diff --git a/pkg/configv1/application_lambda_test.go b/pkg/configv1/application_lambda_test.go deleted file mode 100644 index eb3299865b..0000000000 --- a/pkg/configv1/application_lambda_test.go +++ /dev/null @@ -1,181 +0,0 @@ -// Copyright 2024 The PipeCD Authors. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package config - -import ( - "encoding/json" - "testing" - "time" - - "github.com/pipe-cd/pipecd/pkg/model" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestLambdaApplicationConfig(t *testing.T) { - testcases := []struct { - fileName string - expectedKind Kind - expectedAPIVersion string - expectedSpec interface{} - expectedError error - }{ - { - fileName: "testdata/application/lambda-app.yaml", - expectedKind: KindLambdaApp, - expectedAPIVersion: "pipecd.dev/v1beta1", - expectedSpec: &LambdaApplicationSpec{ - GenericApplicationSpec: GenericApplicationSpec{ - Timeout: Duration(6 * time.Hour), - Trigger: Trigger{ - OnCommit: OnCommit{ - Disabled: false, - }, - OnCommand: OnCommand{ - Disabled: false, - }, - OnOutOfSync: OnOutOfSync{ - Disabled: newBoolPointer(true), - MinWindow: Duration(5 * time.Minute), - }, - OnChain: OnChain{ - Disabled: newBoolPointer(true), - }, - }, - Planner: DeploymentPlanner{ - AutoRollback: newBoolPointer(true), - }, - }, - Input: LambdaDeploymentInput{ - FunctionManifestFile: "function.yaml", - AutoRollback: newBoolPointer(true), - }, - }, - expectedError: nil, - }, - { - fileName: "testdata/application/lambda-app-canary.yaml", - expectedKind: KindLambdaApp, - expectedAPIVersion: "pipecd.dev/v1beta1", - expectedSpec: &LambdaApplicationSpec{ - GenericApplicationSpec: GenericApplicationSpec{ - Timeout: Duration(6 * time.Hour), - Pipeline: &DeploymentPipeline{ - Stages: []PipelineStage{ - { - Name: model.StageLambdaCanaryRollout, - LambdaCanaryRolloutStageOptions: &LambdaCanaryRolloutStageOptions{}, - }, - { - Name: model.StageLambdaPromote, - LambdaPromoteStageOptions: &LambdaPromoteStageOptions{ - Percent: Percentage{ - Number: 10, - HasSuffix: false, - }, - }, - With: json.RawMessage(`{"percent":10}`), - }, - { - Name: model.StageLambdaPromote, - LambdaPromoteStageOptions: &LambdaPromoteStageOptions{ - Percent: Percentage{ - Number: 100, - HasSuffix: false, - }, - }, - With: json.RawMessage(`{"percent":100}`), - }, - }, - }, - Trigger: Trigger{ - OnOutOfSync: OnOutOfSync{ - Disabled: newBoolPointer(true), - MinWindow: Duration(5 * time.Minute), - }, - OnChain: OnChain{ - Disabled: newBoolPointer(true), - }, - }, - Planner: DeploymentPlanner{ - AutoRollback: newBoolPointer(true), - }, - }, - Input: LambdaDeploymentInput{ - FunctionManifestFile: "function.yaml", - AutoRollback: newBoolPointer(true), - }, - }, - expectedError: nil, - }, - { - fileName: "testdata/application/lambda-app-bluegreen.yaml", - expectedKind: KindLambdaApp, - expectedAPIVersion: "pipecd.dev/v1beta1", - expectedSpec: &LambdaApplicationSpec{ - GenericApplicationSpec: GenericApplicationSpec{ - Timeout: Duration(6 * time.Hour), - Pipeline: &DeploymentPipeline{ - Stages: []PipelineStage{ - { - Name: model.StageLambdaCanaryRollout, - LambdaCanaryRolloutStageOptions: &LambdaCanaryRolloutStageOptions{}, - }, - { - Name: model.StageLambdaPromote, - LambdaPromoteStageOptions: &LambdaPromoteStageOptions{ - Percent: Percentage{ - Number: 100, - HasSuffix: false, - }, - }, - With: json.RawMessage(`{"percent":100}`), - }, - }, - }, - Trigger: Trigger{ - OnOutOfSync: OnOutOfSync{ - Disabled: newBoolPointer(true), - MinWindow: Duration(5 * time.Minute), - }, - OnChain: OnChain{ - Disabled: newBoolPointer(true), - }, - }, - Planner: DeploymentPlanner{ - AutoRollback: newBoolPointer(true), - }, - }, - Input: LambdaDeploymentInput{ - FunctionManifestFile: "function.yaml", - AutoRollback: newBoolPointer(true), - }, - }, - expectedError: nil, - }, - } - for _, tc := range testcases { - t.Run(tc.fileName, func(t *testing.T) { - cfg, err := LoadFromYAML(tc.fileName) - require.Equal(t, tc.expectedError, err) - if err == nil { - assert.Equal(t, tc.expectedKind, cfg.Kind) - assert.Equal(t, tc.expectedAPIVersion, cfg.APIVersion) - assert.Equal(t, tc.expectedSpec, cfg.spec) - } - }) - } -} diff --git a/pkg/configv1/application_terraform_test.go b/pkg/configv1/application_terraform_test.go deleted file mode 100644 index 6018a4c70b..0000000000 --- a/pkg/configv1/application_terraform_test.go +++ /dev/null @@ -1,263 +0,0 @@ -// Copyright 2024 The PipeCD Authors. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package config - -import ( - "encoding/json" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/pipe-cd/pipecd/pkg/model" -) - -func TestTerraformApplicationtConfig(t *testing.T) { - testcases := []struct { - fileName string - expectedKind Kind - expectedAPIVersion string - expectedSpec interface{} - expectedError error - }{ - { - fileName: "testdata/application/terraform-app-empty.yaml", - expectedKind: KindTerraformApp, - expectedAPIVersion: "pipecd.dev/v1beta1", - expectedSpec: &TerraformApplicationSpec{ - GenericApplicationSpec: GenericApplicationSpec{ - Timeout: Duration(6 * time.Hour), - Trigger: Trigger{ - OnCommit: OnCommit{ - Disabled: false, - }, - OnCommand: OnCommand{ - Disabled: false, - }, - OnOutOfSync: OnOutOfSync{ - Disabled: newBoolPointer(true), - MinWindow: Duration(5 * time.Minute), - }, - OnChain: OnChain{ - Disabled: newBoolPointer(true), - }, - }, - Planner: DeploymentPlanner{ - AutoRollback: newBoolPointer(true), - }, - }, - Input: TerraformDeploymentInput{}, - }, - expectedError: nil, - }, - { - fileName: "testdata/application/terraform-app.yaml", - expectedKind: KindTerraformApp, - expectedAPIVersion: "pipecd.dev/v1beta1", - expectedSpec: &TerraformApplicationSpec{ - GenericApplicationSpec: GenericApplicationSpec{ - Timeout: Duration(6 * time.Hour), - Trigger: Trigger{ - OnCommit: OnCommit{ - Disabled: false, - }, - OnCommand: OnCommand{ - Disabled: false, - }, - OnOutOfSync: OnOutOfSync{ - Disabled: newBoolPointer(true), - MinWindow: Duration(5 * time.Minute), - }, - OnChain: OnChain{ - Disabled: newBoolPointer(true), - }, - }, - Planner: DeploymentPlanner{ - AutoRollback: newBoolPointer(true), - }, - }, - Input: TerraformDeploymentInput{ - Workspace: "dev", - TerraformVersion: "0.12.23", - }, - }, - expectedError: nil, - }, - { - fileName: "testdata/application/terraform-app-secret-management.yaml", - expectedKind: KindTerraformApp, - expectedAPIVersion: "pipecd.dev/v1beta1", - expectedSpec: &TerraformApplicationSpec{ - GenericApplicationSpec: GenericApplicationSpec{ - Timeout: Duration(6 * time.Hour), - Trigger: Trigger{ - OnCommit: OnCommit{ - Disabled: false, - }, - OnCommand: OnCommand{ - Disabled: false, - }, - OnOutOfSync: OnOutOfSync{ - Disabled: newBoolPointer(false), - MinWindow: Duration(5 * time.Minute), - }, - OnChain: OnChain{ - Disabled: newBoolPointer(true), - }, - }, - Planner: DeploymentPlanner{ - AutoRollback: newBoolPointer(true), - }, - Encryption: &SecretEncryption{ - EncryptedSecrets: map[string]string{ - "serviceAccount": "ENCRYPTED_DATA_GENERATED_FROM_WEB", - }, - DecryptionTargets: []string{ - "service-account.yaml", - }, - }, - }, - Input: TerraformDeploymentInput{ - Workspace: "dev", - TerraformVersion: "0.12.23", - }, - }, - expectedError: nil, - }, - { - fileName: "testdata/application/terraform-app-with-approval.yaml", - expectedKind: KindTerraformApp, - expectedAPIVersion: "pipecd.dev/v1beta1", - expectedSpec: &TerraformApplicationSpec{ - GenericApplicationSpec: GenericApplicationSpec{ - Pipeline: &DeploymentPipeline{ - Stages: []PipelineStage{ - { - Name: model.StageTerraformPlan, - TerraformPlanStageOptions: &TerraformPlanStageOptions{}, - }, - { - Name: model.StageWaitApproval, - WaitApprovalStageOptions: &WaitApprovalStageOptions{ - Approvers: []string{"foo", "bar"}, - Timeout: Duration(6 * time.Hour), - MinApproverNum: 1, - }, - With: json.RawMessage(`{"approvers":["foo","bar"]}`), - }, - { - Name: model.StageTerraformApply, - TerraformApplyStageOptions: &TerraformApplyStageOptions{}, - }, - }, - }, - Timeout: Duration(6 * time.Hour), - Trigger: Trigger{ - OnCommit: OnCommit{ - Disabled: false, - }, - OnCommand: OnCommand{ - Disabled: false, - }, - OnOutOfSync: OnOutOfSync{ - Disabled: newBoolPointer(true), - MinWindow: Duration(5 * time.Minute), - }, - OnChain: OnChain{ - Disabled: newBoolPointer(true), - }, - }, - Planner: DeploymentPlanner{ - AutoRollback: newBoolPointer(true), - }, - }, - Input: TerraformDeploymentInput{ - Workspace: "dev", - TerraformVersion: "0.12.23", - }, - }, - expectedError: nil, - }, - { - fileName: "testdata/application/terraform-app-with-exit.yaml", - expectedKind: KindTerraformApp, - expectedAPIVersion: "pipecd.dev/v1beta1", - expectedSpec: &TerraformApplicationSpec{ - GenericApplicationSpec: GenericApplicationSpec{ - Pipeline: &DeploymentPipeline{ - Stages: []PipelineStage{ - { - Name: model.StageTerraformPlan, - TerraformPlanStageOptions: &TerraformPlanStageOptions{ - ExitOnNoChanges: true, - }, - With: json.RawMessage(`{"exitOnNoChanges":true}`), - }, - { - Name: model.StageWaitApproval, - WaitApprovalStageOptions: &WaitApprovalStageOptions{ - Approvers: []string{"foo", "bar"}, - Timeout: Duration(6 * time.Hour), - MinApproverNum: 1, - }, - With: json.RawMessage(`{"approvers":["foo","bar"]}`), - }, - { - Name: model.StageTerraformApply, - TerraformApplyStageOptions: &TerraformApplyStageOptions{}, - }, - }, - }, - Timeout: Duration(6 * time.Hour), - Trigger: Trigger{ - OnCommit: OnCommit{ - Disabled: false, - }, - OnCommand: OnCommand{ - Disabled: false, - }, - OnOutOfSync: OnOutOfSync{ - Disabled: newBoolPointer(true), - MinWindow: Duration(5 * time.Minute), - }, - OnChain: OnChain{ - Disabled: newBoolPointer(true), - }, - }, - Planner: DeploymentPlanner{ - AutoRollback: newBoolPointer(true), - }, - }, - Input: TerraformDeploymentInput{ - Workspace: "dev", - TerraformVersion: "0.12.23", - }, - }, - expectedError: nil, - }, - } - for _, tc := range testcases { - t.Run(tc.fileName, func(t *testing.T) { - cfg, err := LoadFromYAML(tc.fileName) - require.Equal(t, tc.expectedError, err) - if err == nil { - assert.Equal(t, tc.expectedKind, cfg.Kind) - assert.Equal(t, tc.expectedAPIVersion, cfg.APIVersion) - assert.Equal(t, tc.expectedSpec, cfg.spec) - } - }) - } -} diff --git a/pkg/configv1/application_test.go b/pkg/configv1/application_test.go index b97bfaba39..1ab3e1afaa 100644 --- a/pkg/configv1/application_test.go +++ b/pkg/configv1/application_test.go @@ -15,8 +15,6 @@ package config import ( - "encoding/json" - "fmt" "testing" "time" @@ -76,34 +74,6 @@ func TestHasStage(t *testing.T) { } } -func TestValidateWaitApprovalStageOptions(t *testing.T) { - testcases := []struct { - name string - minApproverNum int - wantErr bool - }{ - { - name: "valid", - minApproverNum: 1, - wantErr: false, - }, - { - name: "invalid", - minApproverNum: -1, - wantErr: true, - }, - } - for _, tc := range testcases { - t.Run(tc.name, func(t *testing.T) { - w := &WaitApprovalStageOptions{ - MinApproverNum: tc.minApproverNum, - } - err := w.Validate() - assert.Equal(t, tc.wantErr, err != nil) - }) - } -} - func TestFindSlackUsersAndGroups(t *testing.T) { testcases := []struct { name string @@ -647,201 +617,6 @@ func TestGenericPostSyncConfiguration(t *testing.T) { } } -func TestGenericAnalysisConfiguration(t *testing.T) { - testcases := []struct { - fileName string - expectedKind Kind - expectedAPIVersion string - expectedSpec interface{} - expectedError error - }{ - { - fileName: "testdata/application/generic-analysis.yaml", - expectedKind: KindKubernetesApp, - expectedAPIVersion: "pipecd.dev/v1beta1", - expectedSpec: &KubernetesApplicationSpec{ - GenericApplicationSpec: GenericApplicationSpec{ - Timeout: Duration(6 * time.Hour), - Trigger: Trigger{ - OnOutOfSync: OnOutOfSync{ - Disabled: newBoolPointer(true), - MinWindow: Duration(5 * time.Minute), - }, - OnChain: OnChain{ - Disabled: newBoolPointer(true), - }, - }, - Planner: DeploymentPlanner{ - AutoRollback: newBoolPointer(true), - }, - Pipeline: &DeploymentPipeline{ - Stages: []PipelineStage{ - { - Name: model.StageAnalysis, - AnalysisStageOptions: &AnalysisStageOptions{ - Duration: Duration(10 * time.Minute), - Metrics: []TemplatableAnalysisMetrics{ - { - AnalysisMetrics: AnalysisMetrics{ - Strategy: AnalysisStrategyThreshold, - Provider: "prometheus-dev", - Query: "grpc_error_percentage", - Expected: AnalysisExpected{Max: floatPointer(0.1)}, - Interval: Duration(1 * time.Minute), - Timeout: Duration(30 * time.Second), - FailureLimit: 1, - Deviation: AnalysisDeviationEither, - }, - }, - { - AnalysisMetrics: AnalysisMetrics{ - Strategy: AnalysisStrategyThreshold, - Provider: "prometheus-dev", - Query: "grpc_succeed_percentage", - Expected: AnalysisExpected{Min: floatPointer(0.9)}, - Interval: Duration(1 * time.Minute), - Timeout: Duration(30 * time.Second), - FailureLimit: 1, - Deviation: AnalysisDeviationEither, - }, - }, - }, - }, - With: json.RawMessage(`{"duration":"10m","metrics":[{"expected":{"max":0.1},"failureLimit":1,"interval":"1m","provider":"prometheus-dev","query":"grpc_error_percentage"},{"expected":{"min":0.9},"failureLimit":1,"interval":"1m","provider":"prometheus-dev","query":"grpc_succeed_percentage"}]}`), - }, - { - Name: model.StageAnalysis, - AnalysisStageOptions: &AnalysisStageOptions{ - Duration: Duration(10 * time.Minute), - Logs: []TemplatableAnalysisLog{ - { - AnalysisLog: AnalysisLog{ - Provider: "stackdriver-dev", - Query: "resource.labels.pod_id=\"pod1\"\n", - Interval: Duration(1 * time.Minute), - FailureLimit: 3, - }, - }, - }, - }, - With: json.RawMessage(`{"duration":"10m","logs":[{"failureLimit":3,"interval":"1m","provider":"stackdriver-dev","query":"resource.labels.pod_id=\"pod1\"\n"}]}`), - }, - { - Name: model.StageAnalysis, - AnalysisStageOptions: &AnalysisStageOptions{ - Duration: Duration(10 * time.Minute), - HTTPS: []TemplatableAnalysisHTTP{ - { - AnalysisHTTP: AnalysisHTTP{ - URL: "https://canary-endpoint.dev", - Method: "GET", - ExpectedCode: 200, - FailureLimit: 1, - Interval: Duration(1 * time.Minute), - }, - }, - }, - }, - With: json.RawMessage(`{"duration":"10m","https":[{"expectedCode":200,"failureLimit":1,"interval":"1m","method":"GET","url":"https://canary-endpoint.dev"}]}`), - }, - }, - }, - }, - Input: KubernetesDeploymentInput{ - AutoRollback: newBoolPointer(true), - }, - VariantLabel: KubernetesVariantLabel{ - Key: "pipecd.dev/variant", - PrimaryValue: "primary", - BaselineValue: "baseline", - CanaryValue: "canary", - }, - }, - expectedError: nil, - }, - } - for _, tc := range testcases { - t.Run(tc.fileName, func(t *testing.T) { - cfg, err := LoadFromYAML(tc.fileName) - require.Equal(t, tc.expectedError, err) - if err == nil { - assert.Equal(t, tc.expectedKind, cfg.Kind) - assert.Equal(t, tc.expectedAPIVersion, cfg.APIVersion) - assert.Equal(t, tc.expectedSpec, cfg.spec) - } - }) - } -} - -func TestCustomSyncConfig(t *testing.T) { - testcases := []struct { - fileName string - expectedKind Kind - expectedAPIVersion string - expectedSpec interface{} - expectedError error - }{ - { - fileName: "testdata/application/custom-sync.yaml", - expectedKind: KindLambdaApp, - expectedAPIVersion: "pipecd.dev/v1beta1", - expectedSpec: &LambdaApplicationSpec{ - GenericApplicationSpec: GenericApplicationSpec{ - Timeout: Duration(6 * time.Hour), - Pipeline: &DeploymentPipeline{ - Stages: []PipelineStage{ - { - Name: model.StageCustomSync, - Desc: "deploy by sam", - CustomSyncOptions: &CustomSyncOptions{ - Timeout: Duration(6 * time.Hour), - Envs: map[string]string{ - "AWS_PROFILE": "default", - }, - Run: "sam build\nsam deploy -g --profile $AWS_PROFILE\n", - }, - With: json.RawMessage(`{"envs":{"AWS_PROFILE":"default"},"run":"sam build\nsam deploy -g --profile $AWS_PROFILE\n","timeout":"6h"}`), - }, - }, - }, - Trigger: Trigger{ - OnOutOfSync: OnOutOfSync{ - Disabled: newBoolPointer(true), - MinWindow: Duration(5 * time.Minute), - }, - OnChain: OnChain{ - Disabled: newBoolPointer(true), - }, - }, - Planner: DeploymentPlanner{ - AutoRollback: newBoolPointer(true), - }, - }, - Input: LambdaDeploymentInput{ - FunctionManifestFile: "function.yaml", - AutoRollback: newBoolPointer(true), - }, - }, - expectedError: nil, - }, - { - fileName: "testdata/application/custom-sync-without-run.yaml", - expectedError: fmt.Errorf("the CUSTOM_SYNC stage requires run field"), - }, - } - for _, tc := range testcases { - t.Run(tc.fileName, func(t *testing.T) { - cfg, err := LoadFromYAML(tc.fileName) - require.Equal(t, tc.expectedError, err) - if err == nil { - assert.Equal(t, tc.expectedKind, cfg.Kind) - assert.Equal(t, tc.expectedAPIVersion, cfg.APIVersion) - assert.Equal(t, tc.expectedSpec, cfg.spec) - } - }) - } -} - func TestScriptSycConfiguration(t *testing.T) { testcases := []struct { name string diff --git a/pkg/configv1/config.go b/pkg/configv1/config.go index 495330b7a7..5fe663744a 100644 --- a/pkg/configv1/config.go +++ b/pkg/configv1/config.go @@ -49,6 +49,8 @@ const ( KindCloudRunApp Kind = "CloudRunApp" // KindECSApp represents application configuration for an AWS ECS. KindECSApp Kind = "ECSApp" + // KindApplication represents a generic application configuration. + KindApplication Kind = "Application" ) const ( @@ -76,6 +78,9 @@ type Config struct { APIVersion string spec interface{} + ApplicationSpec *GenericApplicationSpec + + // TODO: remove these fields KubernetesApplicationSpec *KubernetesApplicationSpec TerraformApplicationSpec *TerraformApplicationSpec CloudRunApplicationSpec *CloudRunApplicationSpec @@ -99,6 +104,10 @@ func (c *Config) init(kind Kind, apiVersion string) error { c.APIVersion = apiVersion switch kind { + case KindApplication: + c.ApplicationSpec = &GenericApplicationSpec{} + c.spec = c.ApplicationSpec + case KindKubernetesApp: c.KubernetesApplicationSpec = &KubernetesApplicationSpec{} c.spec = c.KubernetesApplicationSpec @@ -240,6 +249,8 @@ func (k Kind) ToApplicationKind() (model.ApplicationKind, bool) { func (c *Config) GetGenericApplication() (GenericApplicationSpec, bool) { switch c.Kind { + case KindApplication: + return *c.ApplicationSpec, true case KindKubernetesApp: return c.KubernetesApplicationSpec.GenericApplicationSpec, true case KindTerraformApp: