From c953b382d5f6c02d718c7411056fcb15912f20fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nils=20Gustav=20Str=C3=A5b=C3=B8?= <65334626+nilsgstrabo@users.noreply.github.com> Date: Fri, 23 Feb 2024 10:25:32 +0100 Subject: [PATCH] 1120 read only file system (#1049) * add property UseReadOnlyFileSystem to RadixDeployment * add UseReadOnlyFileSystem to radixjobcomponent test * update test cases and add type RadixEmptyDirVolumeMount * started refactor of volume mount validation * refactor RA validation * adding volume mount unit tests * RA validatation tests for volumeMount * added test for deployment emptyDir * add UseReadOnlyFileSystem to container security context * move csi provisioner config * rename from UseReadOnlyFileSystem to ReadOnlyFileSystem * update chart version --------- Co-authored-by: Anneli --- charts/radix-operator/Chart.yaml | 4 +- .../templates/radixapplication.yaml | 58 + json-schema/radixapplication.json | 82 ++ pkg/apis/batch/kubejob.go | 3 +- pkg/apis/batch/syncer_test.go | 68 +- pkg/apis/deployment/deployment_test.go | 35 + pkg/apis/deployment/kubedeployment.go | 3 +- pkg/apis/deployment/radixcomponent.go | 9 + pkg/apis/deployment/radixcomponent_test.go | 47 + pkg/apis/deployment/radixjobcomponent.go | 1 + pkg/apis/deployment/radixjobcomponent_test.go | 47 + pkg/apis/deployment/volumemount.go | 233 ++-- pkg/apis/deployment/volumemount_test.go | 33 +- pkg/apis/radix/v1/radixapptypes.go | 115 +- pkg/apis/radix/v1/radixdeploytypes.go | 11 + .../radix/v1/radixenvironmentconfigtypes.go | 51 +- pkg/apis/radix/v1/zz_generated.deepcopy.go | 52 + pkg/apis/radixvalidators/errors.go | 61 +- pkg/apis/radixvalidators/validate_ra.go | 152 ++- pkg/apis/radixvalidators/validate_ra_test.go | 1134 +++++++++-------- pkg/apis/securitycontext/container.go | 6 +- .../utils/applicationcomponent_builder.go | 8 + .../utils/applicationjobcomponent_builder.go | 86 +- .../utils/componentenvironment_builder.go | 7 + pkg/apis/utils/deploymentcomponent_builder.go | 24 +- .../utils/jobcomponentenvironment_builder.go | 62 +- 26 files changed, 1554 insertions(+), 838 deletions(-) diff --git a/charts/radix-operator/Chart.yaml b/charts/radix-operator/Chart.yaml index 041d6322c..10b8774de 100644 --- a/charts/radix-operator/Chart.yaml +++ b/charts/radix-operator/Chart.yaml @@ -1,7 +1,7 @@ apiVersion: v2 name: radix-operator -version: 1.29.6 -appVersion: 1.49.6 +version: 1.30.0 +appVersion: 1.50.0 kubeVersion: ">=1.24.0" description: Radix Operator keywords: diff --git a/charts/radix-operator/templates/radixapplication.yaml b/charts/radix-operator/templates/radixapplication.yaml index 7c01bb89c..42a9a679b 100644 --- a/charts/radix-operator/templates/radixapplication.yaml +++ b/charts/radix-operator/templates/radixapplication.yaml @@ -463,6 +463,9 @@ spec: description: Defines minimum number of required GPUs. type: string type: object + readOnlyFileSystem: + description: Controls if the filesystem shall be read-only. + type: boolean replicas: description: 'Number of desired replicas. More info: https://www.radix.equinor.com/references/reference-radix-config/#replicas' minimum: 0 @@ -726,6 +729,12 @@ spec: skuName: description: 'SKU Type of Azure storage. More info: https://learn.microsoft.com/en-us/rest/api/storagerp/srp_sku_types' + enum: + - Standard_LRS + - Premium_LRS + - Standard_GRS + - Standard_RAGRS + - "" type: string streaming: description: 'Configure Streaming mode. Used @@ -781,11 +790,27 @@ spec: Default false. This must be turned on when HNS enabled account is mounted. type: boolean + required: + - container type: object container: description: 'Deprecated. Only required by the deprecated type: blob.' type: string + emptyDir: + description: EmptyDir settings for EmptyDir volume + properties: + sizeLimit: + anyOf: + - type: integer + - type: string + description: SizeLimit defines the size of the + emptyDir volume + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + required: + - sizeLimit + type: object gid: description: GID defines the group ID (number) which will be set as owner of the mounted volume. Deprecated, @@ -794,6 +819,7 @@ spec: name: description: User-defined name of the volume mount. Must be unique for the component. + maxLength: 40 minLength: 1 type: string path: @@ -935,6 +961,9 @@ spec: maxLength: 15 pattern: ^(([a-z0-9][-a-z0-9]*)?[a-z0-9])?$ type: string + readOnlyFileSystem: + description: Controls if the filesystem shall be read-only. + type: boolean resources: description: 'Configures CPU and memory resources for the component. More info: https://www.radix.equinor.com/references/reference-radix-config/#resources-common' @@ -1353,6 +1382,9 @@ spec: minLength: 1 type: string type: object + readOnlyFileSystem: + description: Controls if the filesystem shall be read-only. + type: boolean resources: description: 'Environment specific configuration for CPU and memory resources. More info: https://www.radix.equinor.com/references/reference-radix-config/#resources-3' @@ -1616,6 +1648,12 @@ spec: skuName: description: 'SKU Type of Azure storage. More info: https://learn.microsoft.com/en-us/rest/api/storagerp/srp_sku_types' + enum: + - Standard_LRS + - Premium_LRS + - Standard_GRS + - Standard_RAGRS + - "" type: string streaming: description: 'Configure Streaming mode. Used @@ -1671,11 +1709,27 @@ spec: Default false. This must be turned on when HNS enabled account is mounted. type: boolean + required: + - container type: object container: description: 'Deprecated. Only required by the deprecated type: blob.' type: string + emptyDir: + description: EmptyDir settings for EmptyDir volume + properties: + sizeLimit: + anyOf: + - type: integer + - type: string + description: SizeLimit defines the size of the + emptyDir volume + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + required: + - sizeLimit + type: object gid: description: GID defines the group ID (number) which will be set as owner of the mounted volume. Deprecated, @@ -1684,6 +1738,7 @@ spec: name: description: User-defined name of the volume mount. Must be unique for the component. + maxLength: 40 minLength: 1 type: string path: @@ -1833,6 +1888,9 @@ spec: x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map + readOnlyFileSystem: + description: Controls if the filesystem shall be read-only. + type: boolean resources: description: 'Configures CPU and memory resources for the job. More info: https://www.radix.equinor.com/references/reference-radix-config/#resources-common-2' diff --git a/json-schema/radixapplication.json b/json-schema/radixapplication.json index dffebf6a1..7295ab113 100644 --- a/json-schema/radixapplication.json +++ b/json-schema/radixapplication.json @@ -462,6 +462,10 @@ }, "type": "object" }, + "readOnlyFileSystem": { + "description": "Controls if the filesystem shall be read-only.", + "type": "boolean" + }, "replicas": { "description": "Number of desired replicas. More info: https://www.radix.equinor.com/references/reference-radix-config/#replicas", "minimum": 0, @@ -708,6 +712,13 @@ }, "skuName": { "description": "SKU Type of Azure storage. More info: https://learn.microsoft.com/en-us/rest/api/storagerp/srp_sku_types", + "enum": [ + "Standard_LRS", + "Premium_LRS", + "Standard_GRS", + "Standard_RAGRS", + "" + ], "type": "string" }, "streaming": { @@ -759,18 +770,44 @@ "type": "boolean" } }, + "required": [ + "container" + ], "type": "object" }, "container": { "description": "Deprecated. Only required by the deprecated type: blob.", "type": "string" }, + "emptyDir": { + "description": "EmptyDir settings for EmptyDir volume", + "properties": { + "sizeLimit": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + } + ], + "description": "SizeLimit defines the size of the emptyDir volume", + "pattern": "^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$", + "x-kubernetes-int-or-string": true + } + }, + "required": [ + "sizeLimit" + ], + "type": "object" + }, "gid": { "description": "GID defines the group ID (number) which will be set as owner of the mounted volume. Deprecated, use BlobFuse2 or AzureFile instead.", "type": "string" }, "name": { "description": "User-defined name of the volume mount. Must be unique for the component.", + "maxLength": 40, "minLength": 1, "type": "string" }, @@ -935,6 +972,10 @@ "pattern": "^(([a-z0-9][-a-z0-9]*)?[a-z0-9])?$", "type": "string" }, + "readOnlyFileSystem": { + "description": "Controls if the filesystem shall be read-only.", + "type": "boolean" + }, "resources": { "description": "Configures CPU and memory resources for the component. More info: https://www.radix.equinor.com/references/reference-radix-config/#resources-common", "properties": { @@ -1392,6 +1433,10 @@ }, "type": "object" }, + "readOnlyFileSystem": { + "description": "Controls if the filesystem shall be read-only.", + "type": "boolean" + }, "resources": { "description": "Environment specific configuration for CPU and memory resources. More info: https://www.radix.equinor.com/references/reference-radix-config/#resources-3", "properties": { @@ -1638,6 +1683,13 @@ }, "skuName": { "description": "SKU Type of Azure storage. More info: https://learn.microsoft.com/en-us/rest/api/storagerp/srp_sku_types", + "enum": [ + "Standard_LRS", + "Premium_LRS", + "Standard_GRS", + "Standard_RAGRS", + "" + ], "type": "string" }, "streaming": { @@ -1689,18 +1741,44 @@ "type": "boolean" } }, + "required": [ + "container" + ], "type": "object" }, "container": { "description": "Deprecated. Only required by the deprecated type: blob.", "type": "string" }, + "emptyDir": { + "description": "EmptyDir settings for EmptyDir volume", + "properties": { + "sizeLimit": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + } + ], + "description": "SizeLimit defines the size of the emptyDir volume", + "pattern": "^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$", + "x-kubernetes-int-or-string": true + } + }, + "required": [ + "sizeLimit" + ], + "type": "object" + }, "gid": { "description": "GID defines the group ID (number) which will be set as owner of the mounted volume. Deprecated, use BlobFuse2 or AzureFile instead.", "type": "string" }, "name": { "description": "User-defined name of the volume mount. Must be unique for the component.", + "maxLength": 40, "minLength": 1, "type": "string" }, @@ -1874,6 +1952,10 @@ ], "x-kubernetes-list-type": "map" }, + "readOnlyFileSystem": { + "description": "Controls if the filesystem shall be read-only.", + "type": "boolean" + }, "resources": { "description": "Configures CPU and memory resources for the job. More info: https://www.radix.equinor.com/references/reference-radix-config/#resources-common-2", "properties": { diff --git a/pkg/apis/batch/kubejob.go b/pkg/apis/batch/kubejob.go index a918c189f..daf584493 100644 --- a/pkg/apis/batch/kubejob.go +++ b/pkg/apis/batch/kubejob.go @@ -230,6 +230,7 @@ func (s *syncer) getContainers(rd *radixv1.RadixDeployment, jobComponent *radixv resources := s.getContainerResources(batchJob, jobComponent) image := getJobImage(jobComponent, batchJob) + securityContext := securitycontext.Container(securitycontext.WithContainerSeccompProfileType(corev1.SeccompProfileTypeRuntimeDefault), securitycontext.WithReadOnlyRootFileSystem(jobComponent.GetReadOnlyFileSystem())) container := corev1.Container{ Name: jobComponent.Name, Image: image, @@ -237,7 +238,7 @@ func (s *syncer) getContainers(rd *radixv1.RadixDeployment, jobComponent *radixv Env: environmentVariables, Ports: ports, VolumeMounts: volumeMounts, - SecurityContext: securitycontext.Container(securitycontext.WithContainerSeccompProfileType(corev1.SeccompProfileTypeRuntimeDefault)), + SecurityContext: securityContext, Resources: resources, } diff --git a/pkg/apis/batch/syncer_test.go b/pkg/apis/batch/syncer_test.go index 35d2a744b..a62a4b97d 100644 --- a/pkg/apis/batch/syncer_test.go +++ b/pkg/apis/batch/syncer_test.go @@ -85,6 +85,17 @@ func (s *syncerTestSuite) SetupTest() { s.T().Setenv(defaults.OperatorDefaultUserGroupEnvironmentVariable, "any-group") } +func (s *syncerTestSuite) SetupSubTest() { + s.kubeClient = fake.NewSimpleClientset() + s.radixClient = fakeradix.NewSimpleClientset() + s.promClient = prometheusfake.NewSimpleClientset() + s.kubeUtil, _ = kube.New(s.kubeClient, s.radixClient, secretproviderfake.NewSimpleClientset()) + s.T().Setenv(defaults.OperatorEnvLimitDefaultMemoryEnvironmentVariable, "1500Mi") + s.T().Setenv(defaults.OperatorRollingUpdateMaxUnavailable, "25%") + s.T().Setenv(defaults.OperatorRollingUpdateMaxSurge, "25%") + s.T().Setenv(defaults.OperatorDefaultUserGroupEnvironmentVariable, "any-group") +} + func (s *syncerTestSuite) Test_RestoreStatus() { created, started, ended := metav1.NewTime(time.Date(2020, 1, 1, 0, 0, 0, 0, time.Local)), metav1.NewTime(time.Date(2020, 1, 2, 0, 0, 0, 0, time.Local)), metav1.NewTime(time.Date(2020, 1, 3, 0, 0, 0, 0, time.Local)) expectedStatus := radixv1.RadixBatchStatus{ @@ -800,6 +811,59 @@ func (s *syncerTestSuite) Test_JobWithPayload() { s.Equal(corev1.VolumeMount{Name: jobPayloadVolumeName, ReadOnly: true, MountPath: payloadPath}, jobs.Items[0].Spec.Template.Spec.Containers[0].VolumeMounts[0]) } +func (s *syncerTestSuite) Test_ReadOnlyFileSystem() { + appName, batchName, namespace, rdName := "any-app", "any-job", "any-ns", "any-rd" + type scenarioSpec struct { + readOnlyFileSystem *bool + expectedReadOnlyFileSystem *bool + } + tests := map[string]scenarioSpec{ + "notSet": {readOnlyFileSystem: nil, expectedReadOnlyFileSystem: nil}, + "false": {readOnlyFileSystem: pointers.Ptr(false), expectedReadOnlyFileSystem: pointers.Ptr(false)}, + "true": {readOnlyFileSystem: pointers.Ptr(true), expectedReadOnlyFileSystem: pointers.Ptr(true)}, + } + for name, test := range tests { + s.Run(name, func() { + batch := &radixv1.RadixBatch{ + ObjectMeta: metav1.ObjectMeta{Name: batchName}, + Spec: radixv1.RadixBatchSpec{ + RadixDeploymentJobRef: radixv1.RadixDeploymentJobComponentSelector{ + LocalObjectReference: radixv1.LocalObjectReference{Name: rdName}, + Job: "anyjob", + }, + Jobs: []radixv1.RadixBatchJob{ + {Name: "any"}, + }, + }, + } + + rd := &radixv1.RadixDeployment{ + ObjectMeta: metav1.ObjectMeta{Name: rdName}, + Spec: radixv1.RadixDeploymentSpec{ + AppName: appName, + Jobs: []radixv1.RadixDeployJobComponent{ + { + Name: "anyjob", + ReadOnlyFileSystem: test.readOnlyFileSystem, + }, + }, + }, + } + batch, err := s.radixClient.RadixV1().RadixBatches(namespace).Create(context.Background(), batch, metav1.CreateOptions{}) + s.Require().NoError(err) + _, err = s.radixClient.RadixV1().RadixDeployments(namespace).Create(context.Background(), rd, metav1.CreateOptions{}) + s.Require().NoError(err) + + sut := s.createSyncer(batch) + s.Require().NoError(sut.OnSync()) + jobs, _ := s.kubeClient.BatchV1().Jobs(namespace).List(context.Background(), metav1.ListOptions{}) + s.Require().Len(jobs.Items, 1) + job1 := jobs.Items[0] + s.Equal(test.expectedReadOnlyFileSystem, job1.Spec.Template.Spec.Containers[0].SecurityContext.ReadOnlyRootFilesystem) + }) + } +} + func (s *syncerTestSuite) Test_JobWithResources() { appName, batchName, componentName, namespace, rdName := "any-app", "any-job", "compute", "any-ns", "any-rd" job1Name, job2Name := "job1", "job2" @@ -1194,8 +1258,6 @@ func (s *syncerTestSuite) Test_HandleJobStopWhenMissingRadixDeploymentConfig() { }, }, } - batch, err := s.radixClient.RadixV1().RadixBatches(namespace).Create(context.Background(), batch, metav1.CreateOptions{}) - s.Require().NoError(err) type expectedJobStatusSpec struct { name string @@ -1257,6 +1319,8 @@ func (s *syncerTestSuite) Test_HandleJobStopWhenMissingRadixDeploymentConfig() { for _, scenario := range scenarios { scenario := scenario s.Run(scenario.testName, func() { + batch, err := s.radixClient.RadixV1().RadixBatches(namespace).Create(context.Background(), batch, metav1.CreateOptions{}) + s.Require().NoError(err) for jobName, stop := range scenario.stopStatus { i := slice.FindIndex(batch.Spec.Jobs, func(j radixv1.RadixBatchJob) bool { return j.Name == jobName }) batch.Spec.Jobs[i].Stop = pointers.Ptr(stop) diff --git a/pkg/apis/deployment/deployment_test.go b/pkg/apis/deployment/deployment_test.go index f249913fd..56589cf30 100644 --- a/pkg/apis/deployment/deployment_test.go +++ b/pkg/apis/deployment/deployment_test.go @@ -762,6 +762,41 @@ func TestObjectSynced_MultiComponent_NonActiveCluster_ContainsOnlyClusterSpecifi assert.Equal(t, "radixquote", quoteIngress.Labels[kube.RadixComponentLabel], "Ingress should have the corresponding component") } +func TestObjectSynced_ReadOnlyFileSystem(t *testing.T) { + type scenarioSpec struct { + readOnlyFileSystem *bool + expectedReadOnlyFileSystem *bool + } + + tests := map[string]scenarioSpec{ + "notSet": {readOnlyFileSystem: nil, expectedReadOnlyFileSystem: nil}, + "false": {readOnlyFileSystem: utils.BoolPtr(false), expectedReadOnlyFileSystem: utils.BoolPtr(false)}, + "true": {readOnlyFileSystem: utils.BoolPtr(true), expectedReadOnlyFileSystem: utils.BoolPtr(true)}, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + tu, client, kubeUtil, radixclient, prometheusclient, _ := setupTest(t) + defer teardownTest() + envNamespace := utils.GetEnvironmentNamespace("anyapp", "test") + _, err := applyDeploymentWithSync(tu, client, kubeUtil, radixclient, prometheusclient, utils.ARadixDeployment(). + WithAppName("any-app"). + WithEnvironment("any-env"). + WithComponents( + utils.NewDeployComponentBuilder(). + WithName("app"). + WithReadOnlyFileSystem(test.readOnlyFileSystem))) + + assert.NoError(t, err) + deployments, _ := client.AppsV1().Deployments(envNamespace).List(context.TODO(), metav1.ListOptions{}) + for _, deployment := range deployments.Items { + assert.Equal(t, test.expectedReadOnlyFileSystem, deployment.Spec.Template.Spec.Containers[0].SecurityContext.ReadOnlyRootFilesystem) + } + }) + } + +} + func TestObjectSynced_MultiComponent_ActiveCluster_ContainsAllAliasesAndSupportingObjects(t *testing.T) { tu, client, kubeUtil, radixclient, prometheusclient, _ := setupTest(t) defer teardownTest() diff --git a/pkg/apis/deployment/kubedeployment.go b/pkg/apis/deployment/kubedeployment.go index e7b2a0947..6ecca34bc 100644 --- a/pkg/apis/deployment/kubedeployment.go +++ b/pkg/apis/deployment/kubedeployment.go @@ -275,10 +275,11 @@ func (deploy *Deployment) setDesiredDeploymentProperties(deployComponent v1.Radi } desiredDeployment.Spec.Template.Spec.Volumes = volumes + containerSecurityCtx := securitycontext.Container(securitycontext.WithContainerSeccompProfileType(corev1.SeccompProfileTypeRuntimeDefault), securitycontext.WithReadOnlyRootFileSystem(deployComponent.GetReadOnlyFileSystem())) desiredDeployment.Spec.Template.Spec.Containers[0].Image = deployComponent.GetImage() desiredDeployment.Spec.Template.Spec.Containers[0].Ports = getContainerPorts(deployComponent) desiredDeployment.Spec.Template.Spec.Containers[0].ImagePullPolicy = corev1.PullAlways - desiredDeployment.Spec.Template.Spec.Containers[0].SecurityContext = securitycontext.Container(securitycontext.WithContainerSeccompProfileType(corev1.SeccompProfileTypeRuntimeDefault)) + desiredDeployment.Spec.Template.Spec.Containers[0].SecurityContext = containerSecurityCtx desiredDeployment.Spec.Template.Spec.Containers[0].Resources = utils.GetResourceRequirements(deployComponent) volumeMounts, err := GetRadixDeployComponentVolumeMounts(deployComponent, deploy.radixDeployment.GetName()) diff --git a/pkg/apis/deployment/radixcomponent.go b/pkg/apis/deployment/radixcomponent.go index f9ab77d68..951f124c2 100644 --- a/pkg/apis/deployment/radixcomponent.go +++ b/pkg/apis/deployment/radixcomponent.go @@ -2,6 +2,7 @@ package deployment import ( "dario.cat/mergo" + commonutils "github.com/equinor/radix-common/utils" mergoutils "github.com/equinor/radix-common/utils/mergo" "github.com/equinor/radix-common/utils/slice" "github.com/equinor/radix-operator/pkg/apis/pipeline" @@ -70,6 +71,7 @@ func GetRadixComponentsForEnv(radixApplication *radixv1.RadixApplication, env st deployComponent.PublicPort = getRadixComponentPort(&radixComponent) deployComponent.Authentication = auth deployComponent.Identity = identity + deployComponent.ReadOnlyFileSystem = getRadixCommonComponentReadOnlyFileSystem(&radixComponent, environmentSpecificConfig) deployComponents = append(deployComponents, deployComponent) } @@ -77,6 +79,13 @@ func GetRadixComponentsForEnv(radixApplication *radixv1.RadixApplication, env st return deployComponents, nil } +func getRadixCommonComponentReadOnlyFileSystem(radixComponent radixv1.RadixCommonComponent, environmentSpecificConfig radixv1.RadixCommonEnvironmentConfig) *bool { + if !commonutils.IsNil(environmentSpecificConfig) && environmentSpecificConfig.GetReadOnlyFileSystem() != nil { + return environmentSpecificConfig.GetReadOnlyFileSystem() + } + return radixComponent.GetReadOnlyFileSystem() +} + func getRadixComponentAlwaysPullImageOnDeployFlag(radixComponent *radixv1.RadixComponent, environmentSpecificConfig *radixv1.RadixEnvironmentConfig) bool { if environmentSpecificConfig != nil { return coalesceBool(environmentSpecificConfig.AlwaysPullImageOnDeploy, radixComponent.AlwaysPullImageOnDeploy, false) diff --git a/pkg/apis/deployment/radixcomponent_test.go b/pkg/apis/deployment/radixcomponent_test.go index ff76e857e..d4f1be006 100644 --- a/pkg/apis/deployment/radixcomponent_test.go +++ b/pkg/apis/deployment/radixcomponent_test.go @@ -251,6 +251,53 @@ func TestGetRadixComponentsForEnv_PublicPort_OldPublic(t *testing.T) { assert.Equal(t, false, deployComponent[0].Public) } +func TestGetRadixComponentsForEnv_ReadOnlyFileSystem(t *testing.T) { + componentImages := make(pipeline.DeployComponentImages) + componentImages["app"] = pipeline.DeployComponentImage{ImagePath: anyImagePath} + envVarsMap := make(radixv1.EnvVarsMap) + envVarsMap[defaults.RadixCommitHashEnvironmentVariable] = "anycommit" + envVarsMap[defaults.RadixGitTagsEnvironmentVariable] = "anytag" + + // Test cases with different values for ReadOnlyFileSystem + testCases := []struct { + description string + readOnlyFileSystem *bool + readOnlyFileSystemEnv *bool + + expectedReadOnlyFilesystem *bool + }{ + {"No configuration set", nil, nil, nil}, + {"Env controls when readOnlyFileSystem is nil, set to true", nil, utils.BoolPtr(true), utils.BoolPtr(true)}, + {"Env controls when readOnlyFileSystem is nil, set to false", nil, utils.BoolPtr(false), utils.BoolPtr(false)}, + {"readOnlyFileSystem set to true, no env config", utils.BoolPtr(true), nil, utils.BoolPtr(true)}, + {"Both readOnlyFileSystem and readOnlyFileSystemEnv set to true", utils.BoolPtr(true), utils.BoolPtr(true), utils.BoolPtr(true)}, + {"Env overrides to false when both is set", utils.BoolPtr(true), utils.BoolPtr(false), utils.BoolPtr(false)}, + {"readOnlyFileSystem set to false, no env config", utils.BoolPtr(false), nil, utils.BoolPtr(false)}, + {"Env overrides to true when both is set", utils.BoolPtr(false), utils.BoolPtr(true), utils.BoolPtr(true)}, + {"Both readOnlyFileSystem and readOnlyFileSystemEnv set to false", utils.BoolPtr(false), utils.BoolPtr(false), utils.BoolPtr(false)}, + } + + for _, testCase := range testCases { + t.Run(testCase.description, func(t *testing.T) { + ra := utils.ARadixApplication(). + WithComponents( + utils.NewApplicationComponentBuilder(). + WithName(componentName). + WithReadOnlyFileSystem(testCase.readOnlyFileSystem). + WithEnvironmentConfigs( + utils.AnEnvironmentConfig(). + WithEnvironment(env). + WithReadOnlyFileSystem(testCase.readOnlyFileSystemEnv), + utils.AnEnvironmentConfig(). + WithEnvironment("prod").WithReadOnlyFileSystem(utils.BoolPtr(false)), + )).BuildRA() + + deployComponent, _ := GetRadixComponentsForEnv(ra, env, componentImages, envVarsMap, nil) + assert.Equal(t, testCase.expectedReadOnlyFilesystem, deployComponent[0].ReadOnlyFileSystem) + }) + } +} + func TestGetRadixComponentsForEnv_ListOfExternalAliasesForComponent_GetListOfAliases(t *testing.T) { componentImages := make(pipeline.DeployComponentImages) componentImages["app"] = pipeline.DeployComponentImage{ImagePath: anyImagePath} diff --git a/pkg/apis/deployment/radixjobcomponent.go b/pkg/apis/deployment/radixjobcomponent.go index 54feebe98..c91cd8a89 100644 --- a/pkg/apis/deployment/radixjobcomponent.go +++ b/pkg/apis/deployment/radixjobcomponent.go @@ -111,6 +111,7 @@ func (c *jobComponentsBuilder) buildJobComponent(radixJobComponent v1.RadixJobCo TimeLimitSeconds: getRadixJobComponentTimeLimitSeconds(radixJobComponent, environmentSpecificConfig), Identity: identity, Notifications: notifications, + ReadOnlyFileSystem: getRadixCommonComponentReadOnlyFileSystem(&radixJobComponent, environmentSpecificConfig), } if environmentSpecificConfig != nil { diff --git a/pkg/apis/deployment/radixjobcomponent_test.go b/pkg/apis/deployment/radixjobcomponent_test.go index 707efecd6..790512fd5 100644 --- a/pkg/apis/deployment/radixjobcomponent_test.go +++ b/pkg/apis/deployment/radixjobcomponent_test.go @@ -611,3 +611,50 @@ func TestGetRadixJobComponentsForEnv_ImageWithImageTagName(t *testing.T) { }) } } + +func TestGetRadixJobComponentsForEnv_ReadOnlyFileSystem(t *testing.T) { + const ( + environment = "dev" + ) + // Test cases with different values for ReadOnlyFileSystem + testCases := []struct { + name string + readOnlyFileSystem *bool + readOnlyFileSystemEnv *bool + + expectedReadOnlyFile *bool + }{ + {"No configuration set", nil, nil, nil}, + {"Env controls when readOnlyFileSystem is nil, set to true", nil, utils.BoolPtr(true), utils.BoolPtr(true)}, + {"Env controls when readOnlyFileSystem is nil, set to false", nil, utils.BoolPtr(false), utils.BoolPtr(false)}, + {"readOnlyFileSystem set to true, no env config", utils.BoolPtr(true), nil, utils.BoolPtr(true)}, + {"Both readOnlyFileSystem and readOnlyFileSystemEnv set to true", utils.BoolPtr(true), utils.BoolPtr(true), utils.BoolPtr(true)}, + {"Env overrides to false when both is set", utils.BoolPtr(true), utils.BoolPtr(false), utils.BoolPtr(false)}, + {"readOnlyFileSystem set to false, no env config", utils.BoolPtr(false), nil, utils.BoolPtr(false)}, + {"Env overrides to true when both is set", utils.BoolPtr(false), utils.BoolPtr(true), utils.BoolPtr(true)}, + {"Both readOnlyFileSystem and readOnlyFileSystemEnv set to false", utils.BoolPtr(false), utils.BoolPtr(false), utils.BoolPtr(false)}, + } + + for _, ts := range testCases { + t.Run(ts.name, func(t *testing.T) { + componentImages := make(pipeline.DeployComponentImages) + var componentBuilders []utils.RadixApplicationJobComponentBuilder + + componentBuilder := utils.NewApplicationJobComponentBuilder(). + WithName("jobComponentName"). + WithReadOnlyFileSystem(ts.readOnlyFileSystem). + WithEnvironmentConfig(utils.NewJobComponentEnvironmentBuilder(). + WithEnvironment(environment). + WithReadOnlyFileSystem(ts.readOnlyFileSystemEnv)) + componentBuilders = append(componentBuilders, componentBuilder) + + ra := utils.ARadixApplication().WithEnvironment(environment, "master").WithJobComponents(componentBuilders...).BuildRA() + + deployJobComponent, err := NewJobComponentsBuilder(ra, environment, componentImages, make(v1.EnvVarsMap), nil).JobComponents() + assert.NoError(t, err) + + assert.Equal(t, ts.expectedReadOnlyFile, deployJobComponent[0].ReadOnlyFileSystem) + + }) + } +} diff --git a/pkg/apis/deployment/volumemount.go b/pkg/apis/deployment/volumemount.go index ece5e6653..08c8d52e1 100644 --- a/pkg/apis/deployment/volumemount.go +++ b/pkg/apis/deployment/volumemount.go @@ -65,43 +65,66 @@ const ( volumeNameMaxLength = 63 ) +// These are valid storage class provisioners +const ( + // provisionerBlobCsiAzure Use of azure/csi driver for blob in Azure storage account + provisionerBlobCsiAzure string = "blob.csi.azure.com" + // provisionerFileCsiAzure Use of azure/csi driver for files in Azure storage account + provisionerFileCsiAzure string = "file.csi.azure.com" +) + +// getStorageClassProvisionerByVolumeMountType convert volume mount type to Storage Class provisioner +func getStorageClassProvisionerByVolumeMountType(radixVolumeMount *radixv1.RadixVolumeMount) (string, bool) { + if radixVolumeMount.BlobFuse2 != nil { + return provisionerBlobCsiAzure, true + } + if radixVolumeMount.AzureFile != nil { + return provisionerFileCsiAzure, true + } + switch radixVolumeMount.Type { + case radixv1.MountTypeBlobFuse2FuseCsiAzure, radixv1.MountTypeBlobFuse2Fuse2CsiAzure, radixv1.MountTypeBlobFuse2NfsCsiAzure: + return provisionerBlobCsiAzure, true + case radixv1.MountTypeAzureFileCsiAzure: + return provisionerFileCsiAzure, true + } + return "", false +} + +// isKnownCsiAzureVolumeMount Supported volume mount type CSI Azure Blob volume +func isKnownCsiAzureVolumeMount(volumeMount string) bool { + switch volumeMount { + case string(radixv1.MountTypeBlobFuse2FuseCsiAzure), string(radixv1.MountTypeBlobFuse2Fuse2CsiAzure), string(radixv1.MountTypeBlobFuse2NfsCsiAzure), string(radixv1.MountTypeAzureFileCsiAzure): + return true + } + return false +} + // GetRadixDeployComponentVolumeMounts Gets list of v1.VolumeMount for radixv1.RadixCommonDeployComponent func GetRadixDeployComponentVolumeMounts(deployComponent radixv1.RadixCommonDeployComponent, radixDeploymentName string) ([]corev1.VolumeMount, error) { componentName := deployComponent.GetName() volumeMounts := make([]corev1.VolumeMount, 0) - externalVolumeMounts, err := getRadixComponentExternalVolumeMounts(deployComponent, componentName) + componentVolumeMounts, err := getRadixComponentVolumeMounts(deployComponent, componentName) if err != nil { return nil, err } - volumeMounts = append(volumeMounts, externalVolumeMounts...) + volumeMounts = append(volumeMounts, componentVolumeMounts...) secretRefsVolumeMounts := getRadixComponentSecretRefsVolumeMounts(deployComponent, componentName, radixDeploymentName) volumeMounts = append(volumeMounts, secretRefsVolumeMounts...) return volumeMounts, nil } -func getRadixComponentExternalVolumeMounts(deployComponent radixv1.RadixCommonDeployComponent, componentName string) ([]corev1.VolumeMount, error) { +func getRadixComponentVolumeMounts(deployComponent radixv1.RadixCommonDeployComponent, componentName string) ([]corev1.VolumeMount, error) { if isDeployComponentJobSchedulerDeployment(deployComponent) { return nil, nil } var volumeMounts []corev1.VolumeMount - for _, radixVolumeMount := range deployComponent.GetVolumeMounts() { - switch GetCsiAzureVolumeMountType(&radixVolumeMount) { - case radixv1.MountTypeBlob: - volumeMounts = append(volumeMounts, corev1.VolumeMount{ - Name: getBlobFuseVolumeMountName(radixVolumeMount, componentName), - MountPath: radixVolumeMount.Path, - }) - case radixv1.MountTypeAzureFileCsiAzure, radixv1.MountTypeBlobFuse2FuseCsiAzure, radixv1.MountTypeBlobFuse2Fuse2CsiAzure, radixv1.MountTypeBlobFuse2NfsCsiAzure: - volumeMountName, err := getCsiAzureVolumeMountName(componentName, &radixVolumeMount) - if err != nil { - return nil, err - } - volumeMounts = append(volumeMounts, corev1.VolumeMount{ - Name: volumeMountName, - MountPath: radixVolumeMount.Path, - }) + for _, volumeMount := range deployComponent.GetVolumeMounts() { + name, err := getVolumeMountVolumeName(&volumeMount, deployComponent.GetName()) + if err != nil { + return nil, err } + volumeMounts = append(volumeMounts, corev1.VolumeMount{Name: name, MountPath: volumeMount.Path}) } return volumeMounts, nil } @@ -136,26 +159,26 @@ func getCsiAzureKeyVaultSecretMountPath(azureKeyVault radixv1.RadixAzureKeyVault return *azureKeyVault.Path } -func getBlobFuseVolumeMountName(volumeMount radixv1.RadixVolumeMount, componentName string) string { +func getBlobFuseVolumeMountName(volumeMount *radixv1.RadixVolumeMount, componentName string) string { return trimVolumeNameToValidLength(fmt.Sprintf(blobFuseVolumeNameTemplate, componentName, volumeMount.Name)) } -func getCsiAzureVolumeMountName(componentName string, radixVolumeMount *radixv1.RadixVolumeMount) (string, error) { - csiVolumeType, err := getCsiRadixVolumeTypeIdForName(radixVolumeMount) +func getCsiAzureVolumeMountName(volumeMount *radixv1.RadixVolumeMount, componentName string) (string, error) { + csiVolumeType, err := getCsiRadixVolumeTypeIdForName(volumeMount) if err != nil { return "", err } - if len(radixVolumeMount.Name) == 0 { + if len(volumeMount.Name) == 0 { return "", fmt.Errorf("name is empty for volume mount in the component %s", componentName) } - csiAzureVolumeStorageName := GetRadixVolumeMountStorage(radixVolumeMount) + csiAzureVolumeStorageName := GetRadixVolumeMountStorage(volumeMount) if len(csiAzureVolumeStorageName) == 0 { - return "", fmt.Errorf("storage is empty for volume mount %s in the component %s", radixVolumeMount.Name, componentName) + return "", fmt.Errorf("storage is empty for volume mount %s in the component %s", volumeMount.Name, componentName) } - if len(radixVolumeMount.Path) == 0 { - return "", fmt.Errorf("path is empty for volume mount %s in the component %s", radixVolumeMount.Name, componentName) + if len(volumeMount.Path) == 0 { + return "", fmt.Errorf("path is empty for volume mount %s in the component %s", volumeMount.Name, componentName) } - return trimVolumeNameToValidLength(fmt.Sprintf(csiVolumeNameTemplate, csiVolumeType, componentName, radixVolumeMount.Name, csiAzureVolumeStorageName)), nil + return trimVolumeNameToValidLength(fmt.Sprintf(csiVolumeNameTemplate, csiVolumeType, componentName, volumeMount.Name, csiAzureVolumeStorageName)), nil } // GetCsiAzureVolumeMountType Gets the CSI Azure volume mount type @@ -208,13 +231,13 @@ func (deploy *Deployment) GetVolumesForComponent(deployComponent radixv1.RadixCo func GetVolumes(kubeclient kubernetes.Interface, kubeutil *kube.Kube, namespace string, environment string, deployComponent radixv1.RadixCommonDeployComponent, radixDeploymentName string) ([]corev1.Volume, error) { var volumes []corev1.Volume - blobVolumes, err := getExternalVolumes(kubeclient, namespace, environment, deployComponent) + volumeMountVolumes, err := getComponentVolumeMountVolumes(kubeclient, namespace, environment, deployComponent) if err != nil { return nil, err } - volumes = append(volumes, blobVolumes...) + volumes = append(volumes, volumeMountVolumes...) - storageRefsVolumes, err := getStorageRefsVolumes(kubeutil, namespace, deployComponent, radixDeploymentName) + storageRefsVolumes, err := getComponentSecretRefsVolumes(kubeutil, namespace, deployComponent, radixDeploymentName) if err != nil { return nil, err } @@ -223,9 +246,9 @@ func GetVolumes(kubeclient kubernetes.Interface, kubeutil *kube.Kube, namespace return volumes, nil } -func getStorageRefsVolumes(kubeutil *kube.Kube, namespace string, deployComponent radixv1.RadixCommonDeployComponent, radixDeploymentName string) ([]corev1.Volume, error) { +func getComponentSecretRefsVolumes(kubeutil *kube.Kube, namespace string, deployComponent radixv1.RadixCommonDeployComponent, radixDeploymentName string) ([]corev1.Volume, error) { var volumes []corev1.Volume - azureKeyVaultVolumes, err := getStorageRefsAzureKeyVaultVolumes(kubeutil, namespace, deployComponent, radixDeploymentName) + azureKeyVaultVolumes, err := getComponentSecretRefsAzureKeyVaultVolumes(kubeutil, namespace, deployComponent, radixDeploymentName) if err != nil { return nil, err } @@ -233,7 +256,7 @@ func getStorageRefsVolumes(kubeutil *kube.Kube, namespace string, deployComponen return volumes, nil } -func getStorageRefsAzureKeyVaultVolumes(kubeutil *kube.Kube, namespace string, deployComponent radixv1.RadixCommonDeployComponent, radixDeploymentName string) ([]corev1.Volume, error) { +func getComponentSecretRefsAzureKeyVaultVolumes(kubeutil *kube.Kube, namespace string, deployComponent radixv1.RadixCommonDeployComponent, radixDeploymentName string) ([]corev1.Volume, error) { secretRef := deployComponent.GetSecretRefs() var volumes []corev1.Volume for _, azureKeyVault := range secretRef.AzureKeyVaults { @@ -275,27 +298,68 @@ func getStorageRefsAzureKeyVaultVolumes(kubeutil *kube.Kube, namespace string, d return volumes, nil } -func getExternalVolumes(kubeclient kubernetes.Interface, namespace string, environment string, deployComponent radixv1.RadixCommonDeployComponent) ([]corev1.Volume, error) { +func getComponentVolumeMountVolumes(kubeclient kubernetes.Interface, namespace string, environment string, deployComponent radixv1.RadixCommonDeployComponent) ([]corev1.Volume, error) { var volumes []corev1.Volume + + volumeSourceFunc := func(volumeMount *radixv1.RadixVolumeMount) (*corev1.VolumeSource, error) { + switch { + case volumeMount.HasDeprecatedVolume(): + return getComponentVolumeMountDeprecatedVolumeSource(volumeMount, namespace, environment, deployComponent.GetName(), kubeclient) + case volumeMount.HasBlobFuse2(): + return getComponentVolumeMountBlobFuse2VolumeSource(volumeMount, namespace, environment, deployComponent.GetName(), kubeclient) + case volumeMount.HasAzureFile(): + return getComponentVolumeMountAzureFileVolumeSource(volumeMount, namespace, environment, deployComponent.GetName(), kubeclient) + case volumeMount.HasEmptyDir(): + return getComponentVolumeMountEmptyDirVolumeSource(volumeMount.EmptyDir), nil + } + return nil, fmt.Errorf("missing configuration for volumeMount %s", volumeMount.Name) + } + for _, volumeMount := range deployComponent.GetVolumeMounts() { - volumeMountType := GetCsiAzureVolumeMountType(&volumeMount) - switch volumeMountType { - case radixv1.MountTypeBlob: - volumes = append(volumes, getBlobFuseVolume(namespace, environment, deployComponent.GetName(), volumeMount)) - case radixv1.MountTypeBlobFuse2FuseCsiAzure, radixv1.MountTypeBlobFuse2Fuse2CsiAzure, radixv1.MountTypeBlobFuse2NfsCsiAzure, radixv1.MountTypeAzureFileCsiAzure: - volume, err := getCsiAzureVolume(kubeclient, namespace, deployComponent.GetName(), &volumeMount) - if err != nil { - return nil, err - } - volumes = append(volumes, *volume) - default: - return nil, fmt.Errorf("unsupported volume type %s", volumeMountType) + volumeSource, err := volumeSourceFunc(&volumeMount) + if err != nil { + return nil, err + } + volumeName, err := getVolumeMountVolumeName(&volumeMount, deployComponent.GetName()) + if err != nil { + return nil, err } + volumes = append(volumes, corev1.Volume{ + Name: volumeName, + VolumeSource: *volumeSource, + }) } return volumes, nil } -func getCsiAzureVolume(kubeclient kubernetes.Interface, namespace, componentName string, radixVolumeMount *radixv1.RadixVolumeMount) (*corev1.Volume, error) { +func getComponentVolumeMountDeprecatedVolumeSource(volumeMount *radixv1.RadixVolumeMount, namespace, environment, componentName string, kubeclient kubernetes.Interface) (*corev1.VolumeSource, error) { + switch volumeMount.Type { + case radixv1.MountTypeBlob: + return getBlobFuseVolume(namespace, environment, componentName, volumeMount), nil + case radixv1.MountTypeAzureFileCsiAzure, radixv1.MountTypeBlobFuse2FuseCsiAzure: + return getCsiAzureVolume(kubeclient, namespace, componentName, volumeMount) + } + + return nil, fmt.Errorf("unsupported volume type %s", volumeMount.Type) +} + +func getComponentVolumeMountBlobFuse2VolumeSource(volumeMount *radixv1.RadixVolumeMount, namespace, environment, componentName string, kubeclient kubernetes.Interface) (*corev1.VolumeSource, error) { + return getCsiAzureVolume(kubeclient, namespace, componentName, volumeMount) +} + +func getComponentVolumeMountAzureFileVolumeSource(volumeMount *radixv1.RadixVolumeMount, namespace, environment, componentName string, kubeclient kubernetes.Interface) (*corev1.VolumeSource, error) { + return getCsiAzureVolume(kubeclient, namespace, componentName, volumeMount) +} + +func getComponentVolumeMountEmptyDirVolumeSource(spec *radixv1.RadixEmptyDirVolumeMount) *corev1.VolumeSource { + return &corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{ + SizeLimit: &spec.SizeLimit, + }, + } +} + +func getCsiAzureVolume(kubeclient kubernetes.Interface, namespace, componentName string, radixVolumeMount *radixv1.RadixVolumeMount) (*corev1.VolumeSource, error) { existingNotTerminatingPvcForComponentStorage, err := getPvcNotTerminating(kubeclient, namespace, componentName, radixVolumeMount) if err != nil { return nil, err @@ -310,20 +374,45 @@ func getCsiAzureVolume(kubeclient kubernetes.Interface, namespace, componentName return nil, err } } - volumeMountName, err := getCsiAzureVolumeMountName(componentName, radixVolumeMount) - if err != nil { - return nil, err - } - return &corev1.Volume{ - Name: volumeMountName, - VolumeSource: corev1.VolumeSource{ - PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ - ClaimName: pvcName, - }, + return &corev1.VolumeSource{ + PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ + ClaimName: pvcName, }, }, nil } +func getVolumeMountVolumeName(volumeMount *radixv1.RadixVolumeMount, componentName string) (string, error) { + switch { + case volumeMount.HasDeprecatedVolume(): + return getVolumeMountDeprecatedVolumeName(volumeMount, componentName) + case volumeMount.HasBlobFuse2(): + return getVolumeMountBlobFuse2VolumeName(volumeMount, componentName) + case volumeMount.HasAzureFile(): + return getVolumeMountAzureFileVolumeName(volumeMount, componentName) + } + + return fmt.Sprintf("radix-vm-%s", volumeMount.Name), nil +} + +func getVolumeMountAzureFileVolumeName(volumeMount *radixv1.RadixVolumeMount, componentName string) (string, error) { + return getCsiAzureVolumeMountName(volumeMount, componentName) +} + +func getVolumeMountBlobFuse2VolumeName(volumeMount *radixv1.RadixVolumeMount, componentName string) (string, error) { + return getCsiAzureVolumeMountName(volumeMount, componentName) +} + +func getVolumeMountDeprecatedVolumeName(volumeMount *radixv1.RadixVolumeMount, componentName string) (string, error) { + switch volumeMount.Type { + case radixv1.MountTypeBlob: + return getBlobFuseVolumeMountName(volumeMount, componentName), nil + case radixv1.MountTypeBlobFuse2FuseCsiAzure, radixv1.MountTypeAzureFileCsiAzure: + return getCsiAzureVolumeMountName(volumeMount, componentName) + } + + return "", fmt.Errorf("unsupported type %s", volumeMount.Type) +} + func getPvcNotTerminating(kubeclient kubernetes.Interface, namespace string, componentName string, radixVolumeMount *radixv1.RadixVolumeMount) (*corev1.PersistentVolumeClaim, error) { existingPvcForComponentStorage, err := kubeclient.CoreV1().PersistentVolumeClaims(namespace).List(context.TODO(), metav1.ListOptions{ LabelSelector: getLabelSelectorForCsiAzurePersistenceVolumeClaimForComponentStorage(componentName, radixVolumeMount.Name), @@ -345,7 +434,7 @@ func getPvcNotTerminating(kubeclient kubernetes.Interface, namespace string, com } func createCsiAzurePersistentVolumeClaimName(componentName string, radixVolumeMount *radixv1.RadixVolumeMount) (string, error) { - volumeName, err := getCsiAzureVolumeMountName(componentName, radixVolumeMount) + volumeName, err := getCsiAzureVolumeMountName(radixVolumeMount, componentName) if err != nil { return "", err } @@ -357,7 +446,7 @@ func GetCsiAzureStorageClassName(namespace, volumeName string) string { return fmt.Sprintf(csiStorageClassNameTemplate, namespace, volumeName) // volumeName: --- } -func getBlobFuseVolume(namespace, environment, componentName string, volumeMount radixv1.RadixVolumeMount) corev1.Volume { +func getBlobFuseVolume(namespace, environment, componentName string, volumeMount *radixv1.RadixVolumeMount) *corev1.VolumeSource { secretName := defaults.GetBlobFuseCredsSecretName(componentName, volumeMount.Name) flexVolumeOptions := make(map[string]string) @@ -366,15 +455,12 @@ func getBlobFuseVolume(namespace, environment, componentName string, volumeMount flexVolumeOptions["mountoptions"] = defaultMountOptions flexVolumeOptions["tmppath"] = fmt.Sprintf(blobFuseVolumeNodeMountPathTemplate, namespace, componentName, environment, radixv1.MountTypeBlob, volumeMount.Name, volumeMount.Container) - return corev1.Volume{ - Name: getBlobFuseVolumeMountName(volumeMount, componentName), - VolumeSource: corev1.VolumeSource{ - FlexVolume: &corev1.FlexVolumeSource{ - Driver: blobfuseDriver, - Options: flexVolumeOptions, - SecretRef: &corev1.LocalObjectReference{ - Name: secretName, - }, + return &corev1.VolumeSource{ + FlexVolume: &corev1.FlexVolumeSource{ + Driver: blobfuseDriver, + Options: flexVolumeOptions, + SecretRef: &corev1.LocalObjectReference{ + Name: secretName, }, }, } @@ -728,12 +814,11 @@ func (deploy *Deployment) garbageCollectOrphanedCsiAzurePersistentVolumes(exclud if err != nil { return err } - csiAzureStorageClassProvisioners := radixv1.GetCsiAzureStorageClassProvisioners() for _, pv := range pvList.Items { switch { case pv.Spec.ClaimRef == nil || pv.Spec.ClaimRef.Kind != persistentVolumeClaimKind: continue - case pv.Spec.CSI == nil || !slice.ContainsString(csiAzureStorageClassProvisioners, pv.Spec.CSI.Driver): + case pv.Spec.CSI == nil || !slice.ContainsString([]string{provisionerBlobCsiAzure, provisionerFileCsiAzure}, pv.Spec.CSI.Driver): continue case slice.ContainsString(excludePvcNames, pv.Spec.ClaimRef.Name): continue @@ -862,7 +947,7 @@ func (deploy *Deployment) createCsiAzurePersistentVolumeClaim(storageClass *stor // getOrCreateCsiAzureVolumeMountStorageClass returns creates or existing StorageClass, storageClassIsCreated=true, if created; error, if any func (deploy *Deployment) getOrCreateCsiAzureVolumeMountStorageClass(appName, volumeRootMount, namespace, componentName string, radixVolumeMount *radixv1.RadixVolumeMount, volumeName string, scMap map[string]*storagev1.StorageClass) (*storagev1.StorageClass, bool, error) { - var volumeMountProvisioner, foundProvisioner = radixv1.GetStorageClassProvisionerByVolumeMountType(radixVolumeMount) + var volumeMountProvisioner, foundProvisioner = getStorageClassProvisionerByVolumeMountType(radixVolumeMount) if !foundProvisioner { return nil, false, fmt.Errorf("not found Storage Class provisioner for volume mount type %s", string(GetCsiAzureVolumeMountType(radixVolumeMount))) } @@ -915,11 +1000,11 @@ func findCsiAzureVolumeForComponent(volumeMountMap map[string]*radixv1.RadixVolu return false } for _, radixVolumeMount := range volumeMounts { - if radixVolumeMount.BlobFuse2 == nil && radixVolumeMount.AzureFile == nil && !radixv1.IsKnownCsiAzureVolumeMount(string(GetCsiAzureVolumeMountType(&radixVolumeMount))) { + if radixVolumeMount.BlobFuse2 == nil && radixVolumeMount.AzureFile == nil && !isKnownCsiAzureVolumeMount(string(GetCsiAzureVolumeMountType(&radixVolumeMount))) { continue } radixVolumeMount := radixVolumeMount - volumeMountName, err := getCsiAzureVolumeMountName(componentName, &radixVolumeMount) + volumeMountName, err := getCsiAzureVolumeMountName(&radixVolumeMount, componentName) if err != nil { return false } diff --git a/pkg/apis/deployment/volumemount_test.go b/pkg/apis/deployment/volumemount_test.go index 1a603f0ed..e0e31a07a 100644 --- a/pkg/apis/deployment/volumemount_test.go +++ b/pkg/apis/deployment/volumemount_test.go @@ -16,6 +16,7 @@ import ( prometheusclient "github.com/prometheus-operator/prometheus-operator/pkg/client/versioned" prometheusfake "github.com/prometheus-operator/prometheus-operator/pkg/client/versioned/fake" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" @@ -1372,6 +1373,32 @@ func (suite *VolumeMountTestSuite) Test_CreateOrUpdateCsiAzureKeyVaultResources( }) } +func Test_EmptyDir(t *testing.T) { + appName, envName, compName := "anyapp", "anyenv", "anycomp" + + tu, kubeclient, kubeUtil, radixclient, prometheusclient, _ := setupTest(t) + builder := utils.NewDeploymentBuilder(). + WithRadixApplication(utils.NewRadixApplicationBuilder().WithAppName(appName).WithRadixRegistration(utils.NewRegistrationBuilder().WithName(appName))). + WithAppName(appName). + WithEnvironment(envName). + WithComponents( + utils.NewDeployComponentBuilder().WithName(compName).WithVolumeMounts( + v1.RadixVolumeMount{Name: "cache", Path: "/cache", EmptyDir: &v1.RadixEmptyDirVolumeMount{SizeLimit: resource.MustParse("50M")}}, + v1.RadixVolumeMount{Name: "log", Path: "/log", EmptyDir: &v1.RadixEmptyDirVolumeMount{SizeLimit: resource.MustParse("100M")}}, + ), + ) + + rd, err := applyDeploymentWithSync(tu, kubeclient, kubeUtil, radixclient, prometheusclient, builder) + require.NoError(t, err) + assert.NotNil(t, rd) + + deployment, err := kubeclient.AppsV1().Deployments(utils.GetEnvironmentNamespace(appName, envName)).Get(context.Background(), compName, metav1.GetOptions{}) + require.NoError(t, err) + assert.Len(t, deployment.Spec.Template.Spec.Containers[0].VolumeMounts, 2) + assert.Len(t, deployment.Spec.Template.Spec.Volumes, 2) + +} + func createRandomStorageClass(props expectedPvcScProperties, namespace, componentName string) storagev1.StorageClass { return createExpectedStorageClass(props, func(sc *storagev1.StorageClass) { sc.ObjectMeta.Name = utils.RandString(10) @@ -1418,7 +1445,7 @@ func getPropsCsiBlobVolume1Storage1(modify func(*expectedPvcScProperties)) expec requestsVolumeMountSize: "1Mi", volumeAccessMode: corev1.ReadOnlyMany, // default access mode volumeName: "csi-az-blob-some-component-volume1-storage1", - scProvisioner: v1.ProvisionerBlobCsiAzure, + scProvisioner: provisionerBlobCsiAzure, scSecretName: "some-component-volume1-csiazurecreds", scTmpPath: "/tmp/any-app-some-env/csi-az-blob/some-component/volume1/storage1", scGid: "1000", @@ -1447,7 +1474,7 @@ func getPropsCsiBlobFuse2Volume1Storage1(modify func(*expectedPvcScProperties)) requestsVolumeMountSize: "1Mi", volumeAccessMode: corev1.ReadOnlyMany, // default access mode volumeName: "csi-blobfuse2-fuse2-some-component-volume1-storage1", - scProvisioner: v1.ProvisionerBlobCsiAzure, + scProvisioner: provisionerBlobCsiAzure, scSecretName: "some-component-volume1-csiazurecreds", scTmpPath: "/tmp/any-app-some-env/csi-blobfuse2-fuse2/some-component/volume1/storage1", scGid: "1000", @@ -1476,7 +1503,7 @@ func getPropsCsiFileVolume2Storage2(modify func(*expectedPvcScProperties)) expec requestsVolumeMountSize: "1Mi", volumeAccessMode: corev1.ReadOnlyMany, // default access mode volumeName: "csi-az-file-some-component-volume2-storage2", - scProvisioner: v1.ProvisionerFileCsiAzure, + scProvisioner: provisionerFileCsiAzure, scSecretName: "some-component-volume2-csiazurecreds", scTmpPath: "/tmp/any-app-some-env/csi-az-file/some-component/volume2/storage2", scGid: "1000", diff --git a/pkg/apis/radix/v1/radixapptypes.go b/pkg/apis/radix/v1/radixapptypes.go index da88507a5..d13920647 100644 --- a/pkg/apis/radix/v1/radixapptypes.go +++ b/pkg/apis/radix/v1/radixapptypes.go @@ -4,6 +4,7 @@ import ( "strings" commonUtils "github.com/equinor/radix-common/utils" + "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -410,6 +411,10 @@ type RadixComponent struct { // More info: https://www.radix.equinor.com/references/reference-radix-config/#enabled // +optional Enabled *bool `json:"enabled,omitempty"` + + // Controls if the filesystem shall be read-only. + // +optional + ReadOnlyFileSystem *bool `json:"readOnlyFileSystem,omitempty"` } // RadixEnvironmentConfig defines environment specific settings for component. @@ -485,6 +490,10 @@ type RadixEnvironmentConfig struct { // More info: https://www.radix.equinor.com/references/reference-radix-config/#enabled // +optional Enabled *bool `json:"enabled,omitempty"` + + // Controls if the filesystem shall be read-only. + // +optional + ReadOnlyFileSystem *bool `json:"readOnlyFileSystem,omitempty"` } // RadixJobComponent defines a single job component within a RadixApplication @@ -591,6 +600,10 @@ type RadixJobComponent struct { // Notifications about batch or job status changes // +optional Notifications *Notifications `json:"notifications,omitempty"` + + // Controls if the filesystem shall be read-only. + // +optional + ReadOnlyFileSystem *bool `json:"readOnlyFileSystem,omitempty"` } // RadixJobComponentEnvironmentConfig defines environment specific settings @@ -661,6 +674,10 @@ type RadixJobComponentEnvironmentConfig struct { // Notifications about batch or job status changes // +optional Notifications *Notifications `json:"notifications,omitempty"` + + // Controls if the filesystem shall be read-only. + // +optional + ReadOnlyFileSystem *bool `json:"readOnlyFileSystem,omitempty"` } // RadixJobComponentPayload defines the path and where the payload received @@ -731,6 +748,7 @@ type RadixVolumeMount struct { // User-defined name of the volume mount. // Must be unique for the component. // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=40 Name string `json:"name"` // Deprecated. Only required by the deprecated type: blob. @@ -787,6 +805,31 @@ type RadixVolumeMount struct { // AzureFile settings for Azure File CSI driver AzureFile *RadixAzureFileVolumeMount `json:"azureFile,omitempty"` + + // EmptyDir settings for EmptyDir volume + EmptyDir *RadixEmptyDirVolumeMount `json:"emptyDir,omitempty"` +} + +func (v *RadixVolumeMount) HasDeprecatedVolume() bool { + return len(v.Type) > 0 +} + +func (v *RadixVolumeMount) HasBlobFuse2() bool { + return v.BlobFuse2 != nil +} + +func (v *RadixVolumeMount) HasAzureFile() bool { + return v.AzureFile != nil +} + +func (v *RadixVolumeMount) HasEmptyDir() bool { + return v.EmptyDir != nil +} + +type RadixEmptyDirVolumeMount struct { + // SizeLimit defines the size of the emptyDir volume + // +kubebuilder:validation:Required + SizeLimit resource.Quantity `json:"sizeLimit"` } // BlobFuse2Protocol Holds protocols of BlobFuse2 Azure Storage FUSE driver @@ -806,11 +849,10 @@ type RadixBlobFuse2VolumeMount struct { // Holds protocols of BlobFuse2 Azure Storage FUSE driver. Default is fuse2. // +kubebuilder:validation:Enum=fuse2;nfs;"" // +optional - Protocol BlobFuse2Protocol `json:"protocol"` + Protocol BlobFuse2Protocol `json:"protocol,omitempty"` // Container. Name of the container in the external storage resource. - // +optional - Container string `json:"container,omitempty"` + Container string `json:"container"` // GID defines the group ID (number) which will be set as owner of the mounted volume. // +optional @@ -822,6 +864,7 @@ type RadixBlobFuse2VolumeMount struct { // SKU Type of Azure storage. // More info: https://learn.microsoft.com/en-us/rest/api/storagerp/srp_sku_types + // +kubebuilder:validation:Enum=Standard_LRS;Premium_LRS;Standard_GRS;Standard_RAGRS;"" // +optional SkuName string `json:"skuName,omitempty"` // Available values: Standard_LRS (default), Premium_LRS, Standard_GRS, Standard_RAGRS. https://docs.microsoft.com/en-us/rest/api/storagerp/srp_sku_types @@ -937,56 +980,6 @@ const ( MountTypeAzureFileCsiAzure MountType = "azure-file" ) -// These are valid storage class provisioners -const ( - // ProvisionerBlobCsiAzure Use of azure/csi driver for blob in Azure storage account - ProvisionerBlobCsiAzure string = "blob.csi.azure.com" - // ProvisionerFileCsiAzure Use of azure/csi driver for files in Azure storage account - ProvisionerFileCsiAzure string = "file.csi.azure.com" -) - -// GetStorageClassProvisionerByVolumeMountType convert volume mount type to Storage Class provisioner -func GetStorageClassProvisionerByVolumeMountType(radixVolumeMount *RadixVolumeMount) (string, bool) { - if radixVolumeMount.BlobFuse2 != nil { - return ProvisionerBlobCsiAzure, true - } - if radixVolumeMount.AzureFile != nil { - return ProvisionerFileCsiAzure, true - } - switch radixVolumeMount.Type { - case MountTypeBlobFuse2FuseCsiAzure, MountTypeBlobFuse2Fuse2CsiAzure, MountTypeBlobFuse2NfsCsiAzure: - return ProvisionerBlobCsiAzure, true - case MountTypeAzureFileCsiAzure: - return ProvisionerFileCsiAzure, true - } - return "", false -} - -// GetCsiAzureStorageClassProvisioners CSI Azure provisioners -func GetCsiAzureStorageClassProvisioners() []string { - return []string{ProvisionerBlobCsiAzure, ProvisionerFileCsiAzure} -} - -// IsKnownVolumeMount Gets if volume mount is supported -func IsKnownVolumeMount(volumeMount string) bool { - return IsKnownBlobFlexVolumeMount(volumeMount) || - IsKnownCsiAzureVolumeMount(volumeMount) -} - -// IsKnownCsiAzureVolumeMount Supported volume mount type CSI Azure Blob volume -func IsKnownCsiAzureVolumeMount(volumeMount string) bool { - switch volumeMount { - case string(MountTypeBlobFuse2FuseCsiAzure), string(MountTypeBlobFuse2Fuse2CsiAzure), string(MountTypeBlobFuse2NfsCsiAzure), string(MountTypeAzureFileCsiAzure): - return true - } - return false -} - -// IsKnownBlobFlexVolumeMount Supported volume mount type Azure Blobfuse -func IsKnownBlobFlexVolumeMount(volumeMount string) bool { - return volumeMount == string(MountTypeBlob) -} - // RadixNode defines node attributes, where container should be scheduled type RadixNode struct { // Defines rules for allowed GPU types. @@ -1351,6 +1344,8 @@ type RadixCommonComponent interface { GetEnvironmentConfigByName(environment string) RadixCommonEnvironmentConfig // GetEnabledForEnvironment Checks if the component is enabled for any of the environments GetEnabledForEnvironment(environment string) bool + // GetReadOnlyFileSystem Gets if filesystem shall be read-only + GetReadOnlyFileSystem() *bool } func (component *RadixComponent) GetName() string { @@ -1408,7 +1403,8 @@ func (component *RadixComponent) getEnabled() bool { func (component *RadixComponent) GetEnvironmentConfig() []RadixCommonEnvironmentConfig { var environmentConfigs []RadixCommonEnvironmentConfig for _, environmentConfig := range component.EnvironmentConfig { - environmentConfigs = append(environmentConfigs, environmentConfig) + environmentConfig := environmentConfig + environmentConfigs = append(environmentConfigs, &environmentConfig) } return environmentConfigs } @@ -1434,6 +1430,10 @@ func (component *RadixComponent) GetEnabledForEnv(envConfig RadixCommonEnvironme return getEnabled(component, envConfig) } +func (component *RadixComponent) GetReadOnlyFileSystem() *bool { + return component.ReadOnlyFileSystem +} + func (component *RadixComponent) GetEnabledForEnvironment(environment string) bool { return getEnabledForEnvironment(component, environment) } @@ -1509,7 +1509,8 @@ func (component *RadixJobComponent) getEnabled() bool { func (component *RadixJobComponent) GetEnvironmentConfig() []RadixCommonEnvironmentConfig { var environmentConfigs []RadixCommonEnvironmentConfig for _, environmentConfig := range component.EnvironmentConfig { - environmentConfigs = append(environmentConfigs, environmentConfig) + environmentConfig := environmentConfig + environmentConfigs = append(environmentConfigs, &environmentConfig) } return environmentConfigs } @@ -1535,6 +1536,10 @@ func (component *RadixJobComponent) GetEnabledForEnvironment(environment string) return getEnabledForEnvironment(component, environment) } +func (component *RadixJobComponent) GetReadOnlyFileSystem() *bool { + return component.ReadOnlyFileSystem +} + func getEnvironmentConfigByName(environment string, environmentConfigs []RadixCommonEnvironmentConfig) RadixCommonEnvironmentConfig { for _, environmentConfig := range environmentConfigs { if strings.EqualFold(environment, environmentConfig.GetEnvironment()) { diff --git a/pkg/apis/radix/v1/radixdeploytypes.go b/pkg/apis/radix/v1/radixdeploytypes.go index dbba40fc7..ee72bfb6d 100644 --- a/pkg/apis/radix/v1/radixdeploytypes.go +++ b/pkg/apis/radix/v1/radixdeploytypes.go @@ -125,6 +125,7 @@ type RadixDeployComponent struct { Node RadixNode `json:"node,omitempty"` Authentication *Authentication `json:"authentication,omitempty"` Identity *Identity `json:"identity,omitempty"` + ReadOnlyFileSystem *bool `json:"readOnlyFileSystem,omitempty"` } func (deployComponent *RadixDeployComponent) GetName() string { @@ -220,6 +221,10 @@ func (deployComponent *RadixDeployComponent) GetIdentity() *Identity { return deployComponent.Identity } +func (deployComponent *RadixDeployComponent) GetReadOnlyFileSystem() *bool { + return deployComponent.ReadOnlyFileSystem +} + func (deployComponent *RadixDeployComponent) SetName(name string) { deployComponent.Name = name } @@ -328,6 +333,10 @@ func (deployJobComponent *RadixDeployJobComponent) GetNotifications() *Notificat return deployJobComponent.Notifications } +func (deployJobComponent *RadixDeployJobComponent) GetReadOnlyFileSystem() *bool { + return deployJobComponent.ReadOnlyFileSystem +} + func (deployJobComponent *RadixDeployJobComponent) SetName(name string) { deployJobComponent.Name = name } @@ -373,6 +382,7 @@ type RadixDeployJobComponent struct { BackoffLimit *int32 `json:"backoffLimit,omitempty"` Identity *Identity `json:"identity,omitempty"` Notifications *Notifications `json:"notifications,omitempty"` + ReadOnlyFileSystem *bool `json:"readOnlyFileSystem,omitempty"` } type RadixComponentType string @@ -409,6 +419,7 @@ type RadixCommonDeployComponent interface { SetName(name string) SetVolumeMounts(mounts []RadixVolumeMount) GetIdentity() *Identity + GetReadOnlyFileSystem() *bool } // RadixCommonDeployComponentFactory defines a common component factory diff --git a/pkg/apis/radix/v1/radixenvironmentconfigtypes.go b/pkg/apis/radix/v1/radixenvironmentconfigtypes.go index d9852db52..20cbe1ba6 100644 --- a/pkg/apis/radix/v1/radixenvironmentconfigtypes.go +++ b/pkg/apis/radix/v1/radixenvironmentconfigtypes.go @@ -14,90 +14,99 @@ type RadixCommonEnvironmentConfig interface { GetHorizontalScaling() *RadixHorizontalScaling GetReplicas() *int GetIdentity() *Identity + GetReadOnlyFileSystem() *bool getEnabled() *bool } -func (config RadixEnvironmentConfig) GetEnvironment() string { +func (config *RadixEnvironmentConfig) GetEnvironment() string { return config.Environment } -func (config RadixEnvironmentConfig) GetSecretRefs() RadixSecretRefs { +func (config *RadixEnvironmentConfig) GetSecretRefs() RadixSecretRefs { return config.SecretRefs } -func (config RadixEnvironmentConfig) GetVariables() EnvVarsMap { +func (config *RadixEnvironmentConfig) GetVariables() EnvVarsMap { return config.Variables } -func (config RadixEnvironmentConfig) GetResources() ResourceRequirements { +func (config *RadixEnvironmentConfig) GetResources() ResourceRequirements { return config.Resources } -func (config RadixEnvironmentConfig) GetNode() RadixNode { +func (config *RadixEnvironmentConfig) GetNode() RadixNode { return config.Node } -func (config RadixEnvironmentConfig) GetImageTagName() string { +func (config *RadixEnvironmentConfig) GetImageTagName() string { return config.ImageTagName } -func (config RadixEnvironmentConfig) GetHorizontalScaling() *RadixHorizontalScaling { +func (config *RadixEnvironmentConfig) GetHorizontalScaling() *RadixHorizontalScaling { return config.HorizontalScaling } -func (config RadixEnvironmentConfig) GetReplicas() *int { +func (config *RadixEnvironmentConfig) GetReplicas() *int { return config.Replicas } -func (config RadixEnvironmentConfig) GetIdentity() *Identity { +func (config *RadixEnvironmentConfig) GetIdentity() *Identity { return config.Identity } -func (config RadixEnvironmentConfig) getEnabled() *bool { +func (config *RadixEnvironmentConfig) GetReadOnlyFileSystem() *bool { + return config.ReadOnlyFileSystem +} + +func (config *RadixEnvironmentConfig) getEnabled() *bool { return config.Enabled } -func (config RadixJobComponentEnvironmentConfig) GetEnvironment() string { +func (config *RadixJobComponentEnvironmentConfig) GetEnvironment() string { return config.Environment } -func (config RadixJobComponentEnvironmentConfig) GetSecretRefs() RadixSecretRefs { +func (config *RadixJobComponentEnvironmentConfig) GetSecretRefs() RadixSecretRefs { return config.SecretRefs } -func (config RadixJobComponentEnvironmentConfig) GetVariables() EnvVarsMap { +func (config *RadixJobComponentEnvironmentConfig) GetVariables() EnvVarsMap { return config.Variables } -func (config RadixJobComponentEnvironmentConfig) GetResources() ResourceRequirements { +func (config *RadixJobComponentEnvironmentConfig) GetResources() ResourceRequirements { return config.Resources } -func (config RadixJobComponentEnvironmentConfig) GetNode() RadixNode { +func (config *RadixJobComponentEnvironmentConfig) GetNode() RadixNode { return config.Node } -func (config RadixJobComponentEnvironmentConfig) GetImageTagName() string { +func (config *RadixJobComponentEnvironmentConfig) GetImageTagName() string { return config.ImageTagName } -func (config RadixJobComponentEnvironmentConfig) GetHorizontalScaling() *RadixHorizontalScaling { +func (config *RadixJobComponentEnvironmentConfig) GetHorizontalScaling() *RadixHorizontalScaling { return nil } -func (config RadixJobComponentEnvironmentConfig) GetReplicas() *int { +func (config *RadixJobComponentEnvironmentConfig) GetReplicas() *int { return numbers.IntPtr(1) } -func (config RadixJobComponentEnvironmentConfig) GetIdentity() *Identity { +func (config *RadixJobComponentEnvironmentConfig) GetIdentity() *Identity { return config.Identity } // GetNotifications Get job component notifications -func (config RadixJobComponentEnvironmentConfig) GetNotifications() *Notifications { +func (config *RadixJobComponentEnvironmentConfig) GetNotifications() *Notifications { return config.Notifications } -func (config RadixJobComponentEnvironmentConfig) getEnabled() *bool { +func (config *RadixJobComponentEnvironmentConfig) GetReadOnlyFileSystem() *bool { + return config.ReadOnlyFileSystem +} + +func (config *RadixJobComponentEnvironmentConfig) getEnabled() *bool { return config.Enabled } diff --git a/pkg/apis/radix/v1/zz_generated.deepcopy.go b/pkg/apis/radix/v1/zz_generated.deepcopy.go index 765ebcb71..223191191 100644 --- a/pkg/apis/radix/v1/zz_generated.deepcopy.go +++ b/pkg/apis/radix/v1/zz_generated.deepcopy.go @@ -1228,6 +1228,11 @@ func (in *RadixComponent) DeepCopyInto(out *RadixComponent) { *out = new(bool) **out = **in } + if in.ReadOnlyFileSystem != nil { + in, out := &in.ReadOnlyFileSystem, &out.ReadOnlyFileSystem + *out = new(bool) + **out = **in + } return } @@ -1404,6 +1409,11 @@ func (in *RadixDeployComponent) DeepCopyInto(out *RadixDeployComponent) { *out = new(Identity) (*in).DeepCopyInto(*out) } + if in.ReadOnlyFileSystem != nil { + in, out := &in.ReadOnlyFileSystem, &out.ReadOnlyFileSystem + *out = new(bool) + **out = **in + } return } @@ -1510,6 +1520,11 @@ func (in *RadixDeployJobComponent) DeepCopyInto(out *RadixDeployJobComponent) { *out = new(Notifications) (*in).DeepCopyInto(*out) } + if in.ReadOnlyFileSystem != nil { + in, out := &in.ReadOnlyFileSystem, &out.ReadOnlyFileSystem + *out = new(bool) + **out = **in + } return } @@ -1699,6 +1714,23 @@ func (in *RadixDeploymentSpec) DeepCopy() *RadixDeploymentSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RadixEmptyDirVolumeMount) DeepCopyInto(out *RadixEmptyDirVolumeMount) { + *out = *in + out.SizeLimit = in.SizeLimit.DeepCopy() + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RadixEmptyDirVolumeMount. +func (in *RadixEmptyDirVolumeMount) DeepCopy() *RadixEmptyDirVolumeMount { + if in == nil { + return nil + } + out := new(RadixEmptyDirVolumeMount) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *RadixEnvironment) DeepCopyInto(out *RadixEnvironment) { *out = *in @@ -1777,6 +1809,11 @@ func (in *RadixEnvironmentConfig) DeepCopyInto(out *RadixEnvironmentConfig) { *out = new(bool) **out = **in } + if in.ReadOnlyFileSystem != nil { + in, out := &in.ReadOnlyFileSystem, &out.ReadOnlyFileSystem + *out = new(bool) + **out = **in + } return } @@ -2024,6 +2061,11 @@ func (in *RadixJobComponent) DeepCopyInto(out *RadixJobComponent) { *out = new(Notifications) (*in).DeepCopyInto(*out) } + if in.ReadOnlyFileSystem != nil { + in, out := &in.ReadOnlyFileSystem, &out.ReadOnlyFileSystem + *out = new(bool) + **out = **in + } return } @@ -2082,6 +2124,11 @@ func (in *RadixJobComponentEnvironmentConfig) DeepCopyInto(out *RadixJobComponen *out = new(Notifications) (*in).DeepCopyInto(*out) } + if in.ReadOnlyFileSystem != nil { + in, out := &in.ReadOnlyFileSystem, &out.ReadOnlyFileSystem + *out = new(bool) + **out = **in + } return } @@ -2436,6 +2483,11 @@ func (in *RadixVolumeMount) DeepCopyInto(out *RadixVolumeMount) { *out = new(RadixAzureFileVolumeMount) **out = **in } + if in.EmptyDir != nil { + in, out := &in.EmptyDir, &out.EmptyDir + *out = new(RadixEmptyDirVolumeMount) + (*in).DeepCopyInto(*out) + } return } diff --git a/pkg/apis/radixvalidators/errors.go b/pkg/apis/radixvalidators/errors.go index 924bb7604..69e048be8 100644 --- a/pkg/apis/radixvalidators/errors.go +++ b/pkg/apis/radixvalidators/errors.go @@ -33,15 +33,19 @@ var ( ErrMaxReplicasForHPANotSetOrZero = errors.New("max replicas for hpanot set or zero") ErrMinReplicasGreaterThanMaxReplicas = errors.New("min replicas greater than max replicas") ErrNoScalingResourceSet = errors.New("no scaling resource set") - ErrEmptyVolumeMountTypeOrDriverSection = errors.New("empty volume mount type or driver section") - ErrMultipleVolumeMountTypesDefined = errors.New("multiple volume mount types defined") - ErrEmptyVolumeMountNameOrPath = errors.New("empty volume mount name or path") - ErrEmptyVolumeMountStorage = errors.New("empty volume mount storage") - ErrEmptyBlobFuse2VolumeMountContainer = errors.New("empty blob fuse 2 volume mount container") - ErrUnsupportedBlobFuse2VolumeMountProtocol = errors.New("unsupported blob fuse 2 volume mount protocol") - ErrDuplicatePathForVolumeMountType = errors.New("duplicate path for volume mount") - ErrDuplicateNameForVolumeMountType = errors.New("duplicate name for volume mount") - ErrunknownVolumeMountType = errors.New("unknown volume mount type") + ErrVolumeMountMissingType = errors.New("no types defined") + ErrVolumeMountInvalidType = errors.New("invalid type") + ErrVolumeMountMultipleTypes = errors.New("multiple types defined") + ErrVolumeMountMissingName = errors.New("name not set") + ErrVolumeMountDuplicateName = errors.New("duplicate name") + ErrVolumeMountMissingPath = errors.New("path not set") + ErrVolumeMountDuplicatePath = errors.New("duplicate path") + ErrVolumeMountMissingStorage = errors.New("storage not set") + ErrVolumeMountMissingContainer = errors.New("container not set") + ErrVolumeMountInvalidProtocol = errors.New("invalid protocol") + ErrVolumeMountInvalidRequestsStorage = errors.New("requestsStorage is invalid") + ErrVolumeMountMissingSizeLimit = errors.New("sizeLimit is not set") + ErrVolumeMountTypeNotImplemented = errors.New("type not implemented") ErrApplicationNameNotLowercase = errors.New("application name not lowercase") ErrPublicImageComponentCannotHaveSourceOrDockerfileSet = errors.New("public image component cannot have source or dockerfile") ErrComponentWithTagInEnvironmentConfigForEnvironmentRequiresDynamicTag = errors.New("component with tag in environment config for environment requires dynamic") @@ -257,43 +261,24 @@ func NoScalingResourceSetErrorWithMessage(component, environment string) error { return errors.WithMessagef(ErrNoScalingResourceSet, "no scaling resource is set for component %s in environment %s. See documentation for more info", component, environment) } -func emptyVolumeMountTypeOrDriverSectionErrorWithMessage(component, environment string) error { - return errors.WithMessagef(ErrEmptyVolumeMountTypeOrDriverSection, "non of volume mount type, blobfuse2 or azureFile options are defined in the volumeMount for component %s in environment %s. See documentation for more info", component, environment) +func volumeMountValidationError(name string, cause error) error { + return fmt.Errorf("volumeMount %s failed validation. %w", name, cause) } -func multipleVolumeMountTypesDefinedErrorWithMessage(component, environment string) error { - return errors.WithMessagef(ErrMultipleVolumeMountTypesDefined, "multiple volume mount types defined in the volumeMount for component %s in environment %s. See documentation for more info", component, environment) +func volumeMountDeprecatedSourceValidationError(cause error) error { + return fmt.Errorf("deprecated arguments failed validation. %w", cause) } -func emptyVolumeMountNameOrPathErrorWithMessage(component, environment string) error { - return errors.WithMessagef(ErrEmptyVolumeMountNameOrPath, "missing volume mount name and path of volumeMount for component %s in environment %s. See documentation for more info", component, environment) +func volumeMountBlobFuse2ValidationError(cause error) error { + return fmt.Errorf("blobFuse2 failed validation. %w", cause) } -func emptyVolumeMountStorageErrorWithMessage(component, environment string) error { - return errors.WithMessagef(ErrEmptyVolumeMountStorage, "missing volume mount storage of volumeMount for component %s in environment %s. See documentation for more info", component, environment) +func volumeMountAzureFileValidationError(cause error) error { + return fmt.Errorf("azureFile failed validation. %w", cause) } -func emptyBlobFuse2VolumeMountContainerErrorWithMessage(component, environment string) error { - return errors.WithMessagef(ErrEmptyBlobFuse2VolumeMountContainer, "missing BlobFuse2 volume mount container of volumeMount for component %s in environment %s. See documentation for more info", component, environment) -} - -func unsupportedBlobFuse2VolumeMountProtocolErrorWithMessage(component, environment string) error { - return errors.WithMessagef(ErrUnsupportedBlobFuse2VolumeMountProtocol, "unsupported BlobFuse2 volume mount protocol of volumeMount for component %s in environment %s. See documentation for more info", component, environment) -} - -func duplicatePathForVolumeMountTypeWithMessage(path, volumeMountType, component, environment string) error { - return errors.WithMessagef(ErrDuplicatePathForVolumeMountType, "duplicate path %s for volume mount type %s, for component %s in environment %s. See documentation for more info", - path, volumeMountType, component, environment) -} - -func duplicateNameForVolumeMountTypeWithMessage(name, volumeMountType, component, environment string) error { - return errors.WithMessagef(ErrDuplicateNameForVolumeMountType, "duplicate names %s for volume mount type %s, for component %s in environment %s. See documentation for more info", - name, volumeMountType, component, environment) -} - -func unknownVolumeMountTypeErrorWithMessage(volumeMountType, component, environment string) error { - return errors.WithMessagef(ErrunknownVolumeMountType, "not recognized volume mount type %s for component %s in environment %s. See documentation for more info", - volumeMountType, component, environment) +func volumeMountEmptyDirValidationError(cause error) error { + return fmt.Errorf("emptyDir failed validation. %w", cause) } // ApplicationNameNotLowercaseErrorWithMessage Indicates that application name contains upper case letters diff --git a/pkg/apis/radixvalidators/validate_ra.go b/pkg/apis/radixvalidators/validate_ra.go index 535477c01..69d210549 100644 --- a/pkg/apis/radixvalidators/validate_ra.go +++ b/pkg/apis/radixvalidators/validate_ra.go @@ -6,6 +6,7 @@ import ( "net" "net/url" "regexp" + "slices" "strconv" "strings" "time" @@ -35,7 +36,6 @@ const ( var ( validOAuthSessionStoreTypes = []string{string(radixv1.SessionStoreCookie), string(radixv1.SessionStoreRedis)} validOAuthCookieSameSites = []string{string(radixv1.SameSiteStrict), string(radixv1.SameSiteLax), string(radixv1.SameSiteNone), string(radixv1.SameSiteEmpty)} - blobFuse2Protocols = []string{string(radixv1.BlobFuse2ProtocolFuse2), string(radixv1.BlobFuse2ProtocolNfs)} requiredRadixApplicationValidators = []RadixApplicationValidator{ validateRadixApplicationAppName, @@ -339,6 +339,7 @@ func validateComponents(app *radixv1.RadixApplication) error { if err != nil { errs = append(errs, err) } + } } @@ -1156,16 +1157,16 @@ func validateHPAConfigForRA(app *radixv1.RadixApplication) error { func validateVolumeMountConfigForRA(app *radixv1.RadixApplication) error { for _, component := range app.Spec.Components { for _, envConfig := range component.EnvironmentConfig { - if err := validateVolumeMounts(component.Name, envConfig.Environment, envConfig.VolumeMounts); err != nil { - return err + if err := validateVolumeMounts(envConfig.VolumeMounts); err != nil { + return fmt.Errorf("failed volumeMount validation for component %s in environment %s. %w", component.Name, envConfig.Environment, err) } } } for _, job := range app.Spec.Jobs { for _, envConfig := range job.EnvironmentConfig { - if err := validateVolumeMounts(job.Name, envConfig.Environment, envConfig.VolumeMounts); err != nil { - return err + if err := validateVolumeMounts(envConfig.VolumeMounts); err != nil { + return fmt.Errorf("failed volumeMount validation for job %s in environment %s. %w", job.Name, envConfig.Environment, err) } } } @@ -1249,69 +1250,113 @@ func getRadixCommonComponentByName(app *radixv1.RadixApplication, componentName return nil, nil } -func validateVolumeMounts(componentName, environment string, volumeMounts []radixv1.RadixVolumeMount) error { +func validateVolumeMounts(volumeMounts []radixv1.RadixVolumeMount) error { if len(volumeMounts) == 0 { return nil } - mountsInComponent := make(map[string]volumeMountConfigMaps) + for _, v := range volumeMounts { + if len(strings.TrimSpace(v.Name)) == 0 { + return ErrVolumeMountMissingName + } + + if len(strings.TrimSpace(v.Path)) == 0 { + return volumeMountValidationError(v.Name, ErrVolumeMountMissingPath) + } + + if len(slice.FindAll(volumeMounts, func(rvm radixv1.RadixVolumeMount) bool { return rvm.Name == v.Name })) > 1 { + return volumeMountValidationError(v.Name, ErrVolumeMountDuplicateName) + } + + if len(slice.FindAll(volumeMounts, func(rvm radixv1.RadixVolumeMount) bool { return rvm.Path == v.Path })) > 1 { + return volumeMountValidationError(v.Name, ErrVolumeMountDuplicatePath) + } + + volumeSourceCount := len(slice.FindAll( + []bool{v.HasDeprecatedVolume(), v.HasBlobFuse2(), v.HasAzureFile(), v.HasEmptyDir()}, + func(b bool) bool { return b }), + ) + if volumeSourceCount > 1 { + return volumeMountValidationError(v.Name, ErrVolumeMountMultipleTypes) + } + if volumeSourceCount == 0 { + return volumeMountValidationError(v.Name, ErrVolumeMountMissingType) + } - for _, volumeMount := range volumeMounts { - volumeMountType := string(deployment.GetCsiAzureVolumeMountType(&volumeMount)) - volumeMountStorage := deployment.GetRadixVolumeMountStorage(&volumeMount) switch { - case len(volumeMount.Type) == 0 && volumeMount.BlobFuse2 == nil && volumeMount.AzureFile == nil: - return emptyVolumeMountTypeOrDriverSectionErrorWithMessage(componentName, environment) - case multipleVolumeTypesDefined(&volumeMount): - return multipleVolumeMountTypesDefinedErrorWithMessage(componentName, environment) - case strings.TrimSpace(volumeMount.Name) == "" || - strings.TrimSpace(volumeMount.Path) == "": - return emptyVolumeMountNameOrPathErrorWithMessage(componentName, environment) - case volumeMount.BlobFuse2 == nil && volumeMount.AzureFile == nil && len(volumeMount.Type) > 0 && len(volumeMountStorage) == 0: - return emptyVolumeMountStorageErrorWithMessage(componentName, environment) - case volumeMount.BlobFuse2 != nil: - switch { - case len(volumeMount.BlobFuse2.Container) == 0: - return emptyBlobFuse2VolumeMountContainerErrorWithMessage(componentName, environment) - case len(string(volumeMount.BlobFuse2.Protocol)) > 0 && !commonUtils.ContainsString(blobFuse2Protocols, string(volumeMount.BlobFuse2.Protocol)): - return unsupportedBlobFuse2VolumeMountProtocolErrorWithMessage(componentName, environment) + case v.HasDeprecatedVolume(): + if err := validateVolumeMountDeprecatedSource(&v); err != nil { + return volumeMountValidationError(v.Name, err) } - fallthrough - case radixv1.IsKnownVolumeMount(volumeMountType): - { - if _, exists := mountsInComponent[volumeMountType]; !exists { - mountsInComponent[volumeMountType] = volumeMountConfigMaps{names: make(map[string]bool), path: make(map[string]bool)} - } - volumeMountConfigMap := mountsInComponent[volumeMountType] - if _, exists := volumeMountConfigMap.names[volumeMount.Name]; exists { - return duplicateNameForVolumeMountTypeWithMessage(volumeMount.Name, volumeMountType, componentName, environment) - } - volumeMountConfigMap.names[volumeMount.Name] = true - if _, exists := volumeMountConfigMap.path[volumeMount.Path]; exists { - return duplicatePathForVolumeMountTypeWithMessage(volumeMount.Path, volumeMountType, componentName, environment) - } - volumeMountConfigMap.path[volumeMount.Path] = true + case v.HasBlobFuse2(): + if err := validateVolumeMountBlobFuse2(v.BlobFuse2); err != nil { + return volumeMountValidationError(v.Name, err) } - default: - return unknownVolumeMountTypeErrorWithMessage(volumeMountType, componentName, environment) + case v.HasAzureFile(): + if err := validateVolumeMountAzureFile(v.AzureFile); err != nil { + return volumeMountValidationError(v.Name, err) + } + case v.HasEmptyDir(): + if err := validateVolumeMountEmptyDir(v.EmptyDir); err != nil { + return volumeMountValidationError(v.Name, err) + } + } + } + + return nil +} + +func validateVolumeMountDeprecatedSource(v *radixv1.RadixVolumeMount) error { + if !slices.Contains([]radixv1.MountType{radixv1.MountTypeBlob, radixv1.MountTypeBlobFuse2FuseCsiAzure, radixv1.MountTypeAzureFileCsiAzure}, v.Type) { + return volumeMountDeprecatedSourceValidationError(ErrVolumeMountInvalidType) + } + + if len(v.RequestsStorage) > 0 { + if _, err := resource.ParseQuantity(v.RequestsStorage); err != nil { + return volumeMountDeprecatedSourceValidationError(fmt.Errorf("%w. %w", ErrVolumeMountInvalidRequestsStorage, err)) + } + } + + switch v.Type { + case radixv1.MountTypeBlob: + if len(v.Container) == 0 { + return volumeMountDeprecatedSourceValidationError(ErrVolumeMountMissingContainer) + } + case radixv1.MountTypeBlobFuse2FuseCsiAzure, radixv1.MountTypeAzureFileCsiAzure: + if len(v.Storage) == 0 { + return volumeMountDeprecatedSourceValidationError(ErrVolumeMountMissingStorage) } } return nil } -func multipleVolumeTypesDefined(volumeMount *radixv1.RadixVolumeMount) bool { - count := 0 - if len(volumeMount.Type) > 0 { - count++ +func validateVolumeMountBlobFuse2(fuse2 *radixv1.RadixBlobFuse2VolumeMount) error { + if !slices.Contains([]radixv1.BlobFuse2Protocol{radixv1.BlobFuse2ProtocolFuse2, radixv1.BlobFuse2ProtocolNfs, ""}, fuse2.Protocol) { + return volumeMountBlobFuse2ValidationError(ErrVolumeMountInvalidProtocol) } - if volumeMount.BlobFuse2 != nil { - count++ + + if len(fuse2.Container) == 0 { + return volumeMountBlobFuse2ValidationError(ErrVolumeMountMissingContainer) } - if volumeMount.AzureFile != nil { - count++ + + if len(fuse2.RequestsStorage) > 0 { + if _, err := resource.ParseQuantity(fuse2.RequestsStorage); err != nil { + return volumeMountBlobFuse2ValidationError(fmt.Errorf("%w. %w", ErrVolumeMountInvalidRequestsStorage, err)) + } + } + return nil +} + +func validateVolumeMountAzureFile(_ *radixv1.RadixAzureFileVolumeMount) error { + return volumeMountAzureFileValidationError(ErrVolumeMountTypeNotImplemented) +} + +func validateVolumeMountEmptyDir(emptyDir *radixv1.RadixEmptyDirVolumeMount) error { + if emptyDir.SizeLimit.IsZero() { + return volumeMountEmptyDirValidationError(ErrVolumeMountMissingSizeLimit) } - return count > 1 + return nil } func validateIdentity(identity *radixv1.Identity) error { @@ -1341,11 +1386,6 @@ func validateExpectedAzureIdentity(azureIdentity radixv1.AzureIdentity) error { return nil } -type volumeMountConfigMaps struct { - names map[string]bool - path map[string]bool -} - func doesComponentExistInEnvironment(app *radixv1.RadixApplication, componentName string, environment string) bool { for _, component := range app.Spec.Components { if component.Name == componentName { diff --git a/pkg/apis/radixvalidators/validate_ra_test.go b/pkg/apis/radixvalidators/validate_ra_test.go index 9c25a8e0f..78e7b193e 100644 --- a/pkg/apis/radixvalidators/validate_ra_test.go +++ b/pkg/apis/radixvalidators/validate_ra_test.go @@ -10,7 +10,7 @@ import ( "github.com/equinor/radix-common/utils/pointers" dnsaliasconfig "github.com/equinor/radix-operator/pkg/apis/config/dnsalias" "github.com/equinor/radix-operator/pkg/apis/defaults" - v1 "github.com/equinor/radix-operator/pkg/apis/radix/v1" + radixv1 "github.com/equinor/radix-operator/pkg/apis/radix/v1" "github.com/equinor/radix-operator/pkg/apis/radixvalidators" commonTest "github.com/equinor/radix-operator/pkg/apis/test" "github.com/equinor/radix-operator/pkg/apis/utils" @@ -18,12 +18,13 @@ import ( radixfake "github.com/equinor/radix-operator/pkg/client/clientset/versioned/fake" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" kubefake "k8s.io/client-go/kubernetes/fake" ) -type updateRAFunc func(rr *v1.RadixApplication) +type updateRAFunc func(rr *radixv1.RadixApplication) func Test_valid_ra_returns_true(t *testing.T) { _, client := validRASetup() @@ -53,9 +54,9 @@ func Test_application_name_casing_is_validated(t *testing.T) { expectedError error updateRa updateRAFunc }{ - {"Mixed case name", radixvalidators.ApplicationNameNotLowercaseErrorWithMessage(mixedCaseName), func(ra *v1.RadixApplication) { ra.Name = mixedCaseName }}, - {"Lower case name", radixvalidators.ApplicationNameNotLowercaseErrorWithMessage(lowerCaseName), func(ra *v1.RadixApplication) { ra.Name = lowerCaseName }}, - {"Upper case name", radixvalidators.ApplicationNameNotLowercaseErrorWithMessage(upperCaseName), func(ra *v1.RadixApplication) { ra.Name = upperCaseName }}, + {"Mixed case name", radixvalidators.ApplicationNameNotLowercaseErrorWithMessage(mixedCaseName), func(ra *radixv1.RadixApplication) { ra.Name = mixedCaseName }}, + {"Lower case name", radixvalidators.ApplicationNameNotLowercaseErrorWithMessage(lowerCaseName), func(ra *radixv1.RadixApplication) { ra.Name = lowerCaseName }}, + {"Upper case name", radixvalidators.ApplicationNameNotLowercaseErrorWithMessage(upperCaseName), func(ra *radixv1.RadixApplication) { ra.Name = upperCaseName }}, } for _, testcase := range testScenarios { @@ -97,7 +98,7 @@ func Test_invalid_ra(t *testing.T) { unsupportedResource := "unsupportedResource" invalidResourceValue := "asdfasd" conflictingVariableName := "some-variable" - invalidCertificateVerification := v1.VerificationType("obviously_an_invalid_value") + invalidCertificateVerification := radixv1.VerificationType("obviously_an_invalid_value") name50charsLong := "a123456789a123456789a123456789a123456789a123456789" var testScenarios = []struct { @@ -105,148 +106,148 @@ func Test_invalid_ra(t *testing.T) { expectedError error updateRA updateRAFunc }{ - {"no error", nil, func(ra *v1.RadixApplication) {}}, - {"too long app name", radixvalidators.InvalidAppNameLengthErrorWithMessage(wayTooLongName), func(ra *v1.RadixApplication) { + {"no error", nil, func(ra *radixv1.RadixApplication) {}}, + {"too long app name", radixvalidators.InvalidAppNameLengthErrorWithMessage(wayTooLongName), func(ra *radixv1.RadixApplication) { ra.Name = wayTooLongName }}, - {"invalid app name", radixvalidators.InvalidLowerCaseAlphaNumericDashResourceNameErrorWithMessage("app name", invalidResourceName), func(ra *v1.RadixApplication) { + {"invalid app name", radixvalidators.InvalidLowerCaseAlphaNumericDashResourceNameErrorWithMessage("app name", invalidResourceName), func(ra *radixv1.RadixApplication) { ra.Name = invalidResourceName }}, - {"empty name", radixvalidators.ResourceNameCannotBeEmptyErrorWithMessage("app name"), func(ra *v1.RadixApplication) { + {"empty name", radixvalidators.ResourceNameCannotBeEmptyErrorWithMessage("app name"), func(ra *radixv1.RadixApplication) { ra.Name = "" }}, - {"no related rr", radixvalidators.NoRegistrationExistsForApplicationErrorWithMessage(noReleatedRRAppName), func(ra *v1.RadixApplication) { + {"no related rr", radixvalidators.NoRegistrationExistsForApplicationErrorWithMessage(noReleatedRRAppName), func(ra *radixv1.RadixApplication) { ra.Name = noReleatedRRAppName }}, - {"non existing env for component", radixvalidators.EnvironmentReferencedByComponentDoesNotExistErrorWithMessage(noExistingEnvironment, validRAFirstComponentName), func(ra *v1.RadixApplication) { - ra.Spec.Components[0].EnvironmentConfig = []v1.RadixEnvironmentConfig{ + {"non existing env for component", radixvalidators.EnvironmentReferencedByComponentDoesNotExistErrorWithMessage(noExistingEnvironment, validRAFirstComponentName), func(ra *radixv1.RadixApplication) { + ra.Spec.Components[0].EnvironmentConfig = []radixv1.RadixEnvironmentConfig{ { Environment: noExistingEnvironment, }, } }}, - {"invalid component name", radixvalidators.InvalidLowerCaseAlphaNumericDashResourceNameErrorWithMessage("component name", invalidResourceName), func(ra *v1.RadixApplication) { + {"invalid component name", radixvalidators.InvalidLowerCaseAlphaNumericDashResourceNameErrorWithMessage("component name", invalidResourceName), func(ra *radixv1.RadixApplication) { ra.Spec.Components[0].Name = invalidResourceName }}, - {"uppercase component name", radixvalidators.InvalidLowerCaseAlphaNumericDashResourceNameErrorWithMessage("component name", invalidUpperCaseResourceName), func(ra *v1.RadixApplication) { + {"uppercase component name", radixvalidators.InvalidLowerCaseAlphaNumericDashResourceNameErrorWithMessage("component name", invalidUpperCaseResourceName), func(ra *radixv1.RadixApplication) { ra.Spec.Components[0].Name = invalidUpperCaseResourceName }}, - {"duplicate component name", radixvalidators.DuplicateComponentOrJobNameErrorWithMessage([]string{validRAFirstComponentName}), func(ra *v1.RadixApplication) { + {"duplicate component name", radixvalidators.DuplicateComponentOrJobNameErrorWithMessage([]string{validRAFirstComponentName}), func(ra *radixv1.RadixApplication) { ra.Spec.Components = append(ra.Spec.Components, *ra.Spec.Components[0].DeepCopy()) }}, - {"component name with oauth auxiliary name suffix", radixvalidators.ComponentNameReservedSuffixErrorWithMessage(oauthAuxSuffixComponentName, "component", defaults.OAuthProxyAuxiliaryComponentSuffix), func(ra *v1.RadixApplication) { + {"component name with oauth auxiliary name suffix", radixvalidators.ComponentNameReservedSuffixErrorWithMessage(oauthAuxSuffixComponentName, "component", defaults.OAuthProxyAuxiliaryComponentSuffix), func(ra *radixv1.RadixApplication) { ra.Spec.Components[0].Name = oauthAuxSuffixComponentName }}, - {"invalid port name", radixvalidators.InvalidLowerCaseAlphaNumericDashResourceNameErrorWithMessage("port name", invalidResourceName), func(ra *v1.RadixApplication) { + {"invalid port name", radixvalidators.InvalidLowerCaseAlphaNumericDashResourceNameErrorWithMessage("port name", invalidResourceName), func(ra *radixv1.RadixApplication) { ra.Spec.Components[0].Ports[0].Name = invalidResourceName }}, - {"too long port name", radixvalidators.InvalidPortNameLengthErrorWithMessage(tooLongPortName), func(ra *v1.RadixApplication) { + {"too long port name", radixvalidators.InvalidPortNameLengthErrorWithMessage(tooLongPortName), func(ra *radixv1.RadixApplication) { ra.Spec.Components[0].PublicPort = tooLongPortName ra.Spec.Components[0].Ports[0].Name = tooLongPortName }}, - {"invalid build secret name", radixvalidators.InvalidResourceNameErrorWithMessage("build secret name", invalidVariableName), func(ra *v1.RadixApplication) { - ra.Spec.Build = &v1.BuildSpec{ + {"invalid build secret name", radixvalidators.InvalidResourceNameErrorWithMessage("build secret name", invalidVariableName), func(ra *radixv1.RadixApplication) { + ra.Spec.Build = &radixv1.BuildSpec{ Secrets: []string{invalidVariableName}, } }}, - {"too long build secret name", radixvalidators.InvalidStringValueMaxLengthErrorWithMessage("build secret name", wayTooLongName, 253), func(ra *v1.RadixApplication) { - ra.Spec.Build = &v1.BuildSpec{ + {"too long build secret name", radixvalidators.InvalidStringValueMaxLengthErrorWithMessage("build secret name", wayTooLongName, 253), func(ra *radixv1.RadixApplication) { + ra.Spec.Build = &radixv1.BuildSpec{ Secrets: []string{wayTooLongName}, } }}, - {"invalid secret name", radixvalidators.InvalidResourceNameErrorWithMessage("secret name", invalidVariableName), func(ra *v1.RadixApplication) { + {"invalid secret name", radixvalidators.InvalidResourceNameErrorWithMessage("secret name", invalidVariableName), func(ra *radixv1.RadixApplication) { ra.Spec.Components[1].Secrets[0] = invalidVariableName }}, - {"too long secret name", radixvalidators.InvalidStringValueMaxLengthErrorWithMessage("secret name", wayTooLongName, 253), func(ra *v1.RadixApplication) { + {"too long secret name", radixvalidators.InvalidStringValueMaxLengthErrorWithMessage("secret name", wayTooLongName, 253), func(ra *radixv1.RadixApplication) { ra.Spec.Components[1].Secrets[0] = wayTooLongName }}, - {"invalid environment variable name", radixvalidators.InvalidResourceNameErrorWithMessage("environment variable name", invalidVariableName), func(ra *v1.RadixApplication) { + {"invalid environment variable name", radixvalidators.InvalidResourceNameErrorWithMessage("environment variable name", invalidVariableName), func(ra *radixv1.RadixApplication) { ra.Spec.Components[1].EnvironmentConfig[0].Variables[invalidVariableName] = "Any value" }}, - {"too long environment variable name", radixvalidators.InvalidStringValueMaxLengthErrorWithMessage("environment variable name", wayTooLongName, 253), func(ra *v1.RadixApplication) { + {"too long environment variable name", radixvalidators.InvalidStringValueMaxLengthErrorWithMessage("environment variable name", wayTooLongName, 253), func(ra *radixv1.RadixApplication) { ra.Spec.Components[1].EnvironmentConfig[0].Variables[wayTooLongName] = "Any value" }}, - {"conflicting variable and secret name", radixvalidators.SecretNameConflictsWithEnvironmentVariableWithMessage(validRASecondComponentName, conflictingVariableName), func(ra *v1.RadixApplication) { + {"conflicting variable and secret name", radixvalidators.SecretNameConflictsWithEnvironmentVariableWithMessage(validRASecondComponentName, conflictingVariableName), func(ra *radixv1.RadixApplication) { ra.Spec.Components[1].EnvironmentConfig[0].Variables[conflictingVariableName] = "Any value" ra.Spec.Components[1].Secrets[0] = conflictingVariableName }}, - {"invalid common environment variable name", radixvalidators.InvalidResourceNameErrorWithMessage("environment variable name", invalidVariableName), func(ra *v1.RadixApplication) { + {"invalid common environment variable name", radixvalidators.InvalidResourceNameErrorWithMessage("environment variable name", invalidVariableName), func(ra *radixv1.RadixApplication) { ra.Spec.Components[1].Variables[invalidVariableName] = "Any value" }}, - {"too long common environment variable name", radixvalidators.InvalidStringValueMaxLengthErrorWithMessage("environment variable name", wayTooLongName, 253), func(ra *v1.RadixApplication) { + {"too long common environment variable name", radixvalidators.InvalidStringValueMaxLengthErrorWithMessage("environment variable name", wayTooLongName, 253), func(ra *radixv1.RadixApplication) { ra.Spec.Components[1].Variables[wayTooLongName] = "Any value" }}, - {"conflicting common variable and secret name", radixvalidators.SecretNameConflictsWithEnvironmentVariableWithMessage(validRASecondComponentName, conflictingVariableName), func(ra *v1.RadixApplication) { + {"conflicting common variable and secret name", radixvalidators.SecretNameConflictsWithEnvironmentVariableWithMessage(validRASecondComponentName, conflictingVariableName), func(ra *radixv1.RadixApplication) { ra.Spec.Components[1].Variables[conflictingVariableName] = "Any value" ra.Spec.Components[1].Secrets[0] = conflictingVariableName }}, - {"conflicting common variable and secret name when not environment config", radixvalidators.SecretNameConflictsWithEnvironmentVariableWithMessage(validRASecondComponentName, conflictingVariableName), func(ra *v1.RadixApplication) { + {"conflicting common variable and secret name when not environment config", radixvalidators.SecretNameConflictsWithEnvironmentVariableWithMessage(validRASecondComponentName, conflictingVariableName), func(ra *radixv1.RadixApplication) { ra.Spec.Components[1].Variables[conflictingVariableName] = "Any value" ra.Spec.Components[1].Secrets[0] = conflictingVariableName ra.Spec.Components[1].EnvironmentConfig = nil }}, - {"invalid number of replicas", radixvalidators.InvalidNumberOfReplicaError(radixvalidators.MaxReplica + 1), func(ra *v1.RadixApplication) { + {"invalid number of replicas", radixvalidators.InvalidNumberOfReplicaError(radixvalidators.MaxReplica + 1), func(ra *radixv1.RadixApplication) { *ra.Spec.Components[0].EnvironmentConfig[0].Replicas = radixvalidators.MaxReplica + 1 }}, - {"invalid env name", radixvalidators.InvalidLowerCaseAlphaNumericDashResourceNameErrorWithMessage("env name", invalidResourceName), func(ra *v1.RadixApplication) { + {"invalid env name", radixvalidators.InvalidLowerCaseAlphaNumericDashResourceNameErrorWithMessage("env name", invalidResourceName), func(ra *radixv1.RadixApplication) { ra.Spec.Environments[0].Name = invalidResourceName }}, - {"invalid branch name", radixvalidators.InvalidBranchNameErrorWithMessage(invalidBranchName), func(ra *v1.RadixApplication) { + {"invalid branch name", radixvalidators.InvalidBranchNameErrorWithMessage(invalidBranchName), func(ra *radixv1.RadixApplication) { ra.Spec.Environments[0].Build.From = invalidBranchName }}, - {"too long branch name", radixvalidators.InvalidStringValueMaxLengthErrorWithMessage("branch from", wayTooLongName, 253), func(ra *v1.RadixApplication) { + {"too long branch name", radixvalidators.InvalidStringValueMaxLengthErrorWithMessage("branch from", wayTooLongName, 253), func(ra *radixv1.RadixApplication) { ra.Spec.Environments[0].Build.From = wayTooLongName }}, - {"dns app alias non existing component", radixvalidators.ComponentForDNSAppAliasNotDefinedError(nonExistingComponent), func(ra *v1.RadixApplication) { + {"dns app alias non existing component", radixvalidators.ComponentForDNSAppAliasNotDefinedError(nonExistingComponent), func(ra *radixv1.RadixApplication) { ra.Spec.DNSAppAlias.Component = nonExistingComponent }}, - {"dns app alias non existing env", radixvalidators.EnvForDNSAppAliasNotDefinedErrorWithMessage(noExistingEnvironment), func(ra *v1.RadixApplication) { + {"dns app alias non existing env", radixvalidators.EnvForDNSAppAliasNotDefinedErrorWithMessage(noExistingEnvironment), func(ra *radixv1.RadixApplication) { ra.Spec.DNSAppAlias.Environment = noExistingEnvironment }}, - {"dns alias is empty", radixvalidators.ResourceNameCannotBeEmptyErrorWithMessage("dnsAlias component"), func(ra *v1.RadixApplication) { + {"dns alias is empty", radixvalidators.ResourceNameCannotBeEmptyErrorWithMessage("dnsAlias component"), func(ra *radixv1.RadixApplication) { ra.Spec.DNSAlias[0].Component = "" }}, - {"dns alias is empty", radixvalidators.ResourceNameCannotBeEmptyErrorWithMessage("dnsAlias environment"), func(ra *v1.RadixApplication) { + {"dns alias is empty", radixvalidators.ResourceNameCannotBeEmptyErrorWithMessage("dnsAlias environment"), func(ra *radixv1.RadixApplication) { ra.Spec.DNSAlias[0].Environment = "" }}, - {"dns alias is invalid", radixvalidators.InvalidLowerCaseAlphaNumericDashResourceNameErrorWithMessage("dnsAlias component", "component.abc"), func(ra *v1.RadixApplication) { + {"dns alias is invalid", radixvalidators.InvalidLowerCaseAlphaNumericDashResourceNameErrorWithMessage("dnsAlias component", "component.abc"), func(ra *radixv1.RadixApplication) { ra.Spec.DNSAlias[0].Component = "component.abc" }}, - {"dns alias is invalid", radixvalidators.InvalidLowerCaseAlphaNumericDashResourceNameErrorWithMessage("dnsAlias environment", "environment.abc"), func(ra *v1.RadixApplication) { + {"dns alias is invalid", radixvalidators.InvalidLowerCaseAlphaNumericDashResourceNameErrorWithMessage("dnsAlias environment", "environment.abc"), func(ra *radixv1.RadixApplication) { ra.Spec.DNSAlias[0].Environment = "environment.abc" }}, - {"dns alias non existing component", radixvalidators.ComponentForDNSAliasNotDefinedError(nonExistingComponent), func(ra *v1.RadixApplication) { + {"dns alias non existing component", radixvalidators.ComponentForDNSAliasNotDefinedError(nonExistingComponent), func(ra *radixv1.RadixApplication) { ra.Spec.DNSAlias[0].Component = nonExistingComponent }}, - {"dns alias non existing env", radixvalidators.EnvForDNSAliasNotDefinedError(noExistingEnvironment), func(ra *v1.RadixApplication) { + {"dns alias non existing env", radixvalidators.EnvForDNSAliasNotDefinedError(noExistingEnvironment), func(ra *radixv1.RadixApplication) { ra.Spec.DNSAlias[0].Environment = noExistingEnvironment }}, - {"dns alias alias is empty", radixvalidators.ResourceNameCannotBeEmptyErrorWithMessage("dnsAlias alias"), func(ra *v1.RadixApplication) { + {"dns alias alias is empty", radixvalidators.ResourceNameCannotBeEmptyErrorWithMessage("dnsAlias alias"), func(ra *radixv1.RadixApplication) { ra.Spec.DNSAlias[0].Alias = "" }}, - {"dns alias alias is invalid", radixvalidators.InvalidLowerCaseAlphaNumericDashResourceNameErrorWithMessage("dnsAlias alias", "my.alias"), func(ra *v1.RadixApplication) { + {"dns alias alias is invalid", radixvalidators.InvalidLowerCaseAlphaNumericDashResourceNameErrorWithMessage("dnsAlias alias", "my.alias"), func(ra *radixv1.RadixApplication) { ra.Spec.DNSAlias[0].Alias = "my.alias" }}, - {"dns alias alias is invalid", radixvalidators.DuplicateAliasForDNSAliasError("my-alias"), func(ra *v1.RadixApplication) { + {"dns alias alias is invalid", radixvalidators.DuplicateAliasForDNSAliasError("my-alias"), func(ra *radixv1.RadixApplication) { ra.Spec.DNSAlias = append(ra.Spec.DNSAlias, ra.Spec.DNSAlias[0]) }}, - {"dns alias with no public port", radixvalidators.ComponentForDNSAliasIsNotMarkedAsPublicError(validRAComponentNameApp2), func(ra *v1.RadixApplication) { + {"dns alias with no public port", radixvalidators.ComponentForDNSAliasIsNotMarkedAsPublicError(validRAComponentNameApp2), func(ra *radixv1.RadixApplication) { ra.Spec.Components[3].PublicPort = "" ra.Spec.Components[3].Public = false - ra.Spec.DNSAlias[0] = v1.DNSAlias{ + ra.Spec.DNSAlias[0] = radixv1.DNSAlias{ Alias: "my-alias", Component: ra.Spec.Components[3].Name, Environment: ra.Spec.Environments[0].Name, } }}, - {"dns alias non existing component", radixvalidators.ComponentForDNSAppAliasNotDefinedError(nonExistingComponent), func(ra *v1.RadixApplication) { + {"dns alias non existing component", radixvalidators.ComponentForDNSAppAliasNotDefinedError(nonExistingComponent), func(ra *radixv1.RadixApplication) { ra.Spec.DNSAppAlias.Component = nonExistingComponent }}, - {"dns alias non existing env", radixvalidators.EnvForDNSAppAliasNotDefinedErrorWithMessage(noExistingEnvironment), func(ra *v1.RadixApplication) { + {"dns alias non existing env", radixvalidators.EnvForDNSAppAliasNotDefinedErrorWithMessage(noExistingEnvironment), func(ra *radixv1.RadixApplication) { ra.Spec.DNSAppAlias.Environment = noExistingEnvironment }}, - {"dns external alias non existing component", radixvalidators.ComponentForDNSExternalAliasNotDefinedErrorWithMessage(nonExistingComponent), func(ra *v1.RadixApplication) { - ra.Spec.DNSExternalAlias = []v1.ExternalAlias{ + {"dns external alias non existing component", radixvalidators.ComponentForDNSExternalAliasNotDefinedErrorWithMessage(nonExistingComponent), func(ra *radixv1.RadixApplication) { + ra.Spec.DNSExternalAlias = []radixv1.ExternalAlias{ { Alias: "some.alias.com", Component: nonExistingComponent, @@ -254,8 +255,8 @@ func Test_invalid_ra(t *testing.T) { }, } }}, - {"dns external alias non existing environment", radixvalidators.EnvForDNSExternalAliasNotDefinedErrorWithMessage(noExistingEnvironment), func(ra *v1.RadixApplication) { - ra.Spec.DNSExternalAlias = []v1.ExternalAlias{ + {"dns external alias non existing environment", radixvalidators.EnvForDNSExternalAliasNotDefinedErrorWithMessage(noExistingEnvironment), func(ra *radixv1.RadixApplication) { + ra.Spec.DNSExternalAlias = []radixv1.ExternalAlias{ { Alias: "some.alias.com", Component: ra.Spec.Components[0].Name, @@ -263,19 +264,19 @@ func Test_invalid_ra(t *testing.T) { }, } }}, - {"dns external alias non existing alias", radixvalidators.ErrExternalAliasCannotBeEmpty, func(ra *v1.RadixApplication) { - ra.Spec.DNSExternalAlias = []v1.ExternalAlias{ + {"dns external alias non existing alias", radixvalidators.ErrExternalAliasCannotBeEmpty, func(ra *radixv1.RadixApplication) { + ra.Spec.DNSExternalAlias = []radixv1.ExternalAlias{ { Component: ra.Spec.Components[0].Name, Environment: ra.Spec.Environments[0].Name, }, } }}, - {"dns external alias with no public port", radixvalidators.ComponentForDNSExternalAliasIsNotMarkedAsPublicErrorWithMessage(validRAFirstComponentName), func(ra *v1.RadixApplication) { + {"dns external alias with no public port", radixvalidators.ComponentForDNSExternalAliasIsNotMarkedAsPublicErrorWithMessage(validRAFirstComponentName), func(ra *radixv1.RadixApplication) { // Backward compatible setting ra.Spec.Components[0].Public = false ra.Spec.Components[0].PublicPort = "" - ra.Spec.DNSExternalAlias = []v1.ExternalAlias{ + ra.Spec.DNSExternalAlias = []radixv1.ExternalAlias{ { Alias: "some.alias.com", Component: ra.Spec.Components[0].Name, @@ -283,9 +284,9 @@ func Test_invalid_ra(t *testing.T) { }, } }}, - {"duplicate dns external alias", radixvalidators.DuplicateExternalAliasErrorWithMessage(), func(ra *v1.RadixApplication) { + {"duplicate dns external alias", radixvalidators.DuplicateExternalAliasErrorWithMessage(), func(ra *radixv1.RadixApplication) { ra.Spec.Components[0].Public = true - ra.Spec.DNSExternalAlias = []v1.ExternalAlias{ + ra.Spec.DNSExternalAlias = []radixv1.ExternalAlias{ { Alias: "duplicate.alias.com", Component: ra.Spec.Components[0].Name, @@ -298,346 +299,346 @@ func Test_invalid_ra(t *testing.T) { }, } }}, - {"resource limit unsupported resource", radixvalidators.InvalidResourceErrorWithMessage(unsupportedResource), func(ra *v1.RadixApplication) { + {"resource limit unsupported resource", radixvalidators.InvalidResourceErrorWithMessage(unsupportedResource), func(ra *radixv1.RadixApplication) { ra.Spec.Components[0].EnvironmentConfig[0].Resources.Limits[unsupportedResource] = "250m" }}, - {"memory resource limit wrong format", radixvalidators.MemoryResourceRequirementFormatErrorWithMessage(invalidResourceValue), func(ra *v1.RadixApplication) { + {"memory resource limit wrong format", radixvalidators.MemoryResourceRequirementFormatErrorWithMessage(invalidResourceValue), func(ra *radixv1.RadixApplication) { ra.Spec.Components[0].EnvironmentConfig[0].Resources.Limits["memory"] = invalidResourceValue }}, - {"memory resource request wrong format", radixvalidators.MemoryResourceRequirementFormatErrorWithMessage(invalidResourceValue), func(ra *v1.RadixApplication) { + {"memory resource request wrong format", radixvalidators.MemoryResourceRequirementFormatErrorWithMessage(invalidResourceValue), func(ra *radixv1.RadixApplication) { ra.Spec.Components[0].EnvironmentConfig[0].Resources.Requests["memory"] = invalidResourceValue }}, - {"memory resource request larger than limit", radixvalidators.ResourceRequestOverLimitErrorWithMessage("memory", "249Mi", "250Ki"), func(ra *v1.RadixApplication) { + {"memory resource request larger than limit", radixvalidators.ResourceRequestOverLimitErrorWithMessage("memory", "249Mi", "250Ki"), func(ra *radixv1.RadixApplication) { ra.Spec.Components[0].EnvironmentConfig[0].Resources.Limits["memory"] = "250Ki" ra.Spec.Components[0].EnvironmentConfig[0].Resources.Requests["memory"] = "249Mi" }}, - {"cpu resource limit wrong format", radixvalidators.CPUResourceRequirementFormatErrorWithMessage(invalidResourceValue), func(ra *v1.RadixApplication) { + {"cpu resource limit wrong format", radixvalidators.CPUResourceRequirementFormatErrorWithMessage(invalidResourceValue), func(ra *radixv1.RadixApplication) { ra.Spec.Components[0].EnvironmentConfig[0].Resources.Limits["cpu"] = invalidResourceValue }}, - {"cpu resource request wrong format", radixvalidators.CPUResourceRequirementFormatErrorWithMessage(invalidResourceValue), func(ra *v1.RadixApplication) { + {"cpu resource request wrong format", radixvalidators.CPUResourceRequirementFormatErrorWithMessage(invalidResourceValue), func(ra *radixv1.RadixApplication) { ra.Spec.Components[0].EnvironmentConfig[0].Resources.Requests["cpu"] = invalidResourceValue }}, - {"cpu resource request larger than limit", radixvalidators.ResourceRequestOverLimitErrorWithMessage("cpu", "251m", "250m"), func(ra *v1.RadixApplication) { + {"cpu resource request larger than limit", radixvalidators.ResourceRequestOverLimitErrorWithMessage("cpu", "251m", "250m"), func(ra *radixv1.RadixApplication) { ra.Spec.Components[0].EnvironmentConfig[0].Resources.Limits["cpu"] = "250m" ra.Spec.Components[0].EnvironmentConfig[0].Resources.Requests["cpu"] = "251m" }}, - {"resource request unsupported resource", radixvalidators.InvalidResourceErrorWithMessage(unsupportedResource), func(ra *v1.RadixApplication) { + {"resource request unsupported resource", radixvalidators.InvalidResourceErrorWithMessage(unsupportedResource), func(ra *radixv1.RadixApplication) { ra.Spec.Components[0].EnvironmentConfig[0].Resources.Requests[unsupportedResource] = "250m" }}, - {"common resource limit unsupported resource", radixvalidators.InvalidResourceErrorWithMessage(unsupportedResource), func(ra *v1.RadixApplication) { + {"common resource limit unsupported resource", radixvalidators.InvalidResourceErrorWithMessage(unsupportedResource), func(ra *radixv1.RadixApplication) { ra.Spec.Components[0].Resources.Limits[unsupportedResource] = "250m" }}, - {"common resource request unsupported resource", radixvalidators.InvalidResourceErrorWithMessage(unsupportedResource), func(ra *v1.RadixApplication) { + {"common resource request unsupported resource", radixvalidators.InvalidResourceErrorWithMessage(unsupportedResource), func(ra *radixv1.RadixApplication) { ra.Spec.Components[0].Resources.Requests[unsupportedResource] = "250m" }}, - {"common memory resource limit wrong format", radixvalidators.MemoryResourceRequirementFormatErrorWithMessage(invalidResourceValue), func(ra *v1.RadixApplication) { + {"common memory resource limit wrong format", radixvalidators.MemoryResourceRequirementFormatErrorWithMessage(invalidResourceValue), func(ra *radixv1.RadixApplication) { ra.Spec.Components[0].Resources.Limits["memory"] = invalidResourceValue }}, - {"common memory resource request wrong format", radixvalidators.MemoryResourceRequirementFormatErrorWithMessage(invalidResourceValue), func(ra *v1.RadixApplication) { + {"common memory resource request wrong format", radixvalidators.MemoryResourceRequirementFormatErrorWithMessage(invalidResourceValue), func(ra *radixv1.RadixApplication) { ra.Spec.Components[0].Resources.Requests["memory"] = invalidResourceValue }}, - {"common cpu resource limit wrong format", radixvalidators.CPUResourceRequirementFormatErrorWithMessage(invalidResourceValue), func(ra *v1.RadixApplication) { + {"common cpu resource limit wrong format", radixvalidators.CPUResourceRequirementFormatErrorWithMessage(invalidResourceValue), func(ra *radixv1.RadixApplication) { ra.Spec.Components[0].Resources.Limits["cpu"] = invalidResourceValue }}, - {"common cpu resource request wrong format", radixvalidators.CPUResourceRequirementFormatErrorWithMessage(invalidResourceValue), func(ra *v1.RadixApplication) { + {"common cpu resource request wrong format", radixvalidators.CPUResourceRequirementFormatErrorWithMessage(invalidResourceValue), func(ra *radixv1.RadixApplication) { ra.Spec.Components[0].Resources.Requests["cpu"] = invalidResourceValue }}, - {"cpu resource limit is empty", nil, func(ra *v1.RadixApplication) { + {"cpu resource limit is empty", nil, func(ra *radixv1.RadixApplication) { ra.Spec.Components[0].EnvironmentConfig[0].Resources.Limits["cpu"] = "" ra.Spec.Components[0].EnvironmentConfig[0].Resources.Requests["cpu"] = "251m" }}, - {"cpu resource limit not set", nil, func(ra *v1.RadixApplication) { + {"cpu resource limit not set", nil, func(ra *radixv1.RadixApplication) { ra.Spec.Components[0].EnvironmentConfig[0].Resources.Requests["cpu"] = "251m" }}, - {"memory resource limit is empty", nil, func(ra *v1.RadixApplication) { + {"memory resource limit is empty", nil, func(ra *radixv1.RadixApplication) { ra.Spec.Components[0].EnvironmentConfig[0].Resources.Limits["memory"] = "" ra.Spec.Components[0].EnvironmentConfig[0].Resources.Requests["memory"] = "249Mi" }}, - {"memory resource limit not set", nil, func(ra *v1.RadixApplication) { + {"memory resource limit not set", nil, func(ra *radixv1.RadixApplication) { ra.Spec.Components[0].EnvironmentConfig[0].Resources.Requests["memory"] = "249Mi" }}, - {"wrong public image config", radixvalidators.PublicImageComponentCannotHaveSourceOrDockerfileSetWithMessage(validRAFirstComponentName), func(ra *v1.RadixApplication) { + {"wrong public image config", radixvalidators.PublicImageComponentCannotHaveSourceOrDockerfileSetWithMessage(validRAFirstComponentName), func(ra *radixv1.RadixApplication) { ra.Spec.Components[0].Image = "redis:5.0-alpine" ra.Spec.Components[0].SourceFolder = "./api" ra.Spec.Components[0].DockerfileName = ".Dockerfile" }}, - {"inconcistent dynamic tag config for environment", radixvalidators.ComponentWithTagInEnvironmentConfigForEnvironmentRequiresDynamicTagWithMessage(validRAFirstComponentName, "prod"), func(ra *v1.RadixApplication) { + {"inconcistent dynamic tag config for environment", radixvalidators.ComponentWithTagInEnvironmentConfigForEnvironmentRequiresDynamicTagWithMessage(validRAFirstComponentName, "prod"), func(ra *radixv1.RadixApplication) { ra.Spec.Components[0].Image = "radixcanary.azurecr.io/my-private-image:some-tag" ra.Spec.Components[0].EnvironmentConfig[0].ImageTagName = "any-tag" }}, - {"invalid verificationType for component", radixvalidators.InvalidVerificationTypeWithMessage(string(invalidCertificateVerification)), func(ra *v1.RadixApplication) { - ra.Spec.Components[0].Authentication = &v1.Authentication{ - ClientCertificate: &v1.ClientCertificate{ + {"invalid verificationType for component", radixvalidators.InvalidVerificationTypeWithMessage(string(invalidCertificateVerification)), func(ra *radixv1.RadixApplication) { + ra.Spec.Components[0].Authentication = &radixv1.Authentication{ + ClientCertificate: &radixv1.ClientCertificate{ Verification: &invalidCertificateVerification, }, } }}, - {"invalid verificationType for environment", radixvalidators.InvalidVerificationTypeWithMessage(string(invalidCertificateVerification)), func(ra *v1.RadixApplication) { - ra.Spec.Components[0].EnvironmentConfig[0].Authentication = &v1.Authentication{ - ClientCertificate: &v1.ClientCertificate{ + {"invalid verificationType for environment", radixvalidators.InvalidVerificationTypeWithMessage(string(invalidCertificateVerification)), func(ra *radixv1.RadixApplication) { + ra.Spec.Components[0].EnvironmentConfig[0].Authentication = &radixv1.Authentication{ + ClientCertificate: &radixv1.ClientCertificate{ Verification: &invalidCertificateVerification, }, } }}, - {"duplicate job name", radixvalidators.DuplicateComponentOrJobNameErrorWithMessage([]string{validRAFirstJobName}), func(ra *v1.RadixApplication) { + {"duplicate job name", radixvalidators.DuplicateComponentOrJobNameErrorWithMessage([]string{validRAFirstJobName}), func(ra *radixv1.RadixApplication) { ra.Spec.Jobs = append(ra.Spec.Jobs, *ra.Spec.Jobs[0].DeepCopy()) }}, - {"job name with oauth auxiliary name suffix", radixvalidators.ComponentNameReservedSuffixErrorWithMessage(oauthAuxSuffixJobName, "job", defaults.OAuthProxyAuxiliaryComponentSuffix), func(ra *v1.RadixApplication) { + {"job name with oauth auxiliary name suffix", radixvalidators.ComponentNameReservedSuffixErrorWithMessage(oauthAuxSuffixJobName, "job", defaults.OAuthProxyAuxiliaryComponentSuffix), func(ra *radixv1.RadixApplication) { ra.Spec.Jobs[0].Name = oauthAuxSuffixJobName }}, - {"invalid job secret name", radixvalidators.InvalidResourceNameErrorWithMessage("secret name", invalidVariableName), func(ra *v1.RadixApplication) { + {"invalid job secret name", radixvalidators.InvalidResourceNameErrorWithMessage("secret name", invalidVariableName), func(ra *radixv1.RadixApplication) { ra.Spec.Jobs[0].Secrets[0] = invalidVariableName }}, - {"too long job secret name", radixvalidators.InvalidStringValueMaxLengthErrorWithMessage("secret name", wayTooLongName, 253), func(ra *v1.RadixApplication) { + {"too long job secret name", radixvalidators.InvalidStringValueMaxLengthErrorWithMessage("secret name", wayTooLongName, 253), func(ra *radixv1.RadixApplication) { ra.Spec.Jobs[0].Secrets[0] = wayTooLongName }}, - {"invalid job common environment variable name", radixvalidators.InvalidResourceNameErrorWithMessage("environment variable name", invalidVariableName), func(ra *v1.RadixApplication) { + {"invalid job common environment variable name", radixvalidators.InvalidResourceNameErrorWithMessage("environment variable name", invalidVariableName), func(ra *radixv1.RadixApplication) { ra.Spec.Jobs[0].Variables[invalidVariableName] = "Any value" }}, - {"too long job common environment variable name", radixvalidators.InvalidStringValueMaxLengthErrorWithMessage("environment variable name", wayTooLongName, 253), func(ra *v1.RadixApplication) { + {"too long job common environment variable name", radixvalidators.InvalidStringValueMaxLengthErrorWithMessage("environment variable name", wayTooLongName, 253), func(ra *radixv1.RadixApplication) { ra.Spec.Jobs[0].Variables[wayTooLongName] = "Any value" }}, - {"invalid job environment variable name", radixvalidators.InvalidResourceNameErrorWithMessage("environment variable name", invalidVariableName), func(ra *v1.RadixApplication) { + {"invalid job environment variable name", radixvalidators.InvalidResourceNameErrorWithMessage("environment variable name", invalidVariableName), func(ra *radixv1.RadixApplication) { ra.Spec.Jobs[0].EnvironmentConfig[0].Variables[invalidVariableName] = "Any value" }}, - {"too long job environment variable name", radixvalidators.InvalidStringValueMaxLengthErrorWithMessage("environment variable name", wayTooLongName, 253), func(ra *v1.RadixApplication) { + {"too long job environment variable name", radixvalidators.InvalidStringValueMaxLengthErrorWithMessage("environment variable name", wayTooLongName, 253), func(ra *radixv1.RadixApplication) { ra.Spec.Jobs[0].EnvironmentConfig[0].Variables[wayTooLongName] = "Any value" }}, - {"conflicting job variable and secret name", radixvalidators.SecretNameConflictsWithEnvironmentVariableWithMessage("job", conflictingVariableName), func(ra *v1.RadixApplication) { + {"conflicting job variable and secret name", radixvalidators.SecretNameConflictsWithEnvironmentVariableWithMessage("job", conflictingVariableName), func(ra *radixv1.RadixApplication) { ra.Spec.Jobs[0].EnvironmentConfig[0].Variables[conflictingVariableName] = "Any value" ra.Spec.Jobs[0].Secrets[0] = conflictingVariableName }}, - {"non existing env for job", radixvalidators.EnvironmentReferencedByComponentDoesNotExistErrorWithMessage(noExistingEnvironment, validRAFirstJobName), func(ra *v1.RadixApplication) { - ra.Spec.Jobs[0].EnvironmentConfig = []v1.RadixJobComponentEnvironmentConfig{ + {"non existing env for job", radixvalidators.EnvironmentReferencedByComponentDoesNotExistErrorWithMessage(noExistingEnvironment, validRAFirstJobName), func(ra *radixv1.RadixApplication) { + ra.Spec.Jobs[0].EnvironmentConfig = []radixv1.RadixJobComponentEnvironmentConfig{ { Environment: noExistingEnvironment, }, } }}, - {"scheduler port is not set", radixvalidators.SchedulerPortCannotBeEmptyForJobErrorWithMessage(validRAFirstJobName), func(ra *v1.RadixApplication) { + {"scheduler port is not set", radixvalidators.SchedulerPortCannotBeEmptyForJobErrorWithMessage(validRAFirstJobName), func(ra *radixv1.RadixApplication) { ra.Spec.Jobs[0].SchedulerPort = nil }}, - {"payload is empty struct", radixvalidators.PayloadPathCannotBeEmptyForJobErrorWithMessage(validRAFirstJobName), func(ra *v1.RadixApplication) { - ra.Spec.Jobs[0].Payload = &v1.RadixJobComponentPayload{} + {"payload is empty struct", radixvalidators.PayloadPathCannotBeEmptyForJobErrorWithMessage(validRAFirstJobName), func(ra *radixv1.RadixApplication) { + ra.Spec.Jobs[0].Payload = &radixv1.RadixJobComponentPayload{} }}, - {"payload path is empty string", radixvalidators.PayloadPathCannotBeEmptyForJobErrorWithMessage(validRAFirstJobName), func(ra *v1.RadixApplication) { - ra.Spec.Jobs[0].Payload = &v1.RadixJobComponentPayload{Path: ""} + {"payload path is empty string", radixvalidators.PayloadPathCannotBeEmptyForJobErrorWithMessage(validRAFirstJobName), func(ra *radixv1.RadixApplication) { + ra.Spec.Jobs[0].Payload = &radixv1.RadixJobComponentPayload{Path: ""} }}, - {"job resource limit unsupported resource", radixvalidators.InvalidResourceErrorWithMessage(unsupportedResource), func(ra *v1.RadixApplication) { + {"job resource limit unsupported resource", radixvalidators.InvalidResourceErrorWithMessage(unsupportedResource), func(ra *radixv1.RadixApplication) { ra.Spec.Jobs[0].EnvironmentConfig[0].Resources.Limits[unsupportedResource] = "250m" }}, - {"job memory resource limit wrong format", radixvalidators.MemoryResourceRequirementFormatErrorWithMessage(invalidResourceValue), func(ra *v1.RadixApplication) { + {"job memory resource limit wrong format", radixvalidators.MemoryResourceRequirementFormatErrorWithMessage(invalidResourceValue), func(ra *radixv1.RadixApplication) { ra.Spec.Jobs[0].EnvironmentConfig[0].Resources.Limits["memory"] = invalidResourceValue }}, - {"job memory resource request wrong format", radixvalidators.MemoryResourceRequirementFormatErrorWithMessage(invalidResourceValue), func(ra *v1.RadixApplication) { + {"job memory resource request wrong format", radixvalidators.MemoryResourceRequirementFormatErrorWithMessage(invalidResourceValue), func(ra *radixv1.RadixApplication) { ra.Spec.Jobs[0].EnvironmentConfig[0].Resources.Requests["memory"] = invalidResourceValue }}, - {"job memory resource request larger than limit", radixvalidators.ResourceRequestOverLimitErrorWithMessage("memory", "249Mi", "250Ki"), func(ra *v1.RadixApplication) { + {"job memory resource request larger than limit", radixvalidators.ResourceRequestOverLimitErrorWithMessage("memory", "249Mi", "250Ki"), func(ra *radixv1.RadixApplication) { ra.Spec.Jobs[0].EnvironmentConfig[0].Resources.Limits["memory"] = "250Ki" ra.Spec.Jobs[0].EnvironmentConfig[0].Resources.Requests["memory"] = "249Mi" }}, - {"job cpu resource limit wrong format", radixvalidators.CPUResourceRequirementFormatErrorWithMessage(invalidResourceValue), func(ra *v1.RadixApplication) { + {"job cpu resource limit wrong format", radixvalidators.CPUResourceRequirementFormatErrorWithMessage(invalidResourceValue), func(ra *radixv1.RadixApplication) { ra.Spec.Jobs[0].EnvironmentConfig[0].Resources.Limits["cpu"] = invalidResourceValue }}, - {"job cpu resource request wrong format", radixvalidators.CPUResourceRequirementFormatErrorWithMessage(invalidResourceValue), func(ra *v1.RadixApplication) { + {"job cpu resource request wrong format", radixvalidators.CPUResourceRequirementFormatErrorWithMessage(invalidResourceValue), func(ra *radixv1.RadixApplication) { ra.Spec.Jobs[0].EnvironmentConfig[0].Resources.Requests["cpu"] = invalidResourceValue }}, - {"job cpu resource request larger than limit", radixvalidators.ResourceRequestOverLimitErrorWithMessage("cpu", "251m", "250m"), func(ra *v1.RadixApplication) { + {"job cpu resource request larger than limit", radixvalidators.ResourceRequestOverLimitErrorWithMessage("cpu", "251m", "250m"), func(ra *radixv1.RadixApplication) { ra.Spec.Jobs[0].EnvironmentConfig[0].Resources.Limits["cpu"] = "250m" ra.Spec.Jobs[0].EnvironmentConfig[0].Resources.Requests["cpu"] = "251m" }}, - {"job resource request unsupported resource", radixvalidators.InvalidResourceErrorWithMessage(unsupportedResource), func(ra *v1.RadixApplication) { + {"job resource request unsupported resource", radixvalidators.InvalidResourceErrorWithMessage(unsupportedResource), func(ra *radixv1.RadixApplication) { ra.Spec.Jobs[0].EnvironmentConfig[0].Resources.Requests[unsupportedResource] = "250m" }}, - {"job common resource limit unsupported resource", radixvalidators.InvalidResourceErrorWithMessage(unsupportedResource), func(ra *v1.RadixApplication) { + {"job common resource limit unsupported resource", radixvalidators.InvalidResourceErrorWithMessage(unsupportedResource), func(ra *radixv1.RadixApplication) { ra.Spec.Jobs[0].Resources.Limits[unsupportedResource] = "250m" }}, - {"job common resource request unsupported resource", radixvalidators.InvalidResourceErrorWithMessage(unsupportedResource), func(ra *v1.RadixApplication) { + {"job common resource request unsupported resource", radixvalidators.InvalidResourceErrorWithMessage(unsupportedResource), func(ra *radixv1.RadixApplication) { ra.Spec.Jobs[0].Resources.Requests[unsupportedResource] = "250m" }}, - {"job common memory resource limit wrong format", radixvalidators.MemoryResourceRequirementFormatErrorWithMessage(invalidResourceValue), func(ra *v1.RadixApplication) { + {"job common memory resource limit wrong format", radixvalidators.MemoryResourceRequirementFormatErrorWithMessage(invalidResourceValue), func(ra *radixv1.RadixApplication) { ra.Spec.Jobs[0].Resources.Limits["memory"] = invalidResourceValue }}, - {"job common memory resource request wrong format", radixvalidators.MemoryResourceRequirementFormatErrorWithMessage(invalidResourceValue), func(ra *v1.RadixApplication) { + {"job common memory resource request wrong format", radixvalidators.MemoryResourceRequirementFormatErrorWithMessage(invalidResourceValue), func(ra *radixv1.RadixApplication) { ra.Spec.Jobs[0].Resources.Requests["memory"] = invalidResourceValue }}, - {"job common cpu resource limit wrong format", radixvalidators.CPUResourceRequirementFormatErrorWithMessage(invalidResourceValue), func(ra *v1.RadixApplication) { + {"job common cpu resource limit wrong format", radixvalidators.CPUResourceRequirementFormatErrorWithMessage(invalidResourceValue), func(ra *radixv1.RadixApplication) { ra.Spec.Jobs[0].Resources.Limits["cpu"] = invalidResourceValue }}, - {"job common cpu resource request wrong format", radixvalidators.CPUResourceRequirementFormatErrorWithMessage(invalidResourceValue), func(ra *v1.RadixApplication) { + {"job common cpu resource request wrong format", radixvalidators.CPUResourceRequirementFormatErrorWithMessage(invalidResourceValue), func(ra *radixv1.RadixApplication) { ra.Spec.Jobs[0].Resources.Requests["cpu"] = invalidResourceValue }}, - {"job cpu resource limit is empty", nil, func(ra *v1.RadixApplication) { + {"job cpu resource limit is empty", nil, func(ra *radixv1.RadixApplication) { ra.Spec.Jobs[0].EnvironmentConfig[0].Resources.Limits["cpu"] = "" ra.Spec.Jobs[0].EnvironmentConfig[0].Resources.Requests["cpu"] = "251m" }}, - {"job cpu resource limit not set", nil, func(ra *v1.RadixApplication) { + {"job cpu resource limit not set", nil, func(ra *radixv1.RadixApplication) { ra.Spec.Jobs[0].EnvironmentConfig[0].Resources.Requests["cpu"] = "251m" }}, - {"job memory resource limit is empty", nil, func(ra *v1.RadixApplication) { + {"job memory resource limit is empty", nil, func(ra *radixv1.RadixApplication) { ra.Spec.Jobs[0].EnvironmentConfig[0].Resources.Limits["memory"] = "" ra.Spec.Jobs[0].EnvironmentConfig[0].Resources.Requests["memory"] = "249Mi" }}, - {"job memory resource limit not set", nil, func(ra *v1.RadixApplication) { + {"job memory resource limit not set", nil, func(ra *radixv1.RadixApplication) { ra.Spec.Jobs[0].EnvironmentConfig[0].Resources.Requests["memory"] = "249Mi" }}, - {"job wrong public image config", radixvalidators.PublicImageComponentCannotHaveSourceOrDockerfileSetWithMessage(validRAFirstJobName), func(ra *v1.RadixApplication) { + {"job wrong public image config", radixvalidators.PublicImageComponentCannotHaveSourceOrDockerfileSetWithMessage(validRAFirstJobName), func(ra *radixv1.RadixApplication) { ra.Spec.Jobs[0].Image = "redis:5.0-alpine" ra.Spec.Jobs[0].SourceFolder = "./api" ra.Spec.Jobs[0].DockerfileName = ".Dockerfile" }}, - {"job inconcistent dynamic tag config for environment", radixvalidators.ComponentWithTagInEnvironmentConfigForEnvironmentRequiresDynamicTagWithMessage(validRAFirstJobName, "dev"), func(ra *v1.RadixApplication) { + {"job inconcistent dynamic tag config for environment", radixvalidators.ComponentWithTagInEnvironmentConfigForEnvironmentRequiresDynamicTagWithMessage(validRAFirstJobName, "dev"), func(ra *radixv1.RadixApplication) { ra.Spec.Jobs[0].Image = "radixcanary.azurecr.io/my-private-image:some-tag" ra.Spec.Jobs[0].EnvironmentConfig[0].ImageTagName = "any-tag" }}, - {"too long app name together with env name", fmt.Errorf("summary length of app name and environment together should not exceed 62 characters"), func(ra *v1.RadixApplication) { + {"too long app name together with env name", fmt.Errorf("summary length of app name and environment together should not exceed 62 characters"), func(ra *radixv1.RadixApplication) { ra.Name = name50charsLong - ra.Spec.Environments = append(ra.Spec.Environments, v1.Environment{Name: "extra-14-chars"}) + ra.Spec.Environments = append(ra.Spec.Environments, radixv1.Environment{Name: "extra-14-chars"}) }}, - {"missing OAuth clientId for dev env - common OAuth config", radixvalidators.OAuthClientIdEmptyErrorWithMessage(validRAFirstComponentName, "dev"), func(rr *v1.RadixApplication) { - rr.Spec.Components[0].Authentication.OAuth2 = &v1.OAuth2{} + {"missing OAuth clientId for dev env - common OAuth config", radixvalidators.OAuthClientIdEmptyErrorWithMessage(validRAFirstComponentName, "dev"), func(rr *radixv1.RadixApplication) { + rr.Spec.Components[0].Authentication.OAuth2 = &radixv1.OAuth2{} }}, - {"missing OAuth clientId for prod env - environmentConfig OAuth config", radixvalidators.OAuthClientIdEmptyErrorWithMessage(validRAFirstComponentName, "prod"), func(rr *v1.RadixApplication) { + {"missing OAuth clientId for prod env - environmentConfig OAuth config", radixvalidators.OAuthClientIdEmptyErrorWithMessage(validRAFirstComponentName, "prod"), func(rr *radixv1.RadixApplication) { rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.ClientID = "" }}, - {"OAuth path prefix is root", radixvalidators.OAuthProxyPrefixIsRootErrorWithMessage(validRAFirstComponentName, "prod"), func(rr *v1.RadixApplication) { + {"OAuth path prefix is root", radixvalidators.OAuthProxyPrefixIsRootErrorWithMessage(validRAFirstComponentName, "prod"), func(rr *radixv1.RadixApplication) { rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.ProxyPrefix = "/" }}, - {"invalid OAuth session store type", radixvalidators.OAuthSessionStoreTypeInvalidErrorWithMessage(validRAFirstComponentName, "prod", "invalid-store"), func(rr *v1.RadixApplication) { + {"invalid OAuth session store type", radixvalidators.OAuthSessionStoreTypeInvalidErrorWithMessage(validRAFirstComponentName, "prod", "invalid-store"), func(rr *radixv1.RadixApplication) { rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.SessionStoreType = "invalid-store" }}, - {"missing OAuth redisStore property", radixvalidators.OAuthRedisStoreEmptyErrorWithMessage(validRAFirstComponentName, "prod"), func(rr *v1.RadixApplication) { + {"missing OAuth redisStore property", radixvalidators.OAuthRedisStoreEmptyErrorWithMessage(validRAFirstComponentName, "prod"), func(rr *radixv1.RadixApplication) { rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.RedisStore = nil }}, - {"missing OAuth redis connection URL", radixvalidators.OAuthRedisStoreConnectionURLEmptyErrorWithMessage(validRAFirstComponentName, "prod"), func(rr *v1.RadixApplication) { + {"missing OAuth redis connection URL", radixvalidators.OAuthRedisStoreConnectionURLEmptyErrorWithMessage(validRAFirstComponentName, "prod"), func(rr *radixv1.RadixApplication) { rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.RedisStore.ConnectionURL = "" }}, - {"no error when skipDiscovery=true and login, redeem and jwks urls set", nil, func(rr *v1.RadixApplication) { - rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.OIDC = &v1.OAuth2OIDC{ + {"no error when skipDiscovery=true and login, redeem and jwks urls set", nil, func(rr *radixv1.RadixApplication) { + rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.OIDC = &radixv1.OAuth2OIDC{ SkipDiscovery: commonUtils.BoolPtr(true), JWKSURL: "jwksurl", } rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.LoginURL = "loginurl" rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.RedeemURL = "redeemurl" }}, - {"error when skipDiscovery=true and missing loginUrl", radixvalidators.OAuthLoginUrlEmptyErrorWithMessage(validRAFirstComponentName, "prod"), func(rr *v1.RadixApplication) { - rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.OIDC = &v1.OAuth2OIDC{ + {"error when skipDiscovery=true and missing loginUrl", radixvalidators.OAuthLoginUrlEmptyErrorWithMessage(validRAFirstComponentName, "prod"), func(rr *radixv1.RadixApplication) { + rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.OIDC = &radixv1.OAuth2OIDC{ SkipDiscovery: commonUtils.BoolPtr(true), JWKSURL: "jwksurl", } rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.RedeemURL = "redeemurl" }}, - {"error when skipDiscovery=true and missing redeemUrl", radixvalidators.OAuthRedeemUrlEmptyErrorWithMessage(validRAFirstComponentName, "prod"), func(rr *v1.RadixApplication) { - rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.OIDC = &v1.OAuth2OIDC{ + {"error when skipDiscovery=true and missing redeemUrl", radixvalidators.OAuthRedeemUrlEmptyErrorWithMessage(validRAFirstComponentName, "prod"), func(rr *radixv1.RadixApplication) { + rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.OIDC = &radixv1.OAuth2OIDC{ SkipDiscovery: commonUtils.BoolPtr(true), JWKSURL: "jwksurl", } rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.LoginURL = "loginurl" }}, - {"error when skipDiscovery=true and missing redeemUrl", radixvalidators.OAuthOidcJwksUrlEmptyErrorWithMessage(validRAFirstComponentName, "prod"), func(rr *v1.RadixApplication) { - rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.OIDC = &v1.OAuth2OIDC{ + {"error when skipDiscovery=true and missing redeemUrl", radixvalidators.OAuthOidcJwksUrlEmptyErrorWithMessage(validRAFirstComponentName, "prod"), func(rr *radixv1.RadixApplication) { + rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.OIDC = &radixv1.OAuth2OIDC{ SkipDiscovery: commonUtils.BoolPtr(true), } rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.LoginURL = "loginurl" rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.RedeemURL = "redeemurl" }}, - {"valid OAuth configuration for session store cookie and cookieStore.minimal=true", nil, func(rr *v1.RadixApplication) { - rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.SessionStoreType = v1.SessionStoreCookie - rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.CookieStore = &v1.OAuth2CookieStore{Minimal: commonUtils.BoolPtr(true)} + {"valid OAuth configuration for session store cookie and cookieStore.minimal=true", nil, func(rr *radixv1.RadixApplication) { + rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.SessionStoreType = radixv1.SessionStoreCookie + rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.CookieStore = &radixv1.OAuth2CookieStore{Minimal: commonUtils.BoolPtr(true)} rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.SetAuthorizationHeader = commonUtils.BoolPtr(false) rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.SetXAuthRequestHeaders = commonUtils.BoolPtr(false) - rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.Cookie = &v1.OAuth2Cookie{ + rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.Cookie = &radixv1.OAuth2Cookie{ Expire: "1h", Refresh: "0s", } }}, - {"error when cookieStore.minimal=true and SetAuthorizationHeader=true", radixvalidators.OAuthCookieStoreMinimalIncorrectSetAuthorizationHeaderErrorWithMessage(validRAFirstComponentName, "prod"), func(rr *v1.RadixApplication) { - rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.SessionStoreType = v1.SessionStoreCookie - rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.CookieStore = &v1.OAuth2CookieStore{Minimal: commonUtils.BoolPtr(true)} + {"error when cookieStore.minimal=true and SetAuthorizationHeader=true", radixvalidators.OAuthCookieStoreMinimalIncorrectSetAuthorizationHeaderErrorWithMessage(validRAFirstComponentName, "prod"), func(rr *radixv1.RadixApplication) { + rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.SessionStoreType = radixv1.SessionStoreCookie + rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.CookieStore = &radixv1.OAuth2CookieStore{Minimal: commonUtils.BoolPtr(true)} rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.SetAuthorizationHeader = commonUtils.BoolPtr(true) rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.SetXAuthRequestHeaders = commonUtils.BoolPtr(false) - rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.Cookie = &v1.OAuth2Cookie{ + rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.Cookie = &radixv1.OAuth2Cookie{ Expire: "1h", Refresh: "0s", } }}, - {"error when cookieStore.minimal=true and SetXAuthRequestHeaders=true", radixvalidators.OAuthCookieStoreMinimalIncorrectSetXAuthRequestHeadersErrorWithMessage(validRAFirstComponentName, "prod"), func(rr *v1.RadixApplication) { - rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.SessionStoreType = v1.SessionStoreCookie - rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.CookieStore = &v1.OAuth2CookieStore{Minimal: commonUtils.BoolPtr(true)} + {"error when cookieStore.minimal=true and SetXAuthRequestHeaders=true", radixvalidators.OAuthCookieStoreMinimalIncorrectSetXAuthRequestHeadersErrorWithMessage(validRAFirstComponentName, "prod"), func(rr *radixv1.RadixApplication) { + rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.SessionStoreType = radixv1.SessionStoreCookie + rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.CookieStore = &radixv1.OAuth2CookieStore{Minimal: commonUtils.BoolPtr(true)} rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.SetAuthorizationHeader = commonUtils.BoolPtr(false) rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.SetXAuthRequestHeaders = commonUtils.BoolPtr(true) - rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.Cookie = &v1.OAuth2Cookie{ + rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.Cookie = &radixv1.OAuth2Cookie{ Expire: "1h", Refresh: "0s", } }}, - {"error when cookieStore.minimal=true and Cookie.Refresh>0", radixvalidators.OAuthCookieStoreMinimalIncorrectCookieRefreshIntervalErrorWithMessage(validRAFirstComponentName, "prod"), func(rr *v1.RadixApplication) { - rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.SessionStoreType = v1.SessionStoreCookie - rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.CookieStore = &v1.OAuth2CookieStore{Minimal: commonUtils.BoolPtr(true)} + {"error when cookieStore.minimal=true and Cookie.Refresh>0", radixvalidators.OAuthCookieStoreMinimalIncorrectCookieRefreshIntervalErrorWithMessage(validRAFirstComponentName, "prod"), func(rr *radixv1.RadixApplication) { + rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.SessionStoreType = radixv1.SessionStoreCookie + rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.CookieStore = &radixv1.OAuth2CookieStore{Minimal: commonUtils.BoolPtr(true)} rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.SetAuthorizationHeader = commonUtils.BoolPtr(false) rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.SetXAuthRequestHeaders = commonUtils.BoolPtr(false) - rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.Cookie = &v1.OAuth2Cookie{ + rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.Cookie = &radixv1.OAuth2Cookie{ Expire: "1h", Refresh: "1s", } }}, - {"invalid OAuth cookie same site", radixvalidators.OAuthCookieSameSiteInvalidErrorWithMessage(validRAFirstComponentName, "prod", "invalid-samesite"), func(rr *v1.RadixApplication) { + {"invalid OAuth cookie same site", radixvalidators.OAuthCookieSameSiteInvalidErrorWithMessage(validRAFirstComponentName, "prod", "invalid-samesite"), func(rr *radixv1.RadixApplication) { rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.Cookie.SameSite = "invalid-samesite" }}, - {"invalid OAuth cookie expire timeframe", radixvalidators.OAuthCookieExpireInvalidErrorWithMessage(validRAFirstComponentName, "prod", "invalid-expire"), func(rr *v1.RadixApplication) { + {"invalid OAuth cookie expire timeframe", radixvalidators.OAuthCookieExpireInvalidErrorWithMessage(validRAFirstComponentName, "prod", "invalid-expire"), func(rr *radixv1.RadixApplication) { rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.Cookie.Expire = "invalid-expire" }}, - {"negative OAuth cookie expire timeframe", radixvalidators.OAuthCookieExpireInvalidErrorWithMessage(validRAFirstComponentName, "prod", "-1s"), func(rr *v1.RadixApplication) { + {"negative OAuth cookie expire timeframe", radixvalidators.OAuthCookieExpireInvalidErrorWithMessage(validRAFirstComponentName, "prod", "-1s"), func(rr *radixv1.RadixApplication) { rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.Cookie.Expire = "-1s" }}, - {"invalid OAuth cookie refresh time frame", radixvalidators.OAuthCookieRefreshInvalidErrorWithMessage(validRAFirstComponentName, "prod", "invalid-refresh"), func(rr *v1.RadixApplication) { + {"invalid OAuth cookie refresh time frame", radixvalidators.OAuthCookieRefreshInvalidErrorWithMessage(validRAFirstComponentName, "prod", "invalid-refresh"), func(rr *radixv1.RadixApplication) { rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.Cookie.Refresh = "invalid-refresh" }}, - {"negative OAuth cookie refresh time frame", radixvalidators.OAuthCookieRefreshInvalidErrorWithMessage(validRAFirstComponentName, "prod", "-1s"), func(rr *v1.RadixApplication) { + {"negative OAuth cookie refresh time frame", radixvalidators.OAuthCookieRefreshInvalidErrorWithMessage(validRAFirstComponentName, "prod", "-1s"), func(rr *radixv1.RadixApplication) { rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.Cookie.Refresh = "-1s" }}, - {"oauth cookie expire equals refresh", radixvalidators.OAuthCookieRefreshMustBeLessThanExpireErrorWithMessage(validRAFirstComponentName, "prod"), func(rr *v1.RadixApplication) { + {"oauth cookie expire equals refresh", radixvalidators.OAuthCookieRefreshMustBeLessThanExpireErrorWithMessage(validRAFirstComponentName, "prod"), func(rr *radixv1.RadixApplication) { rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.Cookie.Expire = "1h" rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.Cookie.Refresh = "1h" }}, - {"oauth cookie expire less than refresh", radixvalidators.OAuthCookieRefreshMustBeLessThanExpireErrorWithMessage(validRAFirstComponentName, "prod"), func(rr *v1.RadixApplication) { + {"oauth cookie expire less than refresh", radixvalidators.OAuthCookieRefreshMustBeLessThanExpireErrorWithMessage(validRAFirstComponentName, "prod"), func(rr *radixv1.RadixApplication) { rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.Cookie.Expire = "30m" rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.Cookie.Refresh = "1h" }}, - {"duplicate name in job/component boundary", radixvalidators.DuplicateComponentOrJobNameErrorWithMessage([]string{validRAFirstComponentName}), func(ra *v1.RadixApplication) { + {"duplicate name in job/component boundary", radixvalidators.DuplicateComponentOrJobNameErrorWithMessage([]string{validRAFirstComponentName}), func(ra *radixv1.RadixApplication) { job := *ra.Spec.Jobs[0].DeepCopy() job.Name = validRAFirstComponentName ra.Spec.Jobs = append(ra.Spec.Jobs, job) }}, - {"no mask size postfix in egress rule destination", radixvalidators.DuplicateComponentOrJobNameErrorWithMessage([]string{validRAFirstComponentName}), func(ra *v1.RadixApplication) { + {"no mask size postfix in egress rule destination", radixvalidators.DuplicateComponentOrJobNameErrorWithMessage([]string{validRAFirstComponentName}), func(ra *radixv1.RadixApplication) { job := *ra.Spec.Jobs[0].DeepCopy() job.Name = validRAFirstComponentName ra.Spec.Jobs = append(ra.Spec.Jobs, job) }}, - {"identity.azure.clientId cannot be empty for component", radixvalidators.ResourceNameCannotBeEmptyErrorWithMessage("identity.azure.clientId"), func(ra *v1.RadixApplication) { + {"identity.azure.clientId cannot be empty for component", radixvalidators.ResourceNameCannotBeEmptyErrorWithMessage("identity.azure.clientId"), func(ra *radixv1.RadixApplication) { ra.Spec.Components[0].Identity.Azure.ClientId = " " }}, - {"identity.azure.clientId cannot be empty for component environment config", radixvalidators.ResourceNameCannotBeEmptyErrorWithMessage("identity.azure.clientId"), func(ra *v1.RadixApplication) { + {"identity.azure.clientId cannot be empty for component environment config", radixvalidators.ResourceNameCannotBeEmptyErrorWithMessage("identity.azure.clientId"), func(ra *radixv1.RadixApplication) { ra.Spec.Components[0].EnvironmentConfig[0].Identity.Azure.ClientId = " " }}, - {"identity.azure.clientId cannot be empty for job", radixvalidators.ResourceNameCannotBeEmptyErrorWithMessage("identity.azure.clientId"), func(ra *v1.RadixApplication) { + {"identity.azure.clientId cannot be empty for job", radixvalidators.ResourceNameCannotBeEmptyErrorWithMessage("identity.azure.clientId"), func(ra *radixv1.RadixApplication) { ra.Spec.Jobs[0].Identity.Azure.ClientId = " " }}, - {"identity.azure.clientId cannot be empty for job environment config", radixvalidators.ResourceNameCannotBeEmptyErrorWithMessage("identity.azure.clientId"), func(ra *v1.RadixApplication) { + {"identity.azure.clientId cannot be empty for job environment config", radixvalidators.ResourceNameCannotBeEmptyErrorWithMessage("identity.azure.clientId"), func(ra *radixv1.RadixApplication) { ra.Spec.Jobs[0].EnvironmentConfig[0].Identity.Azure.ClientId = " " }}, - {"invalid identity.azure.clientId for component", radixvalidators.InvalidUUIDErrorWithMessage("identity.azure.clientId", "1111-22-33-44"), func(ra *v1.RadixApplication) { + {"invalid identity.azure.clientId for component", radixvalidators.InvalidUUIDErrorWithMessage("identity.azure.clientId", "1111-22-33-44"), func(ra *radixv1.RadixApplication) { ra.Spec.Components[0].Identity.Azure.ClientId = "1111-22-33-44" }}, - {"invalid identity.azure.clientId for component environment config", radixvalidators.InvalidUUIDErrorWithMessage("identity.azure.clientId", "1111-22-33-44"), func(ra *v1.RadixApplication) { + {"invalid identity.azure.clientId for component environment config", radixvalidators.InvalidUUIDErrorWithMessage("identity.azure.clientId", "1111-22-33-44"), func(ra *radixv1.RadixApplication) { ra.Spec.Components[0].EnvironmentConfig[0].Identity.Azure.ClientId = "1111-22-33-44" }}, - {"invalid identity.azure.clientId for job", radixvalidators.InvalidUUIDErrorWithMessage("identity.azure.clientId", "1111-22-33-44"), func(ra *v1.RadixApplication) { + {"invalid identity.azure.clientId for job", radixvalidators.InvalidUUIDErrorWithMessage("identity.azure.clientId", "1111-22-33-44"), func(ra *radixv1.RadixApplication) { ra.Spec.Jobs[0].Identity.Azure.ClientId = "1111-22-33-44" }}, - {"invalid identity.azure.clientId for job environment config", radixvalidators.InvalidUUIDErrorWithMessage("identity.azure.clientId", "1111-22-33-44"), func(ra *v1.RadixApplication) { + {"invalid identity.azure.clientId for job environment config", radixvalidators.InvalidUUIDErrorWithMessage("identity.azure.clientId", "1111-22-33-44"), func(ra *radixv1.RadixApplication) { ra.Spec.Jobs[0].EnvironmentConfig[0].Identity.Azure.ClientId = "1111-22-33-44" }}, } @@ -673,67 +674,67 @@ func Test_ValidRAComponentLimitRequest_NoError(t *testing.T) { name string updateRA updateRAFunc }{ - {"resource memory correct format: 50", func(ra *v1.RadixApplication) { + {"resource memory correct format: 50", func(ra *radixv1.RadixApplication) { ra.Spec.Components[0].EnvironmentConfig[0].Resources.Limits["memory"] = "50" ra.Spec.Components[0].EnvironmentConfig[0].Resources.Requests["memory"] = "50" }}, - {"resource limit correct format: 50T", func(ra *v1.RadixApplication) { + {"resource limit correct format: 50T", func(ra *radixv1.RadixApplication) { ra.Spec.Components[0].EnvironmentConfig[0].Resources.Limits["memory"] = "50T" ra.Spec.Components[0].EnvironmentConfig[0].Resources.Requests["memory"] = "50T" }}, - {"resource limit correct format: 50G", func(ra *v1.RadixApplication) { + {"resource limit correct format: 50G", func(ra *radixv1.RadixApplication) { ra.Spec.Components[0].EnvironmentConfig[0].Resources.Limits["memory"] = "50G" ra.Spec.Components[0].EnvironmentConfig[0].Resources.Requests["memory"] = "50G" }}, - {"resource limit correct format: 50M", func(ra *v1.RadixApplication) { + {"resource limit correct format: 50M", func(ra *radixv1.RadixApplication) { ra.Spec.Components[0].EnvironmentConfig[0].Resources.Limits["memory"] = "50M" ra.Spec.Components[0].EnvironmentConfig[0].Resources.Requests["memory"] = "50M" }}, - {"resource limit correct format: 50k", func(ra *v1.RadixApplication) { + {"resource limit correct format: 50k", func(ra *radixv1.RadixApplication) { ra.Spec.Components[0].EnvironmentConfig[0].Resources.Limits["memory"] = "50k" ra.Spec.Components[0].EnvironmentConfig[0].Resources.Requests["memory"] = "50k" }}, - {"resource limit correct format: 50Gi", func(ra *v1.RadixApplication) { + {"resource limit correct format: 50Gi", func(ra *radixv1.RadixApplication) { ra.Spec.Components[0].EnvironmentConfig[0].Resources.Limits["memory"] = "50Gi" ra.Spec.Components[0].EnvironmentConfig[0].Resources.Requests["memory"] = "50Gi" }}, - {"resource limit correct format: 50Mi", func(ra *v1.RadixApplication) { + {"resource limit correct format: 50Mi", func(ra *radixv1.RadixApplication) { ra.Spec.Components[0].EnvironmentConfig[0].Resources.Limits["memory"] = "50Mi" ra.Spec.Components[0].EnvironmentConfig[0].Resources.Requests["memory"] = "50Mi" }}, - {"resource limit correct format: 50Ki", func(ra *v1.RadixApplication) { + {"resource limit correct format: 50Ki", func(ra *radixv1.RadixApplication) { ra.Spec.Components[0].EnvironmentConfig[0].Resources.Limits["memory"] = "50Ki" ra.Spec.Components[0].EnvironmentConfig[0].Resources.Requests["memory"] = "50Ki" }}, - {"common resource memory correct format: 50", func(ra *v1.RadixApplication) { + {"common resource memory correct format: 50", func(ra *radixv1.RadixApplication) { ra.Spec.Components[0].Resources.Limits["memory"] = "50" ra.Spec.Components[0].Resources.Requests["memory"] = "50" }}, - {"common resource limit correct format: 50T", func(ra *v1.RadixApplication) { + {"common resource limit correct format: 50T", func(ra *radixv1.RadixApplication) { ra.Spec.Components[0].Resources.Limits["memory"] = "50T" ra.Spec.Components[0].Resources.Requests["memory"] = "50T" }}, - {"common resource limit correct format: 50G", func(ra *v1.RadixApplication) { + {"common resource limit correct format: 50G", func(ra *radixv1.RadixApplication) { ra.Spec.Components[0].Resources.Limits["memory"] = "50G" ra.Spec.Components[0].Resources.Requests["memory"] = "50G" }}, - {"common resource limit correct format: 50M", func(ra *v1.RadixApplication) { + {"common resource limit correct format: 50M", func(ra *radixv1.RadixApplication) { ra.Spec.Components[0].Resources.Limits["memory"] = "50M" ra.Spec.Components[0].Resources.Requests["memory"] = "50M" }}, - {"common resource limit correct format: 50k", func(ra *v1.RadixApplication) { + {"common resource limit correct format: 50k", func(ra *radixv1.RadixApplication) { ra.Spec.Components[0].Resources.Limits["memory"] = "50k" ra.Spec.Components[0].Resources.Requests["memory"] = "50k" }}, - {"common resource limit correct format: 50Gi", func(ra *v1.RadixApplication) { + {"common resource limit correct format: 50Gi", func(ra *radixv1.RadixApplication) { ra.Spec.Components[0].Resources.Limits["memory"] = "50Gi" ra.Spec.Components[0].Resources.Requests["memory"] = "50Gi" }}, - {"common resource limit correct format: 50Mi", func(ra *v1.RadixApplication) { + {"common resource limit correct format: 50Mi", func(ra *radixv1.RadixApplication) { ra.Spec.Components[0].Resources.Limits["memory"] = "50Mi" ra.Spec.Components[0].Resources.Requests["memory"] = "50Mi" }}, - {"common resource limit correct format: 50Ki", func(ra *v1.RadixApplication) { + {"common resource limit correct format: 50Ki", func(ra *radixv1.RadixApplication) { ra.Spec.Components[0].Resources.Limits["memory"] = "50Ki" ra.Spec.Components[0].Resources.Requests["memory"] = "50Ki" }}, @@ -756,67 +757,67 @@ func Test_ValidRAJobLimitRequest_NoError(t *testing.T) { name string updateRA updateRAFunc }{ - {"resource memory correct format: 50", func(ra *v1.RadixApplication) { + {"resource memory correct format: 50", func(ra *radixv1.RadixApplication) { ra.Spec.Jobs[0].EnvironmentConfig[0].Resources.Limits["memory"] = "50" ra.Spec.Jobs[0].EnvironmentConfig[0].Resources.Requests["memory"] = "50" }}, - {"resource limit correct format: 50T", func(ra *v1.RadixApplication) { + {"resource limit correct format: 50T", func(ra *radixv1.RadixApplication) { ra.Spec.Jobs[0].EnvironmentConfig[0].Resources.Limits["memory"] = "50T" ra.Spec.Jobs[0].EnvironmentConfig[0].Resources.Requests["memory"] = "50T" }}, - {"resource limit correct format: 50G", func(ra *v1.RadixApplication) { + {"resource limit correct format: 50G", func(ra *radixv1.RadixApplication) { ra.Spec.Jobs[0].EnvironmentConfig[0].Resources.Limits["memory"] = "50G" ra.Spec.Jobs[0].EnvironmentConfig[0].Resources.Requests["memory"] = "50G" }}, - {"resource limit correct format: 50M", func(ra *v1.RadixApplication) { + {"resource limit correct format: 50M", func(ra *radixv1.RadixApplication) { ra.Spec.Jobs[0].EnvironmentConfig[0].Resources.Limits["memory"] = "50M" ra.Spec.Jobs[0].EnvironmentConfig[0].Resources.Requests["memory"] = "50M" }}, - {"resource limit correct format: 50k", func(ra *v1.RadixApplication) { + {"resource limit correct format: 50k", func(ra *radixv1.RadixApplication) { ra.Spec.Jobs[0].EnvironmentConfig[0].Resources.Limits["memory"] = "50k" ra.Spec.Jobs[0].EnvironmentConfig[0].Resources.Requests["memory"] = "50k" }}, - {"resource limit correct format: 50Gi", func(ra *v1.RadixApplication) { + {"resource limit correct format: 50Gi", func(ra *radixv1.RadixApplication) { ra.Spec.Jobs[0].EnvironmentConfig[0].Resources.Limits["memory"] = "50Gi" ra.Spec.Jobs[0].EnvironmentConfig[0].Resources.Requests["memory"] = "50Gi" }}, - {"resource limit correct format: 50Mi", func(ra *v1.RadixApplication) { + {"resource limit correct format: 50Mi", func(ra *radixv1.RadixApplication) { ra.Spec.Jobs[0].EnvironmentConfig[0].Resources.Limits["memory"] = "50Mi" ra.Spec.Jobs[0].EnvironmentConfig[0].Resources.Requests["memory"] = "50Mi" }}, - {"resource limit correct format: 50Ki", func(ra *v1.RadixApplication) { + {"resource limit correct format: 50Ki", func(ra *radixv1.RadixApplication) { ra.Spec.Jobs[0].EnvironmentConfig[0].Resources.Limits["memory"] = "50Ki" ra.Spec.Jobs[0].EnvironmentConfig[0].Resources.Requests["memory"] = "50Ki" }}, - {"common resource memory correct format: 50", func(ra *v1.RadixApplication) { + {"common resource memory correct format: 50", func(ra *radixv1.RadixApplication) { ra.Spec.Jobs[0].Resources.Limits["memory"] = "50" ra.Spec.Jobs[0].Resources.Requests["memory"] = "50" }}, - {"common resource limit correct format: 50T", func(ra *v1.RadixApplication) { + {"common resource limit correct format: 50T", func(ra *radixv1.RadixApplication) { ra.Spec.Jobs[0].Resources.Limits["memory"] = "50T" ra.Spec.Jobs[0].Resources.Requests["memory"] = "50T" }}, - {"common resource limit correct format: 50G", func(ra *v1.RadixApplication) { + {"common resource limit correct format: 50G", func(ra *radixv1.RadixApplication) { ra.Spec.Jobs[0].Resources.Limits["memory"] = "50G" ra.Spec.Jobs[0].Resources.Requests["memory"] = "50G" }}, - {"common resource limit correct format: 50M", func(ra *v1.RadixApplication) { + {"common resource limit correct format: 50M", func(ra *radixv1.RadixApplication) { ra.Spec.Jobs[0].Resources.Limits["memory"] = "50M" ra.Spec.Jobs[0].Resources.Requests["memory"] = "50M" }}, - {"common resource limit correct format: 50k", func(ra *v1.RadixApplication) { + {"common resource limit correct format: 50k", func(ra *radixv1.RadixApplication) { ra.Spec.Jobs[0].Resources.Limits["memory"] = "50k" ra.Spec.Jobs[0].Resources.Requests["memory"] = "50k" }}, - {"common resource limit correct format: 50Gi", func(ra *v1.RadixApplication) { + {"common resource limit correct format: 50Gi", func(ra *radixv1.RadixApplication) { ra.Spec.Jobs[0].Resources.Limits["memory"] = "50Gi" ra.Spec.Jobs[0].Resources.Requests["memory"] = "50Gi" }}, - {"common resource limit correct format: 50Mi", func(ra *v1.RadixApplication) { + {"common resource limit correct format: 50Mi", func(ra *radixv1.RadixApplication) { ra.Spec.Jobs[0].Resources.Limits["memory"] = "50Mi" ra.Spec.Jobs[0].Resources.Requests["memory"] = "50Mi" }}, - {"common resource limit correct format: 50Ki", func(ra *v1.RadixApplication) { + {"common resource limit correct format: 50Ki", func(ra *radixv1.RadixApplication) { ra.Spec.Jobs[0].Resources.Limits["memory"] = "50Ki" ra.Spec.Jobs[0].Resources.Requests["memory"] = "50Ki" }}, @@ -839,19 +840,19 @@ func Test_InvalidRAComponentLimitRequest_Error(t *testing.T) { name string updateRA updateRAFunc }{ - {"resource limit incorrect format: 50MB", func(ra *v1.RadixApplication) { + {"resource limit incorrect format: 50MB", func(ra *radixv1.RadixApplication) { ra.Spec.Components[0].EnvironmentConfig[0].Resources.Limits["memory"] = "50MB" ra.Spec.Components[0].EnvironmentConfig[0].Resources.Requests["memory"] = "50MB" }}, - {"resource limit incorrect format: 50K", func(ra *v1.RadixApplication) { + {"resource limit incorrect format: 50K", func(ra *radixv1.RadixApplication) { ra.Spec.Components[0].EnvironmentConfig[0].Resources.Limits["memory"] = "50K" ra.Spec.Components[0].EnvironmentConfig[0].Resources.Requests["memory"] = "50K" }}, - {"common resource limit incorrect format: 50MB", func(ra *v1.RadixApplication) { + {"common resource limit incorrect format: 50MB", func(ra *radixv1.RadixApplication) { ra.Spec.Components[0].Resources.Limits["memory"] = "50MB" ra.Spec.Components[0].Resources.Requests["memory"] = "50MB" }}, - {"common resource limit incorrect format: 50K", func(ra *v1.RadixApplication) { + {"common resource limit incorrect format: 50K", func(ra *radixv1.RadixApplication) { ra.Spec.Components[0].Resources.Limits["memory"] = "50K" ra.Spec.Components[0].Resources.Requests["memory"] = "50K" }}, @@ -874,19 +875,19 @@ func Test_InvalidRAJobLimitRequest_Error(t *testing.T) { name string updateRA updateRAFunc }{ - {"resource limit incorrect format: 50MB", func(ra *v1.RadixApplication) { + {"resource limit incorrect format: 50MB", func(ra *radixv1.RadixApplication) { ra.Spec.Jobs[0].EnvironmentConfig[0].Resources.Limits["memory"] = "50MB" ra.Spec.Jobs[0].EnvironmentConfig[0].Resources.Requests["memory"] = "50MB" }}, - {"resource limit incorrect format: 50K", func(ra *v1.RadixApplication) { + {"resource limit incorrect format: 50K", func(ra *radixv1.RadixApplication) { ra.Spec.Jobs[0].EnvironmentConfig[0].Resources.Limits["memory"] = "50K" ra.Spec.Jobs[0].EnvironmentConfig[0].Resources.Requests["memory"] = "50K" }}, - {"common resource limit incorrect format: 50MB", func(ra *v1.RadixApplication) { + {"common resource limit incorrect format: 50MB", func(ra *radixv1.RadixApplication) { ra.Spec.Jobs[0].Resources.Limits["memory"] = "50MB" ra.Spec.Jobs[0].Resources.Requests["memory"] = "50MB" }}, - {"common resource limit incorrect format: 50K", func(ra *v1.RadixApplication) { + {"common resource limit incorrect format: 50K", func(ra *radixv1.RadixApplication) { ra.Spec.Jobs[0].Resources.Limits["memory"] = "50K" ra.Spec.Jobs[0].Resources.Requests["memory"] = "50K" }}, @@ -913,7 +914,7 @@ func Test_PublicPort(t *testing.T) { }{ { name: "matching port name for public component, old public does not exist", - updateRA: func(ra *v1.RadixApplication) { + updateRA: func(ra *radixv1.RadixApplication) { ra.Spec.Components[0].PublicPort = "http" ra.Spec.Components[0].Ports[0].Name = "http" ra.Spec.Components[0].Public = false @@ -924,7 +925,7 @@ func Test_PublicPort(t *testing.T) { { // For backwards compatibility name: "matching port name for public component, old public exists (ignored)", - updateRA: func(ra *v1.RadixApplication) { + updateRA: func(ra *radixv1.RadixApplication) { ra.Spec.Components[0].PublicPort = "http" ra.Spec.Components[0].Ports[0].Name = "http" ra.Spec.Components[0].Public = true @@ -934,7 +935,7 @@ func Test_PublicPort(t *testing.T) { }, { name: "port name is irrelevant for non-public component if old public does not exist", - updateRA: func(ra *v1.RadixApplication) { + updateRA: func(ra *radixv1.RadixApplication) { ra.Spec.Components[0].PublicPort = "" ra.Spec.Components[0].Ports[0].Name = "test" ra.Spec.Components[0].Public = false @@ -947,7 +948,7 @@ func Test_PublicPort(t *testing.T) { { // For backwards compatibility name: "old public is used if it exists and new publicPort does not exist", - updateRA: func(ra *v1.RadixApplication) { + updateRA: func(ra *radixv1.RadixApplication) { ra.Spec.Components[0].PublicPort = "" ra.Spec.Components[0].Ports[0].Name = "test" ra.Spec.Components[0].Public = true @@ -957,7 +958,7 @@ func Test_PublicPort(t *testing.T) { }, { name: "missing port name for public component, old public does not exist", - updateRA: func(ra *v1.RadixApplication) { + updateRA: func(ra *radixv1.RadixApplication) { ra.Spec.Components[0].PublicPort = "http" ra.Spec.Components[0].Ports[0].Name = "test" ra.Spec.Components[0].Public = false @@ -968,7 +969,7 @@ func Test_PublicPort(t *testing.T) { { // For backwards compatibility name: "missing port name for public component, old public exists (ignored)", - updateRA: func(ra *v1.RadixApplication) { + updateRA: func(ra *radixv1.RadixApplication) { ra.Spec.Components[0].PublicPort = "http" ra.Spec.Components[0].Ports[0].Name = "test" ra.Spec.Components[0].Public = true @@ -978,8 +979,8 @@ func Test_PublicPort(t *testing.T) { }, { name: "duplicate port name for public component, old public does not exist", - updateRA: func(ra *v1.RadixApplication) { - newPorts := []v1.ComponentPort{ + updateRA: func(ra *radixv1.RadixApplication) { + newPorts := []radixv1.ComponentPort{ { Name: "http", Port: 8080, @@ -999,8 +1000,8 @@ func Test_PublicPort(t *testing.T) { { // For backwards compatibility name: "duplicate port name for public component, old public exists (ignored)", - updateRA: func(ra *v1.RadixApplication) { - newPorts := []v1.ComponentPort{ + updateRA: func(ra *radixv1.RadixApplication) { + newPorts := []radixv1.ComponentPort{ { Name: "http", Port: 8080, @@ -1019,8 +1020,8 @@ func Test_PublicPort(t *testing.T) { }, { name: "privileged port used in radixConfig", - updateRA: func(ra *v1.RadixApplication) { - newPorts := []v1.ComponentPort{ + updateRA: func(ra *radixv1.RadixApplication) { + newPorts := []radixv1.ComponentPort{ { Name: "http", Port: 1000, @@ -1035,8 +1036,8 @@ func Test_PublicPort(t *testing.T) { }, { name: "oauth2 require public port", - updateRA: func(ra *v1.RadixApplication) { - ra.Spec.Components[0].Ports = []v1.ComponentPort{{Name: "http", Port: 1000}} + updateRA: func(ra *radixv1.RadixApplication) { + ra.Spec.Components[0].Ports = []radixv1.ComponentPort{{Name: "http", Port: 1000}} ra.Spec.Components[0].PublicPort = "" }, isValid: false, @@ -1044,7 +1045,7 @@ func Test_PublicPort(t *testing.T) { }, { name: "oauth2 require ports", - updateRA: func(ra *v1.RadixApplication) { + updateRA: func(ra *radixv1.RadixApplication) { ra.Spec.Components[0].Ports = nil ra.Spec.Components[0].PublicPort = "" }, @@ -1080,7 +1081,7 @@ func Test_Variables(t *testing.T) { }{ { name: "check that user defined variable with legal prefix succeeds", - updateRA: func(ra *v1.RadixApplication) { + updateRA: func(ra *radixv1.RadixApplication) { ra.Spec.Components[1].Variables["RADIX"] = "any value" }, isValid: true, @@ -1088,7 +1089,7 @@ func Test_Variables(t *testing.T) { }, { name: "check that user defined variable with legal prefix succeeds", - updateRA: func(ra *v1.RadixApplication) { + updateRA: func(ra *radixv1.RadixApplication) { ra.Spec.Components[1].Variables["RADIXX_SOMETHING"] = "any value" }, isValid: true, @@ -1096,7 +1097,7 @@ func Test_Variables(t *testing.T) { }, { name: "check that user defined variable with legal prefix succeeds", - updateRA: func(ra *v1.RadixApplication) { + updateRA: func(ra *radixv1.RadixApplication) { ra.Spec.Components[1].Variables["SOMETHING_RADIX_SOMETHING"] = "any value" }, isValid: true, @@ -1104,7 +1105,7 @@ func Test_Variables(t *testing.T) { }, { name: "check that user defined variable with legal prefix succeeds", - updateRA: func(ra *v1.RadixApplication) { + updateRA: func(ra *radixv1.RadixApplication) { ra.Spec.Components[1].Variables["S_RADIXOPERATOR"] = "any value" }, isValid: true, @@ -1112,7 +1113,7 @@ func Test_Variables(t *testing.T) { }, { name: "check that user defined variable with illegal prefix fails", - updateRA: func(ra *v1.RadixApplication) { + updateRA: func(ra *radixv1.RadixApplication) { ra.Spec.Components[1].Variables["RADIX_SOMETHING"] = "any value" }, isValid: false, @@ -1120,7 +1121,7 @@ func Test_Variables(t *testing.T) { }, { name: "check that user defined variable with illegal prefix fails", - updateRA: func(ra *v1.RadixApplication) { + updateRA: func(ra *radixv1.RadixApplication) { ra.Spec.Components[1].Variables["RADIXOPERATOR_SOMETHING"] = "any value" }, isValid: false, @@ -1128,7 +1129,7 @@ func Test_Variables(t *testing.T) { }, { name: "check that user defined variable with illegal prefix fails", - updateRA: func(ra *v1.RadixApplication) { + updateRA: func(ra *radixv1.RadixApplication) { ra.Spec.Components[1].Variables["RADIXOPERATOR_"] = "any value" }, isValid: false, @@ -1155,163 +1156,186 @@ func Test_Variables(t *testing.T) { } func Test_ValidationOfVolumeMounts_Errors(t *testing.T) { - type volumeMountsFunc func() []v1.RadixVolumeMount - type setVolumeMountsFunc func(*v1.RadixApplication, []v1.RadixVolumeMount) + type volumeMountsFunc func() []radixv1.RadixVolumeMount + type setVolumeMountsFunc func(*radixv1.RadixApplication, []radixv1.RadixVolumeMount) - setRaComponentVolumeMounts := func(ra *v1.RadixApplication, volumeMounts []v1.RadixVolumeMount) { + setRaComponentVolumeMounts := func(ra *radixv1.RadixApplication, volumeMounts []radixv1.RadixVolumeMount) { ra.Spec.Components[0].EnvironmentConfig[0].VolumeMounts = volumeMounts } - setRaJobsVolumeMounts := func(ra *v1.RadixApplication, volumeMounts []v1.RadixVolumeMount) { + setRaJobsVolumeMounts := func(ra *radixv1.RadixApplication, volumeMounts []radixv1.RadixVolumeMount) { ra.Spec.Jobs[0].EnvironmentConfig[0].VolumeMounts = volumeMounts } setComponentAndJobsVolumeMounts := []setVolumeMountsFunc{setRaComponentVolumeMounts, setRaJobsVolumeMounts} - var testScenarios = []struct { - name string - volumeMounts volumeMountsFunc - updateRA []setVolumeMountsFunc - isValid bool - isErrorNil bool - testContainedByError string + var testScenarios = map[string]struct { + volumeMounts volumeMountsFunc + updateRA []setVolumeMountsFunc + expectedError error }{ - { - "incorrect mount type", - func() []v1.RadixVolumeMount { - volumeMounts := []v1.RadixVolumeMount{ + "name not set": { + volumeMounts: func() []radixv1.RadixVolumeMount { + volumeMounts := []radixv1.RadixVolumeMount{ { - Type: "new-type", - Name: "some_name", - Storage: "some_container_name", - Path: "some_path", + Name: "", }, } return volumeMounts }, - setComponentAndJobsVolumeMounts, - false, - false, - "not recognized volume mount type", + updateRA: setComponentAndJobsVolumeMounts, + expectedError: radixvalidators.ErrVolumeMountMissingName, }, - { - "blob mount type with different name, containers and path", - func() []v1.RadixVolumeMount { - volumeMounts := []v1.RadixVolumeMount{ + "path not set": { + volumeMounts: func() []radixv1.RadixVolumeMount { + volumeMounts := []radixv1.RadixVolumeMount{ { - Type: v1.MountTypeBlob, - Name: "some_name_1", - Container: "some_container_name_1", - Path: "some_path_1", + Name: "anyname", }, + } + + return volumeMounts + }, + updateRA: setComponentAndJobsVolumeMounts, + expectedError: radixvalidators.ErrVolumeMountMissingPath, + }, + "duplicate name": { + volumeMounts: func() []radixv1.RadixVolumeMount { + volumeMounts := []radixv1.RadixVolumeMount{ { - Type: v1.MountTypeBlob, - Name: "some_name_2", - Container: "some_container_name_2", - Path: "some_path_2", + Name: "anyname", + Path: "/path1", + }, + { + Name: "anyname", + Path: "/path2", }, } return volumeMounts }, - setComponentAndJobsVolumeMounts, - true, - true, - "", + updateRA: setComponentAndJobsVolumeMounts, + expectedError: radixvalidators.ErrVolumeMountDuplicateName, }, - { - "blob mount type with duplicate names", - func() []v1.RadixVolumeMount { - volumeMounts := []v1.RadixVolumeMount{ + "duplicate path": { + volumeMounts: func() []radixv1.RadixVolumeMount { + volumeMounts := []radixv1.RadixVolumeMount{ { - Type: v1.MountTypeBlob, - Name: "some_name", - Container: "some_container_name_1", - Path: "some_path_1", + Name: "anyname1", + Path: "/path1", }, { - Type: v1.MountTypeBlob, - Name: "some_name", - Container: "some_container_name_2", - Path: "some_path_2", + Name: "anyname2", + Path: "/path1", }, } return volumeMounts }, - setComponentAndJobsVolumeMounts, - false, - false, - "duplicate names", + updateRA: setComponentAndJobsVolumeMounts, + expectedError: radixvalidators.ErrVolumeMountDuplicatePath, }, - { - "blob mount type with duplicate path", - func() []v1.RadixVolumeMount { - volumeMounts := []v1.RadixVolumeMount{ + "missing type": { + volumeMounts: func() []radixv1.RadixVolumeMount { + volumeMounts := []radixv1.RadixVolumeMount{ { - Type: v1.MountTypeBlob, - Name: "some_name_1", - Container: "some_container_name_1", - Path: "some_path", + Name: "anyname", + Path: "/path", }, + } + + return volumeMounts + }, + updateRA: setComponentAndJobsVolumeMounts, + expectedError: radixvalidators.ErrVolumeMountMissingType, + }, + "multiple types: deprecated source and blobfuse2": { + volumeMounts: func() []radixv1.RadixVolumeMount { + volumeMounts := []radixv1.RadixVolumeMount{ { - Type: v1.MountTypeBlob, - Name: "some_name_2", - Container: "some_container_name_2", - Path: "some_path", + Name: "anyname", + Path: "/path", + Type: radixv1.MountTypeBlob, + BlobFuse2: &radixv1.RadixBlobFuse2VolumeMount{}, }, } return volumeMounts }, - setComponentAndJobsVolumeMounts, - false, - false, - "duplicate path", + updateRA: setComponentAndJobsVolumeMounts, + expectedError: radixvalidators.ErrVolumeMountMultipleTypes, }, - { - "mount volume type is not set", - func() []v1.RadixVolumeMount { - volumeMounts := []v1.RadixVolumeMount{ + "multiple types: blobfuse2 and emptyDir": { + volumeMounts: func() []radixv1.RadixVolumeMount { + volumeMounts := []radixv1.RadixVolumeMount{ { + Name: "anyname", + Path: "/path", + Type: radixv1.MountTypeBlob, + BlobFuse2: &radixv1.RadixBlobFuse2VolumeMount{}, + EmptyDir: &radixv1.RadixEmptyDirVolumeMount{}, + }, + } + + return volumeMounts + }, + updateRA: setComponentAndJobsVolumeMounts, + expectedError: radixvalidators.ErrVolumeMountMultipleTypes, + }, + "deprecated blob: valid": { + volumeMounts: func() []radixv1.RadixVolumeMount { + volumeMounts := []radixv1.RadixVolumeMount{ + { + Type: "blob", Name: "some_name", - Container: "some_container_name", Path: "some_path", + Container: "any-container", + // RequestsStorage: "50M", }, } return volumeMounts }, - setComponentAndJobsVolumeMounts, - false, - false, - "non of volume mount type, blobfuse2 or azureFile options are defined in the volumeMount for component", + updateRA: setComponentAndJobsVolumeMounts, + expectedError: nil, }, - { - "mount volume name is not set", - func() []v1.RadixVolumeMount { - volumeMounts := []v1.RadixVolumeMount{ + "deprecated blob: missing container": { + volumeMounts: func() []radixv1.RadixVolumeMount { + volumeMounts := []radixv1.RadixVolumeMount{ { - Type: v1.MountTypeBlob, - Container: "some_container_name", - Path: "some_path", + Type: "blob", + Name: "some_name", + Path: "some_path", }, } return volumeMounts }, - setComponentAndJobsVolumeMounts, - false, - false, - "missing volume mount name and path of volumeMount for component", + updateRA: setComponentAndJobsVolumeMounts, + expectedError: radixvalidators.ErrVolumeMountMissingContainer, }, - { - "mount volume containers is not set", - func() []v1.RadixVolumeMount { - volumeMounts := []v1.RadixVolumeMount{ + "deprecated azure-blob: valid": { + volumeMounts: func() []radixv1.RadixVolumeMount { + volumeMounts := []radixv1.RadixVolumeMount{ { - Type: v1.MountTypeBlob, + Type: "azure-blob", + Name: "some_name", + Path: "some_path", + Storage: "any-storage", + }, + } + + return volumeMounts + }, + updateRA: setComponentAndJobsVolumeMounts, + expectedError: nil, + }, + "deprecated azure-blob: missing container": { + volumeMounts: func() []radixv1.RadixVolumeMount { + volumeMounts := []radixv1.RadixVolumeMount{ + { + Type: "azure-blob", Name: "some_name", Path: "some_path", }, @@ -1319,150 +1343,244 @@ func Test_ValidationOfVolumeMounts_Errors(t *testing.T) { return volumeMounts }, - setComponentAndJobsVolumeMounts, - false, - false, - "missing volume mount storage of volumeMount for component", + updateRA: setComponentAndJobsVolumeMounts, + expectedError: radixvalidators.ErrVolumeMountMissingStorage, }, - { - "mount volume path is not set", - func() []v1.RadixVolumeMount { - volumeMounts := []v1.RadixVolumeMount{ + "deprecated azure-file: valid": { + volumeMounts: func() []radixv1.RadixVolumeMount { + volumeMounts := []radixv1.RadixVolumeMount{ { - Type: v1.MountTypeBlob, - Name: "some_name", - Container: "some_container_name", + Type: "azure-file", + Name: "some_name", + Path: "some_path", + Storage: "any-storage", }, } return volumeMounts }, - setComponentAndJobsVolumeMounts, - false, - false, - "missing volume mount name and path of volumeMount for component", + updateRA: setComponentAndJobsVolumeMounts, + expectedError: nil, }, - { - "mount volume is blobfuse2 fuse2", - func() []v1.RadixVolumeMount { - volumeMounts := []v1.RadixVolumeMount{ + "deprecated common: invalid type": { + volumeMounts: func() []radixv1.RadixVolumeMount { + volumeMounts := []radixv1.RadixVolumeMount{ + { + Type: "invalid", + Name: "some_name", + Path: "some_path", + }, + } + + return volumeMounts + }, + updateRA: setComponentAndJobsVolumeMounts, + expectedError: radixvalidators.ErrVolumeMountInvalidType, + }, + "deprecated common: invalid requestsStorage": { + volumeMounts: func() []radixv1.RadixVolumeMount { + volumeMounts := []radixv1.RadixVolumeMount{ + { + Type: "blob", + Name: "some_name", + Path: "some_path", + RequestsStorage: "50x", + }, + } + + return volumeMounts + }, + updateRA: setComponentAndJobsVolumeMounts, + expectedError: radixvalidators.ErrVolumeMountInvalidRequestsStorage, + }, + "blobfuse2: valid": { + volumeMounts: func() []radixv1.RadixVolumeMount { + volumeMounts := []radixv1.RadixVolumeMount{ { Name: "some_name", Path: "some_path", - BlobFuse2: &v1.RadixBlobFuse2VolumeMount{ - Protocol: v1.BlobFuse2ProtocolFuse2, - Container: "some-container", + BlobFuse2: &radixv1.RadixBlobFuse2VolumeMount{ + Container: "any-container", }, }, } + return volumeMounts }, - setComponentAndJobsVolumeMounts, true, true, "", + updateRA: setComponentAndJobsVolumeMounts, + expectedError: nil, }, - { - "mount volume is blobfuse2 nfs", - func() []v1.RadixVolumeMount { - volumeMounts := []v1.RadixVolumeMount{ + "blobfuse2: valid protocol fuse2": { + volumeMounts: func() []radixv1.RadixVolumeMount { + volumeMounts := []radixv1.RadixVolumeMount{ { Name: "some_name", Path: "some_path", - BlobFuse2: &v1.RadixBlobFuse2VolumeMount{ - Protocol: v1.BlobFuse2ProtocolNfs, - Container: "some-container", + BlobFuse2: &radixv1.RadixBlobFuse2VolumeMount{ + Protocol: radixv1.BlobFuse2ProtocolFuse2, + Container: "any-container", }, }, } + return volumeMounts }, - setComponentAndJobsVolumeMounts, true, true, "", + updateRA: setComponentAndJobsVolumeMounts, + expectedError: nil, }, - { - "missing container name in mount volume blobfuse2 fuse2", - func() []v1.RadixVolumeMount { - volumeMounts := []v1.RadixVolumeMount{ + "blobfuse2: valid protocol nfs": { + volumeMounts: func() []radixv1.RadixVolumeMount { + volumeMounts := []radixv1.RadixVolumeMount{ { Name: "some_name", Path: "some_path", - BlobFuse2: &v1.RadixBlobFuse2VolumeMount{ - Protocol: v1.BlobFuse2ProtocolFuse2, + BlobFuse2: &radixv1.RadixBlobFuse2VolumeMount{ + Protocol: radixv1.BlobFuse2ProtocolNfs, + Container: "any-container", }, }, } + return volumeMounts }, - setComponentAndJobsVolumeMounts, false, false, "missing BlobFuse2 volume mount container of volumeMount for component", + updateRA: setComponentAndJobsVolumeMounts, + expectedError: nil, }, - { - "missing container name in mount volume blobfuse2 nfs", - func() []v1.RadixVolumeMount { - volumeMounts := []v1.RadixVolumeMount{ + "blobfuse2: valid requestsStorage": { + volumeMounts: func() []radixv1.RadixVolumeMount { + volumeMounts := []radixv1.RadixVolumeMount{ { Name: "some_name", Path: "some_path", - BlobFuse2: &v1.RadixBlobFuse2VolumeMount{ - Protocol: v1.BlobFuse2ProtocolNfs, + BlobFuse2: &radixv1.RadixBlobFuse2VolumeMount{ + Container: "any-container", + RequestsStorage: "50Mi", }, }, } + return volumeMounts }, - setComponentAndJobsVolumeMounts, false, false, "missing BlobFuse2 volume mount container of volumeMount for component", + updateRA: setComponentAndJobsVolumeMounts, + expectedError: nil, }, - { - "default empty protocol is fuse2 in mount volume blobfuse2 fuse2", - func() []v1.RadixVolumeMount { - volumeMounts := []v1.RadixVolumeMount{ + "blobfuse2: invalid protocol": { + volumeMounts: func() []radixv1.RadixVolumeMount { + volumeMounts := []radixv1.RadixVolumeMount{ { Name: "some_name", Path: "some_path", - BlobFuse2: &v1.RadixBlobFuse2VolumeMount{ - Container: "some-container", + BlobFuse2: &radixv1.RadixBlobFuse2VolumeMount{ + Protocol: "invalid", + Container: "any-container", }, }, } + return volumeMounts }, - setComponentAndJobsVolumeMounts, true, true, "", + updateRA: setComponentAndJobsVolumeMounts, + expectedError: radixvalidators.ErrVolumeMountInvalidProtocol, }, - { - "failed unsupported protocol in mount volume blobfuse2 fuse2", - func() []v1.RadixVolumeMount { - volumeMounts := []v1.RadixVolumeMount{ + "blobfuse2: missing container": { + volumeMounts: func() []radixv1.RadixVolumeMount { + volumeMounts := []radixv1.RadixVolumeMount{ + { + Name: "some_name", + Path: "some_path", + BlobFuse2: &radixv1.RadixBlobFuse2VolumeMount{}, + }, + } + + return volumeMounts + }, + updateRA: setComponentAndJobsVolumeMounts, + expectedError: radixvalidators.ErrVolumeMountMissingContainer, + }, + "blobfuse2: invalid requestsStorage": { + volumeMounts: func() []radixv1.RadixVolumeMount { + volumeMounts := []radixv1.RadixVolumeMount{ { Name: "some_name", Path: "some_path", - BlobFuse2: &v1.RadixBlobFuse2VolumeMount{ - Container: "some-container", - Protocol: "some-text", + BlobFuse2: &radixv1.RadixBlobFuse2VolumeMount{ + Container: "any-container", + RequestsStorage: "100x", }, }, } + return volumeMounts }, - setComponentAndJobsVolumeMounts, false, false, "unsupported BlobFuse2 volume mount protocol of volumeMount for component", + updateRA: setComponentAndJobsVolumeMounts, + expectedError: radixvalidators.ErrVolumeMountInvalidRequestsStorage, + }, + "azureFile: not implemented": { + volumeMounts: func() []radixv1.RadixVolumeMount { + volumeMounts := []radixv1.RadixVolumeMount{ + { + Name: "some_name", + Path: "some_path", + AzureFile: &radixv1.RadixAzureFileVolumeMount{}, + }, + } + + return volumeMounts + }, + updateRA: setComponentAndJobsVolumeMounts, + expectedError: radixvalidators.ErrVolumeMountTypeNotImplemented, + }, + "emptyDir: valid": { + volumeMounts: func() []radixv1.RadixVolumeMount { + volumeMounts := []radixv1.RadixVolumeMount{ + { + Name: "some_name", + Path: "some_path", + EmptyDir: &radixv1.RadixEmptyDirVolumeMount{ + SizeLimit: resource.MustParse("50M"), + }, + }, + } + + return volumeMounts + }, + updateRA: setComponentAndJobsVolumeMounts, + expectedError: nil, + }, + "emptyDir: missing sizeLimit": { + volumeMounts: func() []radixv1.RadixVolumeMount { + volumeMounts := []radixv1.RadixVolumeMount{ + { + Name: "some_name", + Path: "some_path", + EmptyDir: &radixv1.RadixEmptyDirVolumeMount{}, + }, + } + + return volumeMounts + }, + updateRA: setComponentAndJobsVolumeMounts, + expectedError: radixvalidators.ErrVolumeMountMissingSizeLimit, }, } _, client := validRASetup() - for _, testcase := range testScenarios { - t.Run(testcase.name, func(t *testing.T) { - if testcase.updateRA == nil || len(testcase.updateRA) == 0 { - assert.FailNow(t, "missing updateRA functions for %s", testcase.name) + for name, test := range testScenarios { + t.Run(name, func(t *testing.T) { + if test.updateRA == nil || len(test.updateRA) == 0 { + assert.FailNow(t, "missing updateRA functions for %s", name) return } - for _, ra := range testcase.updateRA { + for _, ra := range test.updateRA { validRA := createValidRA() - volumes := testcase.volumeMounts() + volumes := test.volumeMounts() ra(validRA, volumes) err := radixvalidators.CanRadixApplicationBeInserted(client, validRA, getDNSAliasConfig()) - isErrorNil := err == nil - - assert.Equal(t, testcase.isValid, err == nil) - assert.Equal(t, testcase.isErrorNil, isErrorNil) - if !isErrorNil { - assert.Contains(t, err.Error(), testcase.testContainedByError) - continue + if test.expectedError == nil { + assert.NoError(t, err) + } else { + assert.ErrorIs(t, err, test.expectedError) } } }) @@ -1478,23 +1596,23 @@ func Test_ValidHPA_NoError(t *testing.T) { }{ { "horizontalScaling is not set", - func(ra *v1.RadixApplication) {}, + func(ra *radixv1.RadixApplication) {}, true, true, }, { "minReplicas and maxReplicas are not set", - func(ra *v1.RadixApplication) { - ra.Spec.Components[0].EnvironmentConfig[0].HorizontalScaling = &v1.RadixHorizontalScaling{} + func(ra *radixv1.RadixApplication) { + ra.Spec.Components[0].EnvironmentConfig[0].HorizontalScaling = &radixv1.RadixHorizontalScaling{} }, false, false, }, { "maxReplicas is not set and minReplicas is set", - func(ra *v1.RadixApplication) { + func(ra *radixv1.RadixApplication) { minReplica := int32(3) - ra.Spec.Components[0].EnvironmentConfig[0].HorizontalScaling = &v1.RadixHorizontalScaling{} + ra.Spec.Components[0].EnvironmentConfig[0].HorizontalScaling = &radixv1.RadixHorizontalScaling{} ra.Spec.Components[0].EnvironmentConfig[0].HorizontalScaling.MinReplicas = &minReplica }, false, @@ -1502,8 +1620,8 @@ func Test_ValidHPA_NoError(t *testing.T) { }, { "minReplicas is not set and maxReplicas is set", - func(ra *v1.RadixApplication) { - ra.Spec.Components[0].EnvironmentConfig[0].HorizontalScaling = &v1.RadixHorizontalScaling{} + func(ra *radixv1.RadixApplication) { + ra.Spec.Components[0].EnvironmentConfig[0].HorizontalScaling = &radixv1.RadixHorizontalScaling{} ra.Spec.Components[0].EnvironmentConfig[0].HorizontalScaling.MaxReplicas = 2 }, true, @@ -1511,8 +1629,8 @@ func Test_ValidHPA_NoError(t *testing.T) { }, { "minReplicas is greater than maxReplicas", - func(ra *v1.RadixApplication) { - ra.Spec.Components[0].EnvironmentConfig[0].HorizontalScaling = &v1.RadixHorizontalScaling{} + func(ra *radixv1.RadixApplication) { + ra.Spec.Components[0].EnvironmentConfig[0].HorizontalScaling = &radixv1.RadixHorizontalScaling{} minReplica := int32(3) ra.Spec.Components[0].EnvironmentConfig[0].HorizontalScaling.MinReplicas = &minReplica ra.Spec.Components[0].EnvironmentConfig[0].HorizontalScaling.MaxReplicas = 2 @@ -1522,8 +1640,8 @@ func Test_ValidHPA_NoError(t *testing.T) { }, { "maxReplicas is greater than minReplicas", - func(ra *v1.RadixApplication) { - ra.Spec.Components[0].EnvironmentConfig[0].HorizontalScaling = &v1.RadixHorizontalScaling{} + func(ra *radixv1.RadixApplication) { + ra.Spec.Components[0].EnvironmentConfig[0].HorizontalScaling = &radixv1.RadixHorizontalScaling{} minReplica := int32(3) ra.Spec.Components[0].EnvironmentConfig[0].HorizontalScaling.MinReplicas = &minReplica ra.Spec.Components[0].EnvironmentConfig[0].HorizontalScaling.MaxReplicas = 4 @@ -1533,12 +1651,12 @@ func Test_ValidHPA_NoError(t *testing.T) { }, { "custom resource scaling for HPA is set, but no resource thresholds are defined", - func(ra *v1.RadixApplication) { - ra.Spec.Components[0].EnvironmentConfig[0].HorizontalScaling = &v1.RadixHorizontalScaling{} + func(ra *radixv1.RadixApplication) { + ra.Spec.Components[0].EnvironmentConfig[0].HorizontalScaling = &radixv1.RadixHorizontalScaling{} minReplica := int32(2) ra.Spec.Components[0].EnvironmentConfig[0].HorizontalScaling.MinReplicas = &minReplica ra.Spec.Components[0].EnvironmentConfig[0].HorizontalScaling.MaxReplicas = 4 - ra.Spec.Components[0].EnvironmentConfig[0].HorizontalScaling.RadixHorizontalScalingResources = &v1.RadixHorizontalScalingResources{} + ra.Spec.Components[0].EnvironmentConfig[0].HorizontalScaling.RadixHorizontalScalingResources = &radixv1.RadixHorizontalScalingResources{} }, false, false, @@ -1571,9 +1689,9 @@ func Test_EgressConfig(t *testing.T) { }{ { name: "egress rule must have valid destination masks", - updateRA: func(ra *v1.RadixApplication) { - ra.Spec.Environments[0].Egress.Rules = []v1.EgressRule{{ - Destinations: []v1.EgressDestination{"notanIPmask"}, + updateRA: func(ra *radixv1.RadixApplication) { + ra.Spec.Environments[0].Egress.Rules = []radixv1.EgressRule{{ + Destinations: []radixv1.EgressDestination{"notanIPmask"}, Ports: nil, }} }, @@ -1581,9 +1699,9 @@ func Test_EgressConfig(t *testing.T) { }, { name: "egress rule must use IPv4 in destination CIDR, zero ports", - updateRA: func(ra *v1.RadixApplication) { - ra.Spec.Environments[0].Egress.Rules = []v1.EgressRule{{ - Destinations: []v1.EgressDestination{"2001:0DB8:0000:000b::/64"}, + updateRA: func(ra *radixv1.RadixApplication) { + ra.Spec.Environments[0].Egress.Rules = []radixv1.EgressRule{{ + Destinations: []radixv1.EgressDestination{"2001:0DB8:0000:000b::/64"}, Ports: nil, }} }, @@ -1591,10 +1709,10 @@ func Test_EgressConfig(t *testing.T) { }, { name: "egress rule must use IPv4 in destination CIDR", - updateRA: func(ra *v1.RadixApplication) { - ra.Spec.Environments[0].Egress.Rules = []v1.EgressRule{{ - Destinations: []v1.EgressDestination{"2001:0DB8:0000:000b::/64"}, - Ports: []v1.EgressPort{{ + updateRA: func(ra *radixv1.RadixApplication) { + ra.Spec.Environments[0].Egress.Rules = []radixv1.EgressRule{{ + Destinations: []radixv1.EgressDestination{"2001:0DB8:0000:000b::/64"}, + Ports: []radixv1.EgressPort{{ Port: 10, Protocol: "TCP", }}, @@ -1604,9 +1722,9 @@ func Test_EgressConfig(t *testing.T) { }, { name: "egress rule must have postfix in IPv4 CIDR", - updateRA: func(ra *v1.RadixApplication) { - ra.Spec.Environments[0].Egress.Rules = []v1.EgressRule{{ - Destinations: []v1.EgressDestination{"10.0.0.1"}, + updateRA: func(ra *radixv1.RadixApplication) { + ra.Spec.Environments[0].Egress.Rules = []radixv1.EgressRule{{ + Destinations: []radixv1.EgressDestination{"10.0.0.1"}, Ports: nil, }} }, @@ -1614,10 +1732,10 @@ func Test_EgressConfig(t *testing.T) { }, { name: "egress rule must have valid ports", - updateRA: func(ra *v1.RadixApplication) { - ra.Spec.Environments[0].Egress.Rules = []v1.EgressRule{{ - Destinations: []v1.EgressDestination{"10.0.0.1"}, - Ports: []v1.EgressPort{{ + updateRA: func(ra *radixv1.RadixApplication) { + ra.Spec.Environments[0].Egress.Rules = []radixv1.EgressRule{{ + Destinations: []radixv1.EgressDestination{"10.0.0.1"}, + Ports: []radixv1.EgressPort{{ Port: 0, Protocol: "TCP", }}, @@ -1627,10 +1745,10 @@ func Test_EgressConfig(t *testing.T) { }, { name: "egress rule must have valid ports", - updateRA: func(ra *v1.RadixApplication) { - ra.Spec.Environments[0].Egress.Rules = []v1.EgressRule{{ - Destinations: []v1.EgressDestination{"10.0.0.1"}, - Ports: []v1.EgressPort{{ + updateRA: func(ra *radixv1.RadixApplication) { + ra.Spec.Environments[0].Egress.Rules = []radixv1.EgressRule{{ + Destinations: []radixv1.EgressDestination{"10.0.0.1"}, + Ports: []radixv1.EgressPort{{ Port: 66000, Protocol: "TCP", }}, @@ -1640,10 +1758,10 @@ func Test_EgressConfig(t *testing.T) { }, { name: "egress rule must contain destination", - updateRA: func(ra *v1.RadixApplication) { - ra.Spec.Environments[0].Egress.Rules = []v1.EgressRule{{ + updateRA: func(ra *radixv1.RadixApplication) { + ra.Spec.Environments[0].Egress.Rules = []radixv1.EgressRule{{ Destinations: nil, - Ports: []v1.EgressPort{{ + Ports: []radixv1.EgressPort{{ Port: 24, Protocol: "TCP", }}, @@ -1653,10 +1771,10 @@ func Test_EgressConfig(t *testing.T) { }, { name: "egress rule must have valid port protocol", - updateRA: func(ra *v1.RadixApplication) { - ra.Spec.Environments[0].Egress.Rules = []v1.EgressRule{{ - Destinations: []v1.EgressDestination{"10.0.0.1/32"}, - Ports: []v1.EgressPort{{ + updateRA: func(ra *radixv1.RadixApplication) { + ra.Spec.Environments[0].Egress.Rules = []radixv1.EgressRule{{ + Destinations: []radixv1.EgressDestination{"10.0.0.1/32"}, + Ports: []radixv1.EgressPort{{ Port: 2000, Protocol: "SCTP", }}, @@ -1666,10 +1784,10 @@ func Test_EgressConfig(t *testing.T) { }, { name: "egress rule must have valid port protocol", - updateRA: func(ra *v1.RadixApplication) { - ra.Spec.Environments[0].Egress.Rules = []v1.EgressRule{{ - Destinations: []v1.EgressDestination{"10.0.0.1/32"}, - Ports: []v1.EgressPort{{ + updateRA: func(ra *radixv1.RadixApplication) { + ra.Spec.Environments[0].Egress.Rules = []radixv1.EgressRule{{ + Destinations: []radixv1.EgressDestination{"10.0.0.1/32"}, + Ports: []radixv1.EgressPort{{ Port: 2000, Protocol: "erwef", }}, @@ -1679,11 +1797,11 @@ func Test_EgressConfig(t *testing.T) { }, { name: "can not exceed max nr of egress rules", - updateRA: func(ra *v1.RadixApplication) { - ra.Spec.Environments[0].Egress.Rules = []v1.EgressRule{} + updateRA: func(ra *radixv1.RadixApplication) { + ra.Spec.Environments[0].Egress.Rules = []radixv1.EgressRule{} for i := 0; i <= 1000; i++ { - ra.Spec.Environments[0].Egress.Rules = append(ra.Spec.Environments[0].Egress.Rules, v1.EgressRule{ - Destinations: []v1.EgressDestination{"10.0.0.0/8"}, + ra.Spec.Environments[0].Egress.Rules = append(ra.Spec.Environments[0].Egress.Rules, radixv1.EgressRule{ + Destinations: []radixv1.EgressDestination{"10.0.0.0/8"}, Ports: nil, }) } @@ -1692,9 +1810,9 @@ func Test_EgressConfig(t *testing.T) { }, { name: "sample egress rule with valid destination, zero ports", - updateRA: func(ra *v1.RadixApplication) { - ra.Spec.Environments[0].Egress.Rules = []v1.EgressRule{{ - Destinations: []v1.EgressDestination{"10.0.0.0/8"}, + updateRA: func(ra *radixv1.RadixApplication) { + ra.Spec.Environments[0].Egress.Rules = []radixv1.EgressRule{{ + Destinations: []radixv1.EgressDestination{"10.0.0.0/8"}, Ports: nil, }} }, @@ -1702,10 +1820,10 @@ func Test_EgressConfig(t *testing.T) { }, { name: "sample egress rule with valid destinations", - updateRA: func(ra *v1.RadixApplication) { - ra.Spec.Environments[0].Egress.Rules = []v1.EgressRule{{ - Destinations: []v1.EgressDestination{"10.0.0.0/8", "192.10.10.10/32"}, - Ports: []v1.EgressPort{ + updateRA: func(ra *radixv1.RadixApplication) { + ra.Spec.Environments[0].Egress.Rules = []radixv1.EgressRule{{ + Destinations: []radixv1.EgressDestination{"10.0.0.0/8", "192.10.10.10/32"}, + Ports: []radixv1.EgressPort{ { Port: 53, Protocol: "udp", @@ -1746,103 +1864,103 @@ func Test_validateNotificationsRA(t *testing.T) { updateRa updateRAFunc }{ {name: "No notification", expectedError: nil, - updateRa: func(ra *v1.RadixApplication) { + updateRa: func(ra *radixv1.RadixApplication) { ra.Spec.Jobs[0].Notifications = nil }, }, {name: "valid webhook with http", expectedError: nil, - updateRa: func(ra *v1.RadixApplication) { - ra.Spec.Jobs[0].Notifications = &v1.Notifications{Webhook: pointers.Ptr("http://api:8090/abc")} + updateRa: func(ra *radixv1.RadixApplication) { + ra.Spec.Jobs[0].Notifications = &radixv1.Notifications{Webhook: pointers.Ptr("http://api:8090/abc")} }, }, {name: "valid webhook with https", expectedError: nil, - updateRa: func(ra *v1.RadixApplication) { - ra.Spec.Jobs[0].Notifications = &v1.Notifications{Webhook: pointers.Ptr("https://api:8090/abc")} + updateRa: func(ra *radixv1.RadixApplication) { + ra.Spec.Jobs[0].Notifications = &radixv1.Notifications{Webhook: pointers.Ptr("https://api:8090/abc")} }, }, {name: "valid webhook in environment", expectedError: nil, - updateRa: func(ra *v1.RadixApplication) { - ra.Spec.Jobs[0].EnvironmentConfig[0].Notifications = &v1.Notifications{Webhook: pointers.Ptr("http://api:8090/abc")} + updateRa: func(ra *radixv1.RadixApplication) { + ra.Spec.Jobs[0].EnvironmentConfig[0].Notifications = &radixv1.Notifications{Webhook: pointers.Ptr("http://api:8090/abc")} }, }, {name: "valid webhook to job component", expectedError: nil, - updateRa: func(ra *v1.RadixApplication) { - ra.Spec.Jobs[0].Notifications = &v1.Notifications{Webhook: pointers.Ptr("http://job3:8099/abc")} + updateRa: func(ra *radixv1.RadixApplication) { + ra.Spec.Jobs[0].Notifications = &radixv1.Notifications{Webhook: pointers.Ptr("http://job3:8099/abc")} }, }, {name: "valid webhook to job component in environment", expectedError: nil, - updateRa: func(ra *v1.RadixApplication) { - ra.Spec.Jobs[0].EnvironmentConfig[0].Notifications = &v1.Notifications{Webhook: pointers.Ptr("http://job3:8099/abc")} + updateRa: func(ra *radixv1.RadixApplication) { + ra.Spec.Jobs[0].EnvironmentConfig[0].Notifications = &radixv1.Notifications{Webhook: pointers.Ptr("http://job3:8099/abc")} }, }, {name: "Invalid webhook URL", expectedError: radixvalidators.InvalidWebhookUrlWithMessage("job", ""), - updateRa: func(ra *v1.RadixApplication) { - ra.Spec.Jobs[0].Notifications = &v1.Notifications{Webhook: &invalidUrl} + updateRa: func(ra *radixv1.RadixApplication) { + ra.Spec.Jobs[0].Notifications = &radixv1.Notifications{Webhook: &invalidUrl} }, }, {name: "Invalid webhook URL in environment", expectedError: radixvalidators.InvalidWebhookUrlWithMessage("job", "dev"), - updateRa: func(ra *v1.RadixApplication) { - ra.Spec.Jobs[0].EnvironmentConfig[0].Notifications = &v1.Notifications{Webhook: &invalidUrl} + updateRa: func(ra *radixv1.RadixApplication) { + ra.Spec.Jobs[0].EnvironmentConfig[0].Notifications = &radixv1.Notifications{Webhook: &invalidUrl} }, }, {name: "Not allowed scheme ftp", expectedError: radixvalidators.NotAllowedSchemeInWebhookUrlWithMessage("ftp", "job", ""), - updateRa: func(ra *v1.RadixApplication) { - ra.Spec.Jobs[0].Notifications = &v1.Notifications{Webhook: pointers.Ptr("ftp://api:8090")} + updateRa: func(ra *radixv1.RadixApplication) { + ra.Spec.Jobs[0].Notifications = &radixv1.Notifications{Webhook: pointers.Ptr("ftp://api:8090")} }, }, {name: "Not allowed scheme ftp in environment", expectedError: radixvalidators.NotAllowedSchemeInWebhookUrlWithMessage("ftp", "job", "dev"), - updateRa: func(ra *v1.RadixApplication) { - ra.Spec.Jobs[0].EnvironmentConfig[0].Notifications = &v1.Notifications{Webhook: pointers.Ptr("ftp://api:8090")} + updateRa: func(ra *radixv1.RadixApplication) { + ra.Spec.Jobs[0].EnvironmentConfig[0].Notifications = &radixv1.Notifications{Webhook: pointers.Ptr("ftp://api:8090")} }, }, {name: "missing port in the webhook", expectedError: radixvalidators.MissingPortInWebhookUrlWithMessage("job", ""), - updateRa: func(ra *v1.RadixApplication) { - ra.Spec.Jobs[0].Notifications = &v1.Notifications{Webhook: pointers.Ptr("http://api/abc")} + updateRa: func(ra *radixv1.RadixApplication) { + ra.Spec.Jobs[0].Notifications = &radixv1.Notifications{Webhook: pointers.Ptr("http://api/abc")} }, }, {name: "missing port in the webhook in environment", expectedError: radixvalidators.MissingPortInWebhookUrlWithMessage("job", "dev"), - updateRa: func(ra *v1.RadixApplication) { - ra.Spec.Jobs[0].EnvironmentConfig[0].Notifications = &v1.Notifications{Webhook: pointers.Ptr("http://api/abc")} + updateRa: func(ra *radixv1.RadixApplication) { + ra.Spec.Jobs[0].EnvironmentConfig[0].Notifications = &radixv1.Notifications{Webhook: pointers.Ptr("http://api/abc")} }, }, {name: "webhook can only reference to an application component or job", expectedError: radixvalidators.OnlyAppComponentAllowedInWebhookUrlWithMessage("job", ""), - updateRa: func(ra *v1.RadixApplication) { - ra.Spec.Jobs[0].Notifications = &v1.Notifications{Webhook: pointers.Ptr("http://notexistingcomponent:8090/abc")} + updateRa: func(ra *radixv1.RadixApplication) { + ra.Spec.Jobs[0].Notifications = &radixv1.Notifications{Webhook: pointers.Ptr("http://notexistingcomponent:8090/abc")} }, }, {name: "webhook can only reference to an application component or job in environment", expectedError: radixvalidators.OnlyAppComponentAllowedInWebhookUrlWithMessage("job", "dev"), - updateRa: func(ra *v1.RadixApplication) { - ra.Spec.Jobs[0].EnvironmentConfig[0].Notifications = &v1.Notifications{Webhook: pointers.Ptr("http://notexistingcomponent:8090/abc")} + updateRa: func(ra *radixv1.RadixApplication) { + ra.Spec.Jobs[0].EnvironmentConfig[0].Notifications = &radixv1.Notifications{Webhook: pointers.Ptr("http://notexistingcomponent:8090/abc")} }, }, {name: "webhook port does not exist in an application component", expectedError: radixvalidators.InvalidPortInWebhookUrlWithMessage("8077", "api", "job", ""), - updateRa: func(ra *v1.RadixApplication) { - ra.Spec.Jobs[0].Notifications = &v1.Notifications{Webhook: pointers.Ptr("http://api:8077")} + updateRa: func(ra *radixv1.RadixApplication) { + ra.Spec.Jobs[0].Notifications = &radixv1.Notifications{Webhook: pointers.Ptr("http://api:8077")} }, }, {name: "webhook port does not exist in an application component in environment", expectedError: radixvalidators.InvalidPortInWebhookUrlWithMessage("8077", "api", "job", "dev"), - updateRa: func(ra *v1.RadixApplication) { - ra.Spec.Jobs[0].EnvironmentConfig[0].Notifications = &v1.Notifications{Webhook: pointers.Ptr("http://api:8077")} + updateRa: func(ra *radixv1.RadixApplication) { + ra.Spec.Jobs[0].EnvironmentConfig[0].Notifications = &radixv1.Notifications{Webhook: pointers.Ptr("http://api:8077")} }, }, {name: "webhook port does not exist in an application job component", expectedError: radixvalidators.InvalidPortInWebhookUrlWithMessage("8077", "job3", "job", ""), - updateRa: func(ra *v1.RadixApplication) { - ra.Spec.Jobs[0].Notifications = &v1.Notifications{Webhook: pointers.Ptr("http://job3:8077")} + updateRa: func(ra *radixv1.RadixApplication) { + ra.Spec.Jobs[0].Notifications = &radixv1.Notifications{Webhook: pointers.Ptr("http://job3:8077")} }, }, {name: "webhook port does not exist in an application job component in environment", expectedError: radixvalidators.InvalidPortInWebhookUrlWithMessage("8077", "job3", "job", "dev"), - updateRa: func(ra *v1.RadixApplication) { - ra.Spec.Jobs[0].EnvironmentConfig[0].Notifications = &v1.Notifications{Webhook: pointers.Ptr("http://job3:8077")} + updateRa: func(ra *radixv1.RadixApplication) { + ra.Spec.Jobs[0].EnvironmentConfig[0].Notifications = &radixv1.Notifications{Webhook: pointers.Ptr("http://job3:8077")} }, }, {name: "not allowed to use in the webhook a public port of an application component", expectedError: radixvalidators.InvalidUseOfPublicPortInWebhookUrlWithMessage("8080", "app", "job", ""), - updateRa: func(ra *v1.RadixApplication) { - ra.Spec.Jobs[0].Notifications = &v1.Notifications{Webhook: pointers.Ptr("http://app:8080")} + updateRa: func(ra *radixv1.RadixApplication) { + ra.Spec.Jobs[0].Notifications = &radixv1.Notifications{Webhook: pointers.Ptr("http://app:8080")} }, }, {name: "not allowed to use in the webhook a public port of an application component in environment", expectedError: radixvalidators.InvalidUseOfPublicPortInWebhookUrlWithMessage("8080", "app", "job", "dev"), - updateRa: func(ra *v1.RadixApplication) { - ra.Spec.Jobs[0].EnvironmentConfig[0].Notifications = &v1.Notifications{Webhook: pointers.Ptr("http://app:8080")} + updateRa: func(ra *radixv1.RadixApplication) { + ra.Spec.Jobs[0].EnvironmentConfig[0].Notifications = &radixv1.Notifications{Webhook: pointers.Ptr("http://app:8080")} }, }, } @@ -1900,12 +2018,12 @@ func Test_ValidateApplicationCanBeAppliedWithDNSAliases(t *testing.T) { }, { name: "Added dns aliases", - applicationBuilder: utils.ARadixApplication().WithDNSAlias(v1.DNSAlias{Alias: alias1, Environment: raEnv, Component: raComponentName}), + applicationBuilder: utils.ARadixApplication().WithDNSAlias(radixv1.DNSAlias{Alias: alias1, Environment: raEnv, Component: raComponentName}), expectedValidationError: nil, }, { name: "Existing dns aliases for the app", - applicationBuilder: utils.ARadixApplication().WithDNSAlias(v1.DNSAlias{Alias: alias1, Environment: raEnv, Component: raComponentName}), + applicationBuilder: utils.ARadixApplication().WithDNSAlias(radixv1.DNSAlias{Alias: alias1, Environment: raEnv, Component: raComponentName}), existingRadixDNSAliases: map[string]commonTest.DNSAlias{ alias1: {AppName: raAppName, Environment: raEnv, Component: raComponentName}, }, @@ -1913,7 +2031,7 @@ func Test_ValidateApplicationCanBeAppliedWithDNSAliases(t *testing.T) { }, { name: "Existing dns aliases for the app and another app", - applicationBuilder: utils.ARadixApplication().WithDNSAlias(v1.DNSAlias{Alias: alias1, Environment: raEnv, Component: raComponentName}), + applicationBuilder: utils.ARadixApplication().WithDNSAlias(radixv1.DNSAlias{Alias: alias1, Environment: raEnv, Component: raComponentName}), existingRadixDNSAliases: map[string]commonTest.DNSAlias{ alias1: {AppName: raAppName, Environment: raEnv, Component: raComponentName}, alias2: {AppName: otherAppName, Environment: someEnv, Component: someComponentName}, @@ -1922,7 +2040,7 @@ func Test_ValidateApplicationCanBeAppliedWithDNSAliases(t *testing.T) { }, { name: "Same alias exists in dns alias for another app", - applicationBuilder: utils.ARadixApplication().WithDNSAlias(v1.DNSAlias{Alias: alias1, Environment: raEnv, Component: raComponentName}), + applicationBuilder: utils.ARadixApplication().WithDNSAlias(radixv1.DNSAlias{Alias: alias1, Environment: raEnv, Component: raComponentName}), existingRadixDNSAliases: map[string]commonTest.DNSAlias{ alias1: {AppName: otherAppName, Environment: someEnv, Component: someComponentName}, }, @@ -1930,17 +2048,17 @@ func Test_ValidateApplicationCanBeAppliedWithDNSAliases(t *testing.T) { }, { name: "Reserved alias api for another app", - applicationBuilder: utils.ARadixApplication().WithDNSAlias(v1.DNSAlias{Alias: "api", Environment: raEnv, Component: raComponentName}), + applicationBuilder: utils.ARadixApplication().WithDNSAlias(radixv1.DNSAlias{Alias: "api", Environment: raEnv, Component: raComponentName}), expectedValidationError: radixvalidators.RadixDNSAliasIsReservedForRadixPlatformApplicationError("api"), }, { name: "Reserved alias api for another service", - applicationBuilder: utils.ARadixApplication().WithDNSAlias(v1.DNSAlias{Alias: "grafana", Environment: raEnv, Component: raComponentName}), + applicationBuilder: utils.ARadixApplication().WithDNSAlias(radixv1.DNSAlias{Alias: "grafana", Environment: raEnv, Component: raComponentName}), expectedValidationError: radixvalidators.RadixDNSAliasIsReservedForRadixPlatformServiceError("grafana"), }, { name: "Reserved alias api for this app", - applicationBuilder: utils.ARadixApplication().WithAppName("radix-api").WithDNSAlias(v1.DNSAlias{Alias: "api", Environment: raEnv, Component: raComponentName}), + applicationBuilder: utils.ARadixApplication().WithAppName("radix-api").WithDNSAlias(radixv1.DNSAlias{Alias: "api", Environment: raEnv, Component: raComponentName}), expectedValidationError: nil, }, } @@ -1967,7 +2085,7 @@ func Test_ValidateApplicationCanBeAppliedWithDNSAliases(t *testing.T) { } } -func createValidRA() *v1.RadixApplication { +func createValidRA() *radixv1.RadixApplication { validRA, _ := utils.GetRadixApplicationFromFile("testdata/radixconfig.yaml") return validRA diff --git a/pkg/apis/securitycontext/container.go b/pkg/apis/securitycontext/container.go index db188c624..bf79474b0 100644 --- a/pkg/apis/securitycontext/container.go +++ b/pkg/apis/securitycontext/container.go @@ -56,7 +56,11 @@ func WithContainerCapabilities(capabilities []corev1.Capability) ContainerOption } } } - +func WithReadOnlyRootFileSystem(readOnly *bool) ContainerOption { + return func(securityContext *corev1.SecurityContext) { + securityContext.ReadOnlyRootFilesystem = readOnly + } +} func Container(options ...ContainerOption) *corev1.SecurityContext { securityContext := &corev1.SecurityContext{ AllowPrivilegeEscalation: commonUtils.BoolPtr(false), diff --git a/pkg/apis/utils/applicationcomponent_builder.go b/pkg/apis/utils/applicationcomponent_builder.go index 46e8a9ff8..bbf8d2cb2 100644 --- a/pkg/apis/utils/applicationcomponent_builder.go +++ b/pkg/apis/utils/applicationcomponent_builder.go @@ -28,6 +28,7 @@ type RadixApplicationComponentBuilder interface { WithVolumeMounts([]v1.RadixVolumeMount) RadixApplicationComponentBuilder WithEnabled(bool) RadixApplicationComponentBuilder WithIdentity(*v1.Identity) RadixApplicationComponentBuilder + WithReadOnlyFileSystem(*bool) RadixApplicationComponentBuilder BuildComponent() v1.RadixComponent } @@ -52,6 +53,7 @@ type radixApplicationComponentBuilder struct { volumeMounts []v1.RadixVolumeMount enabled *bool identity *v1.Identity + readOnlyFileSystem *bool } func (rcb *radixApplicationComponentBuilder) WithName(name string) RadixApplicationComponentBuilder { @@ -178,6 +180,11 @@ func (rcb *radixApplicationComponentBuilder) WithCommonResource(request map[stri return rcb } +func (rcb *radixApplicationComponentBuilder) WithReadOnlyFileSystem(readOnlyFileSystem *bool) RadixApplicationComponentBuilder { + rcb.readOnlyFileSystem = readOnlyFileSystem + return rcb +} + func (rcb *radixApplicationComponentBuilder) BuildComponent() v1.RadixComponent { var environmentConfig = make([]v1.RadixEnvironmentConfig, 0) for _, env := range rcb.environmentConfig { @@ -204,6 +211,7 @@ func (rcb *radixApplicationComponentBuilder) BuildComponent() v1.RadixComponent Authentication: rcb.authentication, Enabled: rcb.enabled, Identity: rcb.identity, + ReadOnlyFileSystem: rcb.readOnlyFileSystem, } } diff --git a/pkg/apis/utils/applicationjobcomponent_builder.go b/pkg/apis/utils/applicationjobcomponent_builder.go index 77bedd5db..d3bdc751f 100644 --- a/pkg/apis/utils/applicationjobcomponent_builder.go +++ b/pkg/apis/utils/applicationjobcomponent_builder.go @@ -25,30 +25,32 @@ type RadixApplicationJobComponentBuilder interface { WithEnabled(bool) RadixApplicationJobComponentBuilder WithIdentity(*v1.Identity) RadixApplicationJobComponentBuilder WithNotifications(*v1.Notifications) RadixApplicationJobComponentBuilder + WithReadOnlyFileSystem(*bool) RadixApplicationJobComponentBuilder BuildJobComponent() v1.RadixJobComponent } type radixApplicationJobComponentBuilder struct { - name string - sourceFolder string - dockerfileName string - image string - ports []v1.ComponentPort - secrets []string - secretRefs v1.RadixSecretRefs - monitoringConfig v1.MonitoringConfig - environmentConfig []RadixJobComponentEnvironmentConfigBuilder - variables v1.EnvVarsMap - resources v1.ResourceRequirements - schedulerPort *int32 - payloadPath *string - node v1.RadixNode - volumes []v1.RadixVolumeMount - timeLimitSeconds *int64 - backoffLimit *int32 - enabled *bool - identity *v1.Identity - notifications *v1.Notifications + name string + sourceFolder string + dockerfileName string + image string + ports []v1.ComponentPort + secrets []string + secretRefs v1.RadixSecretRefs + monitoringConfig v1.MonitoringConfig + environmentConfig []RadixJobComponentEnvironmentConfigBuilder + variables v1.EnvVarsMap + resources v1.ResourceRequirements + schedulerPort *int32 + payloadPath *string + node v1.RadixNode + volumes []v1.RadixVolumeMount + timeLimitSeconds *int64 + backoffLimit *int32 + enabled *bool + identity *v1.Identity + notifications *v1.Notifications + readOnlyFileSystem *bool } func (rcb *radixApplicationJobComponentBuilder) WithTimeLimitSeconds(timeLimitSeconds *int64) RadixApplicationJobComponentBuilder { @@ -173,7 +175,10 @@ func (rcb *radixApplicationJobComponentBuilder) WithNotifications(notifications rcb.notifications = notifications return rcb } - +func (rcb *radixApplicationJobComponentBuilder) WithReadOnlyFileSystem(readOnlyFileSystem *bool) RadixApplicationJobComponentBuilder { + rcb.readOnlyFileSystem = readOnlyFileSystem + return rcb +} func (rcb *radixApplicationJobComponentBuilder) BuildJobComponent() v1.RadixJobComponent { var environmentConfig = make([]v1.RadixJobComponentEnvironmentConfig, 0) for _, env := range rcb.environmentConfig { @@ -186,25 +191,26 @@ func (rcb *radixApplicationJobComponentBuilder) BuildJobComponent() v1.RadixJobC } return v1.RadixJobComponent{ - Name: rcb.name, - SourceFolder: rcb.sourceFolder, - DockerfileName: rcb.dockerfileName, - Image: rcb.image, - Ports: rcb.ports, - Secrets: rcb.secrets, - SecretRefs: rcb.secretRefs, - MonitoringConfig: rcb.monitoringConfig, - EnvironmentConfig: environmentConfig, - Variables: rcb.variables, - Resources: rcb.resources, - SchedulerPort: rcb.schedulerPort, - Payload: payload, - Node: rcb.node, - TimeLimitSeconds: rcb.timeLimitSeconds, - BackoffLimit: rcb.backoffLimit, - Enabled: rcb.enabled, - Identity: rcb.identity, - Notifications: rcb.notifications, + Name: rcb.name, + SourceFolder: rcb.sourceFolder, + DockerfileName: rcb.dockerfileName, + Image: rcb.image, + Ports: rcb.ports, + Secrets: rcb.secrets, + SecretRefs: rcb.secretRefs, + MonitoringConfig: rcb.monitoringConfig, + EnvironmentConfig: environmentConfig, + Variables: rcb.variables, + Resources: rcb.resources, + SchedulerPort: rcb.schedulerPort, + Payload: payload, + Node: rcb.node, + TimeLimitSeconds: rcb.timeLimitSeconds, + BackoffLimit: rcb.backoffLimit, + Enabled: rcb.enabled, + Identity: rcb.identity, + Notifications: rcb.notifications, + ReadOnlyFileSystem: rcb.readOnlyFileSystem, } } diff --git a/pkg/apis/utils/componentenvironment_builder.go b/pkg/apis/utils/componentenvironment_builder.go index 6550b1d22..b8b0edff5 100644 --- a/pkg/apis/utils/componentenvironment_builder.go +++ b/pkg/apis/utils/componentenvironment_builder.go @@ -21,6 +21,7 @@ type RadixEnvironmentConfigBuilder interface { WithIdentity(*v1.Identity) RadixEnvironmentConfigBuilder WithImageTagName(string) RadixEnvironmentConfigBuilder WithHorizontalScaling(*int32, int32, *int32, *int32) RadixEnvironmentConfigBuilder + WithReadOnlyFileSystem(*bool) RadixEnvironmentConfigBuilder } type radixEnvironmentConfigBuilder struct { @@ -38,6 +39,7 @@ type radixEnvironmentConfigBuilder struct { identity *v1.Identity imageTagName string horizontalScaling *v1.RadixHorizontalScaling + readOnlyFileSystem *bool } func (ceb *radixEnvironmentConfigBuilder) WithHorizontalScaling(minReplicas *int32, maxReplicas int32, cpu *int32, memory *int32) RadixEnvironmentConfigBuilder { @@ -138,6 +140,10 @@ func (ceb *radixEnvironmentConfigBuilder) WithImageTagName(imageTagName string) ceb.imageTagName = imageTagName return ceb } +func (ceb *radixEnvironmentConfigBuilder) WithReadOnlyFileSystem(readOnlyFileSystem *bool) RadixEnvironmentConfigBuilder { + ceb.readOnlyFileSystem = readOnlyFileSystem + return ceb +} func (ceb *radixEnvironmentConfigBuilder) BuildEnvironmentConfig() v1.RadixEnvironmentConfig { return v1.RadixEnvironmentConfig{ @@ -155,6 +161,7 @@ func (ceb *radixEnvironmentConfigBuilder) BuildEnvironmentConfig() v1.RadixEnvir Identity: ceb.identity, ImageTagName: ceb.imageTagName, HorizontalScaling: ceb.horizontalScaling, + ReadOnlyFileSystem: ceb.readOnlyFileSystem, } } diff --git a/pkg/apis/utils/deploymentcomponent_builder.go b/pkg/apis/utils/deploymentcomponent_builder.go index 10d3cd0c4..7f49c8f34 100644 --- a/pkg/apis/utils/deploymentcomponent_builder.go +++ b/pkg/apis/utils/deploymentcomponent_builder.go @@ -35,6 +35,7 @@ type DeployComponentBuilder interface { WithRunAsNonRoot(bool) DeployComponentBuilder WithAuthentication(*v1.Authentication) DeployComponentBuilder WithIdentity(*v1.Identity) DeployComponentBuilder + WithReadOnlyFileSystem(*bool) DeployComponentBuilder BuildComponent() v1.RadixDeployComponent } @@ -56,14 +57,15 @@ type deployComponentBuilder struct { secretRefs v1.RadixSecretRefs dnsappalias bool // Deprecated: For backwards comptibility externalAppAlias is still supported, new code should use publicPort instead - externalAppAlias []string - externalDNS []v1.RadixDeployExternalDNS - resources v1.ResourceRequirements - horizontalScaling *v1.RadixHorizontalScaling - volumeMounts []v1.RadixVolumeMount - node v1.RadixNode - authentication *v1.Authentication - identity *v1.Identity + externalAppAlias []string + externalDNS []v1.RadixDeployExternalDNS + resources v1.ResourceRequirements + horizontalScaling *v1.RadixHorizontalScaling + volumeMounts []v1.RadixVolumeMount + node v1.RadixNode + authentication *v1.Authentication + identity *v1.Identity + readOnlyFileSystem *bool } func (dcb *deployComponentBuilder) WithVolumeMounts(volumeMounts ...v1.RadixVolumeMount) DeployComponentBuilder { @@ -242,6 +244,11 @@ func (dcb *deployComponentBuilder) WithIdentity(identity *v1.Identity) DeployCom return dcb } +func (dcb *deployComponentBuilder) WithReadOnlyFileSystem(readOnlyFileSystem *bool) DeployComponentBuilder { + dcb.readOnlyFileSystem = readOnlyFileSystem + return dcb +} + func (dcb *deployComponentBuilder) BuildComponent() v1.RadixDeployComponent { return v1.RadixDeployComponent{ Image: dcb.image, @@ -266,6 +273,7 @@ func (dcb *deployComponentBuilder) BuildComponent() v1.RadixDeployComponent { Node: dcb.node, Authentication: dcb.authentication, Identity: dcb.identity, + ReadOnlyFileSystem: dcb.readOnlyFileSystem, } } diff --git a/pkg/apis/utils/jobcomponentenvironment_builder.go b/pkg/apis/utils/jobcomponentenvironment_builder.go index 16ffe34d5..ab4e71eb7 100644 --- a/pkg/apis/utils/jobcomponentenvironment_builder.go +++ b/pkg/apis/utils/jobcomponentenvironment_builder.go @@ -18,24 +18,26 @@ type RadixJobComponentEnvironmentConfigBuilder interface { WithEnabled(bool) RadixJobComponentEnvironmentConfigBuilder WithIdentity(*v1.Identity) RadixJobComponentEnvironmentConfigBuilder WithNotifications(*v1.Notifications) RadixJobComponentEnvironmentConfigBuilder + WithReadOnlyFileSystem(*bool) RadixJobComponentEnvironmentConfigBuilder BuildEnvironmentConfig() v1.RadixJobComponentEnvironmentConfig } type radixJobComponentEnvironmentConfigBuilder struct { - environment string - variables v1.EnvVarsMap - resources v1.ResourceRequirements - volumeMounts []v1.RadixVolumeMount - imageTagName string - monitoring bool - node v1.RadixNode - runAsNonRoot bool - secretRefs v1.RadixSecretRefs - timeLimitSeconds *int64 - backoffLimit *int32 - enabled *bool - identity *v1.Identity - notifications *v1.Notifications + environment string + variables v1.EnvVarsMap + resources v1.ResourceRequirements + volumeMounts []v1.RadixVolumeMount + imageTagName string + monitoring bool + node v1.RadixNode + runAsNonRoot bool + secretRefs v1.RadixSecretRefs + timeLimitSeconds *int64 + backoffLimit *int32 + enabled *bool + identity *v1.Identity + notifications *v1.Notifications + readOnlyFileSystem *bool } func (ceb *radixJobComponentEnvironmentConfigBuilder) WithTimeLimitSeconds(timeLimitSeconds *int64) RadixJobComponentEnvironmentConfigBuilder { @@ -114,22 +116,26 @@ func (ceb *radixJobComponentEnvironmentConfigBuilder) WithNotifications(notifica ceb.notifications = notifications return ceb } - +func (ceb *radixJobComponentEnvironmentConfigBuilder) WithReadOnlyFileSystem(readOnlyFileSystem *bool) RadixJobComponentEnvironmentConfigBuilder { + ceb.readOnlyFileSystem = readOnlyFileSystem + return ceb +} func (ceb *radixJobComponentEnvironmentConfigBuilder) BuildEnvironmentConfig() v1.RadixJobComponentEnvironmentConfig { return v1.RadixJobComponentEnvironmentConfig{ - Environment: ceb.environment, - Variables: ceb.variables, - Resources: ceb.resources, - VolumeMounts: ceb.volumeMounts, - Monitoring: ceb.monitoring, - ImageTagName: ceb.imageTagName, - Node: ceb.node, - SecretRefs: ceb.secretRefs, - TimeLimitSeconds: ceb.timeLimitSeconds, - BackoffLimit: ceb.backoffLimit, - Enabled: ceb.enabled, - Identity: ceb.identity, - Notifications: ceb.notifications, + Environment: ceb.environment, + Variables: ceb.variables, + Resources: ceb.resources, + VolumeMounts: ceb.volumeMounts, + Monitoring: ceb.monitoring, + ImageTagName: ceb.imageTagName, + Node: ceb.node, + SecretRefs: ceb.secretRefs, + TimeLimitSeconds: ceb.timeLimitSeconds, + BackoffLimit: ceb.backoffLimit, + Enabled: ceb.enabled, + Identity: ceb.identity, + Notifications: ceb.notifications, + ReadOnlyFileSystem: ceb.readOnlyFileSystem, } }