diff --git a/.mockery.yaml b/.mockery.yaml index 990208a9..8ea3e7ed 100644 --- a/.mockery.yaml +++ b/.mockery.yaml @@ -24,3 +24,9 @@ packages: github.com/epam/edp-codebase-operator/v2/pkg/gitprovider: interfaces: GitProjectProvider: + github.com/epam/edp-codebase-operator/v2/pkg/tektoncd: + interfaces: + TriggerTemplateManager: + github.com/epam/edp-codebase-operator/v2/pkg/autodeploy: + interfaces: + Manager: diff --git a/api/v1/cdstagedeploy_types.go b/api/v1/cdstagedeploy_types.go index 47c8c0bc..5ac08382 100644 --- a/api/v1/cdstagedeploy_types.go +++ b/api/v1/cdstagedeploy_types.go @@ -27,6 +27,11 @@ type CDStageDeploySpec struct { // A list of available tags Tags []CodebaseTag `json:"tags"` + + // TriggerType specifies a strategy for auto-deploy. + // +optional + // +kubebuilder:default="Auto" + TriggerType string `json:"strategy,omitempty"` } type CodebaseTag struct { @@ -75,6 +80,26 @@ func (in *CDStageDeploy) GetStageCRName() string { return fmt.Sprintf("%s-%s", in.Spec.Pipeline, in.Spec.Stage) } +func (in *CDStageDeploy) IsPending() bool { + return in.Status.Status == CDStageDeployStatusPending +} + +func (in *CDStageDeploy) IsInQueue() bool { + return in.Status.Status == CDStageDeployStatusInQueue +} + +func (in *CDStageDeploy) IsFailed() bool { + return in.Status.Status == CDStageDeployStatusFailed +} + +func (in *CDStageDeploy) IsCompleted() bool { + return in.Status.Status == CDStageDeployStatusCompleted +} + +func (in *CDStageDeploy) IsRunning() bool { + return in.Status.Status == CDStageDeployStatusRunning +} + // +kubebuilder:object:root=true // CDStageDeployList contains a list of CDStageDeploy. diff --git a/api/v1/labels.go b/api/v1/labels.go index c921bf98..ce44868e 100644 --- a/api/v1/labels.go +++ b/api/v1/labels.go @@ -6,4 +6,7 @@ const ( // CdStageLabel is a label that is used to store the name of the CD stage in the related resources. CdStageLabel = "app.edp.epam.com/cdstage" + + // CdStageDeployLabel is a label that is used to store the name of the CD stage deploy in the related resources. + CdStageDeployLabel = "app.edp.epam.com/cdstagedeploy" ) diff --git a/config/crd/bases/v2.edp.epam.com_cdstagedeployments.yaml b/config/crd/bases/v2.edp.epam.com_cdstagedeployments.yaml index 562499cb..ff4d54cb 100644 --- a/config/crd/bases/v2.edp.epam.com_cdstagedeployments.yaml +++ b/config/crd/bases/v2.edp.epam.com_cdstagedeployments.yaml @@ -60,6 +60,10 @@ spec: stage: description: Name of related stage type: string + strategy: + default: Auto + description: TriggerType specifies a strategy for auto-deploy. + type: string tag: description: Specifies a latest available tag properties: diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 0eb4ecc2..3fc57d54 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -97,7 +97,11 @@ rules: - pipelineruns verbs: - create + - get - list + - patch + - update + - watch - apiGroups: - triggers.tekton.dev resources: diff --git a/controllers/cdstagedeploy/cdstagedeploy_controller.go b/controllers/cdstagedeploy/cdstagedeploy_controller.go index bc33c479..408efc01 100644 --- a/controllers/cdstagedeploy/cdstagedeploy_controller.go +++ b/controllers/cdstagedeploy/cdstagedeploy_controller.go @@ -81,7 +81,7 @@ func (r *ReconcileCDStageDeploy) SetupWithManager(mgr ctrl.Manager) error { //+kubebuilder:rbac:groups=v2.edp.epam.com,namespace=placeholder,resources=cdstagedeployments/status,verbs=get;update;patch //+kubebuilder:rbac:groups=v2.edp.epam.com,namespace=placeholder,resources=cdstagedeployments/finalizers,verbs=update //+kubebuilder:rbac:groups=triggers.tekton.dev,namespace=placeholder,resources=triggertemplates,verbs=get;list;watch; -//+kubebuilder:rbac:groups=tekton.dev,namespace=placeholder,resources=pipelineruns,verbs=create;list +//+kubebuilder:rbac:groups=tekton.dev,namespace=placeholder,resources=pipelineruns,verbs=get;list;watch;create;update;patch // Reconcile reads that state of the cluster for a CDStageDeploy object and makes changes based on the state. func (r *ReconcileCDStageDeploy) Reconcile(ctx context.Context, request reconcile.Request) (reconcile.Result, error) { @@ -103,7 +103,7 @@ func (r *ReconcileCDStageDeploy) Reconcile(ctx context.Context, request reconcil oldStatus := stageDeploy.Status.Status - if err := r.chainFactory(r.client).ServeRequest(ctx, stageDeploy); err != nil { + if err := r.chainFactory(r.client, stageDeploy).ServeRequest(ctx, stageDeploy); err != nil { stageDeploy.SetFailedStatus(err) if statusErr := r.client.Status().Update(ctx, stageDeploy); statusErr != nil { diff --git a/controllers/cdstagedeploy/cdstagedeploy_controller_test.go b/controllers/cdstagedeploy/cdstagedeploy_controller_test.go index ceeffbd1..1524b89d 100644 --- a/controllers/cdstagedeploy/cdstagedeploy_controller_test.go +++ b/controllers/cdstagedeploy/cdstagedeploy_controller_test.go @@ -55,7 +55,7 @@ func TestReconcileCDStageDeploy_Reconcile(t *testing.T) { return fake.NewClientBuilder().WithScheme(scheme).WithObjects(dp).Build() }, chainFactory: func(t *testing.T) chain.CDStageDeployChain { - return func(cl client.Client) chain.CDStageDeployHandler { + return func(cl client.Client, stageDeploy *codebaseApi.CDStageDeploy) chain.CDStageDeployHandler { m := mocks.NewMockCDStageDeployHandler(t) m.On("ServeRequest", mock.Anything, mock.Anything). @@ -88,7 +88,7 @@ func TestReconcileCDStageDeploy_Reconcile(t *testing.T) { return fake.NewClientBuilder().WithScheme(scheme).WithObjects(dp).Build() }, chainFactory: func(t *testing.T) chain.CDStageDeployChain { - return func(cl client.Client) chain.CDStageDeployHandler { + return func(cl client.Client, stageDeploy *codebaseApi.CDStageDeploy) chain.CDStageDeployHandler { m := mocks.NewMockCDStageDeployHandler(t) m.On("ServeRequest", mock.Anything, mock.Anything). @@ -115,7 +115,7 @@ func TestReconcileCDStageDeploy_Reconcile(t *testing.T) { return fake.NewClientBuilder().WithScheme(scheme).Build() }, chainFactory: func(t *testing.T) chain.CDStageDeployChain { - return func(cl client.Client) chain.CDStageDeployHandler { + return func(cl client.Client, stageDeploy *codebaseApi.CDStageDeploy) chain.CDStageDeployHandler { m := mocks.NewMockCDStageDeployHandler(t) m.On("ServeRequest", mock.Anything, mock.Anything). diff --git a/controllers/cdstagedeploy/chain/create_pending_trigger_template.go b/controllers/cdstagedeploy/chain/create_pending_trigger_template.go new file mode 100644 index 00000000..58c6f918 --- /dev/null +++ b/controllers/cdstagedeploy/chain/create_pending_trigger_template.go @@ -0,0 +1,118 @@ +package chain + +import ( + "context" + "errors" + "fmt" + + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + pipelineAPi "github.com/epam/edp-cd-pipeline-operator/v2/api/v1" + + codebaseApi "github.com/epam/edp-codebase-operator/v2/api/v1" + "github.com/epam/edp-codebase-operator/v2/pkg/autodeploy" + "github.com/epam/edp-codebase-operator/v2/pkg/tektoncd" +) + +type CreatePendingTriggerTemplate struct { + k8sClient client.Client + triggerTemplateManager tektoncd.TriggerTemplateManager + autoDeployStrategyManager autodeploy.Manager +} + +func NewCreatePendingTriggerTemplate(k8sClient client.Client, triggerTemplateManager tektoncd.TriggerTemplateManager, autoDeployStrategyManager autodeploy.Manager) *CreatePendingTriggerTemplate { + return &CreatePendingTriggerTemplate{k8sClient: k8sClient, triggerTemplateManager: triggerTemplateManager, autoDeployStrategyManager: autoDeployStrategyManager} +} + +func (h *CreatePendingTriggerTemplate) ServeRequest( + ctx context.Context, + stageDeploy *codebaseApi.CDStageDeploy, +) error { + log := ctrl.LoggerFrom(ctx).WithValues("stage", stageDeploy.Spec.Stage, "pipeline", stageDeploy.Spec.Pipeline, "status", stageDeploy.Status.Status) + + if skipPipelineRunCreation(stageDeploy) { + log.Info("Skip processing TriggerTemplate for auto-deploy.") + + return nil + } + + log.Info("Start processing TriggerTemplate for auto-deploy.") + + pipeline, stage, rawResource, err := getResourcesForPipelineRun(ctx, stageDeploy, h.k8sClient, h.triggerTemplateManager) + if err != nil { + if errors.Is(err, tektoncd.ErrEmptyTriggerTemplateResources) { + log.Info("No resource templates found in the trigger template. Skip processing.", "triggertemplate", stage.Spec.TriggerType) + + stageDeploy.Status.Status = codebaseApi.CDStageDeployStatusCompleted + + return nil + } + + return err + } + + appPayload, err := h.autoDeployStrategyManager.GetAppPayloadForCurrentWithStableStrategy( + ctx, + stageDeploy.Spec.Tag, + pipeline, + stage, + ) + if err != nil { + if errors.Is(err, autodeploy.ErrLasTagNotFound) { + stageDeploy.Status.Status = codebaseApi.CDStageDeployStatusCompleted + return nil + } + + return fmt.Errorf("failed to get app payload: %w", err) + } + + if err = h.triggerTemplateManager.CreatePendingPipelineRun( + ctx, + stageDeploy.Namespace, + stageDeploy.Name, + rawResource, + appPayload, + []byte(stage.Spec.Name), + []byte(pipeline.Spec.Name), + []byte(stage.Spec.ClusterName), + ); err != nil { + return fmt.Errorf("failed to create PendingPipelineRun: %w", err) + } + + stageDeploy.Status.Status = codebaseApi.CDStageDeployStatusInQueue + + log.Info("TriggerTemplate for auto-deploy has been processed successfully.") + + return nil +} + +func getResourcesForPipelineRun( + ctx context.Context, + stageDeploy *codebaseApi.CDStageDeploy, + k8sClient client.Client, + triggerTemplateManager tektoncd.TriggerTemplateManager, +) (*pipelineAPi.CDPipeline, *pipelineAPi.Stage, []byte, error) { + pipeline := &pipelineAPi.CDPipeline{} + if err := k8sClient.Get(ctx, client.ObjectKey{ + Namespace: stageDeploy.Namespace, + Name: stageDeploy.Spec.Pipeline, + }, pipeline); err != nil { + return nil, nil, nil, fmt.Errorf("failed to get CDPipeline: %w", err) + } + + stage := &pipelineAPi.Stage{} + if err := k8sClient.Get(ctx, client.ObjectKey{ + Namespace: stageDeploy.Namespace, + Name: stageDeploy.GetStageCRName(), + }, stage); err != nil { + return pipeline, nil, nil, fmt.Errorf("failed to get Stage: %w", err) + } + + rawResource, err := triggerTemplateManager.GetRawResourceFromTriggerTemplate(ctx, stage.Spec.TriggerTemplate, stageDeploy.Namespace) + if err != nil { + return pipeline, stage, nil, fmt.Errorf("failed to get raw resource from TriggerTemplate: %w", err) + } + + return pipeline, stage, rawResource, nil +} diff --git a/controllers/cdstagedeploy/chain/create_pending_trigger_template_test.go b/controllers/cdstagedeploy/chain/create_pending_trigger_template_test.go new file mode 100644 index 00000000..f3fa45f9 --- /dev/null +++ b/controllers/cdstagedeploy/chain/create_pending_trigger_template_test.go @@ -0,0 +1,569 @@ +package chain + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "testing" + + "github.com/go-logr/logr" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + pipelineApi "github.com/epam/edp-cd-pipeline-operator/v2/api/v1" + + codebaseApi "github.com/epam/edp-codebase-operator/v2/api/v1" + "github.com/epam/edp-codebase-operator/v2/pkg/autodeploy" + autodeploymocks "github.com/epam/edp-codebase-operator/v2/pkg/autodeploy/mocks" + "github.com/epam/edp-codebase-operator/v2/pkg/tektoncd" + tektoncdmocks "github.com/epam/edp-codebase-operator/v2/pkg/tektoncd/mocks" +) + +func TestCreatePendingTriggerTemplate_ServeRequest(t *testing.T) { + t.Parallel() + + scheme := runtime.NewScheme() + require.NoError(t, codebaseApi.AddToScheme(scheme)) + require.NoError(t, pipelineApi.AddToScheme(scheme)) + + type fields struct { + k8sClient func(t *testing.T) client.Client + triggerTemplateManager func(t *testing.T) tektoncd.TriggerTemplateManager + autoDeployStrategyManager func(t *testing.T) autodeploy.Manager + } + + tests := []struct { + name string + stageDeploy *codebaseApi.CDStageDeploy + fields fields + wantErr require.ErrorAssertionFunc + want func(t *testing.T, d *codebaseApi.CDStageDeploy) + }{ + { + name: "should process TriggerTemplate for auto-deploy", + stageDeploy: &codebaseApi.CDStageDeploy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "default", + }, + Spec: codebaseApi.CDStageDeploySpec{ + Pipeline: "pipe1", + Stage: "dev", + }, + Status: codebaseApi.CDStageDeployStatus{ + Status: codebaseApi.CDStageDeployStatusPending, + }, + }, + fields: fields{ + k8sClient: func(t *testing.T) client.Client { + return fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects( + &pipelineApi.CDPipeline{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pipe1", + Namespace: "default", + }, + Spec: pipelineApi.CDPipelineSpec{ + Name: "pipe1", + }, + }, + &pipelineApi.Stage{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pipe1-dev", + Namespace: "default", + }, + Spec: pipelineApi.StageSpec{ + TriggerTemplate: "trigger1", + Name: "dev", + ClusterName: "cluster-secret", + }, + }, + ). + Build() + }, + triggerTemplateManager: func(t *testing.T) tektoncd.TriggerTemplateManager { + m := tektoncdmocks.NewMockTriggerTemplateManager(t) + + m.On("GetRawResourceFromTriggerTemplate", mock.Anything, "trigger1", "default"). + Return([]byte("raw resource"), nil) + m.On( + "CreatePendingPipelineRun", + mock.Anything, + "default", + "test", + []byte("raw resource"), + []byte("{app1: 1.0}"), + []byte("dev"), + []byte("pipe1"), + []byte("cluster-secret"), + ).Return(nil) + + return m + }, + autoDeployStrategyManager: func(t *testing.T) autodeploy.Manager { + m := autodeploymocks.NewMockManager(t) + + m.On("GetAppPayloadForCurrentWithStableStrategy", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(json.RawMessage("{app1: 1.0}"), nil) + + return m + }, + }, + wantErr: require.NoError, + want: func(t *testing.T, d *codebaseApi.CDStageDeploy) { + assert.Equal(t, codebaseApi.CDStageDeployStatusInQueue, d.Status.Status) + }, + }, + { + name: "failed to create PipelineRun", + stageDeploy: &codebaseApi.CDStageDeploy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "default", + }, + Spec: codebaseApi.CDStageDeploySpec{ + Pipeline: "pipe1", + Stage: "dev", + }, + Status: codebaseApi.CDStageDeployStatus{ + Status: codebaseApi.CDStageDeployStatusPending, + }, + }, + fields: fields{ + k8sClient: func(t *testing.T) client.Client { + return fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects( + &pipelineApi.CDPipeline{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pipe1", + Namespace: "default", + }, + Spec: pipelineApi.CDPipelineSpec{ + Name: "pipe1", + }, + }, + &pipelineApi.Stage{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pipe1-dev", + Namespace: "default", + }, + Spec: pipelineApi.StageSpec{ + TriggerTemplate: "trigger1", + Name: "dev", + ClusterName: "cluster-secret", + }, + }, + ). + Build() + }, + triggerTemplateManager: func(t *testing.T) tektoncd.TriggerTemplateManager { + m := tektoncdmocks.NewMockTriggerTemplateManager(t) + + m.On("GetRawResourceFromTriggerTemplate", mock.Anything, "trigger1", "default"). + Return([]byte("raw resource"), nil) + m.On( + "CreatePendingPipelineRun", + mock.Anything, + "default", + "test", + []byte("raw resource"), + []byte("{app1: 1.0}"), + []byte("dev"), + []byte("pipe1"), + []byte("cluster-secret"), + ).Return(errors.New("failed to create PipelineRun")) + + return m + }, + autoDeployStrategyManager: func(t *testing.T) autodeploy.Manager { + m := autodeploymocks.NewMockManager(t) + + m.On("GetAppPayloadForCurrentWithStableStrategy", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(json.RawMessage("{app1: 1.0}"), nil) + + return m + }, + }, + wantErr: func(t require.TestingT, err error, i ...interface{}) { + require.Error(t, err) + assert.Contains(t, err.Error(), "failed to create PipelineRun") + }, + }, + { + name: "failed to get app payload for all latest strategy", + stageDeploy: &codebaseApi.CDStageDeploy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "default", + }, + Spec: codebaseApi.CDStageDeploySpec{ + Pipeline: "pipe1", + Stage: "dev", + }, + Status: codebaseApi.CDStageDeployStatus{ + Status: codebaseApi.CDStageDeployStatusPending, + }, + }, + fields: fields{ + k8sClient: func(t *testing.T) client.Client { + return fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects( + &pipelineApi.CDPipeline{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pipe1", + Namespace: "default", + }, + Spec: pipelineApi.CDPipelineSpec{ + Name: "pipe1", + }, + }, + &pipelineApi.Stage{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pipe1-dev", + Namespace: "default", + }, + Spec: pipelineApi.StageSpec{ + TriggerTemplate: "trigger1", + Name: "dev", + ClusterName: "cluster-secret", + }, + }, + ). + Build() + }, + triggerTemplateManager: func(t *testing.T) tektoncd.TriggerTemplateManager { + m := tektoncdmocks.NewMockTriggerTemplateManager(t) + + m.On("GetRawResourceFromTriggerTemplate", mock.Anything, "trigger1", "default"). + Return([]byte("raw resource"), nil) + + return m + }, + autoDeployStrategyManager: func(t *testing.T) autodeploy.Manager { + m := autodeploymocks.NewMockManager(t) + + m.On("GetAppPayloadForCurrentWithStableStrategy", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil, errors.New("failed to get app payload")) + + return m + }, + }, + wantErr: func(t require.TestingT, err error, i ...interface{}) { + require.Error(t, err) + assert.Contains(t, err.Error(), "failed to get app payload") + }, + }, + { + name: "last tag not found", + stageDeploy: &codebaseApi.CDStageDeploy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "default", + }, + Spec: codebaseApi.CDStageDeploySpec{ + Pipeline: "pipe1", + Stage: "dev", + }, + Status: codebaseApi.CDStageDeployStatus{ + Status: codebaseApi.CDStageDeployStatusPending, + }, + }, + fields: fields{ + k8sClient: func(t *testing.T) client.Client { + return fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects( + &pipelineApi.CDPipeline{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pipe1", + Namespace: "default", + }, + Spec: pipelineApi.CDPipelineSpec{ + Name: "pipe1", + }, + }, + &pipelineApi.Stage{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pipe1-dev", + Namespace: "default", + }, + Spec: pipelineApi.StageSpec{ + TriggerTemplate: "trigger1", + Name: "dev", + ClusterName: "cluster-secret", + }, + }, + ). + Build() + }, + triggerTemplateManager: func(t *testing.T) tektoncd.TriggerTemplateManager { + m := tektoncdmocks.NewMockTriggerTemplateManager(t) + + m.On("GetRawResourceFromTriggerTemplate", mock.Anything, "trigger1", "default"). + Return([]byte("raw resource"), nil) + + return m + }, + autoDeployStrategyManager: func(t *testing.T) autodeploy.Manager { + m := autodeploymocks.NewMockManager(t) + + m.On("GetAppPayloadForCurrentWithStableStrategy", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil, fmt.Errorf("failed to get app payload: %w", autodeploy.ErrLasTagNotFound)) + + return m + }, + }, + wantErr: require.NoError, + want: func(t *testing.T, d *codebaseApi.CDStageDeploy) { + assert.Equal(t, codebaseApi.CDStageDeployStatusCompleted, d.Status.Status) + }, + }, + { + name: "failed to get raw resource from trigger template", + stageDeploy: &codebaseApi.CDStageDeploy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "default", + }, + Spec: codebaseApi.CDStageDeploySpec{ + Pipeline: "pipe1", + Stage: "dev", + }, + Status: codebaseApi.CDStageDeployStatus{ + Status: codebaseApi.CDStageDeployStatusPending, + }, + }, + fields: fields{ + k8sClient: func(t *testing.T) client.Client { + return fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects( + &pipelineApi.CDPipeline{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pipe1", + Namespace: "default", + }, + Spec: pipelineApi.CDPipelineSpec{ + Name: "pipe1", + }, + }, + &pipelineApi.Stage{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pipe1-dev", + Namespace: "default", + }, + Spec: pipelineApi.StageSpec{ + TriggerTemplate: "trigger1", + Name: "dev", + ClusterName: "cluster-secret", + }, + }, + ). + Build() + }, + triggerTemplateManager: func(t *testing.T) tektoncd.TriggerTemplateManager { + m := tektoncdmocks.NewMockTriggerTemplateManager(t) + + m.On("GetRawResourceFromTriggerTemplate", mock.Anything, "trigger1", "default"). + Return(nil, errors.New("failed to get raw resource")) + + return m + }, + autoDeployStrategyManager: func(t *testing.T) autodeploy.Manager { + return autodeploymocks.NewMockManager(t) + }, + }, + wantErr: func(t require.TestingT, err error, i ...interface{}) { + require.Error(t, err) + assert.Contains(t, err.Error(), "failed to get raw resource") + }, + }, + { + name: "no resource templates found in the trigger template", + stageDeploy: &codebaseApi.CDStageDeploy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "default", + }, + Spec: codebaseApi.CDStageDeploySpec{ + Pipeline: "pipe1", + Stage: "dev", + }, + Status: codebaseApi.CDStageDeployStatus{ + Status: codebaseApi.CDStageDeployStatusPending, + }, + }, + fields: fields{ + k8sClient: func(t *testing.T) client.Client { + return fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects( + &pipelineApi.CDPipeline{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pipe1", + Namespace: "default", + }, + Spec: pipelineApi.CDPipelineSpec{ + Name: "pipe1", + }, + }, + &pipelineApi.Stage{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pipe1-dev", + Namespace: "default", + }, + Spec: pipelineApi.StageSpec{ + TriggerTemplate: "trigger1", + Name: "dev", + ClusterName: "cluster-secret", + }, + }, + ). + Build() + }, + triggerTemplateManager: func(t *testing.T) tektoncd.TriggerTemplateManager { + m := tektoncdmocks.NewMockTriggerTemplateManager(t) + + m.On("GetRawResourceFromTriggerTemplate", mock.Anything, "trigger1", "default"). + Return(nil, fmt.Errorf("failed to get TriggerTemplate: %w", tektoncd.ErrEmptyTriggerTemplateResources)) + + return m + }, + autoDeployStrategyManager: func(t *testing.T) autodeploy.Manager { + return autodeploymocks.NewMockManager(t) + }, + }, + wantErr: require.NoError, + want: func(t *testing.T, d *codebaseApi.CDStageDeploy) { + assert.Equal(t, codebaseApi.CDStageDeployStatusCompleted, d.Status.Status) + }, + }, + { + name: "failed to get Stage", + stageDeploy: &codebaseApi.CDStageDeploy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "default", + }, + Spec: codebaseApi.CDStageDeploySpec{ + Pipeline: "pipe1", + Stage: "dev", + }, + Status: codebaseApi.CDStageDeployStatus{ + Status: codebaseApi.CDStageDeployStatusPending, + }, + }, + fields: fields{ + k8sClient: func(t *testing.T) client.Client { + return fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects( + &pipelineApi.CDPipeline{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pipe1", + Namespace: "default", + }, + Spec: pipelineApi.CDPipelineSpec{ + Name: "pipe1", + }, + }, + ). + Build() + }, + triggerTemplateManager: func(t *testing.T) tektoncd.TriggerTemplateManager { + return tektoncdmocks.NewMockTriggerTemplateManager(t) + }, + autoDeployStrategyManager: func(t *testing.T) autodeploy.Manager { + return autodeploymocks.NewMockManager(t) + }, + }, + wantErr: func(t require.TestingT, err error, i ...interface{}) { + require.Error(t, err) + assert.Contains(t, err.Error(), "failed to get Stage") + }, + }, + { + name: "failed to get CDPipeline", + stageDeploy: &codebaseApi.CDStageDeploy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "default", + }, + Spec: codebaseApi.CDStageDeploySpec{ + Pipeline: "pipe1", + Stage: "dev", + }, + Status: codebaseApi.CDStageDeployStatus{ + Status: codebaseApi.CDStageDeployStatusPending, + }, + }, + fields: fields{ + k8sClient: func(t *testing.T) client.Client { + return fake.NewClientBuilder(). + WithScheme(scheme). + Build() + }, + triggerTemplateManager: func(t *testing.T) tektoncd.TriggerTemplateManager { + return tektoncdmocks.NewMockTriggerTemplateManager(t) + }, + autoDeployStrategyManager: func(t *testing.T) autodeploy.Manager { + return autodeploymocks.NewMockManager(t) + }, + }, + wantErr: func(t require.TestingT, err error, i ...interface{}) { + require.Error(t, err) + assert.Contains(t, err.Error(), "failed to get CDPipeline") + }, + }, + { + name: "skip processing TriggerTemplate for auto-deploy", + stageDeploy: &codebaseApi.CDStageDeploy{ + Status: codebaseApi.CDStageDeployStatus{ + Status: codebaseApi.CDStageDeployStatusInQueue, + }, + }, + fields: fields{ + k8sClient: func(t *testing.T) client.Client { + return fake.NewClientBuilder(). + WithScheme(scheme). + Build() + }, + triggerTemplateManager: func(t *testing.T) tektoncd.TriggerTemplateManager { + return tektoncdmocks.NewMockTriggerTemplateManager(t) + }, + autoDeployStrategyManager: func(t *testing.T) autodeploy.Manager { + return autodeploymocks.NewMockManager(t) + }, + }, + wantErr: require.NoError, + want: func(t *testing.T, d *codebaseApi.CDStageDeploy) { + assert.Equal(t, codebaseApi.CDStageDeployStatusInQueue, d.Status.Status) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + h := NewCreatePendingTriggerTemplate( + tt.fields.k8sClient(t), + tt.fields.triggerTemplateManager(t), + tt.fields.autoDeployStrategyManager(t), + ) + + tt.wantErr(t, h.ServeRequest(ctrl.LoggerInto(context.Background(), logr.Discard()), tt.stageDeploy)) + if tt.want != nil { + tt.want(t, tt.stageDeploy) + } + }) + } +} diff --git a/controllers/cdstagedeploy/chain/delete_cdstagedeploy.go b/controllers/cdstagedeploy/chain/delete_cdstagedeploy.go index 98720feb..303705b1 100644 --- a/controllers/cdstagedeploy/chain/delete_cdstagedeploy.go +++ b/controllers/cdstagedeploy/chain/delete_cdstagedeploy.go @@ -23,7 +23,7 @@ func NewDeleteCDStageDeploy(k8sClient client.Client) *DeleteCDStageDeploy { func (h *DeleteCDStageDeploy) ServeRequest(ctx context.Context, stageDeploy *codebaseApi.CDStageDeploy) error { log := ctrl.LoggerFrom(ctx) - if stageDeploy.Status.Status != codebaseApi.CDStageDeployStatusCompleted { + if !stageDeploy.IsCompleted() { log.Info("CDStageDeploy has not been completed yet. Skip deleting.") return nil diff --git a/controllers/cdstagedeploy/chain/factory.go b/controllers/cdstagedeploy/chain/factory.go index b96809eb..54efa40f 100644 --- a/controllers/cdstagedeploy/chain/factory.go +++ b/controllers/cdstagedeploy/chain/factory.go @@ -2,16 +2,32 @@ package chain import ( "sigs.k8s.io/controller-runtime/pkg/client" + + pipelineApi "github.com/epam/edp-cd-pipeline-operator/v2/api/v1" + + codebaseApi "github.com/epam/edp-codebase-operator/v2/api/v1" + "github.com/epam/edp-codebase-operator/v2/pkg/autodeploy" + "github.com/epam/edp-codebase-operator/v2/pkg/tektoncd" ) -type CDStageDeployChain func(cl client.Client) CDStageDeployHandler +type CDStageDeployChain func(cl client.Client, stageDeploy *codebaseApi.CDStageDeploy) CDStageDeployHandler -func CreateDefChain(cl client.Client) CDStageDeployHandler { +func CreateChain(cl client.Client, stageDeploy *codebaseApi.CDStageDeploy) CDStageDeployHandler { c := chain{} + if stageDeploy.Spec.TriggerType == pipelineApi.TriggerTypeAutoStable { + c.Use( + NewCreatePendingTriggerTemplate(cl, tektoncd.NewTektonTriggerTemplateManager(cl), autodeploy.NewStrategyManager(cl)), + NewProcessPendingPipeRuns(cl), + NewDeleteCDStageDeploy(cl), + ) + + return &c + } + c.Use( NewResolveStatus(cl), - NewProcessTriggerTemplate(cl), + NewProcessTriggerTemplate(cl, tektoncd.NewTektonTriggerTemplateManager(cl), autodeploy.NewStrategyManager(cl)), NewDeleteCDStageDeploy(cl), ) diff --git a/controllers/cdstagedeploy/chain/factory_test.go b/controllers/cdstagedeploy/chain/factory_test.go new file mode 100644 index 00000000..9d062ffa --- /dev/null +++ b/controllers/cdstagedeploy/chain/factory_test.go @@ -0,0 +1,47 @@ +package chain + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + pipelineApi "github.com/epam/edp-cd-pipeline-operator/v2/api/v1" + + codebaseApi "github.com/epam/edp-codebase-operator/v2/api/v1" +) + +func TestCreateChain(t *testing.T) { + t.Parallel() + + cl := fake.NewClientBuilder().Build() + + tests := []struct { + name string + stageDeploy *codebaseApi.CDStageDeploy + want CDStageDeployHandler + }{ + { + name: "create Auto-stable chain", + stageDeploy: &codebaseApi.CDStageDeploy{ + Spec: codebaseApi.CDStageDeploySpec{ + TriggerType: pipelineApi.TriggerTypeAutoStable, + }, + }, + }, + { + name: "create Auto chain", + stageDeploy: &codebaseApi.CDStageDeploy{ + Spec: codebaseApi.CDStageDeploySpec{ + TriggerType: pipelineApi.TriggerTypeAutoDeploy, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.NotNil(t, CreateChain(cl, tt.stageDeploy)) + }) + } +} diff --git a/controllers/cdstagedeploy/chain/process_pending_piperuns.go b/controllers/cdstagedeploy/chain/process_pending_piperuns.go new file mode 100644 index 00000000..1a28dc94 --- /dev/null +++ b/controllers/cdstagedeploy/chain/process_pending_piperuns.go @@ -0,0 +1,164 @@ +package chain + +import ( + "context" + "fmt" + + tektonpipelineApi "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" + "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + codebaseApi "github.com/epam/edp-codebase-operator/v2/api/v1" +) + +type ProcessPendingPipeRuns struct { + k8sClient client.Client +} + +func NewProcessPendingPipeRuns(k8sClient client.Client) *ProcessPendingPipeRuns { + return &ProcessPendingPipeRuns{k8sClient: k8sClient} +} + +func (r *ProcessPendingPipeRuns) ServeRequest(ctx context.Context, stageDeploy *codebaseApi.CDStageDeploy) error { + log := ctrl.LoggerFrom(ctx).WithValues("stage", stageDeploy.Spec.Stage, "pipeline", stageDeploy.Spec.Pipeline, "status", stageDeploy.Status.Status) + + if skipProcessingPendingPipeRuns(stageDeploy) { + log.Info("Skip processing pending PipelineRuns for CDStageDeploy.") + + return nil + } + + pipeRuns, err := r.getPipelines(ctx, stageDeploy) + if err != nil { + log.Info("Failed to get PipelineRuns.", "error", err) + + return nil + } + + currentRun, err := getCdStageDeployPipeRun(pipeRuns, stageDeploy.Name) + if err != nil { + log.Info("Failed to get PipelineRun related to CDStageDeploy.", "error", err) + return nil + } + + if currentRun.IsDone() { + stageDeploy.Status.Status = codebaseApi.CDStageDeployStatusCompleted + + log.Info("PipelineRun is done. CdStageDeploy is completed.") + + return nil + } + + if currentRun.Spec.Status == tektonpipelineApi.PipelineRunSpecStatusPending { + if shouldStartPipeRun(currentRun.Name, pipeRuns) { + if err = r.startPipelineRun(ctx, currentRun); err != nil { + log.Info("Failed to start PipelineRun.", "error", err) + + return nil + } + + stageDeploy.Status.Status = codebaseApi.CDStageDeployStatusRunning + } + + return nil + } + + return nil +} + +func skipProcessingPendingPipeRuns(stageDeploy *codebaseApi.CDStageDeploy) bool { + if stageDeploy.IsInQueue() || stageDeploy.IsRunning() { + return false + } + + return true +} + +func (r *ProcessPendingPipeRuns) startPipelineRun(ctx context.Context, pipeRun *tektonpipelineApi.PipelineRun) error { + patch := []byte(`{"spec":{"status":""}}`) + + if err := r.k8sClient.Patch(ctx, pipeRun, client.RawPatch(types.MergePatchType, patch)); err != nil { + return fmt.Errorf("failed to start PipelineRun: %w", err) + } + + return nil +} + +func (r *ProcessPendingPipeRuns) getPipelines(ctx context.Context, stageDeploy *codebaseApi.CDStageDeploy) (*tektonpipelineApi.PipelineRunList, error) { + log := ctrl.LoggerFrom(ctx) + + pipelineRun := &tektonpipelineApi.PipelineRunList{} + + if err := r.k8sClient.List( + ctx, + pipelineRun, + client.InNamespace(stageDeploy.Namespace), + client.MatchingLabels{ + codebaseApi.CdPipelineLabel: stageDeploy.Spec.Pipeline, + codebaseApi.CdStageLabel: stageDeploy.GetStageCRName(), + }, + client.Limit(maxPipelineRuns), + ); err != nil { + return nil, fmt.Errorf("failed to list PipelineRuns: %w", err) + } + + if len(pipelineRun.Items) > pipelineRunWarningLimit { + log.Info("Warning: too many PipelineRuns found. Consider to clean up old PipelineRuns.") + } + + return pipelineRun, nil +} + +func getCdStageDeployPipeRun(runs *tektonpipelineApi.PipelineRunList, cdStageDeployName string) (*tektonpipelineApi.PipelineRun, error) { + for i := range runs.Items { + if runs.Items[i].Labels[codebaseApi.CdStageDeployLabel] == cdStageDeployName { + return &runs.Items[i], nil + } + } + + return nil, fmt.Errorf("pipeline run for CDStageDeploy %v not found", cdStageDeployName) +} + +func shouldStartPipeRun(pipeRunName string, runs *tektonpipelineApi.PipelineRunList) bool { + if !allPipelineRunsCompletedExcept(runs.Items, pipeRunName) { + return false + } + + return isFirsPendingPipelineRun(runs, pipeRunName) +} + +func allPipelineRunsCompletedExcept(pipelineRuns []tektonpipelineApi.PipelineRun, except string) bool { + for i := range pipelineRuns { + if pipelineRuns[i].Name == except || pipelineRuns[i].Spec.Status == tektonpipelineApi.PipelineRunSpecStatusPending { + continue + } + + if !pipelineRuns[i].IsDone() { + return false + } + } + + return true +} + +// isFirsPendingPipelineRun checks if the pending pipeline run is the first one in the chain based on the CreationTimestamp field. +func isFirsPendingPipelineRun(runs *tektonpipelineApi.PipelineRunList, pipeRunName string) bool { + if len(runs.Items) == 0 { + return false + } + + var firstRun *tektonpipelineApi.PipelineRun + + for i := range runs.Items { + if runs.Items[i].Spec.Status != tektonpipelineApi.PipelineRunSpecStatusPending { + continue + } + + if firstRun == nil || runs.Items[i].CreationTimestamp.Before(&firstRun.CreationTimestamp) { + firstRun = &runs.Items[i] + } + } + + return firstRun != nil && firstRun.Name == pipeRunName +} diff --git a/controllers/cdstagedeploy/chain/process_pending_piperuns_test.go b/controllers/cdstagedeploy/chain/process_pending_piperuns_test.go new file mode 100644 index 00000000..d28937a8 --- /dev/null +++ b/controllers/cdstagedeploy/chain/process_pending_piperuns_test.go @@ -0,0 +1,134 @@ +package chain + +import ( + "context" + "testing" + + "github.com/go-logr/logr" + "github.com/stretchr/testify/require" + tektonpipelineApi "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + codebaseApi "github.com/epam/edp-codebase-operator/v2/api/v1" +) + +func TestProcessPendingPipeRuns_ServeRequest(t *testing.T) { + t.Parallel() + + scheme := runtime.NewScheme() + require.NoError(t, tektonpipelineApi.AddToScheme(scheme)) + + tests := []struct { + name string + stageDeploy *codebaseApi.CDStageDeploy + k8sClient func(t *testing.T) client.Client + wantErr require.ErrorAssertionFunc + want func(t *testing.T, stageDeploy *codebaseApi.CDStageDeploy) + }{ + { + name: "skip processing pending PipelineRuns for CDStageDeploy", + stageDeploy: &codebaseApi.CDStageDeploy{ + Status: codebaseApi.CDStageDeployStatus{ + Status: codebaseApi.CDStageDeployStatusCompleted, + }, + }, + k8sClient: func(t *testing.T) client.Client { + return fake.NewClientBuilder().WithScheme(scheme).Build() + }, + wantErr: require.NoError, + want: func(t *testing.T, stageDeploy *codebaseApi.CDStageDeploy) { + require.Equal(t, codebaseApi.CDStageDeployStatusCompleted, stageDeploy.Status.Status) + }, + }, + { + name: "pipelineRun completed, CDStageDeploy should be completed", + stageDeploy: &codebaseApi.CDStageDeploy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pipeline-dev-deploy", + Namespace: "default", + }, + Spec: codebaseApi.CDStageDeploySpec{ + Stage: "dev", + Pipeline: "pipeline", + }, + Status: codebaseApi.CDStageDeployStatus{ + Status: codebaseApi.CDStageDeployStatusRunning, + }, + }, + k8sClient: func(t *testing.T) client.Client { + run := &tektonpipelineApi.PipelineRun{ + ObjectMeta: ctrl.ObjectMeta{ + Name: "test-pipeline-run", + Namespace: "default", + Labels: map[string]string{ + codebaseApi.CdStageDeployLabel: "pipeline-dev-deploy", + codebaseApi.CdPipelineLabel: "pipeline", + codebaseApi.CdStageLabel: "pipeline-dev", + }, + }, + } + run.Status.MarkSucceeded("done", "done") + + return fake.NewClientBuilder().WithScheme(scheme).WithStatusSubresource(run).WithObjects(run).Build() + }, + wantErr: require.NoError, + want: func(t *testing.T, stageDeploy *codebaseApi.CDStageDeploy) { + require.Equal(t, codebaseApi.CDStageDeployStatusCompleted, stageDeploy.Status.Status) + }, + }, + { + name: "should start pending pipelineRun", + stageDeploy: &codebaseApi.CDStageDeploy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pipeline-dev-deploy", + Namespace: "default", + }, + Spec: codebaseApi.CDStageDeploySpec{ + Stage: "dev", + Pipeline: "pipeline", + }, + Status: codebaseApi.CDStageDeployStatus{ + Status: codebaseApi.CDStageDeployStatusInQueue, + }, + }, + k8sClient: func(t *testing.T) client.Client { + run := &tektonpipelineApi.PipelineRun{ + ObjectMeta: ctrl.ObjectMeta{ + Name: "test-pipeline-run", + Namespace: "default", + Labels: map[string]string{ + codebaseApi.CdStageDeployLabel: "pipeline-dev-deploy", + codebaseApi.CdPipelineLabel: "pipeline", + codebaseApi.CdStageLabel: "pipeline-dev", + }, + }, + Spec: tektonpipelineApi.PipelineRunSpec{ + Status: tektonpipelineApi.PipelineRunSpecStatusPending, + }, + } + + return fake.NewClientBuilder().WithScheme(scheme).WithStatusSubresource(run).WithObjects(run).Build() + }, + wantErr: require.NoError, + want: func(t *testing.T, stageDeploy *codebaseApi.CDStageDeploy) { + require.Equal(t, codebaseApi.CDStageDeployStatusRunning, stageDeploy.Status.Status) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := NewProcessPendingPipeRuns(tt.k8sClient(t)) + + tt.wantErr(t, r.ServeRequest(ctrl.LoggerInto(context.Background(), logr.Discard()), tt.stageDeploy)) + + if tt.want != nil { + tt.want(t, tt.stageDeploy) + } + }) + } +} diff --git a/controllers/cdstagedeploy/chain/process_trigger_template.go b/controllers/cdstagedeploy/chain/process_trigger_template.go index c3d597f2..cc845ef5 100644 --- a/controllers/cdstagedeploy/chain/process_trigger_template.go +++ b/controllers/cdstagedeploy/chain/process_trigger_template.go @@ -1,45 +1,40 @@ package chain import ( - "bytes" "context" - "encoding/json" "errors" "fmt" - tektonTriggersApi "github.com/tektoncd/triggers/pkg/apis/triggers/v1beta1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" - pipelineAPi "github.com/epam/edp-cd-pipeline-operator/v2/api/v1" - codebaseApi "github.com/epam/edp-codebase-operator/v2/api/v1" - "github.com/epam/edp-codebase-operator/v2/controllers/codebasebranch/chain/put_codebase_image_stream" - "github.com/epam/edp-codebase-operator/v2/pkg/codebaseimagestream" -) - -var ( - errLasTagNotFound = fmt.Errorf("last tag not found") - errEmptyTriggerTemplateResources = fmt.Errorf("trigger template resources is empty") + "github.com/epam/edp-codebase-operator/v2/pkg/autodeploy" + "github.com/epam/edp-codebase-operator/v2/pkg/tektoncd" ) -type TriggerTemplateApplicationPayload struct { - ImageTag string `json:"imageTag"` -} - type ProcessTriggerTemplate struct { - k8sClient client.Client + k8sClient client.Client + triggerTemplateManager tektoncd.TriggerTemplateManager + autoDeployStrategyManager autodeploy.Manager } -func NewProcessTriggerTemplate(k8sClient client.Client) *ProcessTriggerTemplate { - return &ProcessTriggerTemplate{k8sClient: k8sClient} +func NewProcessTriggerTemplate( + k8sClient client.Client, + triggerTemplateManager tektoncd.TriggerTemplateManager, + autoDeployStrategyManager autodeploy.Manager, +) *ProcessTriggerTemplate { + return &ProcessTriggerTemplate{ + k8sClient: k8sClient, + triggerTemplateManager: triggerTemplateManager, + autoDeployStrategyManager: autoDeployStrategyManager, + } } func (h *ProcessTriggerTemplate) ServeRequest(ctx context.Context, stageDeploy *codebaseApi.CDStageDeploy) error { log := ctrl.LoggerFrom(ctx).WithValues("stage", stageDeploy.Spec.Stage, "pipeline", stageDeploy.Spec.Pipeline, "status", stageDeploy.Status.Status) - if skipPipelineCreation(stageDeploy) { + if skipPipelineRunCreation(stageDeploy) { log.Info("Skip processing TriggerTemplate for auto-deploy.") return nil @@ -47,55 +42,43 @@ func (h *ProcessTriggerTemplate) ServeRequest(ctx context.Context, stageDeploy * log.Info("Start processing TriggerTemplate for auto-deploy.") - pipeline := &pipelineAPi.CDPipeline{} - if err := h.k8sClient.Get(ctx, client.ObjectKey{ - Namespace: stageDeploy.Namespace, - Name: stageDeploy.Spec.Pipeline, - }, pipeline); err != nil { - return fmt.Errorf("failed to get CDPipeline: %w", err) - } - - appPayload, err := h.getAppPayload(ctx, pipeline) + pipeline, stage, rawResource, err := getResourcesForPipelineRun(ctx, stageDeploy, h.k8sClient, h.triggerTemplateManager) if err != nil { - if errors.Is(err, errLasTagNotFound) { + if errors.Is(err, tektoncd.ErrEmptyTriggerTemplateResources) { + log.Info("No resource templates found in the trigger template. Skip processing.", "triggertemplate", stage.Spec.TriggerTemplate) + stageDeploy.Status.Status = codebaseApi.CDStageDeployStatusCompleted + return nil } return err } - stage := &pipelineAPi.Stage{} - if err = h.k8sClient.Get(ctx, client.ObjectKey{ - Namespace: stageDeploy.Namespace, - Name: stageDeploy.GetStageCRName(), - }, stage); err != nil { - return fmt.Errorf("failed to get Stage: %w", err) - } - - rawResource, err := h.getRawTriggerTemplateResource(ctx, stage.Spec.TriggerTemplate, stageDeploy.Namespace) + appPayload, err := h.autoDeployStrategyManager.GetAppPayloadForAllLatestStrategy(ctx, pipeline) if err != nil { - if errors.Is(err, errEmptyTriggerTemplateResources) { - log.Info("No resource templates found in the trigger template. Skip processing.", "triggertemplate", stage.Spec.TriggerType) + if errors.Is(err, autodeploy.ErrLasTagNotFound) { + log.Info("Codebase doesn't have tags in the CodebaseImageStream. Skip auto-deploy.") stageDeploy.Status.Status = codebaseApi.CDStageDeployStatusCompleted return nil } - return err + return fmt.Errorf("failed to get application payload: %w", err) } - if err = h.createTriggerTemplateResource( + if err = h.triggerTemplateManager.CreatePipelineRun( ctx, stageDeploy.Namespace, + stageDeploy.Name, rawResource, appPayload, []byte(stage.Spec.Name), []byte(pipeline.Spec.Name), []byte(stage.Spec.ClusterName), ); err != nil { - return err + return fmt.Errorf("failed to create PipelineRun: %w", err) } stageDeploy.Status.Status = codebaseApi.CDStageDeployStatusRunning @@ -105,94 +88,10 @@ func (h *ProcessTriggerTemplate) ServeRequest(ctx context.Context, stageDeploy * return nil } -func skipPipelineCreation(stageDeploy *codebaseApi.CDStageDeploy) bool { - if stageDeploy.Status.Status != codebaseApi.CDStageDeployStatusFailed && - stageDeploy.Status.Status != codebaseApi.CDStageDeployStatusPending { - return true +func skipPipelineRunCreation(stageDeploy *codebaseApi.CDStageDeploy) bool { + if stageDeploy.IsPending() || stageDeploy.IsFailed() { + return false } - return false -} - -func (h *ProcessTriggerTemplate) getAppPayload(ctx context.Context, pipeline *pipelineAPi.CDPipeline) (json.RawMessage, error) { - log := ctrl.LoggerFrom(ctx) - - appPayload := make(map[string]TriggerTemplateApplicationPayload, len(pipeline.Spec.InputDockerStreams)) - - for _, stream := range pipeline.Spec.InputDockerStreams { - imageStreamName := put_codebase_image_stream.ProcessNameToK8sConvention(stream) - - imageStream := &codebaseApi.CodebaseImageStream{} - if err := h.k8sClient.Get(ctx, client.ObjectKey{ - Namespace: pipeline.Namespace, - Name: put_codebase_image_stream.ProcessNameToK8sConvention(imageStreamName), - }, imageStream); err != nil { - return nil, fmt.Errorf("failed to get %s CodebaseImageStream: %w", imageStreamName, err) - } - - tag, err := codebaseimagestream.GetLastTag(imageStream.Spec.Tags, log) - if err != nil { - log.Info("Codebase doesn't have tags in the CodebaseImageStream. Skip auto-deploy.", "codebase", imageStream.Spec.Codebase, "imagestream", imageStreamName) - - return nil, errLasTagNotFound - } - - appPayload[imageStream.Spec.Codebase] = TriggerTemplateApplicationPayload{ - ImageTag: tag.Name, - } - } - - rawAppPayload, err := json.Marshal(appPayload) - if err != nil { - return nil, fmt.Errorf("failed to marshal application payload: %w", err) - } - - return rawAppPayload, nil -} - -func (h *ProcessTriggerTemplate) getRawTriggerTemplateResource(ctx context.Context, triggerTemplateName, ns string) ([]byte, error) { - template := &tektonTriggersApi.TriggerTemplate{} - if err := h.k8sClient.Get(ctx, client.ObjectKey{ - Namespace: ns, - Name: triggerTemplateName, - }, template); err != nil { - return nil, fmt.Errorf("failed to get TriggerTemplate: %w", err) - } - - if len(template.Spec.ResourceTemplates) == 0 { - return nil, errEmptyTriggerTemplateResources - } - - rawPipeRun := make([]byte, len(template.Spec.ResourceTemplates[0].RawExtension.Raw)) - copy(rawPipeRun, template.Spec.ResourceTemplates[0].RawExtension.Raw) - - return rawPipeRun, nil -} - -func (h *ProcessTriggerTemplate) createTriggerTemplateResource( - ctx context.Context, - ns string, - rawPipeRun, - appPayload, - stage, - pipeline, - clusterSecret []byte, -) error { - rawPipeRun = bytes.ReplaceAll(rawPipeRun, []byte("$(tt.params.APPLICATIONS_PAYLOAD)"), bytes.ReplaceAll(appPayload, []byte(`"`), []byte(`\"`))) - rawPipeRun = bytes.ReplaceAll(rawPipeRun, []byte("$(tt.params.CDSTAGE)"), stage) - rawPipeRun = bytes.ReplaceAll(rawPipeRun, []byte("$(tt.params.CDPIPELINE)"), pipeline) - rawPipeRun = bytes.ReplaceAll(rawPipeRun, []byte("$(tt.params.KUBECONFIG_SECRET_NAME)"), clusterSecret) - - data := &unstructured.Unstructured{} - if err := data.UnmarshalJSON(rawPipeRun); err != nil { - return fmt.Errorf("couldn't unmarshal json from the TriggerTemplate: %w", err) - } - - data.SetNamespace(ns) - - if err := h.k8sClient.Create(ctx, data); err != nil { - return fmt.Errorf("failed to create resource: %w", err) - } - - return nil + return true } diff --git a/controllers/cdstagedeploy/chain/process_trigger_template_test.go b/controllers/cdstagedeploy/chain/process_trigger_template_test.go index 833aa349..ef2f370c 100644 --- a/controllers/cdstagedeploy/chain/process_trigger_template_test.go +++ b/controllers/cdstagedeploy/chain/process_trigger_template_test.go @@ -2,65 +2,28 @@ package chain import ( "context" + "encoding/json" + "errors" + "fmt" "testing" "github.com/go-logr/logr" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - tektonTriggersApi "github.com/tektoncd/triggers/pkg/apis/triggers/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" - pipelineAPi "github.com/epam/edp-cd-pipeline-operator/v2/api/v1" + pipelineApi "github.com/epam/edp-cd-pipeline-operator/v2/api/v1" codebaseApi "github.com/epam/edp-codebase-operator/v2/api/v1" -) - -var ( - pipelineRunTemplate = []byte(`{ - "apiVersion": "tekton.dev/v1", - "kind": "PipelineRun", - "metadata": { - "generateName": "deploy-$(tt.params.CDPIPELINE)-$(tt.params.CDSTAGE)-", - "labels": { - "app.edp.epam.com/cdpipeline": "$(tt.params.CDPIPELINE)", - "app.edp.epam.com/cdstage": "$(tt.params.CDSTAGE)", - "app.edp.epam.com/pipelinetype": "deploy" - } - }, - "spec": { - "params": [ - { - "name": "APPLICATIONS_PAYLOAD", - "value": "$(tt.params.APPLICATIONS_PAYLOAD)" - }, - { - "name": "CDSTAGE", - "value": "$(tt.params.CDSTAGE)" - }, - { - "name": "CDPIPELINE", - "value": "$(tt.params.CDPIPELINE)" - }, - { - "name": "KUBECONFIG_SECRET_NAME", - "value": "$(tt.params.KUBECONFIG_SECRET_NAME)" - } - ], - "pipelineRef": { - "name": "cd-stage-deploy" - }, - "taskRunTemplate": { - "serviceAccountName": "tekton" - }, - "timeouts": { - "pipeline": "1h00m0s" - } - } - }`) + "github.com/epam/edp-codebase-operator/v2/pkg/autodeploy" + autodeploymocks "github.com/epam/edp-codebase-operator/v2/pkg/autodeploy/mocks" + "github.com/epam/edp-codebase-operator/v2/pkg/tektoncd" + tektoncdmocks "github.com/epam/edp-codebase-operator/v2/pkg/tektoncd/mocks" ) func TestProcessTriggerTemplate_ServeRequest(t *testing.T) { @@ -68,428 +31,539 @@ func TestProcessTriggerTemplate_ServeRequest(t *testing.T) { scheme := runtime.NewScheme() require.NoError(t, codebaseApi.AddToScheme(scheme)) - require.NoError(t, pipelineAPi.AddToScheme(scheme)) - require.NoError(t, tektonTriggersApi.AddToScheme(scheme)) + require.NoError(t, pipelineApi.AddToScheme(scheme)) + + type fields struct { + k8sClient func(t *testing.T) client.Client + triggerTemplateManager func(t *testing.T) tektoncd.TriggerTemplateManager + autoDeployStrategyManager func(t *testing.T) autodeploy.Manager + } tests := []struct { name string stageDeploy *codebaseApi.CDStageDeploy - k8sClient func(t *testing.T) client.Client + fields fields wantErr require.ErrorAssertionFunc + want func(t *testing.T, d *codebaseApi.CDStageDeploy) }{ { - name: "should create trigger template resources", + name: "should process TriggerTemplate for auto-deploy", stageDeploy: &codebaseApi.CDStageDeploy{ ObjectMeta: metav1.ObjectMeta{ + Name: "test", Namespace: "default", - Name: "dev-qa", }, Spec: codebaseApi.CDStageDeploySpec{ - Pipeline: "dev", - Stage: "qa", + Pipeline: "pipe1", + Stage: "dev", }, Status: codebaseApi.CDStageDeployStatus{ Status: codebaseApi.CDStageDeployStatusPending, }, }, - k8sClient: func(t *testing.T) client.Client { - return fake.NewClientBuilder(). - WithScheme(scheme). - WithObjects( - &pipelineAPi.CDPipeline{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "default", - Name: "dev", - }, - Spec: pipelineAPi.CDPipelineSpec{ - Name: "dev", - Applications: []string{"app1", "app2"}, - InputDockerStreams: []string{"app1-main", "app2-main"}, - }, - }, - &codebaseApi.CodebaseImageStream{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "default", - Name: "app1-main", - }, - Spec: codebaseApi.CodebaseImageStreamSpec{ - Tags: []codebaseApi.Tag{ - { - Name: "main-0.0.1", - Created: "2024-03-04T11:36:26Z", - }, - { - Name: "main-0.0.2", - Created: "2024-03-05T11:00:00Z", - }, + fields: fields{ + k8sClient: func(t *testing.T) client.Client { + return fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects( + &pipelineApi.CDPipeline{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pipe1", + Namespace: "default", }, - }, - }, - &codebaseApi.CodebaseImageStream{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "default", - Name: "app2-main", - }, - Spec: codebaseApi.CodebaseImageStreamSpec{ - Tags: []codebaseApi.Tag{ - { - Name: "main-0.0.3", - Created: "2024-03-04T11:36:26Z", - }, + Spec: pipelineApi.CDPipelineSpec{ + Name: "pipe1", }, }, - }, - &pipelineAPi.Stage{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "default", - Name: "dev-qa", - }, - Spec: pipelineAPi.StageSpec{ - Name: "qa", - TriggerTemplate: "auto-deploy", - ClusterName: pipelineAPi.InCluster, - }, - }, - &tektonTriggersApi.TriggerTemplate{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "default", - Name: "auto-deploy", - }, - Spec: tektonTriggersApi.TriggerTemplateSpec{ - ResourceTemplates: []tektonTriggersApi.TriggerResourceTemplate{ - { - RawExtension: runtime.RawExtension{ - Raw: pipelineRunTemplate, - }, - }, + &pipelineApi.Stage{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pipe1-dev", + Namespace: "default", + }, + Spec: pipelineApi.StageSpec{ + TriggerTemplate: "trigger1", + Name: "dev", + ClusterName: "cluster-secret", }, }, - }, - ). - Build() + ). + Build() + }, + triggerTemplateManager: func(t *testing.T) tektoncd.TriggerTemplateManager { + m := tektoncdmocks.NewMockTriggerTemplateManager(t) + + m.On("GetRawResourceFromTriggerTemplate", mock.Anything, "trigger1", "default"). + Return([]byte("raw resource"), nil) + m.On( + "CreatePipelineRun", + mock.Anything, + "default", + "test", + []byte("raw resource"), + []byte("{app1: 1.0}"), + []byte("dev"), + []byte("pipe1"), + []byte("cluster-secret"), + ).Return(nil) + + return m + }, + autoDeployStrategyManager: func(t *testing.T) autodeploy.Manager { + m := autodeploymocks.NewMockManager(t) + + m.On("GetAppPayloadForAllLatestStrategy", mock.Anything, mock.Anything). + Return(json.RawMessage("{app1: 1.0}"), nil) + + return m + }, }, wantErr: require.NoError, + want: func(t *testing.T, d *codebaseApi.CDStageDeploy) { + assert.Equal(t, codebaseApi.CDStageDeployStatusRunning, d.Status.Status) + }, }, { - name: "trigger template doesn't contain resources, skip processing", + name: "failed to create PipelineRun", stageDeploy: &codebaseApi.CDStageDeploy{ ObjectMeta: metav1.ObjectMeta{ + Name: "test", Namespace: "default", - Name: "dev-qa", }, Spec: codebaseApi.CDStageDeploySpec{ - Pipeline: "dev", - Stage: "qa", + Pipeline: "pipe1", + Stage: "dev", }, Status: codebaseApi.CDStageDeployStatus{ Status: codebaseApi.CDStageDeployStatusPending, }, }, - k8sClient: func(t *testing.T) client.Client { - return fake.NewClientBuilder(). - WithScheme(scheme). - WithObjects( - &pipelineAPi.CDPipeline{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "default", - Name: "dev", - }, - Spec: pipelineAPi.CDPipelineSpec{ - Name: "dev", - Applications: []string{"app1"}, - InputDockerStreams: []string{"app1-main"}, - }, - }, - &codebaseApi.CodebaseImageStream{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "default", - Name: "app1-main", - }, - Spec: codebaseApi.CodebaseImageStreamSpec{ - Tags: []codebaseApi.Tag{ - { - Name: "main-0.0.1", - Created: "2024-03-04T11:36:26Z", - }, + fields: fields{ + k8sClient: func(t *testing.T) client.Client { + return fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects( + &pipelineApi.CDPipeline{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pipe1", + Namespace: "default", + }, + Spec: pipelineApi.CDPipelineSpec{ + Name: "pipe1", }, }, - }, - &pipelineAPi.Stage{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "default", - Name: "dev-qa", - }, - Spec: pipelineAPi.StageSpec{ - Name: "qa", - TriggerTemplate: "auto-deploy", - }, - }, - &tektonTriggersApi.TriggerTemplate{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "default", - Name: "auto-deploy", + &pipelineApi.Stage{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pipe1-dev", + Namespace: "default", + }, + Spec: pipelineApi.StageSpec{ + TriggerTemplate: "trigger1", + Name: "dev", + ClusterName: "cluster-secret", + }, }, - Spec: tektonTriggersApi.TriggerTemplateSpec{}, - }, - ). - Build() + ). + Build() + }, + triggerTemplateManager: func(t *testing.T) tektoncd.TriggerTemplateManager { + m := tektoncdmocks.NewMockTriggerTemplateManager(t) + + m.On("GetRawResourceFromTriggerTemplate", mock.Anything, "trigger1", "default"). + Return([]byte("raw resource"), nil) + m.On( + "CreatePipelineRun", + mock.Anything, + "default", + "test", + []byte("raw resource"), + []byte("{app1: 1.0}"), + []byte("dev"), + []byte("pipe1"), + []byte("cluster-secret"), + ).Return(errors.New("failed to create PipelineRun")) + + return m + }, + autoDeployStrategyManager: func(t *testing.T) autodeploy.Manager { + m := autodeploymocks.NewMockManager(t) + + m.On("GetAppPayloadForAllLatestStrategy", mock.Anything, mock.Anything). + Return(json.RawMessage("{app1: 1.0}"), nil) + + return m + }, + }, + wantErr: func(t require.TestingT, err error, i ...interface{}) { + require.Error(t, err) + assert.Contains(t, err.Error(), "failed to create PipelineRun") }, - wantErr: require.NoError, }, { - name: "trigger template not found", + name: "failed to get app payload for all latest strategy", stageDeploy: &codebaseApi.CDStageDeploy{ ObjectMeta: metav1.ObjectMeta{ + Name: "test", Namespace: "default", - Name: "dev-qa", }, Spec: codebaseApi.CDStageDeploySpec{ - Pipeline: "dev", - Stage: "qa", + Pipeline: "pipe1", + Stage: "dev", }, Status: codebaseApi.CDStageDeployStatus{ Status: codebaseApi.CDStageDeployStatusPending, }, }, - k8sClient: func(t *testing.T) client.Client { - return fake.NewClientBuilder(). - WithScheme(scheme). - WithObjects( - &pipelineAPi.CDPipeline{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "default", - Name: "dev", - }, - Spec: pipelineAPi.CDPipelineSpec{ - Name: "dev", - Applications: []string{"app1"}, - InputDockerStreams: []string{"app1-main"}, - }, - }, - &codebaseApi.CodebaseImageStream{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "default", - Name: "app1-main", - }, - Spec: codebaseApi.CodebaseImageStreamSpec{ - Tags: []codebaseApi.Tag{ - { - Name: "main-0.0.1", - Created: "2024-03-04T11:36:26Z", - }, + fields: fields{ + k8sClient: func(t *testing.T) client.Client { + return fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects( + &pipelineApi.CDPipeline{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pipe1", + Namespace: "default", + }, + Spec: pipelineApi.CDPipelineSpec{ + Name: "pipe1", }, }, - }, - &pipelineAPi.Stage{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "default", - Name: "dev-qa", - }, - Spec: pipelineAPi.StageSpec{ - Name: "qa", - TriggerTemplate: "auto-deploy", + &pipelineApi.Stage{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pipe1-dev", + Namespace: "default", + }, + Spec: pipelineApi.StageSpec{ + TriggerTemplate: "trigger1", + Name: "dev", + ClusterName: "cluster-secret", + }, }, - }, - ). - Build() + ). + Build() + }, + triggerTemplateManager: func(t *testing.T) tektoncd.TriggerTemplateManager { + m := tektoncdmocks.NewMockTriggerTemplateManager(t) + + m.On("GetRawResourceFromTriggerTemplate", mock.Anything, "trigger1", "default"). + Return([]byte("raw resource"), nil) + + return m + }, + autoDeployStrategyManager: func(t *testing.T) autodeploy.Manager { + m := autodeploymocks.NewMockManager(t) + + m.On("GetAppPayloadForAllLatestStrategy", mock.Anything, mock.Anything). + Return(nil, errors.New("failed to get app payload")) + + return m + }, }, - wantErr: func(t require.TestingT, err error, _ ...interface{}) { + wantErr: func(t require.TestingT, err error, i ...interface{}) { require.Error(t, err) - assert.Contains(t, err.Error(), "failed to get TriggerTemplate") + assert.Contains(t, err.Error(), "failed to get app payload") }, }, { - name: "stage not found", + name: "last tag not found", stageDeploy: &codebaseApi.CDStageDeploy{ ObjectMeta: metav1.ObjectMeta{ + Name: "test", Namespace: "default", - Name: "dev-qa", }, Spec: codebaseApi.CDStageDeploySpec{ - Pipeline: "dev", - Stage: "qa", + Pipeline: "pipe1", + Stage: "dev", }, Status: codebaseApi.CDStageDeployStatus{ Status: codebaseApi.CDStageDeployStatusPending, }, }, - k8sClient: func(t *testing.T) client.Client { - return fake.NewClientBuilder(). - WithScheme(scheme). - WithObjects( - &pipelineAPi.CDPipeline{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "default", - Name: "dev", - }, - Spec: pipelineAPi.CDPipelineSpec{ - Name: "dev", - Applications: []string{"app1"}, - InputDockerStreams: []string{"app1-main"}, - }, - }, - &codebaseApi.CodebaseImageStream{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "default", - Name: "app1-main", + fields: fields{ + k8sClient: func(t *testing.T) client.Client { + return fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects( + &pipelineApi.CDPipeline{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pipe1", + Namespace: "default", + }, + Spec: pipelineApi.CDPipelineSpec{ + Name: "pipe1", + }, }, - Spec: codebaseApi.CodebaseImageStreamSpec{ - Tags: []codebaseApi.Tag{ - { - Name: "main-0.0.1", - Created: "2024-03-04T11:36:26Z", - }, + &pipelineApi.Stage{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pipe1-dev", + Namespace: "default", + }, + Spec: pipelineApi.StageSpec{ + TriggerTemplate: "trigger1", + Name: "dev", + ClusterName: "cluster-secret", }, }, - }, - ). - Build() + ). + Build() + }, + triggerTemplateManager: func(t *testing.T) tektoncd.TriggerTemplateManager { + m := tektoncdmocks.NewMockTriggerTemplateManager(t) + + m.On("GetRawResourceFromTriggerTemplate", mock.Anything, "trigger1", "default"). + Return([]byte("raw resource"), nil) + + return m + }, + autoDeployStrategyManager: func(t *testing.T) autodeploy.Manager { + m := autodeploymocks.NewMockManager(t) + + m.On("GetAppPayloadForAllLatestStrategy", mock.Anything, mock.Anything). + Return(nil, fmt.Errorf("failed to get app payload: %w", autodeploy.ErrLasTagNotFound)) + + return m + }, }, - wantErr: func(t require.TestingT, err error, _ ...interface{}) { - require.Error(t, err) - assert.Contains(t, err.Error(), "failed to get Stage") + wantErr: require.NoError, + want: func(t *testing.T, d *codebaseApi.CDStageDeploy) { + assert.Equal(t, codebaseApi.CDStageDeployStatusCompleted, d.Status.Status) }, }, { - name: "codebase image stream doesn't contain tags, skip processing", + name: "failed to get raw resource from trigger template", stageDeploy: &codebaseApi.CDStageDeploy{ ObjectMeta: metav1.ObjectMeta{ + Name: "test", Namespace: "default", - Name: "dev-qa", }, Spec: codebaseApi.CDStageDeploySpec{ - Pipeline: "dev", - Stage: "qa", + Pipeline: "pipe1", + Stage: "dev", }, Status: codebaseApi.CDStageDeployStatus{ Status: codebaseApi.CDStageDeployStatusPending, }, }, - k8sClient: func(t *testing.T) client.Client { - return fake.NewClientBuilder(). - WithScheme(scheme). - WithObjects( - &pipelineAPi.CDPipeline{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "default", - Name: "dev", - }, - Spec: pipelineAPi.CDPipelineSpec{ - Name: "dev", - Applications: []string{"app1"}, - InputDockerStreams: []string{"app1-main"}, + fields: fields{ + k8sClient: func(t *testing.T) client.Client { + return fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects( + &pipelineApi.CDPipeline{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pipe1", + Namespace: "default", + }, + Spec: pipelineApi.CDPipelineSpec{ + Name: "pipe1", + }, }, - }, - &codebaseApi.CodebaseImageStream{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "default", - Name: "app1-main", + &pipelineApi.Stage{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pipe1-dev", + Namespace: "default", + }, + Spec: pipelineApi.StageSpec{ + TriggerTemplate: "trigger1", + Name: "dev", + ClusterName: "cluster-secret", + }, }, - Spec: codebaseApi.CodebaseImageStreamSpec{}, - }, - ). - Build() + ). + Build() + }, + triggerTemplateManager: func(t *testing.T) tektoncd.TriggerTemplateManager { + m := tektoncdmocks.NewMockTriggerTemplateManager(t) + + m.On("GetRawResourceFromTriggerTemplate", mock.Anything, "trigger1", "default"). + Return(nil, errors.New("failed to get raw resource")) + + return m + }, + autoDeployStrategyManager: func(t *testing.T) autodeploy.Manager { + return autodeploymocks.NewMockManager(t) + }, + }, + wantErr: func(t require.TestingT, err error, i ...interface{}) { + require.Error(t, err) + assert.Contains(t, err.Error(), "failed to get raw resource") }, - wantErr: require.NoError, }, { - name: "codebase image stream not found", + name: "no resource templates found in the trigger template", stageDeploy: &codebaseApi.CDStageDeploy{ ObjectMeta: metav1.ObjectMeta{ + Name: "test", Namespace: "default", - Name: "dev-qa", }, Spec: codebaseApi.CDStageDeploySpec{ - Pipeline: "dev", - Stage: "qa", + Pipeline: "pipe1", + Stage: "dev", }, Status: codebaseApi.CDStageDeployStatus{ Status: codebaseApi.CDStageDeployStatusPending, }, }, - k8sClient: func(t *testing.T) client.Client { - return fake.NewClientBuilder(). - WithScheme(scheme). - WithObjects( - &pipelineAPi.CDPipeline{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "default", - Name: "dev", + fields: fields{ + k8sClient: func(t *testing.T) client.Client { + return fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects( + &pipelineApi.CDPipeline{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pipe1", + Namespace: "default", + }, + Spec: pipelineApi.CDPipelineSpec{ + Name: "pipe1", + }, }, - Spec: pipelineAPi.CDPipelineSpec{ - Name: "dev", - Applications: []string{"app1"}, - InputDockerStreams: []string{"app1-main"}, + &pipelineApi.Stage{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pipe1-dev", + Namespace: "default", + }, + Spec: pipelineApi.StageSpec{ + TriggerTemplate: "trigger1", + Name: "dev", + ClusterName: "cluster-secret", + }, }, - }, - ). - Build() + ). + Build() + }, + triggerTemplateManager: func(t *testing.T) tektoncd.TriggerTemplateManager { + m := tektoncdmocks.NewMockTriggerTemplateManager(t) + + m.On("GetRawResourceFromTriggerTemplate", mock.Anything, "trigger1", "default"). + Return(nil, fmt.Errorf("failed to get TriggerTemplate: %w", tektoncd.ErrEmptyTriggerTemplateResources)) + + return m + }, + autoDeployStrategyManager: func(t *testing.T) autodeploy.Manager { + return autodeploymocks.NewMockManager(t) + }, }, - wantErr: func(t require.TestingT, err error, _ ...interface{}) { - require.Error(t, err) - assert.Contains(t, err.Error(), "failed to get app1-main CodebaseImageStream") + wantErr: require.NoError, + want: func(t *testing.T, d *codebaseApi.CDStageDeploy) { + assert.Equal(t, codebaseApi.CDStageDeployStatusCompleted, d.Status.Status) }, }, { - name: "pipeline not found", + name: "failed to get Stage", stageDeploy: &codebaseApi.CDStageDeploy{ ObjectMeta: metav1.ObjectMeta{ + Name: "test", Namespace: "default", - Name: "dev-qa", }, Spec: codebaseApi.CDStageDeploySpec{ - Pipeline: "dev", - Stage: "qa", + Pipeline: "pipe1", + Stage: "dev", }, Status: codebaseApi.CDStageDeployStatus{ Status: codebaseApi.CDStageDeployStatusPending, }, }, - k8sClient: func(t *testing.T) client.Client { - return fake.NewClientBuilder(). - WithScheme(scheme). - WithObjects(). - Build() + fields: fields{ + k8sClient: func(t *testing.T) client.Client { + return fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects( + &pipelineApi.CDPipeline{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pipe1", + Namespace: "default", + }, + Spec: pipelineApi.CDPipelineSpec{ + Name: "pipe1", + }, + }, + ). + Build() + }, + triggerTemplateManager: func(t *testing.T) tektoncd.TriggerTemplateManager { + return tektoncdmocks.NewMockTriggerTemplateManager(t) + }, + autoDeployStrategyManager: func(t *testing.T) autodeploy.Manager { + return autodeploymocks.NewMockManager(t) + }, }, - wantErr: func(t require.TestingT, err error, _ ...interface{}) { + wantErr: func(t require.TestingT, err error, i ...interface{}) { require.Error(t, err) - assert.Contains(t, err.Error(), "failed to get CDPipeline") + assert.Contains(t, err.Error(), "failed to get Stage") }, }, { - name: "skip processing trigger template for auto-deploy if status is completed", + name: "failed to get CDPipeline", stageDeploy: &codebaseApi.CDStageDeploy{ ObjectMeta: metav1.ObjectMeta{ + Name: "test", Namespace: "default", - Name: "dev-qa", }, Spec: codebaseApi.CDStageDeploySpec{ - Pipeline: "dev", - Stage: "qa", + Pipeline: "pipe1", + Stage: "dev", + }, + Status: codebaseApi.CDStageDeployStatus{ + Status: codebaseApi.CDStageDeployStatusPending, + }, + }, + fields: fields{ + k8sClient: func(t *testing.T) client.Client { + return fake.NewClientBuilder(). + WithScheme(scheme). + Build() + }, + triggerTemplateManager: func(t *testing.T) tektoncd.TriggerTemplateManager { + return tektoncdmocks.NewMockTriggerTemplateManager(t) }, + autoDeployStrategyManager: func(t *testing.T) autodeploy.Manager { + return autodeploymocks.NewMockManager(t) + }, + }, + wantErr: func(t require.TestingT, err error, i ...interface{}) { + require.Error(t, err) + assert.Contains(t, err.Error(), "failed to get CDPipeline") + }, + }, + { + name: "skip processing TriggerTemplate for auto-deploy", + stageDeploy: &codebaseApi.CDStageDeploy{ Status: codebaseApi.CDStageDeployStatus{ - Status: codebaseApi.CDStageDeployStatusCompleted, + Status: codebaseApi.CDStageDeployStatusInQueue, }, }, - k8sClient: func(t *testing.T) client.Client { - return fake.NewClientBuilder(). - WithScheme(scheme). - WithObjects(). - Build() + fields: fields{ + k8sClient: func(t *testing.T) client.Client { + return fake.NewClientBuilder(). + WithScheme(scheme). + Build() + }, + triggerTemplateManager: func(t *testing.T) tektoncd.TriggerTemplateManager { + return tektoncdmocks.NewMockTriggerTemplateManager(t) + }, + autoDeployStrategyManager: func(t *testing.T) autodeploy.Manager { + return autodeploymocks.NewMockManager(t) + }, }, wantErr: require.NoError, + want: func(t *testing.T, d *codebaseApi.CDStageDeploy) { + assert.Equal(t, codebaseApi.CDStageDeployStatusInQueue, d.Status.Status) + }, }, } for _, tt := range tests { - tt := tt t.Run(tt.name, func(t *testing.T) { - t.Parallel() - - h := NewProcessTriggerTemplate(tt.k8sClient(t)) - err := h.ServeRequest(ctrl.LoggerInto(context.Background(), logr.Discard()), tt.stageDeploy) + h := NewProcessTriggerTemplate( + tt.fields.k8sClient(t), + tt.fields.triggerTemplateManager(t), + tt.fields.autoDeployStrategyManager(t), + ) - tt.wantErr(t, err) + tt.wantErr(t, h.ServeRequest(ctrl.LoggerInto(context.Background(), logr.Discard()), tt.stageDeploy)) + if tt.want != nil { + tt.want(t, tt.stageDeploy) + } }) } } diff --git a/controllers/cdstagedeploy/chain/resolve_status.go b/controllers/cdstagedeploy/chain/resolve_status.go index 9827c71b..8dc5c0ab 100644 --- a/controllers/cdstagedeploy/chain/resolve_status.go +++ b/controllers/cdstagedeploy/chain/resolve_status.go @@ -27,7 +27,7 @@ func NewResolveStatus(k8sClient client.Client) *ResolveStatus { func (r *ResolveStatus) ServeRequest(ctx context.Context, stageDeploy *codebaseApi.CDStageDeploy) error { log := ctrl.LoggerFrom(ctx) - if stageDeploy.Status.Status == codebaseApi.CDStageDeployStatusFailed { + if stageDeploy.IsFailed() { log.Info("CDStageDeploy has failed status. Retry to deploy.") return nil } @@ -37,7 +37,7 @@ func (r *ResolveStatus) ServeRequest(ctx context.Context, stageDeploy *codebaseA return fmt.Errorf("failed to get running pipelines: %w", err) } - if stageDeploy.Status.Status == codebaseApi.CDStageDeployStatusPending { + if stageDeploy.IsPending() { if allPipelineRunsCompleted(pipelineRun.Items) { log.Info("CDStageDeploy has pending status. Start deploying.") @@ -51,7 +51,7 @@ func (r *ResolveStatus) ServeRequest(ctx context.Context, stageDeploy *codebaseA return nil } - if stageDeploy.Status.Status == codebaseApi.CDStageDeployStatusInQueue { + if stageDeploy.IsInQueue() { if allPipelineRunsCompleted(pipelineRun.Items) { log.Info("All PipelineRuns have been completed.") @@ -82,7 +82,7 @@ func (r *ResolveStatus) ServeRequest(ctx context.Context, stageDeploy *codebaseA return nil } - if stageDeploy.Status.Status == codebaseApi.CDStageDeployStatusRunning { + if stageDeploy.IsRunning() { if allPipelineRunsCompleted(pipelineRun.Items) { log.Info("All PipelineRuns have been completed.") diff --git a/controllers/codebaseimagestream/chain/put_cd_stage_deploy.go b/controllers/codebaseimagestream/chain/put_cd_stage_deploy.go index 025aef3d..c9110cf8 100644 --- a/controllers/codebaseimagestream/chain/put_cd_stage_deploy.go +++ b/controllers/codebaseimagestream/chain/put_cd_stage_deploy.go @@ -17,7 +17,6 @@ import ( codebaseApi "github.com/epam/edp-codebase-operator/v2/api/v1" "github.com/epam/edp-codebase-operator/v2/pkg/codebaseimagestream" - "github.com/epam/edp-codebase-operator/v2/pkg/util" ) type PutCDStageDeploy struct { @@ -25,12 +24,13 @@ type PutCDStageDeploy struct { } type cdStageDeployCommand struct { - Name string - Namespace string - Pipeline string - Stage string - Tag codebaseApi.CodebaseTag - Tags []codebaseApi.CodebaseTag + Name string + Namespace string + Pipeline string + Stage string + TriggerType string + Tag codebaseApi.CodebaseTag + Tags []codebaseApi.CodebaseTag } func (h PutCDStageDeploy) ServeRequest(ctx context.Context, imageStream *codebaseApi.CodebaseImageStream) error { @@ -92,8 +92,24 @@ func (h PutCDStageDeploy) putCDStageDeploy(ctx context.Context, envLabel, namesp l := ctrl.LoggerFrom(ctx) // use name for CDStageDeploy, it is converted from envLabel and cdpipeline/stage now is cdpipeline-stage name := strings.ReplaceAll(envLabel, "/", "-") + env := strings.Split(envLabel, "/") + pipeline := env[0] + stage := env[1] + stageCrName := fmt.Sprintf("%s-%s", pipeline, stage) + + stageCr := &pipelineApi.Stage{} + if err := h.client.Get( + ctx, + types.NamespacedName{ + Name: stageCrName, + Namespace: namespace, + }, + stageCr, + ); err != nil { + return fmt.Errorf("failed to get CDStage %s: %w", stageCrName, err) + } - skip, err := h.skipCDStageDeployCreation(ctx, envLabel, namespace) + skip, err := h.skipCDStageDeployCreation(ctx, pipeline, namespace, stageCr) if err != nil { return fmt.Errorf("failed to check if CDStageDeploy exists: %w", err) } @@ -104,23 +120,27 @@ func (h PutCDStageDeploy) putCDStageDeploy(ctx context.Context, envLabel, namesp return nil } - cdsd, err := getCreateCommand(ctx, envLabel, name, namespace, spec.Codebase, spec.Tags) + cdsd, err := getCreateCommand(ctx, pipeline, stage, name, namespace, spec.Codebase, stageCr.Spec.TriggerType, spec.Tags) if err != nil { return fmt.Errorf("failed to construct command to create %v cd stage deploy: %w", name, err) } - if err = h.create(ctx, cdsd); err != nil { + if err = h.create(ctx, cdsd, stageCr); err != nil { return fmt.Errorf("failed to create %v cd stage deploy: %w", name, err) } return nil } -func (h PutCDStageDeploy) skipCDStageDeployCreation(ctx context.Context, envLabel, namespace string) (bool, error) { +func (h PutCDStageDeploy) skipCDStageDeployCreation(ctx context.Context, pipeline, namespace string, stage *pipelineApi.Stage) (bool, error) { l := ctrl.LoggerFrom(ctx) - l.Info("Getting CDStageDeploys.") - env := strings.Split(envLabel, "/") + if !stage.IsAutoDeployTriggerType() { + l.Info("CDStage trigger type is not Auto. Don't need to skip CDStageDeploy creation.") + return false, nil + } + + l.Info("Getting CDStageDeploys.") list := &codebaseApi.CDStageDeployList{} if err := h.client.List( @@ -128,8 +148,8 @@ func (h PutCDStageDeploy) skipCDStageDeployCreation(ctx context.Context, envLabe list, client.InNamespace(namespace), client.MatchingLabels{ - codebaseApi.CdPipelineLabel: env[0], - codebaseApi.CdStageLabel: fmt.Sprintf("%s-%s", env[0], env[1]), + codebaseApi.CdPipelineLabel: pipeline, + codebaseApi.CdStageLabel: stage.Name, }, ); err != nil { return false, fmt.Errorf("failed to get CDStageDeploys: %w", err) @@ -148,19 +168,22 @@ func (h PutCDStageDeploy) skipCDStageDeployCreation(ctx context.Context, envLabe } } -func getCreateCommand(ctx context.Context, envLabel, name, namespace, codebase string, tags []codebaseApi.Tag) (*cdStageDeployCommand, error) { - env := strings.Split(envLabel, "/") - +func getCreateCommand( + ctx context.Context, + pipeline, stage, name, namespace, codebase, triggerType string, + tags []codebaseApi.Tag, +) (*cdStageDeployCommand, error) { lastTag, err := codebaseimagestream.GetLastTag(tags, ctrl.LoggerFrom(ctx)) if err != nil { return nil, fmt.Errorf("failed to get last tag: %w", err) } return &cdStageDeployCommand{ - Name: name, - Namespace: namespace, - Pipeline: env[0], - Stage: env[1], + Name: name, + Namespace: namespace, + Pipeline: pipeline, + Stage: stage, + TriggerType: triggerType, Tag: codebaseApi.CodebaseTag{ Codebase: codebase, Tag: lastTag.Name, @@ -174,24 +197,21 @@ func getCreateCommand(ctx context.Context, envLabel, name, namespace, codebase s }, nil } -func (h PutCDStageDeploy) create(ctx context.Context, command *cdStageDeployCommand) error { +func (h PutCDStageDeploy) create(ctx context.Context, command *cdStageDeployCommand, stage *pipelineApi.Stage) error { l := ctrl.LoggerFrom(ctx) l.Info("CDStageDeploy is not present in cluster. Start creating.") stageDeploy := &codebaseApi.CDStageDeploy{ - TypeMeta: metaV1.TypeMeta{ - APIVersion: util.V2APIVersion, - Kind: util.CDStageDeployKind, - }, ObjectMeta: metaV1.ObjectMeta{ GenerateName: command.Name, Namespace: command.Namespace, }, Spec: codebaseApi.CDStageDeploySpec{ - Pipeline: command.Pipeline, - Stage: command.Stage, - Tag: command.Tag, - Tags: command.Tags, + Pipeline: command.Pipeline, + Stage: command.Stage, + Tag: command.Tag, + Tags: command.Tags, + TriggerType: command.TriggerType, }, } @@ -200,18 +220,6 @@ func (h PutCDStageDeploy) create(ctx context.Context, command *cdStageDeployComm codebaseApi.CdStageLabel: stageDeploy.GetStageCRName(), }) - stage := &pipelineApi.Stage{} - if err := h.client.Get( - ctx, - types.NamespacedName{ - Name: stageDeploy.GetStageCRName(), - Namespace: command.Namespace, - }, - stage, - ); err != nil { - return fmt.Errorf("failed to get CDStage %s: %w", command.Stage, err) - } - if err := controllerutil.SetControllerReference(stage, stageDeploy, h.client.Scheme()); err != nil { return fmt.Errorf("failed to set controller reference: %w", err) } diff --git a/controllers/codebaseimagestream/chain/put_cd_stage_deploy_test.go b/controllers/codebaseimagestream/chain/put_cd_stage_deploy_test.go index 2606a853..a1537584 100644 --- a/controllers/codebaseimagestream/chain/put_cd_stage_deploy_test.go +++ b/controllers/codebaseimagestream/chain/put_cd_stage_deploy_test.go @@ -57,6 +57,9 @@ func TestPutCDStageDeploy_ServeRequest(t *testing.T) { Name: "ci-dev", Namespace: "default", }, + Spec: pipelineAPi.StageSpec{ + TriggerType: pipelineAPi.TriggerTypeAutoDeploy, + }, }, &codebaseApi.CDStageDeploy{ ObjectMeta: metaV1.ObjectMeta{ @@ -110,6 +113,9 @@ func TestPutCDStageDeploy_ServeRequest(t *testing.T) { Name: "ci-dev", Namespace: "default", }, + Spec: pipelineAPi.StageSpec{ + TriggerType: pipelineAPi.TriggerTypeAutoDeploy, + }, }, &codebaseApi.CDStageDeploy{ ObjectMeta: metaV1.ObjectMeta{ @@ -163,6 +169,9 @@ func TestPutCDStageDeploy_ServeRequest(t *testing.T) { Name: "ci-dev", Namespace: "default", }, + Spec: pipelineAPi.StageSpec{ + TriggerType: pipelineAPi.TriggerTypeAutoDeploy, + }, }, &codebaseApi.CDStageDeploy{ ObjectMeta: metaV1.ObjectMeta{ @@ -201,6 +210,72 @@ func TestPutCDStageDeploy_ServeRequest(t *testing.T) { require.Len(t, cdStageDeploys.Items, 2) }, }, + { + name: "don't skip CDStageDeploy creation if more than one CDStageDeploy already exists for AutoStable trigger type", + imageStream: &codebaseApi.CodebaseImageStream{ + ObjectMeta: metaV1.ObjectMeta{ + Name: "test-image-stream", + Namespace: "default", + Labels: map[string]string{ + "ci/dev": "", + }, + }, + Spec: codebaseApi.CodebaseImageStreamSpec{ + Codebase: "app", + ImageName: "latest", + Tags: []codebaseApi.Tag{{Name: "latest", Created: time.Now().Format(time.RFC3339)}}, + }, + }, + client: func(t *testing.T) client.Client { + return fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects( + &pipelineAPi.Stage{ + ObjectMeta: metaV1.ObjectMeta{ + Name: "ci-dev", + Namespace: "default", + }, + Spec: pipelineAPi.StageSpec{ + TriggerType: pipelineAPi.TriggerTypeAutoStable, + }, + }, + &codebaseApi.CDStageDeploy{ + ObjectMeta: metaV1.ObjectMeta{ + Name: "test-stage-deploy1", + Namespace: "default", + Labels: map[string]string{ + codebaseApi.CdPipelineLabel: "ci", + codebaseApi.CdStageLabel: "ci-dev", + }, + }, + }, + &codebaseApi.CDStageDeploy{ + ObjectMeta: metaV1.ObjectMeta{ + Name: "test-stage-deploy2", + Namespace: "default", + Labels: map[string]string{ + codebaseApi.CdPipelineLabel: "ci", + codebaseApi.CdStageLabel: "ci-dev", + }, + }, + }, + ).Build() + }, + wantErr: require.NoError, + want: func(t *testing.T, k8scl client.Client) { + cdStageDeploys := &codebaseApi.CDStageDeployList{} + require.NoError(t, + k8scl.List( + context.Background(), + cdStageDeploys, + client.InNamespace("default"), + client.MatchingLabels{ + codebaseApi.CdPipelineLabel: "ci", + codebaseApi.CdStageLabel: "ci-dev", + })) + require.Len(t, cdStageDeploys.Items, 3) + }, + }, { name: "failed to create CDStageDeploy - CDStage not found", imageStream: &codebaseApi.CodebaseImageStream{ @@ -248,7 +323,17 @@ func TestPutCDStageDeploy_ServeRequest(t *testing.T) { client: func(t *testing.T) client.Client { return fake.NewClientBuilder(). WithScheme(scheme). - WithObjects(). + WithObjects( + &pipelineAPi.Stage{ + ObjectMeta: metaV1.ObjectMeta{ + Name: "ci-dev", + Namespace: "default", + }, + Spec: pipelineAPi.StageSpec{ + TriggerType: pipelineAPi.TriggerTypeAutoDeploy, + }, + }, + ). Build() }, wantErr: func(t require.TestingT, err error, i ...interface{}) { diff --git a/deploy-templates/crds/v2.edp.epam.com_cdstagedeployments.yaml b/deploy-templates/crds/v2.edp.epam.com_cdstagedeployments.yaml index 562499cb..ff4d54cb 100644 --- a/deploy-templates/crds/v2.edp.epam.com_cdstagedeployments.yaml +++ b/deploy-templates/crds/v2.edp.epam.com_cdstagedeployments.yaml @@ -60,6 +60,10 @@ spec: stage: description: Name of related stage type: string + strategy: + default: Auto + description: TriggerType specifies a strategy for auto-deploy. + type: string tag: description: Specifies a latest available tag properties: diff --git a/deploy-templates/templates/role_kubernetes.yaml b/deploy-templates/templates/role_kubernetes.yaml index f215052d..aaaa1f81 100644 --- a/deploy-templates/templates/role_kubernetes.yaml +++ b/deploy-templates/templates/role_kubernetes.yaml @@ -96,7 +96,11 @@ rules: - pipelineruns verbs: - create + - get - list + - patch + - update + - watch - apiGroups: - triggers.tekton.dev resources: diff --git a/deploy-templates/templates/role_openshift.yaml b/deploy-templates/templates/role_openshift.yaml index 5aee44f3..7a16f09e 100644 --- a/deploy-templates/templates/role_openshift.yaml +++ b/deploy-templates/templates/role_openshift.yaml @@ -84,7 +84,11 @@ rules: - pipelineruns verbs: - create + - get - list + - patch + - update + - watch - apiGroups: - triggers.tekton.dev resources: diff --git a/docs/api.md b/docs/api.md index 515856b5..e09d7c44 100644 --- a/docs/api.md +++ b/docs/api.md @@ -128,6 +128,15 @@ CDStageDeploySpec defines the desired state of CDStageDeploy. A list of available tags
true + + strategy + string + + TriggerType specifies a strategy for auto-deploy.
+
+ Default: Auto
+ + false diff --git a/go.mod b/go.mod index 423a3dc3..d29f2b1d 100644 --- a/go.mod +++ b/go.mod @@ -3,14 +3,14 @@ module github.com/epam/edp-codebase-operator/v2 go 1.22 replace ( - github.com/epam/edp-cd-pipeline-operator/v2 v2.18.0 => github.com/epam/edp-cd-pipeline-operator/v2 v2.3.0-58.0.20240220142416-885084baa79c github.com/kubernetes-incubator/reference-docs => github.com/kubernetes-sigs/reference-docs v0.0.0-20170929004150-fcf65347b256 github.com/markbates/inflect => github.com/markbates/inflect v1.0.4 + github.com/openshift/api => github.com/openshift/api v0.0.0-20231118005202-0f638a8a4705 ) require ( github.com/andygrunwald/go-jira v1.16.0 - github.com/epam/edp-cd-pipeline-operator/v2 v2.18.0 + github.com/epam/edp-cd-pipeline-operator/v2 v2.3.0-58.0.20241002135711-e8056019eb01 github.com/epam/edp-common v0.0.0-20230710145648-344bbce4120e github.com/go-git/go-git/v5 v5.12.0 github.com/go-logr/logr v1.4.1 @@ -101,6 +101,7 @@ require ( github.com/spf13/pflag v1.0.5 // indirect github.com/stoewer/go-strcase v1.2.0 // indirect github.com/stretchr/objx v0.5.2 // indirect + github.com/tidwall/gjson v1.14.4 // indirect github.com/trivago/tgo v1.0.7 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect go.opencensus.io v0.24.0 // indirect diff --git a/go.sum b/go.sum index bfda30bf..55c0cbee 100644 --- a/go.sum +++ b/go.sum @@ -212,8 +212,8 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.m github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/epam/edp-cd-pipeline-operator/v2 v2.3.0-58.0.20240220142416-885084baa79c h1:NqAc3oLv2vdosWsoFSkMTLbZ1yU/RlJvH54kxdsf+bs= -github.com/epam/edp-cd-pipeline-operator/v2 v2.3.0-58.0.20240220142416-885084baa79c/go.mod h1:U5KH/rmYfCeH7PeZPhVIX6GRSHzFD6wqQS8Tkux6GhQ= +github.com/epam/edp-cd-pipeline-operator/v2 v2.3.0-58.0.20241002135711-e8056019eb01 h1:sp431X1rlNOX6AUKm6blsmcGgiyEHrz18nr0IaSg3Bk= +github.com/epam/edp-cd-pipeline-operator/v2 v2.3.0-58.0.20241002135711-e8056019eb01/go.mod h1:KoYzDNJtoP8uYlZ7sIzrv3MJlIKCdbfMWplX1tyEu/A= github.com/epam/edp-common v0.0.0-20230710145648-344bbce4120e h1:cr/PBxX6OCbfETuIOkl9MbkJoJZhltUJS0dNBioqmHU= github.com/epam/edp-common v0.0.0-20230710145648-344bbce4120e/go.mod h1:0SJsges/ohFjnJ5W59QpuQtkEzlOngYXULajmfnM6rw= github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= @@ -604,8 +604,8 @@ github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg= github.com/onsi/gomega v1.29.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/openshift/api v3.9.0+incompatible h1:fJ/KsefYuZAjmrr3+5U9yZIZbTOpVkDDLDLFresAeYs= -github.com/openshift/api v3.9.0+incompatible/go.mod h1:dh9o4Fs58gpFXGSYfnVxGR9PnV53I8TW84pQaJDdGiY= +github.com/openshift/api v0.0.0-20231118005202-0f638a8a4705 h1:GwpCt0VhL9GjVGJhdF+96RoUkGTf/d+7ICL/3jKWRkA= +github.com/openshift/api v0.0.0-20231118005202-0f638a8a4705/go.mod h1:ctXNyWanKEjGj8sss1KjjHQ3ENKFm33FFnS5BKaIPh4= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= github.com/openzipkin/zipkin-go v0.3.0/go.mod h1:4c3sLeE8xjNqehmF5RpAFLPLJxXscc0R4l6Zg0P1tTQ= @@ -750,8 +750,8 @@ github.com/tektoncd/pipeline v0.59.0 h1:bX+DX3J/SfIokDbXLtKD48ffcfZw0lZ3Ef8QxyOZ github.com/tektoncd/pipeline v0.59.0/go.mod h1:VjXZUGLrOy45B35ufbJZ2DLMt+TSJ3R4ZjKJvi7RWg4= github.com/tektoncd/triggers v0.27.0 h1:c55e/YJF6Vs5BEarqDYksFYuR4sFbmAVEqrLNPZvXUk= github.com/tektoncd/triggers v0.27.0/go.mod h1:DkkAkdSd9aAW9RklUVyFRKQ8kONmZQw4Ur2G1r3wFQo= -github.com/tidwall/gjson v1.12.1 h1:ikuZsLdhr8Ws0IdROXUS1Gi4v9Z4pGqpX/CvJkxvfpo= -github.com/tidwall/gjson v1.12.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM= +github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= diff --git a/main.go b/main.go index 757967ed..da2e0792 100644 --- a/main.go +++ b/main.go @@ -152,7 +152,7 @@ func main() { ctrlLog := ctrl.Log.WithName("controllers") - cdStageDeployCtrl := cdstagedeploy.NewReconcileCDStageDeploy(mgr.GetClient(), ctrlLog, chain.CreateDefChain) + cdStageDeployCtrl := cdstagedeploy.NewReconcileCDStageDeploy(mgr.GetClient(), ctrlLog, chain.CreateChain) if err = cdStageDeployCtrl.SetupWithManager(mgr); err != nil { setupLog.Error(err, "failed to create controller", "controller", "cd-stage-deploy") os.Exit(1) diff --git a/pkg/autodeploy/autodeploy.go b/pkg/autodeploy/autodeploy.go new file mode 100644 index 00000000..a737d9ac --- /dev/null +++ b/pkg/autodeploy/autodeploy.go @@ -0,0 +1,122 @@ +package autodeploy + +import ( + "context" + "encoding/json" + "fmt" + + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + pipelineAPi "github.com/epam/edp-cd-pipeline-operator/v2/api/v1" + + codebaseApi "github.com/epam/edp-codebase-operator/v2/api/v1" + "github.com/epam/edp-codebase-operator/v2/controllers/codebasebranch/chain/put_codebase_image_stream" + "github.com/epam/edp-codebase-operator/v2/pkg/codebaseimagestream" +) + +var ErrLasTagNotFound = fmt.Errorf("last tag not found") + +type Manager interface { + GetAppPayloadForAllLatestStrategy(ctx context.Context, pipeline *pipelineAPi.CDPipeline) (json.RawMessage, error) + GetAppPayloadForCurrentWithStableStrategy(ctx context.Context, current codebaseApi.CodebaseTag, pipeline *pipelineAPi.CDPipeline, stage *pipelineAPi.Stage) (json.RawMessage, error) +} + +var _ Manager = &StrategyManager{} + +type StrategyManager struct { + k8sClient client.Client +} + +type ApplicationPayload struct { + ImageTag string `json:"imageTag"` +} + +func NewStrategyManager(k8sClient client.Client) *StrategyManager { + return &StrategyManager{k8sClient: k8sClient} +} + +func (h *StrategyManager) GetAppPayloadForAllLatestStrategy(ctx context.Context, pipeline *pipelineAPi.CDPipeline) (json.RawMessage, error) { + appPayload := make(map[string]ApplicationPayload, len(pipeline.Spec.InputDockerStreams)) + + for _, stream := range pipeline.Spec.InputDockerStreams { + imageStreamName := put_codebase_image_stream.ProcessNameToK8sConvention(stream) + + codebase, tag, err := h.getLatestTag(ctx, imageStreamName, pipeline.Namespace) + if err != nil { + return nil, err + } + + appPayload[codebase] = ApplicationPayload{ + ImageTag: tag, + } + } + + rawAppPayload, err := json.Marshal(appPayload) + if err != nil { + return nil, fmt.Errorf("failed to marshal application payload: %w", err) + } + + return rawAppPayload, nil +} + +func (h *StrategyManager) GetAppPayloadForCurrentWithStableStrategy( + ctx context.Context, + current codebaseApi.CodebaseTag, + pipeline *pipelineAPi.CDPipeline, + stage *pipelineAPi.Stage, +) (json.RawMessage, error) { + appPayload := make(map[string]ApplicationPayload, len(pipeline.Spec.InputDockerStreams)) + + for _, app := range pipeline.Spec.Applications { + t, ok := stage.GetAnnotations()[fmt.Sprintf("app.edp.epam.com/%s", app)] + if ok { + appPayload[app] = ApplicationPayload{ + ImageTag: t, + } + } + } + + appPayload[current.Codebase] = ApplicationPayload{ + ImageTag: current.Tag, + } + + for _, stream := range pipeline.Spec.InputDockerStreams { + imageStreamName := put_codebase_image_stream.ProcessNameToK8sConvention(stream) + + codebase, tag, err := h.getLatestTag(ctx, imageStreamName, pipeline.Namespace) + if err != nil { + return nil, err + } + + if _, ok := appPayload[codebase]; !ok { + appPayload[codebase] = ApplicationPayload{ + ImageTag: tag, + } + } + } + + rawAppPayload, err := json.Marshal(appPayload) + if err != nil { + return nil, fmt.Errorf("failed to marshal application payload: %w", err) + } + + return rawAppPayload, nil +} + +func (h *StrategyManager) getLatestTag(ctx context.Context, imageStreamName, namespace string) (codebase, tag string, e error) { + imageStream := &codebaseApi.CodebaseImageStream{} + if err := h.k8sClient.Get(ctx, client.ObjectKey{ + Namespace: namespace, + Name: imageStreamName, + }, imageStream); err != nil { + return "", "", fmt.Errorf("failed to get %s CodebaseImageStream: %w", imageStreamName, err) + } + + t, err := codebaseimagestream.GetLastTag(imageStream.Spec.Tags, ctrl.LoggerFrom(ctx)) + if err != nil { + return "", "", ErrLasTagNotFound + } + + return imageStream.Spec.Codebase, t.Name, nil +} diff --git a/pkg/autodeploy/autodeploy_test.go b/pkg/autodeploy/autodeploy_test.go new file mode 100644 index 00000000..a1868bd1 --- /dev/null +++ b/pkg/autodeploy/autodeploy_test.go @@ -0,0 +1,295 @@ +package autodeploy + +import ( + "context" + "testing" + "time" + + "github.com/go-logr/logr" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + pipelineAPi "github.com/epam/edp-cd-pipeline-operator/v2/api/v1" + + codebaseApi "github.com/epam/edp-codebase-operator/v2/api/v1" +) + +func TestStrategyManager_GetAppPayloadForAllLatestStrategy(t *testing.T) { + t.Parallel() + + scheme := runtime.NewScheme() + require.NoError(t, codebaseApi.AddToScheme(scheme)) + + tests := []struct { + name string + pipeline *pipelineAPi.CDPipeline + k8sClient func(t *testing.T) client.Client + want string + wantErr require.ErrorAssertionFunc + }{ + { + name: "get payload successfully", + pipeline: &pipelineAPi.CDPipeline{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pipeline", + Namespace: "default", + }, + Spec: pipelineAPi.CDPipelineSpec{ + InputDockerStreams: []string{"app1-main", "app2-feature/2"}, + }, + }, + k8sClient: func(t *testing.T) client.Client { + return fake.NewClientBuilder().WithScheme(scheme).WithObjects( + &codebaseApi.CodebaseImageStream{ + ObjectMeta: metav1.ObjectMeta{ + Name: "app1-main", + Namespace: "default", + }, + Spec: codebaseApi.CodebaseImageStreamSpec{ + Codebase: "app1", + Tags: []codebaseApi.Tag{ + { + Name: "1.0", + Created: time.Now().Format(time.RFC3339), + }, + { + Name: "1.3", + Created: time.Now().Add(time.Hour * 2).Format(time.RFC3339), + }, + { + Name: "1.1", + Created: time.Now().Add(time.Hour).Format(time.RFC3339), + }, + }, + }, + }, + &codebaseApi.CodebaseImageStream{ + ObjectMeta: metav1.ObjectMeta{ + Name: "app2-feature-2", + Namespace: "default", + }, + Spec: codebaseApi.CodebaseImageStreamSpec{ + Codebase: "app2", + Tags: []codebaseApi.Tag{ + { + Name: "1.0", + Created: time.Now().Format(time.RFC3339), + }, + }, + }, + }, + ).Build() + }, + want: `{"app1":{"imageTag":"1.3"},"app2":{"imageTag":"1.0"}}`, + wantErr: require.NoError, + }, + { + name: "latest tag not found", + pipeline: &pipelineAPi.CDPipeline{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pipeline", + Namespace: "default", + }, + Spec: pipelineAPi.CDPipelineSpec{ + InputDockerStreams: []string{"app1-main"}, + }, + }, + k8sClient: func(t *testing.T) client.Client { + return fake.NewClientBuilder().WithScheme(scheme).WithObjects( + &codebaseApi.CodebaseImageStream{ + ObjectMeta: metav1.ObjectMeta{ + Name: "app1-main", + Namespace: "default", + }, + Spec: codebaseApi.CodebaseImageStreamSpec{ + Codebase: "app1", + }, + }, + ).Build() + }, + want: "", + wantErr: func(t require.TestingT, err error, i ...interface{}) { + require.Error(t, err) + require.Contains(t, err.Error(), "last tag not found") + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + h := NewStrategyManager(tt.k8sClient(t)) + got, err := h.GetAppPayloadForAllLatestStrategy(ctrl.LoggerInto(context.Background(), logr.Discard()), tt.pipeline) + + tt.wantErr(t, err) + if tt.want != "" { + assert.JSONEq(t, tt.want, string(got)) + } + }) + } +} + +func TestStrategyManager_GetAppPayloadForCurrentWithStableStrategy(t *testing.T) { + t.Parallel() + + scheme := runtime.NewScheme() + require.NoError(t, codebaseApi.AddToScheme(scheme)) + require.NoError(t, pipelineAPi.AddToScheme(scheme)) + + tests := []struct { + name string + current codebaseApi.CodebaseTag + pipeline *pipelineAPi.CDPipeline + stage *pipelineAPi.Stage + k8sClient func(t *testing.T) client.Client + want string + wantErr require.ErrorAssertionFunc + }{ + { + name: "get payload successfully", + current: codebaseApi.CodebaseTag{ + Codebase: "app1", + Tag: "1.1", + }, + pipeline: &pipelineAPi.CDPipeline{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pipeline", + Namespace: "default", + }, + Spec: pipelineAPi.CDPipelineSpec{ + InputDockerStreams: []string{"app1-main", "app2-main", "app3-main"}, + Applications: []string{"app1", "app2", "app3"}, + }, + }, + k8sClient: func(t *testing.T) client.Client { + return fake.NewClientBuilder().WithScheme(scheme).WithObjects( + &codebaseApi.CodebaseImageStream{ + ObjectMeta: metav1.ObjectMeta{ + Name: "app1-main", + Namespace: "default", + }, + Spec: codebaseApi.CodebaseImageStreamSpec{ + Codebase: "app1", + Tags: []codebaseApi.Tag{ + { + Name: "1.0", + Created: time.Now().Format(time.RFC3339), + }, + { + Name: "1.3", + Created: time.Now().Add(time.Hour * 2).Format(time.RFC3339), + }, + { + Name: "1.1", + Created: time.Now().Add(time.Hour).Format(time.RFC3339), + }, + }, + }, + }, + &codebaseApi.CodebaseImageStream{ + ObjectMeta: metav1.ObjectMeta{ + Name: "app2-main", + Namespace: "default", + }, + Spec: codebaseApi.CodebaseImageStreamSpec{ + Codebase: "app2", + Tags: []codebaseApi.Tag{ + { + Name: "1.0", + Created: time.Now().Format(time.RFC3339), + }, + { + Name: "1.3", + Created: time.Now().Add(time.Hour * 2).Format(time.RFC3339), + }, + { + Name: "1.1", + Created: time.Now().Add(time.Hour).Format(time.RFC3339), + }, + }, + }, + }, + &codebaseApi.CodebaseImageStream{ + ObjectMeta: metav1.ObjectMeta{ + Name: "app3-main", + Namespace: "default", + }, + Spec: codebaseApi.CodebaseImageStreamSpec{ + Codebase: "app3", + Tags: []codebaseApi.Tag{ + { + Name: "1.0", + Created: time.Now().Format(time.RFC3339), + }, + { + Name: "1.3", + Created: time.Now().Add(time.Hour * 2).Format(time.RFC3339), + }, + { + Name: "1.1", + Created: time.Now().Add(time.Hour).Format(time.RFC3339), + }, + }, + }, + }, + ).Build() + }, + stage: &pipelineAPi.Stage{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "app.edp.epam.com/app3": "1.0", + }, + }, + }, + want: `{"app1":{"imageTag":"1.1"},"app2":{"imageTag":"1.3"},"app3":{"imageTag":"1.0"}}`, + wantErr: require.NoError, + }, + { + name: "codebaseimagestream not found", + current: codebaseApi.CodebaseTag{ + Codebase: "app1", + Tag: "1.1", + }, + pipeline: &pipelineAPi.CDPipeline{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pipeline", + Namespace: "default", + }, + Spec: pipelineAPi.CDPipelineSpec{ + InputDockerStreams: []string{"app1-main"}, + Applications: []string{"app1"}, + }, + }, + stage: &pipelineAPi.Stage{}, + k8sClient: func(t *testing.T) client.Client { + return fake.NewClientBuilder().WithScheme(scheme).WithObjects().Build() + }, + want: "", + wantErr: func(t require.TestingT, err error, i ...interface{}) { + require.Error(t, err) + require.Contains(t, err.Error(), "failed to get app1-main CodebaseImageStream") + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + h := NewStrategyManager(tt.k8sClient(t)) + got, err := h.GetAppPayloadForCurrentWithStableStrategy( + ctrl.LoggerInto(context.Background(), logr.Discard()), + tt.current, + tt.pipeline, + tt.stage, + ) + + tt.wantErr(t, err) + if tt.want != "" { + assert.JSONEq(t, tt.want, string(got)) + } + }) + } +} diff --git a/pkg/autodeploy/mocks/manager_mock.go b/pkg/autodeploy/mocks/manager_mock.go new file mode 100644 index 00000000..a93cca08 --- /dev/null +++ b/pkg/autodeploy/mocks/manager_mock.go @@ -0,0 +1,162 @@ +// Code generated by mockery v2.43.0. DO NOT EDIT. + +package mocks + +import ( + apiv1 "github.com/epam/edp-codebase-operator/v2/api/v1" + + context "context" + + json "encoding/json" + + mock "github.com/stretchr/testify/mock" + + v1 "github.com/epam/edp-cd-pipeline-operator/v2/api/v1" +) + +// MockManager is an autogenerated mock type for the Manager type +type MockManager struct { + mock.Mock +} + +type MockManager_Expecter struct { + mock *mock.Mock +} + +func (_m *MockManager) EXPECT() *MockManager_Expecter { + return &MockManager_Expecter{mock: &_m.Mock} +} + +// GetAppPayloadForAllLatestStrategy provides a mock function with given fields: ctx, pipeline +func (_m *MockManager) GetAppPayloadForAllLatestStrategy(ctx context.Context, pipeline *v1.CDPipeline) (json.RawMessage, error) { + ret := _m.Called(ctx, pipeline) + + if len(ret) == 0 { + panic("no return value specified for GetAppPayloadForAllLatestStrategy") + } + + var r0 json.RawMessage + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *v1.CDPipeline) (json.RawMessage, error)); ok { + return rf(ctx, pipeline) + } + if rf, ok := ret.Get(0).(func(context.Context, *v1.CDPipeline) json.RawMessage); ok { + r0 = rf(ctx, pipeline) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(json.RawMessage) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *v1.CDPipeline) error); ok { + r1 = rf(ctx, pipeline) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockManager_GetAppPayloadForAllLatestStrategy_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetAppPayloadForAllLatestStrategy' +type MockManager_GetAppPayloadForAllLatestStrategy_Call struct { + *mock.Call +} + +// GetAppPayloadForAllLatestStrategy is a helper method to define mock.On call +// - ctx context.Context +// - pipeline *v1.CDPipeline +func (_e *MockManager_Expecter) GetAppPayloadForAllLatestStrategy(ctx interface{}, pipeline interface{}) *MockManager_GetAppPayloadForAllLatestStrategy_Call { + return &MockManager_GetAppPayloadForAllLatestStrategy_Call{Call: _e.mock.On("GetAppPayloadForAllLatestStrategy", ctx, pipeline)} +} + +func (_c *MockManager_GetAppPayloadForAllLatestStrategy_Call) Run(run func(ctx context.Context, pipeline *v1.CDPipeline)) *MockManager_GetAppPayloadForAllLatestStrategy_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*v1.CDPipeline)) + }) + return _c +} + +func (_c *MockManager_GetAppPayloadForAllLatestStrategy_Call) Return(_a0 json.RawMessage, _a1 error) *MockManager_GetAppPayloadForAllLatestStrategy_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockManager_GetAppPayloadForAllLatestStrategy_Call) RunAndReturn(run func(context.Context, *v1.CDPipeline) (json.RawMessage, error)) *MockManager_GetAppPayloadForAllLatestStrategy_Call { + _c.Call.Return(run) + return _c +} + +// GetAppPayloadForCurrentWithStableStrategy provides a mock function with given fields: ctx, current, pipeline, stage +func (_m *MockManager) GetAppPayloadForCurrentWithStableStrategy(ctx context.Context, current apiv1.CodebaseTag, pipeline *v1.CDPipeline, stage *v1.Stage) (json.RawMessage, error) { + ret := _m.Called(ctx, current, pipeline, stage) + + if len(ret) == 0 { + panic("no return value specified for GetAppPayloadForCurrentWithStableStrategy") + } + + var r0 json.RawMessage + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, apiv1.CodebaseTag, *v1.CDPipeline, *v1.Stage) (json.RawMessage, error)); ok { + return rf(ctx, current, pipeline, stage) + } + if rf, ok := ret.Get(0).(func(context.Context, apiv1.CodebaseTag, *v1.CDPipeline, *v1.Stage) json.RawMessage); ok { + r0 = rf(ctx, current, pipeline, stage) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(json.RawMessage) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, apiv1.CodebaseTag, *v1.CDPipeline, *v1.Stage) error); ok { + r1 = rf(ctx, current, pipeline, stage) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockManager_GetAppPayloadForCurrentWithStableStrategy_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetAppPayloadForCurrentWithStableStrategy' +type MockManager_GetAppPayloadForCurrentWithStableStrategy_Call struct { + *mock.Call +} + +// GetAppPayloadForCurrentWithStableStrategy is a helper method to define mock.On call +// - ctx context.Context +// - current apiv1.CodebaseTag +// - pipeline *v1.CDPipeline +// - stage *v1.Stage +func (_e *MockManager_Expecter) GetAppPayloadForCurrentWithStableStrategy(ctx interface{}, current interface{}, pipeline interface{}, stage interface{}) *MockManager_GetAppPayloadForCurrentWithStableStrategy_Call { + return &MockManager_GetAppPayloadForCurrentWithStableStrategy_Call{Call: _e.mock.On("GetAppPayloadForCurrentWithStableStrategy", ctx, current, pipeline, stage)} +} + +func (_c *MockManager_GetAppPayloadForCurrentWithStableStrategy_Call) Run(run func(ctx context.Context, current apiv1.CodebaseTag, pipeline *v1.CDPipeline, stage *v1.Stage)) *MockManager_GetAppPayloadForCurrentWithStableStrategy_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(apiv1.CodebaseTag), args[2].(*v1.CDPipeline), args[3].(*v1.Stage)) + }) + return _c +} + +func (_c *MockManager_GetAppPayloadForCurrentWithStableStrategy_Call) Return(_a0 json.RawMessage, _a1 error) *MockManager_GetAppPayloadForCurrentWithStableStrategy_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockManager_GetAppPayloadForCurrentWithStableStrategy_Call) RunAndReturn(run func(context.Context, apiv1.CodebaseTag, *v1.CDPipeline, *v1.Stage) (json.RawMessage, error)) *MockManager_GetAppPayloadForCurrentWithStableStrategy_Call { + _c.Call.Return(run) + return _c +} + +// NewMockManager creates a new instance of MockManager. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockManager(t interface { + mock.TestingT + Cleanup(func()) +}) *MockManager { + mock := &MockManager{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/tektoncd/mocks/triggertemplatemanager_mock.go b/pkg/tektoncd/mocks/triggertemplatemanager_mock.go new file mode 100644 index 00000000..23b12e1c --- /dev/null +++ b/pkg/tektoncd/mocks/triggertemplatemanager_mock.go @@ -0,0 +1,202 @@ +// Code generated by mockery v2.43.0. DO NOT EDIT. + +package mocks + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" +) + +// MockTriggerTemplateManager is an autogenerated mock type for the TriggerTemplateManager type +type MockTriggerTemplateManager struct { + mock.Mock +} + +type MockTriggerTemplateManager_Expecter struct { + mock *mock.Mock +} + +func (_m *MockTriggerTemplateManager) EXPECT() *MockTriggerTemplateManager_Expecter { + return &MockTriggerTemplateManager_Expecter{mock: &_m.Mock} +} + +// CreatePendingPipelineRun provides a mock function with given fields: ctx, ns, cdStageDeployName, rawPipeRun, appPayload, stage, pipeline, clusterSecret +func (_m *MockTriggerTemplateManager) CreatePendingPipelineRun(ctx context.Context, ns string, cdStageDeployName string, rawPipeRun []byte, appPayload []byte, stage []byte, pipeline []byte, clusterSecret []byte) error { + ret := _m.Called(ctx, ns, cdStageDeployName, rawPipeRun, appPayload, stage, pipeline, clusterSecret) + + if len(ret) == 0 { + panic("no return value specified for CreatePendingPipelineRun") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, string, []byte, []byte, []byte, []byte, []byte) error); ok { + r0 = rf(ctx, ns, cdStageDeployName, rawPipeRun, appPayload, stage, pipeline, clusterSecret) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockTriggerTemplateManager_CreatePendingPipelineRun_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreatePendingPipelineRun' +type MockTriggerTemplateManager_CreatePendingPipelineRun_Call struct { + *mock.Call +} + +// CreatePendingPipelineRun is a helper method to define mock.On call +// - ctx context.Context +// - ns string +// - cdStageDeployName string +// - rawPipeRun []byte +// - appPayload []byte +// - stage []byte +// - pipeline []byte +// - clusterSecret []byte +func (_e *MockTriggerTemplateManager_Expecter) CreatePendingPipelineRun(ctx interface{}, ns interface{}, cdStageDeployName interface{}, rawPipeRun interface{}, appPayload interface{}, stage interface{}, pipeline interface{}, clusterSecret interface{}) *MockTriggerTemplateManager_CreatePendingPipelineRun_Call { + return &MockTriggerTemplateManager_CreatePendingPipelineRun_Call{Call: _e.mock.On("CreatePendingPipelineRun", ctx, ns, cdStageDeployName, rawPipeRun, appPayload, stage, pipeline, clusterSecret)} +} + +func (_c *MockTriggerTemplateManager_CreatePendingPipelineRun_Call) Run(run func(ctx context.Context, ns string, cdStageDeployName string, rawPipeRun []byte, appPayload []byte, stage []byte, pipeline []byte, clusterSecret []byte)) *MockTriggerTemplateManager_CreatePendingPipelineRun_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].([]byte), args[4].([]byte), args[5].([]byte), args[6].([]byte), args[7].([]byte)) + }) + return _c +} + +func (_c *MockTriggerTemplateManager_CreatePendingPipelineRun_Call) Return(_a0 error) *MockTriggerTemplateManager_CreatePendingPipelineRun_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockTriggerTemplateManager_CreatePendingPipelineRun_Call) RunAndReturn(run func(context.Context, string, string, []byte, []byte, []byte, []byte, []byte) error) *MockTriggerTemplateManager_CreatePendingPipelineRun_Call { + _c.Call.Return(run) + return _c +} + +// CreatePipelineRun provides a mock function with given fields: ctx, ns, cdStageDeployName, rawPipeRun, appPayload, stage, pipeline, clusterSecret +func (_m *MockTriggerTemplateManager) CreatePipelineRun(ctx context.Context, ns string, cdStageDeployName string, rawPipeRun []byte, appPayload []byte, stage []byte, pipeline []byte, clusterSecret []byte) error { + ret := _m.Called(ctx, ns, cdStageDeployName, rawPipeRun, appPayload, stage, pipeline, clusterSecret) + + if len(ret) == 0 { + panic("no return value specified for CreatePipelineRun") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, string, []byte, []byte, []byte, []byte, []byte) error); ok { + r0 = rf(ctx, ns, cdStageDeployName, rawPipeRun, appPayload, stage, pipeline, clusterSecret) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockTriggerTemplateManager_CreatePipelineRun_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreatePipelineRun' +type MockTriggerTemplateManager_CreatePipelineRun_Call struct { + *mock.Call +} + +// CreatePipelineRun is a helper method to define mock.On call +// - ctx context.Context +// - ns string +// - cdStageDeployName string +// - rawPipeRun []byte +// - appPayload []byte +// - stage []byte +// - pipeline []byte +// - clusterSecret []byte +func (_e *MockTriggerTemplateManager_Expecter) CreatePipelineRun(ctx interface{}, ns interface{}, cdStageDeployName interface{}, rawPipeRun interface{}, appPayload interface{}, stage interface{}, pipeline interface{}, clusterSecret interface{}) *MockTriggerTemplateManager_CreatePipelineRun_Call { + return &MockTriggerTemplateManager_CreatePipelineRun_Call{Call: _e.mock.On("CreatePipelineRun", ctx, ns, cdStageDeployName, rawPipeRun, appPayload, stage, pipeline, clusterSecret)} +} + +func (_c *MockTriggerTemplateManager_CreatePipelineRun_Call) Run(run func(ctx context.Context, ns string, cdStageDeployName string, rawPipeRun []byte, appPayload []byte, stage []byte, pipeline []byte, clusterSecret []byte)) *MockTriggerTemplateManager_CreatePipelineRun_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].([]byte), args[4].([]byte), args[5].([]byte), args[6].([]byte), args[7].([]byte)) + }) + return _c +} + +func (_c *MockTriggerTemplateManager_CreatePipelineRun_Call) Return(_a0 error) *MockTriggerTemplateManager_CreatePipelineRun_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockTriggerTemplateManager_CreatePipelineRun_Call) RunAndReturn(run func(context.Context, string, string, []byte, []byte, []byte, []byte, []byte) error) *MockTriggerTemplateManager_CreatePipelineRun_Call { + _c.Call.Return(run) + return _c +} + +// GetRawResourceFromTriggerTemplate provides a mock function with given fields: ctx, triggerTemplateName, ns +func (_m *MockTriggerTemplateManager) GetRawResourceFromTriggerTemplate(ctx context.Context, triggerTemplateName string, ns string) ([]byte, error) { + ret := _m.Called(ctx, triggerTemplateName, ns) + + if len(ret) == 0 { + panic("no return value specified for GetRawResourceFromTriggerTemplate") + } + + var r0 []byte + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) ([]byte, error)); ok { + return rf(ctx, triggerTemplateName, ns) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string) []byte); ok { + r0 = rf(ctx, triggerTemplateName, ns) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]byte) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = rf(ctx, triggerTemplateName, ns) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockTriggerTemplateManager_GetRawResourceFromTriggerTemplate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetRawResourceFromTriggerTemplate' +type MockTriggerTemplateManager_GetRawResourceFromTriggerTemplate_Call struct { + *mock.Call +} + +// GetRawResourceFromTriggerTemplate is a helper method to define mock.On call +// - ctx context.Context +// - triggerTemplateName string +// - ns string +func (_e *MockTriggerTemplateManager_Expecter) GetRawResourceFromTriggerTemplate(ctx interface{}, triggerTemplateName interface{}, ns interface{}) *MockTriggerTemplateManager_GetRawResourceFromTriggerTemplate_Call { + return &MockTriggerTemplateManager_GetRawResourceFromTriggerTemplate_Call{Call: _e.mock.On("GetRawResourceFromTriggerTemplate", ctx, triggerTemplateName, ns)} +} + +func (_c *MockTriggerTemplateManager_GetRawResourceFromTriggerTemplate_Call) Run(run func(ctx context.Context, triggerTemplateName string, ns string)) *MockTriggerTemplateManager_GetRawResourceFromTriggerTemplate_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string), args[2].(string)) + }) + return _c +} + +func (_c *MockTriggerTemplateManager_GetRawResourceFromTriggerTemplate_Call) Return(_a0 []byte, _a1 error) *MockTriggerTemplateManager_GetRawResourceFromTriggerTemplate_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockTriggerTemplateManager_GetRawResourceFromTriggerTemplate_Call) RunAndReturn(run func(context.Context, string, string) ([]byte, error)) *MockTriggerTemplateManager_GetRawResourceFromTriggerTemplate_Call { + _c.Call.Return(run) + return _c +} + +// NewMockTriggerTemplateManager creates a new instance of MockTriggerTemplateManager. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockTriggerTemplateManager(t interface { + mock.TestingT + Cleanup(func()) +}) *MockTriggerTemplateManager { + mock := &MockTriggerTemplateManager{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/tektoncd/trigger_template.go b/pkg/tektoncd/trigger_template.go new file mode 100644 index 00000000..3e9e9cb9 --- /dev/null +++ b/pkg/tektoncd/trigger_template.go @@ -0,0 +1,131 @@ +package tektoncd + +import ( + "bytes" + "context" + "errors" + "fmt" + + tektonpipelineApi "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" + tektonTriggersApi "github.com/tektoncd/triggers/pkg/apis/triggers/v1beta1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "sigs.k8s.io/controller-runtime/pkg/client" + + codebaseApi "github.com/epam/edp-codebase-operator/v2/api/v1" +) + +var ErrEmptyTriggerTemplateResources = fmt.Errorf("trigger template resources is empty") + +type TriggerTemplateManager interface { + GetRawResourceFromTriggerTemplate(ctx context.Context, triggerTemplateName, ns string) ([]byte, error) + CreatePipelineRun(ctx context.Context, ns, cdStageDeployName string, rawPipeRun, appPayload, stage, pipeline, clusterSecret []byte) error + CreatePendingPipelineRun(ctx context.Context, ns, cdStageDeployName string, rawPipeRun, appPayload, stage, pipeline, clusterSecret []byte) error +} + +var _ TriggerTemplateManager = &TektonTriggerTemplateManager{} + +type TektonTriggerTemplateManager struct { + k8sClient client.Client +} + +func NewTektonTriggerTemplateManager(k8sClient client.Client) *TektonTriggerTemplateManager { + return &TektonTriggerTemplateManager{k8sClient: k8sClient} +} + +func (h *TektonTriggerTemplateManager) GetRawResourceFromTriggerTemplate(ctx context.Context, triggerTemplateName, ns string) ([]byte, error) { + template := &tektonTriggersApi.TriggerTemplate{} + if err := h.k8sClient.Get(ctx, client.ObjectKey{ + Namespace: ns, + Name: triggerTemplateName, + }, template); err != nil { + return nil, fmt.Errorf("failed to get TriggerTemplate: %w", err) + } + + if len(template.Spec.ResourceTemplates) == 0 { + return nil, ErrEmptyTriggerTemplateResources + } + + rawPipeRun := make([]byte, len(template.Spec.ResourceTemplates[0].RawExtension.Raw)) + copy(rawPipeRun, template.Spec.ResourceTemplates[0].RawExtension.Raw) + + return rawPipeRun, nil +} + +func (h *TektonTriggerTemplateManager) CreatePipelineRun( + ctx context.Context, + ns, + cdStageDeployName string, + rawPipeRun, + appPayload, + stage, + pipeline, + clusterSecret []byte, +) error { + data, err := makeUnstructuredPipelineRun(ns, cdStageDeployName, rawPipeRun, appPayload, stage, pipeline, clusterSecret) + if err != nil { + return err + } + + if err = h.k8sClient.Create(ctx, data); err != nil { + return fmt.Errorf("failed to create resource: %w", err) + } + + return nil +} + +func (h *TektonTriggerTemplateManager) CreatePendingPipelineRun( + ctx context.Context, + ns, + cdStageDeployName string, + rawPipeRun, + appPayload, + stage, + pipeline, + clusterSecret []byte, +) error { + data, err := makeUnstructuredPipelineRun(ns, cdStageDeployName, rawPipeRun, appPayload, stage, pipeline, clusterSecret) + if err != nil { + return err + } + + spec, ok := data.Object["spec"].(map[string]interface{}) + if !ok { + return errors.New("invalid PipelineRun spec") + } + + spec["status"] = tektonpipelineApi.PipelineRunSpecStatusPending + + if err = h.k8sClient.Create(ctx, data); err != nil { + return fmt.Errorf("failed to create resource: %w", err) + } + + return nil +} + +func makeUnstructuredPipelineRun( + ns, + cdStageDeployName string, + rawPipeRun []byte, + appPayload []byte, + stage []byte, + pipeline []byte, + clusterSecret []byte, +) (*unstructured.Unstructured, error) { + rawPipeRun = bytes.ReplaceAll(rawPipeRun, []byte("$(tt.params.APPLICATIONS_PAYLOAD)"), bytes.ReplaceAll(appPayload, []byte(`"`), []byte(`\"`))) + rawPipeRun = bytes.ReplaceAll(rawPipeRun, []byte("$(tt.params.CDSTAGE)"), stage) + rawPipeRun = bytes.ReplaceAll(rawPipeRun, []byte("$(tt.params.CDPIPELINE)"), pipeline) + rawPipeRun = bytes.ReplaceAll(rawPipeRun, []byte("$(tt.params.KUBECONFIG_SECRET_NAME)"), clusterSecret) + + data := &unstructured.Unstructured{} + if err := data.UnmarshalJSON(rawPipeRun); err != nil { + return nil, fmt.Errorf("couldn't unmarshal json from the TriggerTemplate: %w", err) + } + + data.SetNamespace(ns) + + labels := data.GetLabels() + labels[codebaseApi.CdStageDeployLabel] = cdStageDeployName + data.SetLabels(labels) + + return data, nil +} diff --git a/pkg/tektoncd/trigger_template_test.go b/pkg/tektoncd/trigger_template_test.go new file mode 100644 index 00000000..fdb0413f --- /dev/null +++ b/pkg/tektoncd/trigger_template_test.go @@ -0,0 +1,316 @@ +package tektoncd + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + tektonpipelineApi "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" + tektonTriggersApi "github.com/tektoncd/triggers/pkg/apis/triggers/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +var ( + pipelineRunTemplate = []byte(`{ + "apiVersion": "tekton.dev/v1", + "kind": "PipelineRun", + "metadata": { + "generateName": "deploy-$(tt.params.CDPIPELINE)-$(tt.params.CDSTAGE)-", + "labels": { + "app.edp.epam.com/cdpipeline": "$(tt.params.CDPIPELINE)", + "app.edp.epam.com/cdstage": "$(tt.params.CDSTAGE)", + "app.edp.epam.com/pipelinetype": "deploy" + } + }, + "spec": { + "params": [ + { + "name": "APPLICATIONS_PAYLOAD", + "value": "$(tt.params.APPLICATIONS_PAYLOAD)" + }, + { + "name": "CDSTAGE", + "value": "$(tt.params.CDSTAGE)" + }, + { + "name": "CDPIPELINE", + "value": "$(tt.params.CDPIPELINE)" + }, + { + "name": "KUBECONFIG_SECRET_NAME", + "value": "$(tt.params.KUBECONFIG_SECRET_NAME)" + } + ], + "pipelineRef": { + "name": "cd-stage-deploy" + }, + "taskRunTemplate": { + "serviceAccountName": "tekton" + }, + "timeouts": { + "pipeline": "1h00m0s" + } + } + }`) +) + +func TestTektonTriggerTemplateManager_GetRawResourceFromTriggerTemplate(t *testing.T) { + t.Parallel() + + scheme := runtime.NewScheme() + require.NoError(t, tektonTriggersApi.AddToScheme(scheme)) + + tests := []struct { + name string + k8sClient func(t *testing.T) client.Client + want string + wantErr require.ErrorAssertionFunc + }{ + { + name: "get raw resource successfully", + k8sClient: func(t *testing.T) client.Client { + return fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(&tektonTriggersApi.TriggerTemplate{ + ObjectMeta: metav1.ObjectMeta{ + Name: "app-template", + Namespace: "default", + }, + Spec: tektonTriggersApi.TriggerTemplateSpec{ + ResourceTemplates: []tektonTriggersApi.TriggerResourceTemplate{ + { + RawExtension: runtime.RawExtension{ + Raw: pipelineRunTemplate, + }, + }, + }, + }, + }). + Build() + }, + want: string(pipelineRunTemplate), + wantErr: require.NoError, + }, + { + name: "raw resource is empty", + k8sClient: func(t *testing.T) client.Client { + return fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(&tektonTriggersApi.TriggerTemplate{ + ObjectMeta: metav1.ObjectMeta{ + Name: "app-template", + Namespace: "default", + }, + Spec: tektonTriggersApi.TriggerTemplateSpec{}, + }). + Build() + }, + want: "", + wantErr: func(t require.TestingT, err error, i ...interface{}) { + require.Error(t, err) + require.ErrorIs(t, err, ErrEmptyTriggerTemplateResources) + }, + }, + { + name: "failed to get trigger template", + k8sClient: func(t *testing.T) client.Client { + return fake.NewClientBuilder(). + WithScheme(scheme). + Build() + }, + want: "", + wantErr: func(t require.TestingT, err error, i ...interface{}) { + require.Error(t, err) + require.Contains(t, err.Error(), "failed to get TriggerTemplate") + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + h := NewTektonTriggerTemplateManager(tt.k8sClient(t)) + got, err := h.GetRawResourceFromTriggerTemplate(context.Background(), "app-template", "default") + + tt.wantErr(t, err) + if tt.want != "" { + require.JSONEq(t, tt.want, string(got)) + } + }) + } +} + +func TestTektonTriggerTemplateManager_CreatePipelineRun(t *testing.T) { + t.Parallel() + + scheme := runtime.NewScheme() + require.NoError(t, tektonpipelineApi.AddToScheme(scheme)) + require.NoError(t, tektonTriggersApi.AddToScheme(scheme)) + + type args struct { + ns string + cdStageDeployName string + rawPipeRun []byte + appPayload []byte + stage []byte + pipeline []byte + clusterSecret []byte + } + + tests := []struct { + name string + args args + k8sClient func(t *testing.T) client.Client + wantErr require.ErrorAssertionFunc + want func(t *testing.T, k8sCl client.Client) + }{ + { + name: "create pipeline run successfully", + args: args{ + ns: "default", + cdStageDeployName: "deploy-app", + rawPipeRun: pipelineRunTemplate, + appPayload: []byte(`{"app1":"1.1", "app2":"2.0"}`), + stage: []byte("dev"), + pipeline: []byte("pipeline-1"), + clusterSecret: []byte("cl-secret"), + }, + k8sClient: func(t *testing.T) client.Client { + return fake.NewClientBuilder(). + WithScheme(scheme). + Build() + }, + wantErr: require.NoError, + want: func(t *testing.T, k8sCl client.Client) { + l := &tektonpipelineApi.PipelineRunList{} + require.NoError(t, k8sCl.List(context.Background(), l, client.InNamespace("default"))) + require.Len(t, l.Items, 1) + + run := l.Items[0] + + require.Len(t, run.Spec.Params, 4) + for _, p := range run.Spec.Params { + switch p.Name { + case "APPLICATIONS_PAYLOAD": + require.Equal(t, `{"app1":"1.1", "app2":"2.0"}`, p.Value.StringVal) + case "CDSTAGE": + require.Equal(t, "dev", p.Value.StringVal) + case "CDPIPELINE": + require.Equal(t, "pipeline-1", p.Value.StringVal) + case "KUBECONFIG_SECRET_NAME": + require.Equal(t, "cl-secret", p.Value.StringVal) + } + } + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + k8sCl := tt.k8sClient(t) + h := NewTektonTriggerTemplateManager(k8sCl) + err := h.CreatePipelineRun( + context.Background(), + tt.args.ns, + tt.args.cdStageDeployName, + tt.args.rawPipeRun, + tt.args.appPayload, + tt.args.stage, + tt.args.pipeline, + tt.args.clusterSecret, + ) + + tt.wantErr(t, err) + tt.want(t, k8sCl) + }) + } +} + +func TestTektonTriggerTemplateManager_CreatePendingPipelineRun(t *testing.T) { + t.Parallel() + + scheme := runtime.NewScheme() + require.NoError(t, tektonpipelineApi.AddToScheme(scheme)) + require.NoError(t, tektonTriggersApi.AddToScheme(scheme)) + + type args struct { + ns string + cdStageDeployName string + rawPipeRun []byte + appPayload []byte + stage []byte + pipeline []byte + clusterSecret []byte + } + + tests := []struct { + name string + args args + k8sClient func(t *testing.T) client.Client + wantErr require.ErrorAssertionFunc + want func(t *testing.T, k8sCl client.Client) + }{ + { + name: "create pipeline run successfully", + args: args{ + ns: "default", + cdStageDeployName: "deploy-app", + rawPipeRun: pipelineRunTemplate, + appPayload: []byte(`{"app1":"1.1", "app2":"2.0"}`), + stage: []byte("dev"), + pipeline: []byte("pipeline-1"), + clusterSecret: []byte("cl-secret"), + }, + k8sClient: func(t *testing.T) client.Client { + return fake.NewClientBuilder(). + WithScheme(scheme). + Build() + }, + wantErr: require.NoError, + want: func(t *testing.T, k8sCl client.Client) { + l := &tektonpipelineApi.PipelineRunList{} + require.NoError(t, k8sCl.List(context.Background(), l, client.InNamespace("default"))) + require.Len(t, l.Items, 1) + + run := l.Items[0] + + require.Equal(t, tektonpipelineApi.PipelineRunSpecStatusPending, string(run.Spec.Status)) + require.Len(t, run.Spec.Params, 4) + for _, p := range run.Spec.Params { + switch p.Name { + case "APPLICATIONS_PAYLOAD": + require.Equal(t, `{"app1":"1.1", "app2":"2.0"}`, p.Value.StringVal) + case "CDSTAGE": + require.Equal(t, "dev", p.Value.StringVal) + case "CDPIPELINE": + require.Equal(t, "pipeline-1", p.Value.StringVal) + case "KUBECONFIG_SECRET_NAME": + require.Equal(t, "cl-secret", p.Value.StringVal) + } + } + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + k8sCl := tt.k8sClient(t) + h := NewTektonTriggerTemplateManager(k8sCl) + err := h.CreatePendingPipelineRun( + context.Background(), + tt.args.ns, + tt.args.cdStageDeployName, + tt.args.rawPipeRun, + tt.args.appPayload, + tt.args.stage, + tt.args.pipeline, + tt.args.clusterSecret, + ) + + tt.wantErr(t, err) + tt.want(t, k8sCl) + }) + } +} diff --git a/pkg/util/url.go b/pkg/util/url.go index edad1d11..42bbf03f 100644 --- a/pkg/util/url.go +++ b/pkg/util/url.go @@ -42,7 +42,7 @@ func GetRepoUrl(c *codebaseApi.Codebase) (string, error) { return tryGetRepoUrl(&c.Spec) } - log.Info("Strategy is not clone. Start build url...", logCodebaseNameKey, c.Name) + log.Info("TriggerType is not clone. Start build url...", logCodebaseNameKey, c.Name) u := BuildRepoUrl(&c.Spec) diff --git a/pkg/webhook/codebase_webhook_test.go b/pkg/webhook/codebase_webhook_test.go index 1fcea540..234fcc71 100644 --- a/pkg/webhook/codebase_webhook_test.go +++ b/pkg/webhook/codebase_webhook_test.go @@ -229,7 +229,7 @@ func TestCodebaseWebhook_ValidateCreate(t *testing.T) { wantErr: require.NoError, }, { - name: "should return error if Strategy is not valid", + name: "should return error if TriggerType is not valid", client: fake.NewClientBuilder().WithScheme(scheme).Build(), ctx: admission.NewContextWithRequest(context.Background(), admission.Request{ AdmissionRequest: v1.AdmissionRequest{ @@ -359,7 +359,7 @@ func TestCodebaseValidationWebhook_ValidateUpdate(t *testing.T) { }, }, { - name: "should return error if Strategy is not valid", + name: "should return error if TriggerType is not valid", ctx: admission.NewContextWithRequest(context.Background(), admission.Request{ AdmissionRequest: v1.AdmissionRequest{ Name: "codebase",