diff --git a/pkg/corerp/renderers/container/manifest.go b/pkg/corerp/renderers/container/manifest.go index 538832bdc4..6061cce165 100644 --- a/pkg/corerp/renderers/container/manifest.go +++ b/pkg/corerp/renderers/container/manifest.go @@ -161,15 +161,6 @@ func getServiceAccountBase(manifest kubeutil.ObjectManifest, appName string, r * return defaultAccount } -func getObjectMeta(metaObj metav1.ObjectMeta, appName, resourceName, resourceType string, options renderers.RenderOptions) metav1.ObjectMeta { - return metav1.ObjectMeta{ - Name: kubernetes.NormalizeResourceName(resourceName), - Namespace: options.Environment.Namespace, - Labels: labels.Merge(metaObj.Labels, renderers.GetLabels(options, appName, resourceName, resourceType)), - Annotations: labels.Merge(metaObj.Annotations, renderers.GetAnnotations(options)), - } -} - // populateAllBaseResources populates all remaining resources from manifest into outputResources. // These resources must be deployed before Deployment resource by adding them as a dependency. func populateAllBaseResources(ctx context.Context, base kubeutil.ObjectManifest, outputResources []rpv1.OutputResource, options renderers.RenderOptions) []rpv1.OutputResource { @@ -238,3 +229,34 @@ func patchPodSpec(sourceSpec *corev1.PodSpec, patchSpec []byte) (*corev1.PodSpec return patched, nil } + +func mergeLabelSelector(base *metav1.LabelSelector, cur *metav1.LabelSelector) *metav1.LabelSelector { + if base == nil { + base = &metav1.LabelSelector{} + } + + return &metav1.LabelSelector{ + MatchLabels: labels.Merge(base.MatchLabels, cur.MatchLabels), + MatchExpressions: append(base.MatchExpressions, cur.MatchExpressions...), + } +} + +func mergeObjectMeta(base metav1.ObjectMeta, cur metav1.ObjectMeta) metav1.ObjectMeta { + return metav1.ObjectMeta{ + Name: cur.Name, + Namespace: cur.Namespace, + Labels: labels.Merge(base.Labels, cur.Labels), + Annotations: labels.Merge(base.Annotations, cur.Annotations), + } +} + +func getObjectMeta(base metav1.ObjectMeta, appName, resourceName, resourceType string, options renderers.RenderOptions) metav1.ObjectMeta { + cur := metav1.ObjectMeta{ + Name: kubernetes.NormalizeResourceName(resourceName), + Namespace: options.Environment.Namespace, + Labels: renderers.GetLabels(options, appName, resourceName, resourceType), + Annotations: renderers.GetAnnotations(options), + } + + return mergeObjectMeta(base, cur) +} diff --git a/pkg/corerp/renderers/container/manifest_test.go b/pkg/corerp/renderers/container/manifest_test.go index 12684ddf97..f2637754ec 100644 --- a/pkg/corerp/renderers/container/manifest_test.go +++ b/pkg/corerp/renderers/container/manifest_test.go @@ -49,6 +49,160 @@ var ( testOptions = &renderers.RenderOptions{Environment: renderers.EnvironmentOptions{Namespace: "test-ns"}} ) +func TestMergeLabelSelector(t *testing.T) { + labelMergeTests := []struct { + name string + base *metav1.LabelSelector + cur *metav1.LabelSelector + expected *metav1.LabelSelector + }{ + { + name: "base is nil", + base: nil, + cur: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "key1": "value1", + }, + MatchExpressions: []metav1.LabelSelectorRequirement{ + { + Key: "key2", + Operator: metav1.LabelSelectorOpIn, + Values: []string{"value2"}, + }, + }, + }, + expected: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "key1": "value1", + }, + MatchExpressions: []metav1.LabelSelectorRequirement{ + { + Key: "key2", + Operator: metav1.LabelSelectorOpIn, + Values: []string{"value2"}, + }, + }, + }, + }, + { + name: "base includes matchLabels", + base: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "key1": "value1", + }, + }, + cur: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "key2": "value2", + }, + MatchExpressions: []metav1.LabelSelectorRequirement{ + { + Key: "key2", + Operator: metav1.LabelSelectorOpIn, + Values: []string{"value2"}, + }, + }, + }, + expected: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "key1": "value1", + "key2": "value2", + }, + MatchExpressions: []metav1.LabelSelectorRequirement{ + { + Key: "key2", + Operator: metav1.LabelSelectorOpIn, + Values: []string{"value2"}, + }, + }, + }, + }, + } + + for _, tc := range labelMergeTests { + t.Run(tc.name, func(t *testing.T) { + actual := mergeLabelSelector(tc.base, tc.cur) + require.Equal(t, tc.expected, actual) + }) + } +} + +func TestMergeObjectMeta(t *testing.T) { + mergeObjectMetaTests := []struct { + name string + base metav1.ObjectMeta + cur metav1.ObjectMeta + expected metav1.ObjectMeta + }{ + { + name: "base is empty", + base: metav1.ObjectMeta{}, + cur: metav1.ObjectMeta{ + Name: "name", + Namespace: "namespace", + Labels: map[string]string{ + "key1": "value1", + }, + Annotations: map[string]string{ + "key2": "value2", + }, + }, + expected: metav1.ObjectMeta{ + Name: "name", + Namespace: "namespace", + Labels: map[string]string{ + "key1": "value1", + }, + Annotations: map[string]string{ + "key2": "value2", + }, + }, + }, + { + name: "override name and namespace", + base: metav1.ObjectMeta{ + Name: "base", + Namespace: "base namespace", + Labels: map[string]string{ + "key1": "value1", + }, + Annotations: map[string]string{ + "key1": "value1", + }, + }, + cur: metav1.ObjectMeta{ + Name: "name", + Namespace: "namespace", + Labels: map[string]string{ + "key2": "value2", + }, + Annotations: map[string]string{ + "key2": "value2", + }, + }, + expected: metav1.ObjectMeta{ + Name: "name", + Namespace: "namespace", + Labels: map[string]string{ + "key1": "value1", + "key2": "value2", + }, + Annotations: map[string]string{ + "key1": "value1", + "key2": "value2", + }, + }, + }, + } + + for _, tc := range mergeObjectMetaTests { + t.Run(tc.name, func(t *testing.T) { + actual := mergeObjectMeta(tc.base, tc.cur) + require.Equal(t, tc.expected, actual) + }) + } +} + func TestFetchBaseManifest(t *testing.T) { manifestTests := []struct { name string diff --git a/pkg/corerp/renderers/container/render.go b/pkg/corerp/renderers/container/render.go index e93338f3c6..05f93340c5 100644 --- a/pkg/corerp/renderers/container/render.go +++ b/pkg/corerp/renderers/container/render.go @@ -612,14 +612,13 @@ func (r Renderer) makeDeployment( outputResources = append(outputResources, *roleBinding) deps = append(deps, rpv1.LocalIDKubernetesRoleBinding) - deployment.Spec.Template.ObjectMeta = metav1.ObjectMeta{ - Labels: podLabels, - Annotations: map[string]string{}, - } + deployment.Spec.Template.ObjectMeta = mergeObjectMeta(deployment.Spec.Template.ObjectMeta, metav1.ObjectMeta{ + Labels: podLabels, + }) - deployment.Spec.Selector = &metav1.LabelSelector{ + deployment.Spec.Selector = mergeLabelSelector(deployment.Spec.Selector, &metav1.LabelSelector{ MatchLabels: kubernetes.MakeSelectorLabels(applicationName, resource.Name), - } + }) podSpec.Volumes = append(podSpec.Volumes, volumes...) diff --git a/pkg/corerp/renderers/container/render_test.go b/pkg/corerp/renderers/container/render_test.go index 6bb274415d..0592f0d179 100644 --- a/pkg/corerp/renderers/container/render_test.go +++ b/pkg/corerp/renderers/container/render_test.go @@ -17,6 +17,7 @@ limitations under the License. package container import ( + "encoding/json" "fmt" "testing" @@ -34,6 +35,7 @@ import ( resources_azure "github.com/radius-project/radius/pkg/ucp/resources/azure" resources_kubernetes "github.com/radius-project/radius/pkg/ucp/resources/kubernetes" "github.com/radius-project/radius/test/testcontext" + "github.com/radius-project/radius/test/testutil" "github.com/google/uuid" "github.com/stretchr/testify/require" @@ -1700,6 +1702,89 @@ func Test_Render_StrategicPatchMerge(t *testing.T) { require.ElementsMatch(t, expectedContainers, deployment.Spec.Template.Spec.Containers) } +func Test_Render_BaseManifest(t *testing.T) { + manifestTests := []struct { + name string + inFile string + container datamodel.ContainerProperties + outFile string + }{ + { + name: "merge container, envvars, and volumes", + inFile: "basemanifest-input-merge.yaml", + container: datamodel.ContainerProperties{ + BasicResourceProperties: rpv1.BasicResourceProperties{ + Application: applicationResourceID, + }, + Container: datamodel.Container{ + Image: "someimage:latest", + Env: map[string]string{ + envVarName1: envVarValue1, + envVarName2: envVarValue2, + }, + Volumes: map[string]datamodel.VolumeProperties{ + "ephemeralVolume": { + Kind: datamodel.Ephemeral, + Ephemeral: &datamodel.EphemeralVolume{ + VolumeBase: datamodel.VolumeBase{ + MountPath: "/mnt/ephemeral", + }, + ManagedStore: datamodel.ManagedStoreMemory, + }, + }, + }, + }, + }, + outFile: "basemanifest-output-merge.json", + }, + { + name: "inject new sidecar", + inFile: "basemanifest-input-addcontainer.yaml", + container: datamodel.ContainerProperties{ + BasicResourceProperties: rpv1.BasicResourceProperties{ + Application: applicationResourceID, + }, + Container: datamodel.Container{ + Image: "someimage:latest", + Env: map[string]string{ + envVarName1: envVarValue1, + envVarName2: envVarValue2, + }, + }, + }, + outFile: "basemanifest-output-addcontainer.json", + }, + } + + for _, tc := range manifestTests { + t.Run(tc.name, func(t *testing.T) { + inYAML := testutil.ReadFixture(tc.inFile) + tc.container.Runtimes = &datamodel.RuntimeProperties{ + Kubernetes: &datamodel.KubernetesRuntime{ + Base: string(inYAML), + }, + } + + resource := makeResource(t, tc.container) + dependencies := map[string]renderers.RendererDependency{} + + ctx := testcontext.New(t) + renderer := Renderer{} + output, err := renderer.Render(ctx, resource, renderers.RenderOptions{Dependencies: dependencies}) + require.NoError(t, err) + + deployment, _ := kubernetes.FindDeployment(output.Resources) + require.NotNil(t, deployment) + + actual, err := json.MarshalIndent(deployment, "", " ") + require.NoError(t, err) + + outputYaml := testutil.ReadFixture(tc.outFile) + require.Equal(t, string(outputYaml), string(actual), "actual output: %s", string(actual)) + }) + } +} + func renderOptionsEnvAndAppKubeMetadata() renderers.RenderOptions { dependencies := map[string]renderers.RendererDependency{} option := renderers.RenderOptions{Dependencies: dependencies} diff --git a/pkg/corerp/renderers/container/testdata/basemanifest-input-addcontainer.yaml b/pkg/corerp/renderers/container/testdata/basemanifest-input-addcontainer.yaml new file mode 100644 index 0000000000..54c8196aa9 --- /dev/null +++ b/pkg/corerp/renderers/container/testdata/basemanifest-input-addcontainer.yaml @@ -0,0 +1,25 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: test-container + labels: + app: test-container + annotations: + source: base-manifest-test +spec: + replicas: 3 + selector: + matchLabels: + app: test-container + basemanifest: default + template: + spec: + containers: + - name: sidecar + image: "sidecar:latest" + ports: + - containerPort: 80 + protocol: TCP + env: + - name: KEY + value: VALUE diff --git a/pkg/corerp/renderers/container/testdata/basemanifest-input-merge.yaml b/pkg/corerp/renderers/container/testdata/basemanifest-input-merge.yaml new file mode 100644 index 0000000000..21fe452104 --- /dev/null +++ b/pkg/corerp/renderers/container/testdata/basemanifest-input-merge.yaml @@ -0,0 +1,95 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: test-container + labels: + app: test-container + annotations: + source: base-manifest-test +spec: + replicas: 3 + selector: + matchLabels: + app: test-container + basemanifest: default + template: + metadata: + labels: + app: test-container + basemanifest: default + spec: + serviceAccountName: test-container + volumes: + - name: secret-vol + secret: + secretName: test-container-secret0 + containers: + - name: test-container + ports: + - containerPort: 80 + protocol: TCP + volumeMounts: + - name: secret-vol + readOnly: true + mountPath: /etc/secret-vol + env: + - name: TEST_SECRET_KEY + valueFrom: + secretKeyRef: + name: test-container-secret1 + key: secret1 + - name: TEST_CONFIGMAP_KEY + valueFrom: + configMapKeyRef: + name: test-container-config + key: TEST_CONFIGMAP +--- +apiVersion: v1 +kind: Service +metadata: + name: test-container + annotations: + source: base-manifest-test +spec: + selector: + app.kubernetes.io/name: test-container + ports: + - protocol: TCP + port: 3000 + targetPort: 3000 +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: test-container + annotations: + source: base-manifest-test +--- +apiVersion: v1 +kind: Secret +metadata: + name: test-container-secret0 + annotations: + source: base-manifest-test +type: Opaque +stringData: + 'secret0': test-secret-0 +--- +apiVersion: v1 +kind: Secret +metadata: + name: test-container-secret1 + annotations: + source: base-manifest-test +type: Opaque +stringData: + 'secret1': test-secret-1 +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: test-container-config + annotations: + source: base-manifest-test +data: + TEST_CONFIGMAP: test-configmap diff --git a/pkg/corerp/renderers/container/testdata/basemanifest-output-addcontainer.json b/pkg/corerp/renderers/container/testdata/basemanifest-output-addcontainer.json new file mode 100644 index 0000000000..68a684b94d --- /dev/null +++ b/pkg/corerp/renderers/container/testdata/basemanifest-output-addcontainer.json @@ -0,0 +1,84 @@ +{ + "kind": "Deployment", + "apiVersion": "apps/v1", + "metadata": { + "name": "test-container", + "creationTimestamp": null, + "labels": { + "app": "test-container", + "app.kubernetes.io/managed-by": "radius-rp", + "app.kubernetes.io/name": "test-container", + "app.kubernetes.io/part-of": "test-app", + "radius.dev/application": "test-app", + "radius.dev/resource": "test-container", + "radius.dev/resource-type": "applications.core-containers" + }, + "annotations": { + "source": "base-manifest-test" + } + }, + "spec": { + "replicas": 3, + "selector": { + "matchLabels": { + "app": "test-container", + "basemanifest": "default", + "radius.dev/application": "test-app", + "radius.dev/resource": "test-container" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app.kubernetes.io/managed-by": "radius-rp", + "app.kubernetes.io/name": "test-container", + "app.kubernetes.io/part-of": "test-app", + "radius.dev/application": "test-app", + "radius.dev/resource": "test-container", + "radius.dev/resource-type": "applications.core-containers" + } + }, + "spec": { + "containers": [ + { + "name": "sidecar", + "image": "sidecar:latest", + "ports": [ + { + "containerPort": 80, + "protocol": "TCP" + } + ], + "env": [ + { + "name": "KEY", + "value": "VALUE" + } + ], + "resources": {} + }, + { + "name": "test-container", + "image": "someimage:latest", + "env": [ + { + "name": "TEST_VAR_1", + "value": "TEST_VALUE_1" + }, + { + "name": "TEST_VAR_2", + "value": "81" + } + ], + "resources": {} + } + ], + "serviceAccountName": "test-container", + "enableServiceLinks": false + } + }, + "strategy": {} + }, + "status": {} +} \ No newline at end of file diff --git a/pkg/corerp/renderers/container/testdata/basemanifest-output-merge.json b/pkg/corerp/renderers/container/testdata/basemanifest-output-merge.json new file mode 100644 index 0000000000..2fdd583332 --- /dev/null +++ b/pkg/corerp/renderers/container/testdata/basemanifest-output-merge.json @@ -0,0 +1,118 @@ +{ + "kind": "Deployment", + "apiVersion": "apps/v1", + "metadata": { + "name": "test-container", + "creationTimestamp": null, + "labels": { + "app": "test-container", + "app.kubernetes.io/managed-by": "radius-rp", + "app.kubernetes.io/name": "test-container", + "app.kubernetes.io/part-of": "test-app", + "radius.dev/application": "test-app", + "radius.dev/resource": "test-container", + "radius.dev/resource-type": "applications.core-containers" + }, + "annotations": { + "source": "base-manifest-test" + } + }, + "spec": { + "replicas": 3, + "selector": { + "matchLabels": { + "app": "test-container", + "basemanifest": "default", + "radius.dev/application": "test-app", + "radius.dev/resource": "test-container" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "test-container", + "app.kubernetes.io/managed-by": "radius-rp", + "app.kubernetes.io/name": "test-container", + "app.kubernetes.io/part-of": "test-app", + "basemanifest": "default", + "radius.dev/application": "test-app", + "radius.dev/resource": "test-container", + "radius.dev/resource-type": "applications.core-containers" + } + }, + "spec": { + "volumes": [ + { + "name": "secret-vol", + "secret": { + "secretName": "test-container-secret0" + } + }, + { + "name": "ephemeralVolume", + "emptyDir": { + "medium": "Memory" + } + } + ], + "containers": [ + { + "name": "test-container", + "image": "someimage:latest", + "ports": [ + { + "containerPort": 80, + "protocol": "TCP" + } + ], + "env": [ + { + "name": "TEST_SECRET_KEY", + "valueFrom": { + "secretKeyRef": { + "name": "test-container-secret1", + "key": "secret1" + } + } + }, + { + "name": "TEST_CONFIGMAP_KEY", + "valueFrom": { + "configMapKeyRef": { + "name": "test-container-config", + "key": "TEST_CONFIGMAP" + } + } + }, + { + "name": "TEST_VAR_1", + "value": "TEST_VALUE_1" + }, + { + "name": "TEST_VAR_2", + "value": "81" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "secret-vol", + "readOnly": true, + "mountPath": "/etc/secret-vol" + }, + { + "name": "ephemeralVolume", + "mountPath": "/mnt/ephemeral" + } + ] + } + ], + "serviceAccountName": "test-container", + "enableServiceLinks": false + } + }, + "strategy": {} + }, + "status": {} +} \ No newline at end of file