diff --git a/pkg/manifests/template/template.go b/pkg/manifests/template/template.go index 721c16b25..e50ed570a 100644 --- a/pkg/manifests/template/template.go +++ b/pkg/manifests/template/template.go @@ -25,6 +25,47 @@ type Template interface { } func Render(dir string, svc *console.ServiceDeploymentForAgent, utilFactory util.Factory) ([]unstructured.Unstructured, error) { + if len(svc.Renderers) == 0 { + return renderDefault(dir, svc, utilFactory) + } + + var allManifests []unstructured.Unstructured + for _, renderer := range svc.Renderers { + var manifests []unstructured.Unstructured + var err error + + switch renderer.Type { + case console.RendererTypeAuto: + manifests, err = renderDefault(renderer.Path, svc, utilFactory) + case console.RendererTypeRaw: + manifests, err = NewRaw(renderer.Path).Render(svc, utilFactory) + case console.RendererTypeHelm: + svcCopy := *svc + if renderer.Helm != nil { + svcCopy.Helm = &console.ServiceDeploymentForAgent_Helm{ + Values: renderer.Helm.Values, + ValuesFiles: renderer.Helm.ValuesFiles, + Release: renderer.Helm.Release, + } + } + manifests, err = NewHelm(renderer.Path).Render(&svcCopy, utilFactory) + case console.RendererTypeKustomize: + manifests, err = NewKustomize(renderer.Path).Render(svc, utilFactory) + default: + return nil, fmt.Errorf("unknown renderer type: %s", renderer.Type) + } + + if err != nil { + return nil, fmt.Errorf("error rendering path %s with type %s: %w", renderer.Path, renderer.Type, err) + } + + allManifests = append(allManifests, manifests...) + } + + return allManifests, nil +} + +func renderDefault(dir string, svc *console.ServiceDeploymentForAgent, utilFactory util.Factory) ([]unstructured.Unstructured, error) { renderer := RendererRaw _ = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { diff --git a/pkg/manifests/template/template_test.go b/pkg/manifests/template/template_test.go new file mode 100644 index 000000000..b15729da6 --- /dev/null +++ b/pkg/manifests/template/template_test.go @@ -0,0 +1,246 @@ +package template + +import ( + "context" + "log" + "net/http" + "path/filepath" + "sort" + "strings" + "time" + + "github.com/gin-gonic/gin" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + console "github.com/pluralsh/console/go/client" + "github.com/samber/lo" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +var _ = Describe("Default template", func() { + + svc := &console.ServiceDeploymentForAgent{ + Namespace: "default", + Configuration: make([]*console.ServiceDeploymentForAgent_Configuration, 0), + } + Context("Render raw template with no renderers provided", func() { + const name = "nginx" + It("should successfully render the raw template", func() { + dir := filepath.Join("..", "..", "..", "test", "raw") + svc.Configuration = []*console.ServiceDeploymentForAgent_Configuration{ + { + Name: "name", + Value: name, + }, + } + svc.Cluster = &console.ServiceDeploymentForAgent_Cluster{ + ID: "123", + Name: "test", + } + resp, err := Render(dir, svc, utilFactory) + Expect(err).NotTo(HaveOccurred()) + Expect(len(resp)).To(Equal(1)) + Expect(resp[0].GetName()).To(Equal(name)) + }) + It("should skip templating liquid", func() { + dir := filepath.Join("..", "..", "..", "test", "rawTemplated") + svc.Templated = lo.ToPtr(false) + svc.Renderers = []*console.RendererFragment{{Path: dir, Type: console.RendererTypeAuto}} + resp, err := Render(dir, svc, utilFactory) + Expect(err).NotTo(HaveOccurred()) + Expect(len(resp)).To(Equal(1)) + Expect(resp[0].GetName()).To(Equal(name)) + }) + + }) +}) + +var _ = Describe("Default template, AUTO", func() { + + svc := &console.ServiceDeploymentForAgent{ + Namespace: "default", + Configuration: make([]*console.ServiceDeploymentForAgent_Configuration, 0), + } + Context("Render raw template ", func() { + const name = "nginx" + It("should successfully render the raw template", func() { + dir := filepath.Join("..", "..", "..", "test", "raw") + svc.Configuration = []*console.ServiceDeploymentForAgent_Configuration{ + { + Name: "name", + Value: name, + }, + } + svc.Cluster = &console.ServiceDeploymentForAgent_Cluster{ + ID: "123", + Name: "test", + } + svc.Renderers = []*console.RendererFragment{{Path: dir, Type: console.RendererTypeAuto}} + resp, err := Render(dir, svc, utilFactory) + Expect(err).NotTo(HaveOccurred()) + Expect(len(resp)).To(Equal(1)) + Expect(resp[0].GetName()).To(Equal(name)) + }) + It("should skip templating liquid", func() { + dir := filepath.Join("..", "..", "..", "test", "rawTemplated") + svc.Templated = lo.ToPtr(false) + svc.Renderers = []*console.RendererFragment{{Path: dir, Type: console.RendererTypeAuto}} + resp, err := Render(dir, svc, utilFactory) + Expect(err).NotTo(HaveOccurred()) + Expect(len(resp)).To(Equal(1)) + Expect(resp[0].GetName()).To(Equal(name)) + }) + + }) +}) + +var _ = Describe("KUSTOMIZE template, AUTO", func() { + + svc := &console.ServiceDeploymentForAgent{ + Namespace: "default", + Configuration: make([]*console.ServiceDeploymentForAgent_Configuration, 0), + } + Context("Render kustomize template ", func() { + It("should successfully render the kustomize template", func() { + dir := filepath.Join("..", "..", "..", "test", "mixed", "kustomize", "overlays", "dev") + svc.Cluster = &console.ServiceDeploymentForAgent_Cluster{ + ID: "123", + Name: "test", + } + svc.Renderers = []*console.RendererFragment{{Path: dir, Type: console.RendererTypeAuto}} + resp, err := Render(dir, svc, utilFactory) + Expect(err).NotTo(HaveOccurred()) + Expect(len(resp)).To(Equal(3)) + sort.Slice(resp, func(i, j int) bool { + return resp[i].GetKind() < resp[j].GetKind() + }) + Expect(resp[0].GetKind()).To(Equal("ConfigMap")) + Expect(strings.HasPrefix(resp[0].GetName(), "app-config")).Should(BeTrue()) + Expect(resp[1].GetKind()).To(Equal("Deployment")) + Expect(resp[2].GetKind()).To(Equal("Secret")) + Expect(strings.HasPrefix(resp[2].GetName(), "credentials")).Should(BeTrue()) + }) + + }) +}) + +var _ = Describe("RAW and KUSTOMIZE and HELM renderers", Ordered, func() { + svc := &console.ServiceDeploymentForAgent{ + Namespace: "default", + Name: "test", + Configuration: make([]*console.ServiceDeploymentForAgent_Configuration, 0), + } + + r := gin.Default() + r.GET("/version", func(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "major": "1", + "minor": "21", + }) + }) + + srv := &http.Server{ + Addr: ":8080", + Handler: r, + } + + BeforeAll(func() { + // Initializing the server in a goroutine so that + // it won't block the graceful shutdown handling below + go func() { + if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + Expect(err).NotTo(HaveOccurred()) + } + }() + }) + AfterAll(func() { + + // The context is used to inform the server it has 5 seconds to finish + // the request it is currently handling + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) + defer cancel() + if err := srv.Shutdown(ctx); err != nil { + log.Fatal("Server forced to shutdown: ", err) + } + + log.Println("Server exiting") + }) + + Context("Render RAW and KUSTOMIZE and HELM template", func() { + const name = "nginx" + It("should successfully render the raw and kustomize and helm templates", func() { + dir := filepath.Join("..", "..", "..", "test", "mixed") + dirRaw := filepath.Join("..", "..", "..", "test", "mixed", "raw") + dirKustomize := filepath.Join("..", "..", "..", "test", "mixed", "kustomize", "overlays", "dev") + dirHelm := filepath.Join("..", "..", "..", "test", "mixed", "helm", "yet-another-cloudwatch-exporter") + svc.Configuration = []*console.ServiceDeploymentForAgent_Configuration{ + { + Name: "name", + Value: name, + }, + } + svc.Cluster = &console.ServiceDeploymentForAgent_Cluster{ + ID: "123", + Name: "test", + } + svc.Renderers = []*console.RendererFragment{ + {Path: dirRaw, Type: console.RendererTypeRaw}, + {Path: dirKustomize, Type: console.RendererTypeKustomize}, + { + Path: dirHelm, + Type: console.RendererTypeHelm, + Helm: &console.HelmMinimalFragment{ + Release: lo.ToPtr("my-release"), + ValuesFiles: func() []*string { + qa := "./values-qa.yaml" + prod := "./values-prod.yaml" + return []*string{&qa, &prod} + }(), + }, + }, + } + resp, err := Render(dir, svc, utilFactory) + Expect(err).NotTo(HaveOccurred()) + Expect(len(resp)).To(Equal(5)) + Expect(resp[0].GetName()).To(Equal(name)) + + // Find the ServiceMonitor resource to verify helm values + var serviceMonitor *unstructured.Unstructured + for _, r := range resp { + if r.GetKind() == "ServiceMonitor" { + serviceMonitor = &r + break + } + } + Expect(serviceMonitor).NotTo(BeNil()) + + // Verify prod values were applied (since values-prod.yaml is last) + Expect(serviceMonitor.GetNamespace()).To(Equal("prod-monitoring")) + labels := serviceMonitor.GetLabels() + Expect(labels["environment"]).To(Equal("prod")) + + // Verify interval in spec + spec, found, err := unstructured.NestedMap(serviceMonitor.Object, "spec") + Expect(err).NotTo(HaveOccurred()) + Expect(found).To(BeTrue()) + endpoints, found, err := unstructured.NestedSlice(spec, "endpoints") + Expect(err).NotTo(HaveOccurred()) + Expect(found).To(BeTrue()) + Expect(len(endpoints)).To(Equal(1)) + endpoint := endpoints[0].(map[string]interface{}) + Expect(endpoint["interval"]).To(Equal("30s")) + + sort.Slice(resp[1:4], func(i, j int) bool { + return resp[1+i].GetKind() < resp[1+j].GetKind() + }) + Expect(resp[1].GetKind()).To(Equal("ConfigMap")) + Expect(strings.HasPrefix(resp[1].GetName(), "app-config")).Should(BeTrue()) + Expect(resp[2].GetKind()).To(Equal("Deployment")) + Expect(resp[3].GetKind()).To(Equal("Secret")) + Expect(strings.HasPrefix(resp[3].GetName(), "credentials")).Should(BeTrue()) + Expect(resp[4].GetKind()).To(Equal("ServiceMonitor")) + Expect(resp[4].GetName()).To(Equal("my-release")) + }) + + }) +}) diff --git a/test/mixed/helm/yet-another-cloudwatch-exporter/Chart.yaml b/test/mixed/helm/yet-another-cloudwatch-exporter/Chart.yaml new file mode 100644 index 000000000..0e60926e9 --- /dev/null +++ b/test/mixed/helm/yet-another-cloudwatch-exporter/Chart.yaml @@ -0,0 +1,7 @@ +apiVersion: v2 +name: yet-another-cloudwatch-exporter +description: Yace - Yet Another CloudWatch Exporter +type: application +version: 0.1.0 +appVersion: "v0.61.2" + diff --git a/test/mixed/helm/yet-another-cloudwatch-exporter/templates/_helpers.tpl b/test/mixed/helm/yet-another-cloudwatch-exporter/templates/_helpers.tpl new file mode 100644 index 000000000..33a446373 --- /dev/null +++ b/test/mixed/helm/yet-another-cloudwatch-exporter/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "yet-another-cloudwatch-exporter.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "yet-another-cloudwatch-exporter.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "yet-another-cloudwatch-exporter.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "yet-another-cloudwatch-exporter.labels" -}} +helm.sh/chart: {{ include "yet-another-cloudwatch-exporter.chart" . }} +{{ include "yet-another-cloudwatch-exporter.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "yet-another-cloudwatch-exporter.selectorLabels" -}} +app.kubernetes.io/name: {{ include "yet-another-cloudwatch-exporter.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "yet-another-cloudwatch-exporter.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "yet-another-cloudwatch-exporter.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/test/mixed/helm/yet-another-cloudwatch-exporter/templates/job.yaml b/test/mixed/helm/yet-another-cloudwatch-exporter/templates/job.yaml new file mode 100644 index 000000000..b6dbb1e30 --- /dev/null +++ b/test/mixed/helm/yet-another-cloudwatch-exporter/templates/job.yaml @@ -0,0 +1,16 @@ +apiVersion: batch/v1 +kind: Job +metadata: + name: pi + annotations: + "helm.sh/hook": pre-install + "helm.sh/hook-delete-policy": hook-succeeded, before-hook-creation +spec: + template: + spec: + containers: + - name: pi + image: perl:5.34.0 + command: ["perl", "-Mbignum=bpi", "-wle", "print bpi(2000)"] + restartPolicy: Never + backoffLimit: 4 \ No newline at end of file diff --git a/test/mixed/helm/yet-another-cloudwatch-exporter/templates/servicemonitor.yaml b/test/mixed/helm/yet-another-cloudwatch-exporter/templates/servicemonitor.yaml new file mode 100644 index 000000000..bc1921006 --- /dev/null +++ b/test/mixed/helm/yet-another-cloudwatch-exporter/templates/servicemonitor.yaml @@ -0,0 +1,41 @@ +{{- if and ( .Capabilities.APIVersions.Has "monitoring.coreos.com/v1" ) ( .Values.serviceMonitor.enabled ) }} +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: {{ .Release.Name }} +{{- if .Values.serviceMonitor.namespace }} + namespace: {{ .Values.serviceMonitor.namespace }} +{{- end }} + labels: + {{- include "yet-another-cloudwatch-exporter.labels" . | nindent 4 }} + {{- with .Values.serviceMonitor.labels }} + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + endpoints: + - port: http +{{- if .Values.serviceMonitor.interval }} + interval: {{ .Values.serviceMonitor.interval }} +{{- end }} +{{- if .Values.serviceMonitor.telemetryPath }} + path: {{ .Values.serviceMonitor.telemetryPath }} +{{- end }} +{{- if .Values.serviceMonitor.timeout }} + scrapeTimeout: {{ .Values.serviceMonitor.timeout }} +{{- end }} +{{- if .Values.serviceMonitor.relabelings }} + relabelings: +{{ toYaml .Values.serviceMonitor.relabelings | indent 6 }} +{{- end }} +{{- if .Values.serviceMonitor.metricRelabelings }} + metricRelabelings: +{{ toYaml .Values.serviceMonitor.metricRelabelings | indent 6 }} +{{- end }} + jobLabel: {{ template "yet-another-cloudwatch-exporter.fullname" . }} + namespaceSelector: + matchNames: + - {{ .Release.Namespace }} + selector: + matchLabels: + {{- include "yet-another-cloudwatch-exporter.selectorLabels" . | nindent 6 }} +{{- end }} diff --git a/test/mixed/helm/yet-another-cloudwatch-exporter/values-prod.yaml b/test/mixed/helm/yet-another-cloudwatch-exporter/values-prod.yaml new file mode 100644 index 000000000..111fd568f --- /dev/null +++ b/test/mixed/helm/yet-another-cloudwatch-exporter/values-prod.yaml @@ -0,0 +1,6 @@ +serviceMonitor: + enabled: true + namespace: "prod-monitoring" + interval: "30s" + labels: + environment: "prod" \ No newline at end of file diff --git a/test/mixed/helm/yet-another-cloudwatch-exporter/values-qa.yaml b/test/mixed/helm/yet-another-cloudwatch-exporter/values-qa.yaml new file mode 100644 index 000000000..edbb6a7a5 --- /dev/null +++ b/test/mixed/helm/yet-another-cloudwatch-exporter/values-qa.yaml @@ -0,0 +1,6 @@ +serviceMonitor: + enabled: true + namespace: "qa-monitoring" + interval: "15s" + labels: + environment: "qa" \ No newline at end of file diff --git a/test/mixed/helm/yet-another-cloudwatch-exporter/values.yaml b/test/mixed/helm/yet-another-cloudwatch-exporter/values.yaml new file mode 100644 index 000000000..dd9741498 --- /dev/null +++ b/test/mixed/helm/yet-another-cloudwatch-exporter/values.yaml @@ -0,0 +1,7 @@ +serviceMonitor: + # When set true then use a ServiceMonitor to configure scraping + enabled: true + + + + diff --git a/test/mixed/kustomize/base/deployment.yaml b/test/mixed/kustomize/base/deployment.yaml new file mode 100644 index 000000000..a59f693ca --- /dev/null +++ b/test/mixed/kustomize/base/deployment.yaml @@ -0,0 +1,26 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: my-app +spec: + replicas: 1 + selector: + matchLabels: + app: my-app + template: + metadata: + labels: + app: my-app + spec: + containers: + - name: my-app + image: alpine:3.10 + tty: true + stdin: true + env: + - name: foo + value: bar + resources: + limits: + memory: "64Mi" + cpu: "100m" diff --git a/test/mixed/kustomize/base/kustomization.yaml b/test/mixed/kustomize/base/kustomization.yaml new file mode 100644 index 000000000..0c00d6fb7 --- /dev/null +++ b/test/mixed/kustomize/base/kustomization.yaml @@ -0,0 +1,5 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: +- deployment.yaml diff --git a/test/mixed/kustomize/liquid/dev/deployment_env.yaml b/test/mixed/kustomize/liquid/dev/deployment_env.yaml new file mode 100644 index 000000000..53ca04360 --- /dev/null +++ b/test/mixed/kustomize/liquid/dev/deployment_env.yaml @@ -0,0 +1,17 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: my-app +spec: + template: + spec: + containers: + - name: my-app + env: + - name: foo + value: we-are-in-dev + envFrom: + - configMapRef: + name: app-config + - secretRef: + name: credentials diff --git a/test/mixed/kustomize/liquid/dev/kustomization.yaml b/test/mixed/kustomize/liquid/dev/kustomization.yaml new file mode 100644 index 000000000..2b01f31ff --- /dev/null +++ b/test/mixed/kustomize/liquid/dev/kustomization.yaml @@ -0,0 +1,22 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - ../../base + +namespace: my-app-dev + +nameSuffix: -dev + +configMapGenerator: + - literals: + - username=demo-user + name: nginx + +secretGenerator: + - literals: + - password=demo + name: credentials + type: Opaque +patches: + - path: deployment_env.yaml \ No newline at end of file diff --git a/test/mixed/kustomize/liquid/dev/kustomization.yaml.liquid b/test/mixed/kustomize/liquid/dev/kustomization.yaml.liquid new file mode 100644 index 000000000..f57177693 --- /dev/null +++ b/test/mixed/kustomize/liquid/dev/kustomization.yaml.liquid @@ -0,0 +1,22 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - ../../base + +namespace: my-app-dev + +nameSuffix: -dev + +configMapGenerator: + - literals: + - username=demo-user + name: {{ configuration.name }} + +secretGenerator: + - literals: + - password=demo + name: credentials + type: Opaque +patches: + - path: deployment_env.yaml \ No newline at end of file diff --git a/test/mixed/kustomize/overlays/dev/deployment_env.yaml b/test/mixed/kustomize/overlays/dev/deployment_env.yaml new file mode 100644 index 000000000..6f716e3b8 --- /dev/null +++ b/test/mixed/kustomize/overlays/dev/deployment_env.yaml @@ -0,0 +1,17 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: my-app +spec: + template: + spec: + containers: + - name: my-app + env: + - name: foo + value: we-are-in-dev + envFrom: + - configMapRef: + name: app-config + - secretRef: + name: credentials \ No newline at end of file diff --git a/test/mixed/kustomize/overlays/dev/kustomization.yaml b/test/mixed/kustomize/overlays/dev/kustomization.yaml new file mode 100644 index 000000000..0a0e66a5e --- /dev/null +++ b/test/mixed/kustomize/overlays/dev/kustomization.yaml @@ -0,0 +1,22 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: +- ../../base + +namespace: my-app-dev + +nameSuffix: -dev + +configMapGenerator: +- literals: + - username=demo-user + name: app-config + +secretGenerator: +- literals: + - password=demo + name: credentials + type: Opaque +patches: +- path: deployment_env.yaml diff --git a/test/mixed/raw/pod.yaml.liquid b/test/mixed/raw/pod.yaml.liquid new file mode 100644 index 000000000..60874f384 --- /dev/null +++ b/test/mixed/raw/pod.yaml.liquid @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: Pod +metadata: + name: {{ configuration.name }} +spec: + containers: + - name: nginx + image: nginx:1.14.2 + ports: + - containerPort: 80 diff --git a/test/mixed/rawTemplated/pod.yaml.liquid b/test/mixed/rawTemplated/pod.yaml.liquid new file mode 100644 index 000000000..efe5bd384 --- /dev/null +++ b/test/mixed/rawTemplated/pod.yaml.liquid @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx +spec: + containers: + - name: nginx + image: nginx:1.14.2 + ports: + - containerPort: 80