mirror of https://github.com/portainer/portainer
feat(helm): helm apps deployed by portainer not marked as external EE-1624 (#5637)
* helm lib update * helm handler requires kubernetes deployer to modify helm deployed resources * AddAppLabels updated to be more generic - support for adding multiple labels using map * path installed helm release manifest with portainer labels using kubectl * updated helm handler unit tests to use mock KubernetesDeployer * adding labels to manifest retrieved from release * optional namespace support for k8s raw manifest deployment * - inline postprocessing support when extracting - get namespace from yaml support - added and updated tests * lowercase error wrapping * updated libhelm deppull/5790/head
parent
50f63ae865
commit
af98660a55
|
@ -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
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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=
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
||||
|
|
|
@ -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"}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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]))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue