diff --git a/Makefile b/Makefile index a6d36683b..5fc8bac2c 100644 --- a/Makefile +++ b/Makefile @@ -100,6 +100,7 @@ mocks: mockgen -source ./radix-operator/dnsalias/internal/syncerfactory.go -destination ./radix-operator/dnsalias/internal/syncerfactory_mock.go -package internal mockgen -source ./radix-operator/common/handler.go -destination ./radix-operator/common/handler_mock.go -package common mockgen -source ./pipeline-runner/internal/wait/job.go -destination ./pipeline-runner/internal/wait/job_mock.go -package wait + mockgen -source ./pipeline-runner/internal/watcher/radix_deployment_watcher.go -destination ./pipeline-runner/internal/watcher/radix_deployment_watcher_mock.go -package watcher .PHONY: build-pipeline build-pipeline: diff --git a/charts/radix-operator/Chart.yaml b/charts/radix-operator/Chart.yaml index 67942d34f..aca2b58bb 100644 --- a/charts/radix-operator/Chart.yaml +++ b/charts/radix-operator/Chart.yaml @@ -1,7 +1,7 @@ apiVersion: v2 name: radix-operator -version: 1.30.5 -appVersion: 1.50.5 +version: 1.30.6 +appVersion: 1.50.6 kubeVersion: ">=1.24.0" description: Radix Operator keywords: diff --git a/charts/radix-operator/templates/radix-pipeline-rbac.yaml b/charts/radix-operator/templates/radix-pipeline-rbac.yaml index e56b7a848..6fedecb24 100644 --- a/charts/radix-operator/templates/radix-pipeline-rbac.yaml +++ b/charts/radix-operator/templates/radix-pipeline-rbac.yaml @@ -73,6 +73,7 @@ rules: - get - list - create + - delete - apiGroups: - "" resources: diff --git a/pipeline-runner/internal/watcher/radix_deployment_watcher.go b/pipeline-runner/internal/watcher/radix_deployment_watcher.go new file mode 100644 index 000000000..1f4f7280b --- /dev/null +++ b/pipeline-runner/internal/watcher/radix_deployment_watcher.go @@ -0,0 +1,54 @@ +package watcher + +import ( + "context" + "time" + + radixclient "github.com/equinor/radix-operator/pkg/client/clientset/versioned" + "github.com/rs/zerolog/log" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/wait" +) + +// RadixDeploymentWatcher Watcher to wait for namespace to be created +type RadixDeploymentWatcher interface { + WaitForActive(namespace, deploymentName string) error +} + +// radixDeploymentWatcher Implementation of watcher +type radixDeploymentWatcher struct { + radixClient radixclient.Interface + waitTimeout time.Duration +} + +// NewRadixDeploymentWatcher Constructor +func NewRadixDeploymentWatcher(radixClient radixclient.Interface, waitTimeout time.Duration) RadixDeploymentWatcher { + return &radixDeploymentWatcher{ + radixClient, + waitTimeout, + } +} + +// WaitForActive Waits for the radix deployment gets active +func (watcher radixDeploymentWatcher) WaitForActive(namespace, deploymentName string) error { + log.Info().Msgf("Waiting for Radix deployment %s to activate", deploymentName) + if err := watcher.waitFor(func(context.Context) (bool, error) { + rd, err := watcher.radixClient.RadixV1().RadixDeployments(namespace).Get(context.Background(), deploymentName, metav1.GetOptions{}) + if err != nil { + return false, err + } + return rd != nil && !rd.Status.ActiveFrom.IsZero(), nil + }); err != nil { + return err + } + + log.Info().Msgf("Radix deployment %s in namespace %s is active", deploymentName, namespace) + return nil + +} + +func (watcher radixDeploymentWatcher) waitFor(condition wait.ConditionWithContextFunc) error { + timoutContext, cancel := context.WithTimeout(context.Background(), watcher.waitTimeout) + defer cancel() + return wait.PollUntilContextCancel(timoutContext, time.Second, true, condition) +} diff --git a/pipeline-runner/internal/watcher/radix_deployment_watcher_mock.go b/pipeline-runner/internal/watcher/radix_deployment_watcher_mock.go new file mode 100644 index 000000000..b6833aa2b --- /dev/null +++ b/pipeline-runner/internal/watcher/radix_deployment_watcher_mock.go @@ -0,0 +1,48 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ./pipeline-runner/internal/watcher/radix_deployment_watcher.go + +// Package watcher is a generated GoMock package. +package watcher + +import ( + reflect "reflect" + + gomock "github.com/golang/mock/gomock" +) + +// MockRadixDeploymentWatcher is a mock of RadixDeploymentWatcher interface. +type MockRadixDeploymentWatcher struct { + ctrl *gomock.Controller + recorder *MockRadixDeploymentWatcherMockRecorder +} + +// MockRadixDeploymentWatcherMockRecorder is the mock recorder for MockRadixDeploymentWatcher. +type MockRadixDeploymentWatcherMockRecorder struct { + mock *MockRadixDeploymentWatcher +} + +// NewMockRadixDeploymentWatcher creates a new mock instance. +func NewMockRadixDeploymentWatcher(ctrl *gomock.Controller) *MockRadixDeploymentWatcher { + mock := &MockRadixDeploymentWatcher{ctrl: ctrl} + mock.recorder = &MockRadixDeploymentWatcherMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockRadixDeploymentWatcher) EXPECT() *MockRadixDeploymentWatcherMockRecorder { + return m.recorder +} + +// WaitForActive mocks base method. +func (m *MockRadixDeploymentWatcher) WaitForActive(namespace, deploymentName string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "WaitForActive", namespace, deploymentName) + ret0, _ := ret[0].(error) + return ret0 +} + +// WaitForActive indicates an expected call of WaitForActive. +func (mr *MockRadixDeploymentWatcherMockRecorder) WaitForActive(namespace, deploymentName interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WaitForActive", reflect.TypeOf((*MockRadixDeploymentWatcher)(nil).WaitForActive), namespace, deploymentName) +} diff --git a/pipeline-runner/internal/watcher/radix_deployment_watcher_test.go b/pipeline-runner/internal/watcher/radix_deployment_watcher_test.go new file mode 100644 index 000000000..d75ef0483 --- /dev/null +++ b/pipeline-runner/internal/watcher/radix_deployment_watcher_test.go @@ -0,0 +1,68 @@ +package watcher + +import ( + "context" + "errors" + "testing" + "time" + + radixv1 "github.com/equinor/radix-operator/pkg/apis/radix/v1" + radix "github.com/equinor/radix-operator/pkg/client/clientset/versioned/fake" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + kubernetes "k8s.io/client-go/kubernetes/fake" +) + +func setupTest(t *testing.T) (*radix.Clientset, *kubernetes.Clientset) { + radixClient := radix.NewSimpleClientset() + kubeClient := kubernetes.NewSimpleClientset() + return radixClient, kubeClient +} + +func TestDeploy_WaitActiveDeployment(t *testing.T) { + const ( + namespace = "app-dev" + radixDeploymentName = "any-rd-name" + ) + type scenario struct { + name string + hasRadixDevelopment bool + radixDeploymentStatus radixv1.RadixDeployStatus + watcherError error + } + scenarios := []scenario{ + {name: "Active deployment, no fail", hasRadixDevelopment: true, radixDeploymentStatus: radixv1.RadixDeployStatus{ActiveFrom: metav1.Time{Time: time.Now()}}, watcherError: nil}, + {name: "No active radix deployment, fail", hasRadixDevelopment: true, radixDeploymentStatus: radixv1.RadixDeployStatus{}, watcherError: errors.New("context deadline exceeded")}, + {name: "No radix deployment, fail", hasRadixDevelopment: false, watcherError: errors.New("radixdeployments.radix.equinor.com \"any-rd-name\" not found")}, + } + for _, ts := range scenarios { + t.Run(ts.name, func(tt *testing.T) { + radixClient, kubeClient := setupTest(tt) + require.NoError(t, createNamespace(kubeClient, namespace)) + + if ts.hasRadixDevelopment { + _, err := radixClient.RadixV1().RadixDeployments(namespace).Create(context.Background(), &radixv1.RadixDeployment{ + ObjectMeta: metav1.ObjectMeta{Name: radixDeploymentName}, + Status: ts.radixDeploymentStatus, + }, metav1.CreateOptions{}) + require.NoError(tt, err) + } + + watcher := NewRadixDeploymentWatcher(radixClient, time.Millisecond*10) + err := watcher.WaitForActive(namespace, radixDeploymentName) + + if ts.watcherError == nil { + assert.NoError(tt, err) + } else { + assert.EqualError(tt, err, ts.watcherError.Error()) + } + }) + } +} + +func createNamespace(kubeClient *kubernetes.Clientset, namespace string) error { + _, err := kubeClient.CoreV1().Namespaces().Create(context.Background(), &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: namespace}}, metav1.CreateOptions{}) + return err +} diff --git a/pipeline-runner/pipelines/app.go b/pipeline-runner/pipelines/app.go index ed33fbb71..c439f7fce 100644 --- a/pipeline-runner/pipelines/app.go +++ b/pipeline-runner/pipelines/app.go @@ -2,7 +2,9 @@ package pipelines import ( "context" + "time" + "github.com/equinor/radix-operator/pipeline-runner/internal/watcher" "github.com/equinor/radix-operator/pipeline-runner/model" "github.com/equinor/radix-operator/pipeline-runner/steps" jobs "github.com/equinor/radix-operator/pkg/apis/job" @@ -109,7 +111,7 @@ func (cli *PipelineRunner) initStepImplementations(registration *v1.RadixRegistr stepImplementations = append(stepImplementations, steps.NewApplyConfigStep()) stepImplementations = append(stepImplementations, steps.NewBuildStep(nil)) stepImplementations = append(stepImplementations, steps.NewRunPipelinesStep(nil)) - stepImplementations = append(stepImplementations, steps.NewDeployStep(kube.NewNamespaceWatcherImpl(cli.kubeclient))) + stepImplementations = append(stepImplementations, steps.NewDeployStep(kube.NewNamespaceWatcherImpl(cli.kubeclient), watcher.NewRadixDeploymentWatcher(cli.radixclient, time.Minute*5))) stepImplementations = append(stepImplementations, steps.NewPromoteStep()) for _, stepImplementation := range stepImplementations { diff --git a/pipeline-runner/steps/build_test.go b/pipeline-runner/steps/build_test.go index f6532820d..f1886cd03 100644 --- a/pipeline-runner/steps/build_test.go +++ b/pipeline-runner/steps/build_test.go @@ -144,7 +144,7 @@ func (s *buildTestSuite) Test_BuildDeploy_JobSpecAndDeploymentConsistent() { jobWaiter.EXPECT().Wait(gomock.Any()).Return(nil).Times(1) buildStep := steps.NewBuildStep(jobWaiter) buildStep.Init(s.kubeClient, s.radixClient, s.kubeUtil, s.promClient, rr) - deployStep := steps.NewDeployStep(FakeNamespaceWatcher{}) + deployStep := steps.NewDeployStep(FakeNamespaceWatcher{}, FakeRadixDeploymentWatcher{}) deployStep.Init(s.kubeClient, s.radixClient, s.kubeUtil, s.promClient, rr) s.Require().NoError(applyStep.Run(&pipeline)) @@ -273,7 +273,7 @@ func (s *buildTestSuite) Test_BuildJobSpec_MultipleComponents() { jobWaiter.EXPECT().Wait(gomock.Any()).Return(nil).Times(1) buildStep := steps.NewBuildStep(jobWaiter) buildStep.Init(s.kubeClient, s.radixClient, s.kubeUtil, s.promClient, rr) - deployStep := steps.NewDeployStep(FakeNamespaceWatcher{}) + deployStep := steps.NewDeployStep(FakeNamespaceWatcher{}, FakeRadixDeploymentWatcher{}) deployStep.Init(s.kubeClient, s.radixClient, s.kubeUtil, s.promClient, rr) s.Require().NoError(applyStep.Run(&pipeline)) @@ -416,7 +416,7 @@ func (s *buildTestSuite) Test_BuildJobSpec_MultipleComponents_IgnoreDisabled() { jobWaiter.EXPECT().Wait(gomock.Any()).Return(nil).Times(1) buildStep := steps.NewBuildStep(jobWaiter) buildStep.Init(s.kubeClient, s.radixClient, s.kubeUtil, s.promClient, rr) - deployStep := steps.NewDeployStep(FakeNamespaceWatcher{}) + deployStep := steps.NewDeployStep(FakeNamespaceWatcher{}, FakeRadixDeploymentWatcher{}) deployStep.Init(s.kubeClient, s.radixClient, s.kubeUtil, s.promClient, rr) s.Require().NoError(applyStep.Run(&pipeline)) @@ -568,7 +568,7 @@ func (s *buildTestSuite) Test_BuildChangedComponents() { jobWaiter.EXPECT().Wait(gomock.Any()).Return(nil).Times(1) buildStep := steps.NewBuildStep(jobWaiter) buildStep.Init(s.kubeClient, s.radixClient, s.kubeUtil, s.promClient, rr) - deployStep := steps.NewDeployStep(FakeNamespaceWatcher{}) + deployStep := steps.NewDeployStep(FakeNamespaceWatcher{}, FakeRadixDeploymentWatcher{}) deployStep.Init(s.kubeClient, s.radixClient, s.kubeUtil, s.promClient, rr) s.Require().NoError(applyStep.Run(&pipeline)) @@ -1041,7 +1041,7 @@ func (s *buildTestSuite) Test_DetectComponentsToBuild() { } buildStep := steps.NewBuildStep(jobWaiter) buildStep.Init(s.kubeClient, s.radixClient, s.kubeUtil, s.promClient, rr) - deployStep := steps.NewDeployStep(FakeNamespaceWatcher{}) + deployStep := steps.NewDeployStep(FakeNamespaceWatcher{}, FakeRadixDeploymentWatcher{}) deployStep.Init(s.kubeClient, s.radixClient, s.kubeUtil, s.promClient, rr) // Run pipeline steps @@ -1111,7 +1111,7 @@ func (s *buildTestSuite) Test_BuildJobSpec_ImageTagNames() { applyStep := steps.NewApplyConfigStep() applyStep.Init(s.kubeClient, s.radixClient, s.kubeUtil, s.promClient, rr) - deployStep := steps.NewDeployStep(FakeNamespaceWatcher{}) + deployStep := steps.NewDeployStep(FakeNamespaceWatcher{}, FakeRadixDeploymentWatcher{}) deployStep.Init(s.kubeClient, s.radixClient, s.kubeUtil, s.promClient, rr) s.Require().NoError(applyStep.Run(&pipeline)) @@ -1330,7 +1330,7 @@ func (s *buildTestSuite) Test_BuildJobSpec_WithBuildSecrets() { applyStep.Init(s.kubeClient, s.radixClient, s.kubeUtil, s.promClient, rr) buildStep := steps.NewBuildStep(jobWaiter) buildStep.Init(s.kubeClient, s.radixClient, s.kubeUtil, s.promClient, rr) - deployStep := steps.NewDeployStep(FakeNamespaceWatcher{}) + deployStep := steps.NewDeployStep(FakeNamespaceWatcher{}, FakeRadixDeploymentWatcher{}) deployStep.Init(s.kubeClient, s.radixClient, s.kubeUtil, s.promClient, rr) s.Require().NoError(applyStep.Run(&pipeline)) s.Require().NoError(buildStep.Run(&pipeline)) @@ -1694,7 +1694,7 @@ func (s *buildTestSuite) Test_BuildJobSpec_EnvConfigSrcAndImage() { jobWaiter.EXPECT().Wait(gomock.Any()).Return(nil).Times(1) buildStep := steps.NewBuildStep(jobWaiter) buildStep.Init(s.kubeClient, s.radixClient, s.kubeUtil, s.promClient, rr) - deployStep := steps.NewDeployStep(FakeNamespaceWatcher{}) + deployStep := steps.NewDeployStep(FakeNamespaceWatcher{}, FakeRadixDeploymentWatcher{}) deployStep.Init(s.kubeClient, s.radixClient, s.kubeUtil, s.promClient, rr) s.Require().NoError(applyStep.Run(&pipeline)) diff --git a/pipeline-runner/steps/deploy.go b/pipeline-runner/steps/deploy.go index 5f66fb0a2..7bb336116 100644 --- a/pipeline-runner/steps/deploy.go +++ b/pipeline-runner/steps/deploy.go @@ -4,6 +4,7 @@ import ( "context" "fmt" + pipelineRunnerInternal "github.com/equinor/radix-operator/pipeline-runner/internal/watcher" "github.com/equinor/radix-operator/pipeline-runner/model" "github.com/equinor/radix-operator/pipeline-runner/steps/internal" "github.com/equinor/radix-operator/pkg/apis/defaults" @@ -17,16 +18,18 @@ import ( // DeployStepImplementation Step to deploy RD into environment type DeployStepImplementation struct { - stepType pipeline.StepType - namespaceWatcher kube.NamespaceWatcher + stepType pipeline.StepType + namespaceWatcher kube.NamespaceWatcher + radixDeploymentWatcher pipelineRunnerInternal.RadixDeploymentWatcher model.DefaultStepImplementation } // NewDeployStep Constructor -func NewDeployStep(namespaceWatcher kube.NamespaceWatcher) model.Step { +func NewDeployStep(namespaceWatcher kube.NamespaceWatcher, radixDeploymentWatcher pipelineRunnerInternal.RadixDeploymentWatcher) model.Step { return &DeployStepImplementation{ - stepType: pipeline.DeployStep, - namespaceWatcher: namespaceWatcher, + stepType: pipeline.DeployStep, + namespaceWatcher: namespaceWatcher, + radixDeploymentWatcher: radixDeploymentWatcher, } } @@ -69,7 +72,7 @@ func (cli *DeployStepImplementation) deploy(pipelineInfo *model.PipelineInfo) er return nil } -func (cli *DeployStepImplementation) deployToEnv(appName, env string, pipelineInfo *model.PipelineInfo) error { +func (cli *DeployStepImplementation) deployToEnv(appName, envName string, pipelineInfo *model.PipelineInfo) error { defaultEnvVars, err := getDefaultEnvVars(pipelineInfo) if err != nil { return fmt.Errorf("failed to retrieve default env vars for RadixDeployment in app %s. %v", appName, err) @@ -89,7 +92,7 @@ func (cli *DeployStepImplementation) deployToEnv(appName, env string, pipelineIn return err } - currentRd, err := internal.GetCurrentRadixDeployment(cli.GetKubeutil(), utils.GetEnvironmentNamespace(appName, env)) + currentRd, err := internal.GetCurrentRadixDeployment(cli.GetKubeutil(), utils.GetEnvironmentNamespace(appName, envName)) if err != nil { return err } @@ -99,8 +102,8 @@ func (cli *DeployStepImplementation) deployToEnv(appName, env string, pipelineIn pipelineInfo.PipelineArguments.JobName, pipelineInfo.PipelineArguments.ImageTag, pipelineInfo.PipelineArguments.Branch, - pipelineInfo.DeployEnvironmentComponentImages[env], - env, + pipelineInfo.DeployEnvironmentComponentImages[envName], + envName, defaultEnvVars, radixApplicationHash, buildSecretHash, @@ -108,18 +111,26 @@ func (cli *DeployStepImplementation) deployToEnv(appName, env string, pipelineIn pipelineInfo.PipelineArguments.ComponentsToDeploy) if err != nil { - return fmt.Errorf("failed to create radix deployments objects for app %s. %v", appName, err) + return fmt.Errorf("failed to create Radix deployment in environment %s. %w", envName, err) } - err = cli.namespaceWatcher.WaitFor(utils.GetEnvironmentNamespace(cli.GetAppName(), env)) - if err != nil { - return fmt.Errorf("failed to get environment namespace, %s, for app %s. %v", env, appName, err) + namespace := utils.GetEnvironmentNamespace(cli.GetAppName(), envName) + if err = cli.namespaceWatcher.WaitFor(namespace); err != nil { + return fmt.Errorf("failed to get environment namespace %s, for app %s. %w", namespace, appName, err) } - log.Info().Msgf("Apply radix deployment %s on env %s", radixDeployment.GetName(), radixDeployment.GetNamespace()) - _, err = cli.GetRadixclient().RadixV1().RadixDeployments(radixDeployment.GetNamespace()).Create(context.TODO(), radixDeployment, metav1.CreateOptions{}) - if err != nil { - return fmt.Errorf("failed to apply radix deployment for app %s to environment %s. %v", appName, env, err) + radixDeploymentName := radixDeployment.GetName() + log.Info().Msgf("Apply Radix deployment %s to environment %s", radixDeploymentName, envName) + if _, err = cli.GetRadixclient().RadixV1().RadixDeployments(radixDeployment.GetNamespace()).Create(context.Background(), radixDeployment, metav1.CreateOptions{}); err != nil { + return fmt.Errorf("failed to apply Radix deployment for app %s to environment %s. %w", appName, envName, err) + } + + if err := cli.radixDeploymentWatcher.WaitForActive(namespace, radixDeploymentName); err != nil { + log.Error().Err(err).Msgf("Failed to activate Radix deployment %s in environment %s. Deleting deployment", radixDeploymentName, envName) + if err := cli.GetRadixclient().RadixV1().RadixDeployments(radixDeployment.GetNamespace()).Delete(context.Background(), radixDeploymentName, metav1.DeleteOptions{}); err != nil { + log.Error().Err(err).Msgf("Failed to delete Radix deployment") + } + return err } return nil } diff --git a/pipeline-runner/steps/deploy_test.go b/pipeline-runner/steps/deploy_test.go index 97f40462d..a41e6d9e5 100644 --- a/pipeline-runner/steps/deploy_test.go +++ b/pipeline-runner/steps/deploy_test.go @@ -2,13 +2,17 @@ package steps_test import ( "context" + "errors" "fmt" + "strings" "testing" + "github.com/equinor/radix-operator/pipeline-runner/internal/watcher" "github.com/equinor/radix-operator/pkg/apis/config/dnsalias" "github.com/equinor/radix-operator/pkg/apis/defaults" commonTest "github.com/equinor/radix-operator/pkg/apis/test" radix "github.com/equinor/radix-operator/pkg/client/clientset/versioned/fake" + "github.com/golang/mock/gomock" "github.com/stretchr/testify/require" secretproviderfake "sigs.k8s.io/secrets-store-csi-driver/pkg/client/clientset/versioned/fake" @@ -45,12 +49,21 @@ func setupTest(t *testing.T) (*kubernetes.Clientset, *kube.Kube, *radix.Clientse return kubeclient, kubeUtil, radixclient, testUtils } -// FakeNamespaceWatcher Unit tests doesn't handle muliti-threading well +// FakeNamespaceWatcher Unit tests doesn't handle multi-threading well type FakeNamespaceWatcher struct { } +// FakeRadixDeploymentWatcher Unit tests doesn't handle multi-threading well +type FakeRadixDeploymentWatcher struct { +} + // WaitFor Waits for namespace to appear -func (watcher FakeNamespaceWatcher) WaitFor(namespace string) error { +func (watcher FakeNamespaceWatcher) WaitFor(_ string) error { + return nil +} + +// WaitFor Waits for radix deployment gets active +func (watcher FakeRadixDeploymentWatcher) WaitForActive(_, _ string) error { return nil } @@ -74,7 +87,7 @@ func TestDeploy_BranchIsNotMapped_ShouldSkip(t *testing.T) { WithName(anyComponentName)). BuildRA() - cli := steps.NewDeployStep(FakeNamespaceWatcher{}) + cli := steps.NewDeployStep(FakeNamespaceWatcher{}, FakeRadixDeploymentWatcher{}) cli.Init(kubeclient, radixclient, kubeUtil, &monitoring.Clientset{}, rr) targetEnvs := application.GetTargetEnvironments(anyNoMappedBranch, ra) @@ -177,8 +190,8 @@ func TestDeploy_PromotionSetup_ShouldCreateNamespacesForAllBranchesIfNotExists(t WithEnvironmentVariable("DB_PORT", "9876"))). BuildRA() - // Prometheus doesn´t contain any fake - cli := steps.NewDeployStep(FakeNamespaceWatcher{}) + // Prometheus don´t contain any fake + cli := steps.NewDeployStep(FakeNamespaceWatcher{}, FakeRadixDeploymentWatcher{}) cli.Init(kubeclient, radixclient, kubeUtil, &monitoring.Clientset{}, rr) dnsConfig := dnsalias.DNSConfig{ @@ -297,8 +310,8 @@ func TestDeploy_SetCommitID_whenSet(t *testing.T) { WithComponents(utils.AnApplicationComponent().WithName("app")). BuildRA() - // Prometheus doesn´t contain any fake - cli := steps.NewDeployStep(FakeNamespaceWatcher{}) + // Prometheus don´t contain any fake + cli := steps.NewDeployStep(FakeNamespaceWatcher{}, FakeRadixDeploymentWatcher{}) cli.Init(kubeclient, radixclient, kubeUtil, &monitoring.Clientset{}, rr) applicationConfig := application.NewApplicationConfig(kubeclient, kubeUtil, radixclient, rr, ra, nil) @@ -331,3 +344,74 @@ func TestDeploy_SetCommitID_whenSet(t *testing.T) { rd := rds.Items[0] assert.Equal(t, commitID, rd.ObjectMeta.Labels[kube.RadixCommitLabel]) } + +func TestDeploy_WaitActiveDeployment(t *testing.T) { + rr := utils.ARadixRegistration(). + WithName(anyAppName). + BuildRR() + + envName := "dev" + ra := utils.NewRadixApplicationBuilder(). + WithAppName(anyAppName). + WithEnvironment(envName, "master"). + WithComponents(utils.AnApplicationComponent().WithName("app")). + BuildRA() + + type scenario struct { + name string + watcherError error + expectedRadixDeployments int + } + scenarios := []scenario{ + {name: "No fail", watcherError: nil, expectedRadixDeployments: 1}, + {name: "Watch fails", watcherError: errors.New("some error"), expectedRadixDeployments: 0}, + } + for _, ts := range scenarios { + t.Run(ts.name, func(tt *testing.T) { + kubeclient, kubeUtil, radixClient, _ := setupTest(tt) + ctrl := gomock.NewController(tt) + radixDeploymentWatcher := watcher.NewMockRadixDeploymentWatcher(ctrl) + cli := steps.NewDeployStep(FakeNamespaceWatcher{}, radixDeploymentWatcher) + cli.Init(kubeclient, radixClient, kubeUtil, &monitoring.Clientset{}, rr) + + applicationConfig := application.NewApplicationConfig(kubeclient, kubeUtil, radixClient, rr, ra, nil) + + pipelineInfo := &model.PipelineInfo{ + PipelineArguments: model.PipelineArguments{ + JobName: anyJobName, + ImageTag: anyImageTag, + Branch: "master", + }, + TargetEnvironments: []string{envName}, + } + + pipelineInfo.SetApplicationConfig(applicationConfig) + namespace := utils.GetEnvironmentNamespace(anyAppName, envName) + radixDeploymentWatcher.EXPECT(). + WaitForActive(namespace, radixDeploymentNameMatcher{envName: envName, imageTag: anyImageTag}). + Return(ts.watcherError) + err := cli.Run(pipelineInfo) + if ts.watcherError == nil { + assert.NoError(tt, err) + } else { + assert.EqualError(tt, err, ts.watcherError.Error()) + } + rdList, err := radixClient.RadixV1().RadixDeployments(namespace).List(context.Background(), metav1.ListOptions{}) + assert.NoError(tt, err) + assert.Len(tt, rdList.Items, ts.expectedRadixDeployments, "Invalid expected RadixDeployment-s count") + }) + } +} + +type radixDeploymentNameMatcher struct { + envName string + imageTag string +} + +func (m radixDeploymentNameMatcher) Matches(name interface{}) bool { + rdName, ok := name.(string) + return ok && strings.HasPrefix(rdName, m.String()) +} +func (m radixDeploymentNameMatcher) String() string { + return fmt.Sprintf("%s-%s", m.envName, m.imageTag) +} diff --git a/pkg/apis/kube/namespaces.go b/pkg/apis/kube/namespaces.go index f63a19967..24c2136fb 100644 --- a/pkg/apis/kube/namespaces.go +++ b/pkg/apis/kube/namespaces.go @@ -114,7 +114,7 @@ func (watcher NamespaceWatcherImpl) WaitFor(namespace string) error { return err } - log.Info().Msgf("namespace %s exists and is active", namespace) + log.Info().Msgf("Namespace %s exists and is active", namespace) return nil }