diff --git a/pkg/devfile/generator/generators.go b/pkg/devfile/generator/generators.go index 9acf18d4..628187d2 100644 --- a/pkg/devfile/generator/generators.go +++ b/pkg/devfile/generator/generators.go @@ -186,8 +186,20 @@ func GetDeployment(devfileObj parser.DevfileObj, deployParams DeploymentParams) Volumes: deployParams.Volumes, } + globalAttributes, err := devfileObj.Data.GetAttributes() + if err != nil { + return nil, err + } + components, err := devfileObj.Data.GetDevfileContainerComponents(common.DevfileOptions{}) + if err != nil { + return nil, err + } + podTemplateSpec, err := getPodTemplateSpec(globalAttributes, components, podTemplateSpecParams) + if err != nil { + return nil, err + } deploySpecParams := deploymentSpecParams{ - PodTemplateSpec: *getPodTemplateSpec(podTemplateSpecParams), + PodTemplateSpec: *podTemplateSpec, PodSelectorLabels: deployParams.PodSelectorLabels, Replicas: deployParams.Replicas, } diff --git a/pkg/devfile/generator/generators_test.go b/pkg/devfile/generator/generators_test.go index fac1c503..4f465660 100644 --- a/pkg/devfile/generator/generators_test.go +++ b/pkg/devfile/generator/generators_test.go @@ -17,14 +17,16 @@ package generator import ( "fmt" + apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "reflect" + "strings" + "testing" + "github.com/stretchr/testify/assert" appsv1 "k8s.io/api/apps/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/utils/pointer" - "reflect" - "strings" - "testing" v1 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" "github.com/devfile/api/v2/pkg/attributes" @@ -1076,7 +1078,9 @@ func TestGetDeployment(t *testing.T) { name string containerComponents []v1.Component deploymentParams DeploymentParams - expected appsv1.Deployment + expected *appsv1.Deployment + attributes attributes.Attributes + wantErr bool }{ { // Currently dedicatedPod can only filter out annotations @@ -1108,7 +1112,7 @@ func TestGetDeployment(t *testing.T) { Containers: containers, Replicas: pointer.Int32Ptr(1), }, - expected: appsv1.Deployment{ + expected: &appsv1.Deployment{ ObjectMeta: objectMetaDedicatedPod, Spec: appsv1.DeploymentSpec{ Strategy: appsv1.DeploymentStrategy{ @@ -1154,7 +1158,7 @@ func TestGetDeployment(t *testing.T) { }, Containers: containers, }, - expected: appsv1.Deployment{ + expected: &appsv1.Deployment{ ObjectMeta: objectMeta, Spec: appsv1.DeploymentSpec{ Strategy: appsv1.DeploymentStrategy{ @@ -1172,6 +1176,78 @@ func TestGetDeployment(t *testing.T) { }, }, }, + { + name: "pod should have pod-overrides attribute", + containerComponents: []v1.Component{ + testingutil.GenerateDummyContainerComponent("container1", nil, []v1.Endpoint{ + { + Name: "http-8080", + TargetPort: 8080, + }, + }, nil, v1.Annotation{ + Deployment: map[string]string{ + "key1": "value1", + }, + }, nil), + testingutil.GenerateDummyContainerComponent("container2", nil, nil, nil, v1.Annotation{ + Deployment: map[string]string{ + "key2": "value2", + }, + }, nil), + }, + attributes: attributes.Attributes{ + PodOverridesAttribute: apiext.JSON{Raw: []byte("{\"spec\": {\"serviceAccountName\": \"new-service-account\"}}")}, + }, + deploymentParams: DeploymentParams{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "preserved-key": "preserved-value", + }, + }, + Containers: containers, + }, + expected: &appsv1.Deployment{ + ObjectMeta: objectMeta, + Spec: appsv1.DeploymentSpec{ + Strategy: appsv1.DeploymentStrategy{ + Type: appsv1.RecreateDeploymentStrategyType, + }, + Selector: &metav1.LabelSelector{ + MatchLabels: nil, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: objectMeta, + Spec: corev1.PodSpec{ + Containers: containers, + ServiceAccountName: "new-service-account", + }, + }, + }, + }, + }, + { + name: "pod has an invalid pod-overrides attribute that throws error", + containerComponents: []v1.Component{ + testingutil.GenerateDummyContainerComponent("container2", nil, nil, nil, v1.Annotation{ + Deployment: map[string]string{ + "key2": "value2", + }, + }, nil), + }, + attributes: attributes.Attributes{ + PodOverridesAttribute: apiext.JSON{Raw: []byte("{\"spec\": \"serviceAccountName\": \"new-service-account\"}}")}, + }, + deploymentParams: DeploymentParams{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "preserved-key": "preserved-value", + }, + }, + Containers: containers, + }, + expected: nil, + wantErr: trueBool, + }, } for _, tt := range tests { @@ -1186,18 +1262,19 @@ func TestGetDeployment(t *testing.T) { }, } // set up the mock data - mockGetComponents := mockDevfileData.EXPECT().GetComponents(options) - mockGetComponents.Return(tt.containerComponents, nil).AnyTimes() + mockDevfileData.EXPECT().GetAttributes().Return(tt.attributes, nil).AnyTimes() + mockDevfileData.EXPECT().GetDevfileContainerComponents(common.DevfileOptions{}).Return(tt.containerComponents, nil).AnyTimes() + mockDevfileData.EXPECT().GetComponents(options).Return(tt.containerComponents, nil).AnyTimes() devObj := parser.DevfileObj{ Data: mockDevfileData, } deploy, err := GetDeployment(devObj, tt.deploymentParams) // Checks for unexpected error cases - if err != nil { - t.Errorf("TestGetDeployment(): unexpected error %v", err) + if !tt.wantErr == (err != nil) { + t.Errorf("TestGetDeployment(): unexpected error %v, wantErr %v", err, tt.wantErr) } - assert.Equal(t, tt.expected, *deploy, "TestGetDeployment(): The two values should be the same.") + assert.Equal(t, tt.expected, deploy, "TestGetDeployment(): The two values should be the same.") }) } diff --git a/pkg/devfile/generator/utils.go b/pkg/devfile/generator/utils.go index b6ca034c..06297e16 100644 --- a/pkg/devfile/generator/utils.go +++ b/pkg/devfile/generator/utils.go @@ -18,27 +18,33 @@ package generator import ( "encoding/json" "fmt" - "github.com/hashicorp/go-multierror" "path/filepath" "reflect" "strings" v1 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" + "github.com/devfile/api/v2/pkg/attributes" + "github.com/devfile/library/v2/pkg/devfile/parser" "github.com/devfile/library/v2/pkg/devfile/parser/data/v2/common" + "github.com/hashicorp/go-multierror" buildv1 "github.com/openshift/api/build/v1" routev1 "github.com/openshift/api/route/v1" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" extensionsv1 "k8s.io/api/extensions/v1beta1" networkingv1 "k8s.io/api/networking/v1" + apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/apimachinery/pkg/util/strategicpatch" ) -const ContainerOverridesAttribute = "container-overrides" +const ( + ContainerOverridesAttribute = "container-overrides" + PodOverridesAttribute = "pod-overrides" +) // convertEnvs converts environment variables from the devfile structure to kubernetes structure func convertEnvs(vars []v1.EnvVar) []corev1.EnvVar { @@ -235,7 +241,7 @@ type podTemplateSpecParams struct { } // getPodTemplateSpec gets a pod template spec that can be used to create a deployment spec -func getPodTemplateSpec(podTemplateSpecParams podTemplateSpecParams) *corev1.PodTemplateSpec { +func getPodTemplateSpec(globalAttributes attributes.Attributes, components []v1.Component, podTemplateSpecParams podTemplateSpecParams) (*corev1.PodTemplateSpec, error) { podTemplateSpec := &corev1.PodTemplateSpec{ ObjectMeta: podTemplateSpecParams.ObjectMeta, Spec: corev1.PodSpec{ @@ -245,7 +251,115 @@ func getPodTemplateSpec(podTemplateSpecParams podTemplateSpecParams) *corev1.Pod }, } - return podTemplateSpec + if needsPodOverrides(globalAttributes, components) { + patchedPodTemplateSpec, err := applyPodOverrides(globalAttributes, components, podTemplateSpec) + if err != nil { + return nil, err + } + patchedPodTemplateSpec.ObjectMeta = podTemplateSpecParams.ObjectMeta + podTemplateSpec = patchedPodTemplateSpec + } + + return podTemplateSpec, nil +} + +// needsPodOverrides returns true if PodOverridesAttribute is present at Devfile or Container level attributes +func needsPodOverrides(globalAttributes attributes.Attributes, components []v1.Component) bool { + if globalAttributes.Exists(PodOverridesAttribute) { + return true + } + for _, component := range components { + if component.Attributes.Exists(PodOverridesAttribute) { + return true + } + } + return false +} + +// applyPodOverrides returns a list of all the PodOverridesAttribute set at Devfile and Container level attributes +func applyPodOverrides(globalAttributes attributes.Attributes, components []v1.Component, podTemplateSpec *corev1.PodTemplateSpec) (*corev1.PodTemplateSpec, error) { + overrides, err := getPodOverrides(globalAttributes, components) + if err != nil { + return nil, err + } + // Workaround: the definition for corev1.PodSpec does not make containers optional, so even a nil list + // will be interpreted as "delete all containers" as the serialized patch will include "containers": null. + // To avoid this, save the original containers and reset them at the end. + originalContainers := podTemplateSpec.Spec.Containers + // Save fields we do not allow to be configured in pod-overrides + originalInitContainers := podTemplateSpec.Spec.InitContainers + originalVolumes := podTemplateSpec.Spec.Volumes + + patchedTemplateBytes, err := json.Marshal(podTemplateSpec) + if err != nil { + return nil, fmt.Errorf("failed to marshal deployment to yaml: %w", err) + } + for _, override := range overrides { + patchedTemplateBytes, err = strategicpatch.StrategicMergePatch(patchedTemplateBytes, override.Raw, &corev1.PodTemplateSpec{}) + if err != nil { + return nil, fmt.Errorf("error applying pod overrides: %w", err) + } + } + patchedPodTemplateSpec := corev1.PodTemplateSpec{} + if err := json.Unmarshal(patchedTemplateBytes, &patchedPodTemplateSpec); err != nil { + return nil, fmt.Errorf("error applying pod overrides: %w", err) + } + patchedPodTemplateSpec.Spec.Containers = originalContainers + patchedPodTemplateSpec.Spec.InitContainers = originalInitContainers + patchedPodTemplateSpec.Spec.Volumes = originalVolumes + return &patchedPodTemplateSpec, nil +} + +// getPodOverrides returns PodTemplateSpecOverrides for every instance of the pod overrides attribute +// present in the DevWorkspace. The order of elements is +// 1. Pod overrides defined on Container components, in the order they appear in the DevWorkspace +// 2. Pod overrides defined in the global attributes field (.spec.template.attributes) +func getPodOverrides(globalAttributes attributes.Attributes, components []v1.Component) ([]apiext.JSON, error) { + var allOverrides []apiext.JSON + + for _, component := range components { + if component.Attributes.Exists(PodOverridesAttribute) { + override := corev1.PodTemplateSpec{} + // Check format of pod-overrides to detect errors early + if err := component.Attributes.GetInto(PodOverridesAttribute, &override); err != nil { + return nil, fmt.Errorf("failed to parse %s attribute on component %s: %w", PodOverridesAttribute, component.Name, err) + } + // Do not allow overriding containers or volumes + if override.Spec.Containers != nil { + return nil, fmt.Errorf("cannot use %s to override pod containers (component %s)", PodOverridesAttribute, component.Name) + } + if override.Spec.InitContainers != nil { + return nil, fmt.Errorf("cannot use %s to override pod initContainers (component %s)", PodOverridesAttribute, component.Name) + } + if override.Spec.Volumes != nil { + return nil, fmt.Errorf("cannot use %s to override pod volumes (component %s)", PodOverridesAttribute, component.Name) + } + patchData := component.Attributes[PodOverridesAttribute] + allOverrides = append(allOverrides, patchData) + } + } + + if globalAttributes.Exists(PodOverridesAttribute) { + override := corev1.PodTemplateSpec{} + err := globalAttributes.GetInto(PodOverridesAttribute, &override) + if err != nil { + return nil, fmt.Errorf("failed to parse %s attribute for pod: %w", PodOverridesAttribute, err) + } + // Do not allow overriding containers or volumes + if override.Spec.Containers != nil { + return nil, fmt.Errorf("cannot use %s to override pod containers", PodOverridesAttribute) + } + if override.Spec.InitContainers != nil { + return nil, fmt.Errorf("cannot use %s to override pod initContainers", PodOverridesAttribute) + } + if override.Spec.Volumes != nil { + return nil, fmt.Errorf("cannot use %s to override pod volumes", PodOverridesAttribute) + } + patchData := globalAttributes[PodOverridesAttribute] + allOverrides = append(allOverrides, patchData) + } + + return allOverrides, nil } // deploymentSpecParams is a struct that contains the required data to create a deployment spec object diff --git a/pkg/devfile/generator/utils_test.go b/pkg/devfile/generator/utils_test.go index 3806debf..268655ef 100644 --- a/pkg/devfile/generator/utils_test.go +++ b/pkg/devfile/generator/utils_test.go @@ -16,6 +16,7 @@ package generator import ( + "fmt" "github.com/hashicorp/go-multierror" "github.com/stretchr/testify/assert" "k8s.io/utils/pointer" @@ -38,6 +39,7 @@ import ( corev1 "k8s.io/api/core/v1" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/intstr" ) @@ -660,7 +662,11 @@ func TestGetPodTemplateSpec(t *testing.T) { Volumes: volume, InitContainers: container, } - podTemplateSpec := getPodTemplateSpec(podTemplateSpecParams) + + podTemplateSpec, err := getPodTemplateSpec(nil, nil, podTemplateSpecParams) + if err != nil { + t.Errorf("TestGetPodTemplateSpec() error: %s", err.Error()) + } if podTemplateSpec.Name != tt.podName { t.Errorf("TestGetPodTemplateSpec() error: expected podName %s, actual %s", tt.podName, podTemplateSpec.Name) @@ -1848,3 +1854,292 @@ func Test_containerOverridesHandler(t *testing.T) { }) } } + +func Test_applyPodOverrides(t *testing.T) { + + name := "runtime" + devfileContainer := testingutil.GenerateDummyContainerComponent(name, []v1.VolumeMount{{Name: "volume-1", Path: "/projects/test"}}, nil, nil, v1.Annotation{}, pointer.Bool(true)) + k8sContainer := getContainer(containerParams{Name: name, Image: "docker.io/maven:latest"}) + + defaultPodSpec := corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-nodejs-app", + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{*k8sContainer}, + InitContainers: []corev1.Container{ + { + Name: "sidecar", + Image: "nginx", + }, + }, + Volumes: []corev1.Volume{ + { + Name: "volume-1", + }, + }, + }, + } + + type args struct { + globalAttributes attributes.Attributes + components []v1.Component + podTemplateSpec *corev1.PodTemplateSpec + } + tests := []struct { + name string + args args + want *corev1.PodTemplateSpec + wantErr bool + errString *string + }{ + { + name: "Should override field as defined inside pod-overrides Devfile level attribute", + args: args{ + globalAttributes: attributes.Attributes{ + PodOverridesAttribute: apiextensionsv1.JSON{Raw: []byte("{\"spec\": {\"serviceAccountName\": \"new-service-account\"}}")}, + }, + components: []v1.Component{ + devfileContainer, + }, + podTemplateSpec: &defaultPodSpec, + }, + want: func() *corev1.PodTemplateSpec { + podSpec := defaultPodSpec + podSpec.Spec.ServiceAccountName = "new-service-account" + return &podSpec + }(), + wantErr: false, + }, + { + name: "Should override field as defined inside pod-overrides container level attribute", + args: args{ + components: []v1.Component{ + func() v1.Component { + container := devfileContainer + container.Attributes = attributes.Attributes{ + PodOverridesAttribute: apiextensionsv1.JSON{Raw: []byte("{\"spec\": {\"schedulerName\": \"stork\"}}")}, + } + return container + }(), + }, + podTemplateSpec: &defaultPodSpec, + }, + want: func() *corev1.PodTemplateSpec { + podSpec := defaultPodSpec + podSpec.Spec.SchedulerName = "stork" + return &podSpec + }(), + wantErr: false, + }, + { + name: "Should override fields as defined inside pod-overrides attribute at devfile and container level", + args: args{ + globalAttributes: attributes.Attributes{ + PodOverridesAttribute: apiextensionsv1.JSON{Raw: []byte("{\"spec\": {\"serviceAccountName\": \"new-service-account\"}}")}, + }, + components: []v1.Component{ + func() v1.Component { + container := devfileContainer + container.Attributes = attributes.Attributes{ + PodOverridesAttribute: apiextensionsv1.JSON{Raw: []byte("{\"spec\": {\"schedulerName\": \"stork\"}}")}, + } + return container + }(), + }, + podTemplateSpec: &defaultPodSpec, + }, + want: func() *corev1.PodTemplateSpec { + podSpec := defaultPodSpec + podSpec.Spec.ServiceAccountName = "new-service-account" + podSpec.Spec.SchedulerName = "stork" + return &podSpec + }(), + }, + { + name: "Should override field as defined inside pod-overrides attribute with delete $patch directive", + args: args{ + globalAttributes: attributes.Attributes{ + PodOverridesAttribute: apiextensionsv1.JSON{Raw: []byte("{\"spec\": {\"securityContext\": {\"$patch\": \"delete\"}}}")}, + }, + components: []v1.Component{devfileContainer}, + podTemplateSpec: func() *corev1.PodTemplateSpec { + podSpec := defaultPodSpec + podSpec.Spec.SecurityContext = &corev1.PodSecurityContext{ + RunAsNonRoot: pointer.Bool(true), + } + return &podSpec + }(), + }, + want: func() *corev1.PodTemplateSpec { + podSpec := defaultPodSpec + podSpec.Spec.SecurityContext = &corev1.PodSecurityContext{} + return &podSpec + }(), + wantErr: false, + }, + { + name: "Should override field as defined inside pod-overrides attribute with replace $patch directive", + args: args{ + globalAttributes: attributes.Attributes{ + PodOverridesAttribute: apiextensionsv1.JSON{Raw: []byte("{\"spec\": {\"securityContext\": {\"runAsNonRoot\": false, \"$patch\": \"replace\"}}}\n")}, + }, + components: []v1.Component{devfileContainer}, + podTemplateSpec: func() *corev1.PodTemplateSpec { + podSpec := defaultPodSpec + podSpec.Spec.SecurityContext = &corev1.PodSecurityContext{ + RunAsGroup: pointer.Int64(3000), + RunAsUser: pointer.Int64(1000), + RunAsNonRoot: pointer.Bool(true), + } + return &podSpec + }(), + }, + want: func() *corev1.PodTemplateSpec { + podSpec := defaultPodSpec + podSpec.Spec.SecurityContext = &corev1.PodSecurityContext{ + RunAsNonRoot: pointer.Bool(false), + } + return &podSpec + }(), + wantErr: false, + }, + { + name: "Should fail to override invalid json in pod-overrides attribute defined at Devfile level", + args: args{ + globalAttributes: attributes.Attributes{ + PodOverridesAttribute: apiextensionsv1.JSON{Raw: []byte("{\"spec\": \"containers\": []}")}, + }, + components: nil, + podTemplateSpec: &defaultPodSpec, + }, + want: nil, + wantErr: true, + errString: pointer.String(fmt.Sprintf("failed to parse %s attribute for pod", PodOverridesAttribute)), + }, + { + name: "Should fail to override invalid json in pod-overrides attribute defined at component level", + args: args{ + globalAttributes: nil, + components: []v1.Component{ + func() v1.Component { + container := devfileContainer + container.Attributes = attributes.Attributes{ + PodOverridesAttribute: apiextensionsv1.JSON{Raw: []byte("{\"spec\": \"containers\": []}")}, + } + return container + }(), + }, + podTemplateSpec: &defaultPodSpec, + }, + want: nil, + wantErr: true, + errString: pointer.String(fmt.Sprintf("failed to parse %s attribute on component %s", PodOverridesAttribute, devfileContainer.Name)), + }, + { + name: "Should fail to override restricted fields 'containers' at Devfile attribute level", + args: args{ + globalAttributes: attributes.Attributes{ + PodOverridesAttribute: apiextensionsv1.JSON{Raw: []byte("{\"spec\": {\"containers\": [{\"name\": \"container-1\", \"image\": \"busybox\"}]}}")}, + }, + components: nil, + podTemplateSpec: &defaultPodSpec, + }, + want: nil, + wantErr: true, + errString: pointer.String(fmt.Sprintf("cannot use %s to override pod containers", PodOverridesAttribute)), + }, + { + name: "Should fail to override restricted fields 'initContainers' at Devfile attribute level", + args: args{ + globalAttributes: attributes.Attributes{ + PodOverridesAttribute: apiextensionsv1.JSON{Raw: []byte("{\"spec\": {\"initContainers\": [{\"name\": \"sidecar-1\", \"image\": \"nginx:1.0.0\"}]}}")}, + }, + components: nil, + podTemplateSpec: &defaultPodSpec, + }, + want: nil, + wantErr: true, + errString: pointer.String(fmt.Sprintf("cannot use %s to override pod initContainers", PodOverridesAttribute)), + }, + { + name: "Should fail to override restricted fields 'volumes' at Devfile attribute level", + args: args{ + globalAttributes: attributes.Attributes{ + PodOverridesAttribute: apiextensionsv1.JSON{Raw: []byte("{\"spec\": {\"volumes\": [{\"name\": \"volume-2\"}]}}")}, + }, + components: nil, + podTemplateSpec: &defaultPodSpec, + }, + want: nil, + wantErr: true, + errString: pointer.String(fmt.Sprintf("cannot use %s to override pod volumes", PodOverridesAttribute)), + }, + { + name: "Should fail to override restricted fields 'containers' at component attribute level", + args: args{ + components: []v1.Component{ + func() v1.Component { + container := devfileContainer + container.Attributes = attributes.Attributes{ + PodOverridesAttribute: apiextensionsv1.JSON{Raw: []byte("{\"spec\": {\"containers\": [{\"name\": \"container-1\", \"image\": \"busybox\"}]}}")}, + } + return container + }(), + }, + podTemplateSpec: &defaultPodSpec, + }, + want: nil, + wantErr: true, + errString: pointer.String(fmt.Sprintf("cannot use %s to override pod containers (component %s)", PodOverridesAttribute, devfileContainer.Name)), + }, + { + name: "Should fail to override restricted fields 'initContainers' at component attribute level", + args: args{ + components: []v1.Component{ + func() v1.Component { + container := devfileContainer + container.Attributes = attributes.Attributes{ + PodOverridesAttribute: apiextensionsv1.JSON{Raw: []byte("{\"spec\": {\"initContainers\": [{\"name\": \"sidecar-1\", \"image\": \"nginx:1.0.0\"}]}}")}, + } + return container + }(), + }, + podTemplateSpec: &defaultPodSpec, + }, + want: nil, + wantErr: true, + errString: pointer.String(fmt.Sprintf("cannot use %s to override pod initContainers (component %s)", PodOverridesAttribute, devfileContainer.Name)), + }, + { + name: "Should fail to override restricted fields 'volumes' at component attribute level", + args: args{ + components: []v1.Component{ + func() v1.Component { + container := devfileContainer + container.Attributes = attributes.Attributes{ + PodOverridesAttribute: apiextensionsv1.JSON{Raw: []byte("{\"spec\": {\"volumes\": [{\"name\": \"volume-2\"}]}}")}, + } + return container + }(), + }, + podTemplateSpec: &defaultPodSpec, + }, + want: nil, + wantErr: true, + errString: pointer.String(fmt.Sprintf("cannot use %s to override pod volumes (component %s)", PodOverridesAttribute, devfileContainer.Name)), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := applyPodOverrides(tt.args.globalAttributes, tt.args.components, tt.args.podTemplateSpec) + if !tt.wantErr == (err != nil) { + t.Errorf("ApplyPodOverrides() error: %v, wantErr %v", err, tt.wantErr) + } + if tt.wantErr { + assert.Contains(t, err.Error(), *tt.errString) + } + assert.Equalf(t, tt.want, got, "ApplyPodOverrides(%v, %v, %v)", tt.args.globalAttributes, tt.args.components, tt.args.podTemplateSpec) + }) + } +}