diff --git a/api/exec/exectest/kubernetes_mocks.go b/api/exec/exectest/kubernetes_mocks.go new file mode 100644 index 000000000..2809df2c5 --- /dev/null +++ b/api/exec/exectest/kubernetes_mocks.go @@ -0,0 +1,21 @@ +package exectest + +import ( + "net/http" + + portainer "github.com/portainer/portainer/api" +) + +type kubernetesMockDeployer struct{} + +func NewKubernetesDeployer() portainer.KubernetesDeployer { + return &kubernetesMockDeployer{} +} + +func (deployer *kubernetesMockDeployer) Deploy(request *http.Request, endpoint *portainer.Endpoint, data string, namespace string) (string, error) { + return "", nil +} + +func (deployer *kubernetesMockDeployer) ConvertCompose(data []byte) ([]byte, error) { + return nil, nil +} diff --git a/api/exec/kubernetes_deploy.go b/api/exec/kubernetes_deploy.go index 5bb496aed..af97af174 100644 --- a/api/exec/kubernetes_deploy.go +++ b/api/exec/kubernetes_deploy.go @@ -76,7 +76,7 @@ func (deployer *KubernetesDeployer) getToken(request *http.Request, endpoint *po return token, nil } -// Deploy will deploy a Kubernetes manifest inside a specific namespace in a Kubernetes environment(endpoint). +// Deploy will deploy a Kubernetes manifest inside an optional namespace in a Kubernetes environment(endpoint). // Otherwise it will use kubectl to deploy the manifest. func (deployer *KubernetesDeployer) Deploy(request *http.Request, endpoint *portainer.Endpoint, stackConfig string, namespace string) (string, error) { command := path.Join(deployer.binaryPath, "kubectl") @@ -103,7 +103,9 @@ func (deployer *KubernetesDeployer) Deploy(request *http.Request, endpoint *port } args = append(args, "--token", token) - args = append(args, "--namespace", namespace) + if namespace != "" { + args = append(args, "--namespace", namespace) + } args = append(args, "apply", "-f", "-") var stderr bytes.Buffer diff --git a/api/go.mod b/api/go.mod index d47747e7a..55e98e838 100644 --- a/api/go.mod +++ b/api/go.mod @@ -38,7 +38,7 @@ require ( github.com/pkg/errors v0.9.1 github.com/portainer/docker-compose-wrapper v0.0.0-20210909083948-8be0d98451a1 github.com/portainer/libcrypto v0.0.0-20210422035235-c652195c5c3a - github.com/portainer/libhelm v0.0.0-20210913052337-365741c1c320 + github.com/portainer/libhelm v0.0.0-20210929000907-825e93d62108 github.com/portainer/libhttp v0.0.0-20190806161843-ba068f58be33 github.com/robfig/cron/v3 v3.0.1 github.com/sirupsen/logrus v1.8.1 @@ -46,6 +46,7 @@ require ( github.com/xeipuuv/gojsonschema v1.2.0 // indirect golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 + golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect gopkg.in/alecthomas/kingpin.v2 v2.2.6 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b gotest.tools v2.2.0+incompatible // indirect diff --git a/api/go.sum b/api/go.sum index bb42cb92a..f24aa9c04 100644 --- a/api/go.sum +++ b/api/go.sum @@ -210,8 +210,8 @@ github.com/portainer/docker-compose-wrapper v0.0.0-20210909083948-8be0d98451a1 h github.com/portainer/docker-compose-wrapper v0.0.0-20210909083948-8be0d98451a1/go.mod h1:WxDlJWZxCnicdLCPnLNEv7/gRhjeIVuCGmsv+iOPH3c= github.com/portainer/libcrypto v0.0.0-20210422035235-c652195c5c3a h1:qY8TbocN75n5PDl16o0uVr5MevtM5IhdwSelXEd4nFM= github.com/portainer/libcrypto v0.0.0-20210422035235-c652195c5c3a/go.mod h1:n54EEIq+MM0NNtqLeCby8ljL+l275VpolXO0ibHegLE= -github.com/portainer/libhelm v0.0.0-20210913052337-365741c1c320 h1:wkmxoHYjWc7OB6JfSlt83mAVpnAo4/6TdL60PO4DlXk= -github.com/portainer/libhelm v0.0.0-20210913052337-365741c1c320/go.mod h1:YvYAk7krKTzB+rFwDr0jQ3sQu2BtiXK1AR0sZH7nhJA= +github.com/portainer/libhelm v0.0.0-20210929000907-825e93d62108 h1:5e8KAnDa2G3cEHK7aV/ue8lOaoQwBZUzoALslwWkR04= +github.com/portainer/libhelm v0.0.0-20210929000907-825e93d62108/go.mod h1:YvYAk7krKTzB+rFwDr0jQ3sQu2BtiXK1AR0sZH7nhJA= github.com/portainer/libhttp v0.0.0-20190806161843-ba068f58be33 h1:H8HR2dHdBf8HANSkUyVw4o8+4tegGcd+zyKZ3e599II= github.com/portainer/libhttp v0.0.0-20190806161843-ba068f58be33/go.mod h1:Y2TfgviWI4rT2qaOTHr+hq6MdKIE5YjgQAu7qwptTV0= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= @@ -276,6 +276,8 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456 h1:ng0gs1AKnRRuEMZoTLLlbOd+C17zUDepwGQBb/n+JVg= golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= diff --git a/api/http/handler/helm/handler.go b/api/http/handler/helm/handler.go index ed72fe7ec..4ace84c27 100644 --- a/api/http/handler/helm/handler.go +++ b/api/http/handler/helm/handler.go @@ -27,15 +27,17 @@ type Handler struct { requestBouncer requestBouncer dataStore portainer.DataStore kubeConfigService kubernetes.KubeConfigService + kubernetesDeployer portainer.KubernetesDeployer helmPackageManager libhelm.HelmPackageManager } -// NewHandler creates a handler to manage environment(endpoint) group operations. -func NewHandler(bouncer requestBouncer, dataStore portainer.DataStore, helmPackageManager libhelm.HelmPackageManager, kubeConfigService kubernetes.KubeConfigService) *Handler { +// NewHandler creates a handler to manage endpoint group operations. +func NewHandler(bouncer requestBouncer, dataStore portainer.DataStore, kubernetesDeployer portainer.KubernetesDeployer, helmPackageManager libhelm.HelmPackageManager, kubeConfigService kubernetes.KubeConfigService) *Handler { h := &Handler{ Router: mux.NewRouter(), requestBouncer: bouncer, dataStore: dataStore, + kubernetesDeployer: kubernetesDeployer, helmPackageManager: helmPackageManager, kubeConfigService: kubeConfigService, } diff --git a/api/http/handler/helm/helm_delete_test.go b/api/http/handler/helm/helm_delete_test.go index 618d7c969..1270ec9ac 100644 --- a/api/http/handler/helm/helm_delete_test.go +++ b/api/http/handler/helm/helm_delete_test.go @@ -9,6 +9,7 @@ import ( "github.com/portainer/libhelm/binary/test" "github.com/portainer/libhelm/options" portainer "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/exec/exectest" "github.com/portainer/portainer/api/http/security" "github.com/portainer/portainer/api/kubernetes" "github.com/stretchr/testify/assert" @@ -29,9 +30,10 @@ func Test_helmDelete(t *testing.T) { err = store.User().CreateUser(&portainer.User{Username: "admin", Role: portainer.AdministratorRole}) is.NoError(err, "Error creating a user") + kubernetesDeployer := exectest.NewKubernetesDeployer() helmPackageManager := test.NewMockHelmBinaryPackageManager("") kubeConfigService := kubernetes.NewKubeConfigCAService("", "") - h := NewHandler(helper.NewTestRequestBouncer(), store, helmPackageManager, kubeConfigService) + h := NewHandler(helper.NewTestRequestBouncer(), store, kubernetesDeployer, helmPackageManager, kubeConfigService) is.NotNil(h, "Handler should not fail") diff --git a/api/http/handler/helm/helm_install.go b/api/http/handler/helm/helm_install.go index 3985a39f8..57e17825f 100644 --- a/api/http/handler/helm/helm_install.go +++ b/api/http/handler/helm/helm_install.go @@ -1,18 +1,22 @@ package helm import ( - "errors" "fmt" "net/http" "os" "strings" + "github.com/pkg/errors" "github.com/portainer/libhelm/options" "github.com/portainer/libhelm/release" httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" + "github.com/portainer/portainer/api/http/middlewares" + "github.com/portainer/portainer/api/http/security" + "github.com/portainer/portainer/api/kubernetes" "github.com/portainer/portainer/api/kubernetes/validation" + "golang.org/x/sync/errgroup" ) type installChartPayload struct { @@ -131,5 +135,79 @@ func (handler *Handler) installChart(r *http.Request, p installChartPayload) (*r if err != nil { return nil, err } + + manifest, err := handler.applyPortainerLabelsToHelmAppManifest(r, installOpts, release.Manifest) + if err != nil { + return nil, err + } + + err = handler.updateHelmAppManifest(r, manifest, installOpts.Namespace) + if err != nil { + return nil, err + } + return release, nil } + +// applyPortainerLabelsToHelmAppManifest will patch all the resources deployed in the helm release manifest +// with portainer specific labels. This is to mark the resources as managed by portainer - hence the helm apps +// wont appear external in the portainer UI. +func (handler *Handler) applyPortainerLabelsToHelmAppManifest(r *http.Request, installOpts options.InstallOptions, manifest string) ([]byte, error) { + // Patch helm release by adding with portainer labels to all deployed resources + tokenData, err := security.RetrieveTokenData(r) + if err != nil { + return nil, errors.Wrap(err, "unable to retrieve user details from authentication token") + } + user, err := handler.dataStore.User().User(tokenData.ID) + if err != nil { + return nil, errors.Wrap(err, "unable to load user information from the database") + } + + appLabels := kubernetes.GetHelmAppLabels(installOpts.Name, user.Username) + labeledManifest, err := kubernetes.AddAppLabels([]byte(manifest), appLabels) + if err != nil { + return nil, errors.Wrap(err, "failed to label helm release manifest") + } + + return labeledManifest, nil +} + +// updateHelmAppManifest will update the resources of helm release manifest with portainer labels using kubectl. +// The resources of the manifest will be updated in parallel and individuallly since resources of a chart +// can be deployed to different namespaces. +// NOTE: These updates will need to be re-applied when upgrading the helm release +func (handler *Handler) updateHelmAppManifest(r *http.Request, manifest []byte, namespace string) error { + endpoint, err := middlewares.FetchEndpoint(r) + if err != nil { + return errors.Wrap(err, "unable to find an endpoint on request context") + } + + // extract list of yaml resources from helm manifest + yamlResources, err := kubernetes.ExtractDocuments(manifest, nil) + if err != nil { + return errors.Wrap(err, "unable to extract documents from helm release manifest") + } + + // deploy individual resources in parallel + g := new(errgroup.Group) + for _, resource := range yamlResources { + resource := resource // https://golang.org/doc/faq#closures_and_goroutines + g.Go(func() error { + // get resource namespace, fallback to provided namespace if not explicit on resource + resourceNamespace, err := kubernetes.GetNamespace(resource) + if err != nil { + return err + } + if resourceNamespace == "" { + resourceNamespace = namespace + } + _, err = handler.kubernetesDeployer.Deploy(r, endpoint, string(resource), resourceNamespace) + return err + }) + } + if err := g.Wait(); err != nil { + return errors.Wrap(err, "unable to patch helm release using kubectl") + } + + return nil +} diff --git a/api/http/handler/helm/helm_install_test.go b/api/http/handler/helm/helm_install_test.go index 52fdb1b2e..aef13de8b 100644 --- a/api/http/handler/helm/helm_install_test.go +++ b/api/http/handler/helm/helm_install_test.go @@ -13,6 +13,7 @@ import ( "github.com/portainer/libhelm/release" portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/bolt" + "github.com/portainer/portainer/api/exec/exectest" "github.com/portainer/portainer/api/http/security" helper "github.com/portainer/portainer/api/internal/testhelpers" "github.com/portainer/portainer/api/kubernetes" @@ -31,9 +32,10 @@ func Test_helmInstall(t *testing.T) { err = store.User().CreateUser(&portainer.User{Username: "admin", Role: portainer.AdministratorRole}) is.NoError(err, "error creating a user") + kubernetesDeployer := exectest.NewKubernetesDeployer() helmPackageManager := test.NewMockHelmBinaryPackageManager("") kubeConfigService := kubernetes.NewKubeConfigCAService("", "") - h := NewHandler(helper.NewTestRequestBouncer(), store, helmPackageManager, kubeConfigService) + h := NewHandler(helper.NewTestRequestBouncer(), store, kubernetesDeployer, helmPackageManager, kubeConfigService) is.NotNil(h, "Handler should not fail") diff --git a/api/http/handler/helm/helm_list_test.go b/api/http/handler/helm/helm_list_test.go index 459aad736..87901b5de 100644 --- a/api/http/handler/helm/helm_list_test.go +++ b/api/http/handler/helm/helm_list_test.go @@ -11,6 +11,7 @@ import ( "github.com/portainer/libhelm/options" "github.com/portainer/libhelm/release" portainer "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/exec/exectest" "github.com/portainer/portainer/api/kubernetes" "github.com/stretchr/testify/assert" @@ -28,9 +29,10 @@ func Test_helmList(t *testing.T) { err = store.User().CreateUser(&portainer.User{Username: "admin", Role: portainer.AdministratorRole}) assert.NoError(t, err, "error creating a user") + kubernetesDeployer := exectest.NewKubernetesDeployer() helmPackageManager := test.NewMockHelmBinaryPackageManager("") kubeConfigService := kubernetes.NewKubeConfigCAService("", "") - h := NewHandler(helper.NewTestRequestBouncer(), store, helmPackageManager, kubeConfigService) + h := NewHandler(helper.NewTestRequestBouncer(), store, kubernetesDeployer, helmPackageManager, kubeConfigService) // Install a single chart. We expect to get these values back options := options.InstallOptions{Name: "nginx-1", Chart: "nginx", Namespace: "default"} diff --git a/api/http/handler/stacks/create_kubernetes_stack.go b/api/http/handler/stacks/create_kubernetes_stack.go index 08b46069b..f4133ddae 100644 --- a/api/http/handler/stacks/create_kubernetes_stack.go +++ b/api/http/handler/stacks/create_kubernetes_stack.go @@ -304,7 +304,7 @@ func (handler *Handler) deployKubernetesStack(request *http.Request, endpoint *p manifest = convertedConfig } - manifest, err := k.AddAppLabels(manifest, appLabels) + manifest, err := k.AddAppLabels(manifest, appLabels.ToMap()) if err != nil { return "", errors.Wrap(err, "failed to add application labels") } diff --git a/api/http/server.go b/api/http/server.go index 37df3cc78..ac6f1d79b 100644 --- a/api/http/server.go +++ b/api/http/server.go @@ -171,7 +171,7 @@ func (server *Server) Start() error { var fileHandler = file.NewHandler(filepath.Join(server.AssetsPath, "public")) - var endpointHelmHandler = helm.NewHandler(requestBouncer, server.DataStore, server.HelmPackageManager, server.KubeConfigService) + var endpointHelmHandler = helm.NewHandler(requestBouncer, server.DataStore, server.KubernetesDeployer, server.HelmPackageManager, server.KubeConfigService) var helmTemplatesHandler = helm.NewTemplateHandler(requestBouncer, server.HelmPackageManager) diff --git a/api/kubernetes/yaml.go b/api/kubernetes/yaml.go index b8e7b1b68..b90020641 100644 --- a/api/kubernetes/yaml.go +++ b/api/kubernetes/yaml.go @@ -11,6 +11,14 @@ import ( "gopkg.in/yaml.v3" ) +const ( + labelPortainerAppStackID = "io.portainer.kubernetes.application.stackid" + labelPortainerAppName = "io.portainer.kubernetes.application.name" + labelPortainerAppOwner = "io.portainer.kubernetes.application.owner" + labelPortainerAppKind = "io.portainer.kubernetes.application.kind" +) + +// KubeAppLabels are labels applied to all resources deployed in a kubernetes stack type KubeAppLabels struct { StackID int Name string @@ -18,14 +26,49 @@ type KubeAppLabels struct { Kind string } +// ToMap converts KubeAppLabels to a map[string]string +func (kal *KubeAppLabels) ToMap() map[string]string { + return map[string]string{ + labelPortainerAppStackID: strconv.Itoa(kal.StackID), + labelPortainerAppName: kal.Name, + labelPortainerAppOwner: kal.Owner, + labelPortainerAppKind: kal.Kind, + } +} + +// GetHelmAppLabels returns the labels to be applied to portainer deployed helm applications +func GetHelmAppLabels(name, owner string) map[string]string { + return map[string]string{ + labelPortainerAppName: name, + labelPortainerAppOwner: owner, + } +} + // AddAppLabels adds required labels to "Resource"->metadata->labels. // It'll add those labels to all Resource (nodes with a kind property exluding a list) it can find in provided yaml. // Items in the yaml file could either be organised as a list or broken into multi documents. -func AddAppLabels(manifestYaml []byte, appLabels KubeAppLabels) ([]byte, error) { +func AddAppLabels(manifestYaml []byte, appLabels map[string]string) ([]byte, error) { if bytes.Equal(manifestYaml, []byte("")) { return manifestYaml, nil } + postProcessYaml := func(yamlDoc interface{}) error { + addResourceLabels(yamlDoc, appLabels) + return nil + } + + docs, err := ExtractDocuments(manifestYaml, postProcessYaml) + if err != nil { + return nil, err + } + + return bytes.Join(docs, []byte("---\n")), nil +} + +// ExtractDocuments extracts all the documents from a yaml file +// Optionally post-process each document with a function, which can modify the document in place. +// Pass in nil for postProcessYaml to skip post-processing. +func ExtractDocuments(manifestYaml []byte, postProcessYaml func(interface{}) error) ([][]byte, error) { docs := make([][]byte, 0) yamlDecoder := yaml.NewDecoder(bytes.NewReader(manifestYaml)) @@ -43,7 +86,12 @@ func AddAppLabels(manifestYaml []byte, appLabels KubeAppLabels) ([]byte, error) break } - addResourceLabels(m, appLabels) + // optionally post-process yaml + if postProcessYaml != nil { + if err := postProcessYaml(m); err != nil { + return nil, errors.Wrap(err, "failed to post process yaml document") + } + } var out bytes.Buffer yamlEncoder := yaml.NewEncoder(&out) @@ -55,10 +103,29 @@ func AddAppLabels(manifestYaml []byte, appLabels KubeAppLabels) ([]byte, error) docs = append(docs, out.Bytes()) } - return bytes.Join(docs, []byte("---\n")), nil + return docs, nil } -func addResourceLabels(yamlDoc interface{}, appLabels KubeAppLabels) { +// GetNamespace returns the namespace of a kubernetes resource from its metadata +// It returns an empty string if namespace is not found in the resource +func GetNamespace(manifestYaml []byte) (string, error) { + yamlDecoder := yaml.NewDecoder(bytes.NewReader(manifestYaml)) + m := make(map[string]interface{}) + err := yamlDecoder.Decode(&m) + if err != nil { + return "", errors.Wrap(err, "failed to unmarshal yaml manifest when obtaining namespace") + } + + if _, ok := m["metadata"]; ok { + if namespace, ok := m["metadata"].(map[string]interface{})["namespace"]; ok { + return namespace.(string), nil + } + } + + return "", nil +} + +func addResourceLabels(yamlDoc interface{}, appLabels map[string]string) { m, ok := yamlDoc.(map[string]interface{}) if !ok { return @@ -82,7 +149,7 @@ func addResourceLabels(yamlDoc interface{}, appLabels KubeAppLabels) { } } -func addLabels(obj map[string]interface{}, appLabels KubeAppLabels) { +func addLabels(obj map[string]interface{}, appLabels map[string]string) { metadata := make(map[string]interface{}) if m, ok := obj["metadata"]; ok { metadata = m.(map[string]interface{}) @@ -95,17 +162,17 @@ func addLabels(obj map[string]interface{}, appLabels KubeAppLabels) { } } - name := appLabels.Name - if appLabels.Name == "" { - if n, ok := metadata["name"]; ok { - name = n.(string) - } + // merge app labels with existing labels + for k, v := range appLabels { + labels[k] = v } - labels["io.portainer.kubernetes.application.stackid"] = strconv.Itoa(appLabels.StackID) - labels["io.portainer.kubernetes.application.name"] = name - labels["io.portainer.kubernetes.application.owner"] = appLabels.Owner - labels["io.portainer.kubernetes.application.kind"] = appLabels.Kind + // fallback to metadata name if name label not explicitly provided + if name, ok := labels[labelPortainerAppName]; !ok || name == "" { + if n, ok := metadata["name"]; ok { + labels[labelPortainerAppName] = n.(string) + } + } metadata["labels"] = labels obj["metadata"] = metadata diff --git a/api/kubernetes/yaml_test.go b/api/kubernetes/yaml_test.go index 5172357f4..167b97038 100644 --- a/api/kubernetes/yaml_test.go +++ b/api/kubernetes/yaml_test.go @@ -410,7 +410,7 @@ spec: for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - result, err := AddAppLabels([]byte(tt.input), labels) + result, err := AddAppLabels([]byte(tt.input), labels.ToMap()) assert.NoError(t, err) assert.Equal(t, tt.wantOutput, string(result)) }) @@ -451,7 +451,7 @@ spec: targetPort: 5000 ` - result, err := AddAppLabels([]byte(input), labels) + result, err := AddAppLabels([]byte(input), labels.ToMap()) assert.NoError(t, err) assert.Equal(t, expected, string(result)) } @@ -487,7 +487,291 @@ spec: targetPort: 5000 ` - result, err := AddAppLabels([]byte(input), labels) + result, err := AddAppLabels([]byte(input), labels.ToMap()) assert.NoError(t, err) assert.Equal(t, expected, string(result)) } + +func Test_AddAppLabels_HelmApp(t *testing.T) { + labels := GetHelmAppLabels("best-name", "best-owner") + + tests := []struct { + name string + input string + wantOutput string + }{ + { + name: "bitnami nginx configmap", + input: `apiVersion: v1 +kind: ConfigMap +metadata: + name: nginx-test-server-block + labels: + app.kubernetes.io/name: nginx + helm.sh/chart: nginx-9.5.4 + app.kubernetes.io/instance: nginx-test + app.kubernetes.io/managed-by: Helm +data: + server-blocks-paths.conf: |- + include "/opt/bitnami/nginx/conf/server_blocks/ldap/*.conf"; + include "/opt/bitnami/nginx/conf/server_blocks/common/*.conf"; +`, + wantOutput: `apiVersion: v1 +data: + server-blocks-paths.conf: |- + include "/opt/bitnami/nginx/conf/server_blocks/ldap/*.conf"; + include "/opt/bitnami/nginx/conf/server_blocks/common/*.conf"; +kind: ConfigMap +metadata: + labels: + app.kubernetes.io/instance: nginx-test + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: nginx + helm.sh/chart: nginx-9.5.4 + io.portainer.kubernetes.application.name: best-name + io.portainer.kubernetes.application.owner: best-owner + name: nginx-test-server-block +`, + }, + { + name: "bitnami nginx service", + input: `apiVersion: v1 +kind: Service +metadata: + name: nginx-test + labels: + app.kubernetes.io/name: nginx + helm.sh/chart: nginx-9.5.4 + app.kubernetes.io/instance: nginx-test + app.kubernetes.io/managed-by: Helm +spec: + type: LoadBalancer + externalTrafficPolicy: "Cluster" + ports: + - name: http + port: 80 + targetPort: http + selector: + app.kubernetes.io/name: nginx + app.kubernetes.io/instance: nginx-test +`, + wantOutput: `apiVersion: v1 +kind: Service +metadata: + labels: + app.kubernetes.io/instance: nginx-test + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: nginx + helm.sh/chart: nginx-9.5.4 + io.portainer.kubernetes.application.name: best-name + io.portainer.kubernetes.application.owner: best-owner + name: nginx-test +spec: + externalTrafficPolicy: Cluster + ports: + - name: http + port: 80 + targetPort: http + selector: + app.kubernetes.io/instance: nginx-test + app.kubernetes.io/name: nginx + type: LoadBalancer +`, + }, + { + name: "bitnami nginx deployment", + input: `apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-test + labels: + app.kubernetes.io/name: nginx + helm.sh/chart: nginx-9.5.4 + app.kubernetes.io/instance: nginx-test + app.kubernetes.io/managed-by: Helm +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: nginx + app.kubernetes.io/instance: nginx-test + template: + metadata: + labels: + app.kubernetes.io/name: nginx + helm.sh/chart: nginx-9.5.4 + app.kubernetes.io/instance: nginx-test + app.kubernetes.io/managed-by: Helm + spec: + automountServiceAccountToken: false + shareProcessNamespace: false + serviceAccountName: default + containers: + - name: nginx + image: docker.io/bitnami/nginx:1.21.3-debian-10-r0 + imagePullPolicy: "IfNotPresent" +`, + wantOutput: `apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app.kubernetes.io/instance: nginx-test + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: nginx + helm.sh/chart: nginx-9.5.4 + io.portainer.kubernetes.application.name: best-name + io.portainer.kubernetes.application.owner: best-owner + name: nginx-test +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/instance: nginx-test + app.kubernetes.io/name: nginx + template: + metadata: + labels: + app.kubernetes.io/instance: nginx-test + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: nginx + helm.sh/chart: nginx-9.5.4 + spec: + automountServiceAccountToken: false + containers: + - image: docker.io/bitnami/nginx:1.21.3-debian-10-r0 + imagePullPolicy: IfNotPresent + name: nginx + serviceAccountName: default + shareProcessNamespace: false +`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := AddAppLabels([]byte(tt.input), labels) + assert.NoError(t, err) + assert.Equal(t, tt.wantOutput, string(result)) + }) + } +} + +func Test_DocumentSeperator(t *testing.T) { + labels := KubeAppLabels{ + StackID: 123, + Name: "best-name", + Owner: "best-owner", + Kind: "git", + } + + input := `apiVersion: v1 +kind: Service +metadata: + labels: + io.kompose.service: database +--- +apiVersion: v1 +kind: Service +metadata: + labels: + io.kompose.service: backend +` + expected := `apiVersion: v1 +kind: Service +metadata: + labels: + io.kompose.service: database + io.portainer.kubernetes.application.kind: git + io.portainer.kubernetes.application.name: best-name + io.portainer.kubernetes.application.owner: best-owner + io.portainer.kubernetes.application.stackid: "123" +--- +apiVersion: v1 +kind: Service +metadata: + labels: + io.kompose.service: backend + io.portainer.kubernetes.application.kind: git + io.portainer.kubernetes.application.name: best-name + io.portainer.kubernetes.application.owner: best-owner + io.portainer.kubernetes.application.stackid: "123" +` + result, err := AddAppLabels([]byte(input), labels.ToMap()) + assert.NoError(t, err) + assert.Equal(t, expected, string(result)) +} + +func Test_GetNamespace(t *testing.T) { + tests := []struct { + name string + input string + want string + }{ + { + name: "valid namespace", + input: `apiVersion: v1 +kind: Namespace +metadata: + namespace: test-namespace +`, + want: "test-namespace", + }, + { + name: "invalid namespace", + input: `apiVersion: v1 +kind: Namespace +`, + want: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := GetNamespace([]byte(tt.input)) + assert.NoError(t, err) + assert.Equal(t, tt.want, result) + }) + } +} + +func Test_ExtractDocuments(t *testing.T) { + tests := []struct { + name string + input string + want []string + }{ + { + name: "multiple documents", + input: `apiVersion: v1 +kind: Namespace +--- +apiVersion: v1 +kind: Service +`, + want: []string{`apiVersion: v1 +kind: Namespace +`, `apiVersion: v1 +kind: Service +`}, + }, + { + name: "single document", + input: `apiVersion: v1 +kind: Namespace +`, + want: []string{`apiVersion: v1 +kind: Namespace +`}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + results, err := ExtractDocuments([]byte(tt.input), nil) + assert.NoError(t, err) + for i := range results { + assert.Equal(t, tt.want[i], string(results[i])) + } + }) + } +} diff --git a/app/kubernetes/helpers/application/index.js b/app/kubernetes/helpers/application/index.js index 82fcd09ee..c6d7a1465 100644 --- a/app/kubernetes/helpers/application/index.js +++ b/app/kubernetes/helpers/application/index.js @@ -487,6 +487,7 @@ class KubernetesApplicationHelper { const helmApp = new HelmApplication(); helmApp.Name = helmInstance; helmApp.ApplicationType = KubernetesApplicationTypes.HELM; + helmApp.ApplicationOwner = applications[0].ApplicationOwner; helmApp.KubernetesApplications = applications; // the status of helm app is `Ready` based on whether the underlying RunningPodsCount of the k8s app