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