From f57d06d2fc7716ae43a31233526ef88011d6e8d4 Mon Sep 17 00:00:00 2001 From: Pablo Criado-Perez Date: Wed, 5 May 2021 11:10:39 -0600 Subject: [PATCH] Use unstructured instead of v1 Deployment (#83) * allow individual overrides and check for null * updated to support paramaeters * fix merge * fix tests --- pipeline/builder/builder.go | 94 +++++++++++++++++---- pipeline/builder/builder_test.go | 40 +++++++-- pipeline/builder/embedded_manifests_test.go | 7 +- test-deployment.yml | 2 +- 4 files changed, 114 insertions(+), 29 deletions(-) diff --git a/pipeline/builder/builder.go b/pipeline/builder/builder.go index ad7165f..ad73a93 100644 --- a/pipeline/builder/builder.go +++ b/pipeline/builder/builder.go @@ -12,12 +12,10 @@ import ( "github.com/namely/k8s-pipeliner/pipeline/builder/types" "github.com/namely/k8s-pipeliner/pipeline/config" "github.com/pkg/errors" - v1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" ) const ( @@ -325,30 +323,47 @@ func (b *Builder) buildDeployEmbeddedManifestStage(index int, s config.Stage) (* continue } - // if deployment set container overrides - var d v1.Deployment - err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.UnstructuredContent(), &d) - if err != nil { - return nil, errors.Wrapf(err, "could not parse Deployment: %s", u.GetName()) - } - for ii, specContainer := range d.Spec.Template.Spec.Containers { + // if containers in deployment set container overrides + c, _, _ := unstructured.NestedFieldNoCopy(u.Object, "spec", "template", "spec", "containers") + containers := c.([]interface{}) + + for i, unstructuredContainer := range containers { + container := unstructuredContainer.(map[string]interface{}) for _, overrideContainer := range maniStage.ContainerOverrides { - if specContainer.Name != overrideContainer.Name || overrideContainer.Resources == nil { + if container["name"] != overrideContainer.Name || overrideContainer.Resources == nil { continue } - requests, err := overrideResource(specContainer.Resources.Requests, overrideContainer.Resources.Requests) + c := (containers[i]).(map[string]interface{}) + // set resources requests + requests, _, _ := unstructured.NestedFieldNoCopy(c, "resources", "requests") + requestsTyped, err := toResourceList(requests) + if err != nil { + return nil, errors.Wrapf(err, "failed to convert resources requests to a resource list for container: %s", overrideContainer.Name) + } + requests, err = overrideResource(requestsTyped, overrideContainer.Resources.Requests) if err != nil { return nil, errors.Wrapf(err, errOverrideResource, "requests", overrideContainer.Name) } - d.Spec.Template.Spec.Containers[ii].Resources.Requests = requests - limits, err := overrideResource(specContainer.Resources.Limits, overrideContainer.Resources.Limits) + if err := setNestedFieldNoCopy(c, requests, "resources", "requests"); err != nil { + return nil, errors.Wrapf(err, "failed to set resources requests for container: %s", overrideContainer.Name) + } + + // set resources limits + limits, _, _ := unstructured.NestedFieldNoCopy(c, "resources", "limits") + limitsTyped, err := toResourceList(limits) + if err != nil { + return nil, errors.Wrapf(err, "failed to convert resources limits to a resource list for container: %s", overrideContainer.Name) + } + limits, err = overrideResource(limitsTyped, overrideContainer.Resources.Limits) if err != nil { return nil, errors.Wrapf(err, errOverrideResource, "limits", overrideContainer.Name) } - d.Spec.Template.Spec.Containers[ii].Resources.Limits = limits + if err := setNestedFieldNoCopy(c, limits, "resources", "limits"); err != nil { + return nil, errors.Wrapf(err, "failed to set resources requests for container: %s", overrideContainer.Name) + } } } - objs[i] = d.DeepCopyObject() + objs[i] = u } ds.Manifests = append(ds.Manifests, objs...) @@ -458,17 +473,43 @@ func overrideResource(resourceList corev1.ResourceList, override *config.Resourc if override.Memory != "" { memoryQty, err := resource.ParseQuantity(override.Memory) if err != nil { - return nil, errors.Wrapf(err, "could not parse memory") + return result, errors.Wrapf(err, "could not parse memory") } result[corev1.ResourceMemory] = memoryQty } if override.CPU != "" { cpuQty, err := resource.ParseQuantity(override.CPU) if err != nil { - return nil, errors.Wrapf(err, "could not parse memory") + return result, errors.Wrapf(err, "could not parse memory") + } + result[corev1.ResourceCPU] = cpuQty + } + return result, nil +} +func toResourceList(i interface{}) (corev1.ResourceList, error) { + result := make(corev1.ResourceList) + + if i == nil { + return result, nil + } + resourceList, ok := i.(map[string]interface{}) + if !ok { + return result, nil + } + if cpu, found := resourceList[corev1.ResourceCPU.String()]; found { + cpuQty, err := resource.ParseQuantity(fmt.Sprint(cpu)) + if err != nil { + return result, errors.Wrapf(err, "could not parse cpu") } result[corev1.ResourceCPU] = cpuQty } + if memory, found := resourceList[corev1.ResourceMemory.String()]; found { + memoryQty, err := resource.ParseQuantity(fmt.Sprint(memory)) + if err != nil { + return result, errors.Wrapf(err, "could not parse memory") + } + result[corev1.ResourceMemory] = memoryQty + } return result, nil } @@ -506,6 +547,25 @@ func (b *Builder) defaultManifestStage(index int, s config.Stage) *types.Manifes return stage } +func setNestedFieldNoCopy(obj map[string]interface{}, value interface{}, fields ...string) error { + m := obj + + for i, field := range fields[:len(fields)-1] { + if val, ok := m[field]; ok { + if valMap, ok := val.(map[string]interface{}); ok { + m = valMap + } else { + return fmt.Errorf("value cannot be set because %v is not a map[string]interface{}", fields[:i+1]) + } + } else { + newVal := make(map[string]interface{}) + m[field] = newVal + m = newVal + } + } + m[fields[len(fields)-1]] = value + return nil +} func (b *Builder) buildWebHookStage(index int, s config.Stage) (*types.Webhook, error) { stage := &types.Webhook{ diff --git a/pipeline/builder/builder_test.go b/pipeline/builder/builder_test.go index e755bf3..ca20439 100644 --- a/pipeline/builder/builder_test.go +++ b/pipeline/builder/builder_test.go @@ -11,6 +11,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" v1 "k8s.io/api/apps/v1" + "k8s.io/apimachinery/pkg/runtime" ) func TestBuilderAssignsNotifications(t *testing.T) { @@ -289,8 +290,13 @@ func TestBuilderPipelineStages(t *testing.T) { require.NoError(t, err, "error generating pipeline json") assert.Equal(t, "Test DeployEmbeddedManifests Stage", spinnaker.Stages[0].(*types.ManifestStage).Name) - assert.NotNil(t, spinnaker.Stages[0].(*types.ManifestStage).Manifests[0]) - container := spinnaker.Stages[0].(*types.ManifestStage).Manifests[0].(*v1.Deployment).Spec.Template.Spec.Containers[0] + manifest := spinnaker.Stages[0].(*types.ManifestStage).Manifests[0] + assert.NotNil(t, manifest) + var d v1.Deployment + u, _ := runtime.DefaultUnstructuredConverter.ToUnstructured(manifest) + err = runtime.DefaultUnstructuredConverter.FromUnstructured(u, &d) + assert.NoError(t, err) + container := d.Spec.Template.Spec.Containers[0] assert.Equal(t, "2", container.Resources.Limits.Memory().String()) assert.Equal(t, "1", container.Resources.Limits.Cpu().String()) assert.Equal(t, "4", container.Resources.Requests.Memory().String()) @@ -347,7 +353,11 @@ func TestBuilderPipelineStages(t *testing.T) { builder := builder.New(pipeline) spinnaker, err := builder.Pipeline() require.NoError(t, err, "error generating pipeline json") - container := spinnaker.Stages[0].(*types.ManifestStage).Manifests[0].(*v1.Deployment).Spec.Template.Spec.Containers[0] + var d v1.Deployment + u, _ := runtime.DefaultUnstructuredConverter.ToUnstructured(spinnaker.Stages[0].(*types.ManifestStage).Manifests[0]) + err = runtime.DefaultUnstructuredConverter.FromUnstructured(u, &d) + require.NoError(t, err) + container := d.Spec.Template.Spec.Containers[0] assert.Equal(t, "300", container.Resources.Limits.Memory().String()) assert.Equal(t, "400", container.Resources.Limits.Cpu().String()) assert.Equal(t, "100", container.Resources.Requests.Memory().String()) @@ -379,7 +389,11 @@ func TestBuilderPipelineStages(t *testing.T) { builder := builder.New(pipeline) spinnaker, err := builder.Pipeline() require.NoError(t, err, "error generating pipeline json") - container := spinnaker.Stages[0].(*types.ManifestStage).Manifests[0].(*v1.Deployment).Spec.Template.Spec.Containers[0] + var d v1.Deployment + u, _ := runtime.DefaultUnstructuredConverter.ToUnstructured(spinnaker.Stages[0].(*types.ManifestStage).Manifests[0]) + err = runtime.DefaultUnstructuredConverter.FromUnstructured(u, &d) + require.NoError(t, err) + container := d.Spec.Template.Spec.Containers[0] assert.Equal(t, "100", container.Resources.Requests.Memory().String()) assert.Equal(t, "200", container.Resources.Requests.Cpu().String()) }) @@ -410,7 +424,11 @@ func TestBuilderPipelineStages(t *testing.T) { builder := builder.New(pipeline) spinnaker, err := builder.Pipeline() require.NoError(t, err, "error generating pipeline json") - container := spinnaker.Stages[0].(*types.ManifestStage).Manifests[0].(*v1.Deployment).Spec.Template.Spec.Containers[0] + var d v1.Deployment + u, _ := runtime.DefaultUnstructuredConverter.ToUnstructured(spinnaker.Stages[0].(*types.ManifestStage).Manifests[0]) + err = runtime.DefaultUnstructuredConverter.FromUnstructured(u, &d) + require.NoError(t, err) + container := d.Spec.Template.Spec.Containers[0] assert.Equal(t, "300Mi", container.Resources.Limits.Memory().String()) assert.Equal(t, "400m", container.Resources.Limits.Cpu().String()) }) @@ -440,7 +458,11 @@ func TestBuilderPipelineStages(t *testing.T) { builder := builder.New(pipeline) spinnaker, err := builder.Pipeline() require.NoError(t, err, "error generating pipeline json") - container := spinnaker.Stages[0].(*types.ManifestStage).Manifests[0].(*v1.Deployment).Spec.Template.Spec.Containers[0] + var d v1.Deployment + u, _ := runtime.DefaultUnstructuredConverter.ToUnstructured(spinnaker.Stages[0].(*types.ManifestStage).Manifests[0]) + err = runtime.DefaultUnstructuredConverter.FromUnstructured(u, &d) + require.NoError(t, err) + container := d.Spec.Template.Spec.Containers[0] assert.Equal(t, "4", container.Resources.Requests.Memory().String()) assert.Equal(t, "400m", container.Resources.Requests.Cpu().String()) }) @@ -470,7 +492,11 @@ func TestBuilderPipelineStages(t *testing.T) { builder := builder.New(pipeline) spinnaker, err := builder.Pipeline() require.NoError(t, err, "error generating pipeline json") - container := spinnaker.Stages[0].(*types.ManifestStage).Manifests[0].(*v1.Deployment).Spec.Template.Spec.Containers[1] + var d v1.Deployment + u, _ := runtime.DefaultUnstructuredConverter.ToUnstructured(spinnaker.Stages[0].(*types.ManifestStage).Manifests[0]) + err = runtime.DefaultUnstructuredConverter.FromUnstructured(u, &d) + require.NoError(t, err) + container := d.Spec.Template.Spec.Containers[1] assert.Equal(t, "0", container.Resources.Requests.Memory().String()) assert.Equal(t, "400m", container.Resources.Requests.Cpu().String()) }) diff --git a/pipeline/builder/embedded_manifests_test.go b/pipeline/builder/embedded_manifests_test.go index 8b59044..93c492f 100644 --- a/pipeline/builder/embedded_manifests_test.go +++ b/pipeline/builder/embedded_manifests_test.go @@ -4,7 +4,6 @@ import ( "testing" "github.com/stretchr/testify/suite" - v1 "k8s.io/api/apps/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "github.com/namely/k8s-pipeliner/pipeline/builder" @@ -57,7 +56,7 @@ func (em *EmbeddedManifestTest) TestFilesAreBuilt() { em.Require().Len(stg.Manifests, 1) - deploy, ok := stg.Manifests[0].(*v1.Deployment) + deploy, ok := stg.Manifests[0].(*unstructured.Unstructured) em.Require().True(ok) em.Equal("nginx-deployment", deploy.GetName()) } @@ -248,7 +247,7 @@ func (em *EmbeddedManifestTest) TestMonikerAnnotationsAreIncluded() { em.Equal("fake-detail", stg.Moniker.Detail) em.Equal("fake-cluster", stg.Moniker.Cluster) - _, dok := stg.Manifests[0].(*v1.Deployment) + _, dok := stg.Manifests[0].(*unstructured.Unstructured) em.Require().True(dok) } @@ -276,7 +275,7 @@ func (em *EmbeddedManifestTest) TestDeployEmbeddedManifestDefaultProperties() { em.Require().Len(stg.Manifests, 1) - deploy, ok := stg.Manifests[0].(*v1.Deployment) + deploy, ok := stg.Manifests[0].(*unstructured.Unstructured) em.Require().True(ok) em.Equal("nginx-deployment", deploy.GetName()) diff --git a/test-deployment.yml b/test-deployment.yml index 986ce2f..7fb4d27 100644 --- a/test-deployment.yml +++ b/test-deployment.yml @@ -6,7 +6,7 @@ metadata: labels: app: nginx spec: - replicas: 3 + replicas: '${ #toInt(parameters.random) }' selector: matchLabels: app: nginx