pkg/deploy/helm/extra_annotations_and_labels_post_renderer.go

Summary

Maintainability
D
2 days
Test Coverage
C
78%
package helm

import (
    "bytes"
    "context"
    "fmt"
    "os"
    "reflect"
    "sort"
    "strings"

    yaml_v3 "gopkg.in/yaml.v3"
    "helm.sh/helm/v3/pkg/releaseutil"
    "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    "sigs.k8s.io/yaml"

    "github.com/werf/logboek"
    "github.com/werf/werf/pkg/werf"
    "github.com/werf/werf/pkg/werf/global_warnings"
)

var WerfRuntimeAnnotations = map[string]string{
    "werf.io/version": werf.Version,
}

var WerfRuntimeLabels = map[string]string{}

func NewExtraAnnotationsAndLabelsPostRenderer(extraAnnotations, extraLabels map[string]string, ignoreInvalidAnnotationsAndLabels bool) *ExtraAnnotationsAndLabelsPostRenderer {
    return &ExtraAnnotationsAndLabelsPostRenderer{
        ExtraAnnotations:                  extraAnnotations,
        ExtraLabels:                       extraLabels,
        IgnoreInvalidAnnotationsAndLabels: ignoreInvalidAnnotationsAndLabels,
        globalWarnings:                    &defaultGlobalWarnings{},
    }
}

type ExtraAnnotationsAndLabelsPostRenderer struct {
    ExtraAnnotations                  map[string]string
    ExtraLabels                       map[string]string
    IgnoreInvalidAnnotationsAndLabels bool

    globalWarnings globalWarnings
}

type defaultGlobalWarnings struct{}

func (gw *defaultGlobalWarnings) GlobalWarningLn(ctx context.Context, msg string) {
    global_warnings.GlobalWarningLn(ctx, msg)
}

type globalWarnings interface {
    GlobalWarningLn(ctx context.Context, msg string)
}

func replaceNodeByKey(node *yaml_v3.Node, key string, value *yaml_v3.Node) {
    content := node.Content
    end := len(content)

    for pos := 0; pos < end; pos += 2 {
        keyNode := content[pos]

        if keyNode.Tag != "!!str" {
            continue
        }

        var k string
        if err := keyNode.Decode(&k); err != nil {
            continue
        }

        if k == key {
            content[pos+1] = value
            return
        }
    }
}

func findNodeByKey(node *yaml_v3.Node, key string) *yaml_v3.Node {
    content := node.Content
    end := len(content)

    for pos := 0; pos < end; pos += 2 {
        keyNode := content[pos]
        valueNode := content[pos+1]

        if keyNode.Tag != "!!str" {
            continue
        }

        var k string
        if err := keyNode.Decode(&k); err != nil {
            continue
        }

        if k == key {
            return valueNode
        }
    }

    return nil
}

func dereferenceAliasNode(node *yaml_v3.Node) (*yaml_v3.Node, error) {
    dereferencedNode, err := createNode(map[string]interface{}{})
    if err != nil {
        return nil, err
    }

    dereferencedNode.Content = append(dereferencedNode.Content, node.Alias.Content...)

    return dereferencedNode, nil
}

func getMapNode(docNode *yaml_v3.Node) *yaml_v3.Node {
    if docNode.Kind == yaml_v3.DocumentNode {
        if len(docNode.Content) > 0 {
            n := docNode.Content[0]
            if n.Tag == "!!map" {
                return n
            }
        }
    }
    return nil
}

func createNode(v interface{}) (*yaml_v3.Node, error) {
    newNode := &yaml_v3.Node{}
    if err := newNode.Encode(v); err != nil {
        return nil, fmt.Errorf("unable to encode value %#v into yaml node: %w", v, err)
    }
    return newNode, nil
}

func appendToNode(node *yaml_v3.Node, data interface{}) (*yaml_v3.Node, error) {
    newNode, err := createNode(data)
    if err != nil {
        return nil, err
    }

    node.Content = append(node.Content, newNode.Content...)
    return node, nil
}

func validateStringNode(node *yaml_v3.Node) error {
    var v interface{}
    if err := node.Decode(&v); err != nil {
        return fmt.Errorf("unable to decode value %q: %w", node.Value, err)
    }
    if _, ok := v.(string); !ok {
        typeOf := reflect.TypeOf(v)
        if typeOf != nil {
            return fmt.Errorf("invalid node %q: expected string, got %s", node.Value, reflect.TypeOf(v).String())
        } else {
            return fmt.Errorf("invalid node %q: expected string, got null value", node.Value)
        }
    }
    return nil
}

func validateKeyAndValue(keyNode, valueNode *yaml_v3.Node) (errors []error) {
    keyErr := validateStringNode(keyNode)
    if keyErr != nil {
        errors = append(errors, keyErr)
    }

    var valueErr error
    if valueNode.Kind == yaml_v3.AliasNode {
        valueErr = validateStringNode(valueNode.Alias)
    } else {
        valueErr = validateStringNode(valueNode)
    }
    if valueErr != nil {
        errors = append(errors, valueErr)
    }

    return
}

func validateMapStringStringNode(node *yaml_v3.Node) ([]*yaml_v3.Node, []error) {
    content := node.Content
    end := len(content)

    var validValues []*yaml_v3.Node
    var errors []error

    for pos := 0; pos < end; pos += 2 {
        keyNode := content[pos]
        valueNode := content[pos+1]

        if keyNode.Tag == "!!merge" && valueNode.Kind == yaml_v3.AliasNode {
            for pos := 0; pos < len(valueNode.Alias.Content); pos += 2 {
                keyNode := valueNode.Alias.Content[pos]
                valueNode := valueNode.Alias.Content[pos+1]

                newErrs := validateKeyAndValue(keyNode, valueNode)
                if len(newErrs) > 0 {
                    errors = append(errors, newErrs...)
                } else {
                    validValues = append(validValues, keyNode, valueNode)
                }
            }

            continue
        }

        newErrs := validateKeyAndValue(keyNode, valueNode)
        if len(newErrs) > 0 {
            errors = append(errors, newErrs...)
        } else {
            validValues = append(validValues, keyNode, valueNode)
        }
    }

    return validValues, errors
}

func appendExtraData(node *yaml_v3.Node, key string, data interface{}) error {
    if targetNode := findNodeByKey(node, key); targetNode != nil {
        if targetNode.Kind == yaml_v3.AliasNode {
            dereferencedTargetNode, err := dereferenceAliasNode(targetNode)
            if err != nil {
                return err
            }

            appendToNode(dereferencedTargetNode, data)
            replaceNodeByKey(node, key, dereferencedTargetNode)
        } else {
            appendToNode(targetNode, data)
        }

        if targetNode.Kind != yaml_v3.AliasNode && targetNode.Kind != yaml_v3.MappingNode {
            targetNode.Kind = yaml_v3.MappingNode
        }
        targetNode.Tag = "!!map"
    } else {
        appendToNode(node, map[string]interface{}{key: data})
    }

    return nil
}

func (pr *ExtraAnnotationsAndLabelsPostRenderer) Run(renderedManifests *bytes.Buffer) (*bytes.Buffer, error) {
    extraAnnotations := map[string]string{}
    for k, v := range WerfRuntimeAnnotations {
        extraAnnotations[k] = v
    }
    for k, v := range pr.ExtraAnnotations {
        extraAnnotations[k] = v
    }

    extraLabels := map[string]string{}
    for k, v := range WerfRuntimeLabels {
        extraLabels[k] = v
    }
    for k, v := range pr.ExtraLabels {
        extraLabels[k] = v
    }

    splitManifestsByKeys := releaseutil.SplitManifests(renderedManifests.String())

    manifestsKeys := make([]string, 0, len(splitManifestsByKeys))
    for k := range splitManifestsByKeys {
        manifestsKeys = append(manifestsKeys, k)
    }
    sort.Sort(releaseutil.BySplitManifestsOrder(manifestsKeys))

    splitModifiedManifests := make([]string, 0)

    for _, manifestKey := range manifestsKeys {
        manifestContent := splitManifestsByKeys[manifestKey]

        if os.Getenv("WERF_HELM_V3_EXTRA_ANNOTATIONS_AND_LABELS_DEBUG") == "1" {
            fmt.Printf("ExtraAnnotationsAndLabelsPostRenderer -- original manifest BEGIN\n")
            fmt.Printf("%s\n", manifestContent)
            fmt.Printf("ExtraAnnotationsAndLabelsPostRenderer -- original manifest END\n")
        }

        var obj unstructured.Unstructured
        if err := yaml.Unmarshal([]byte(manifestContent), &obj); err != nil {
            logboek.Warn().LogF("Unable to decode yaml manifest as unstructured object: %s: will not add extra annotations and labels to this object:\n%s\n---\n", err, manifestContent)
            splitModifiedManifests = append(splitModifiedManifests, manifestContent)
            continue
        }
        if obj.GetKind() == "" {
            logboek.Debug().LogF("Skipping empty object\n")
            continue
        }

        var objNode yaml_v3.Node
        if err := yaml_v3.Unmarshal([]byte(manifestContent), &objNode); err != nil {
            logboek.Warn().LogF("Unable to decode yaml manifest as map slice: %s: will not add extra annotations and labels to this object:\n%s\n---\n", err, manifestContent)
            splitModifiedManifests = append(splitModifiedManifests, manifestContent)
            continue
        }

        if os.Getenv("WERF_HELM_V3_EXTRA_ANNOTATIONS_AND_LABELS_DEBUG") == "1" {
            fmt.Printf("Unpacket obj annotations: %#v\n", obj.GetAnnotations())
        }

        if objMapNode := getMapNode(&objNode); objMapNode != nil {
            if metadataNode := findNodeByKey(objMapNode, "metadata"); metadataNode != nil {

                if obj.IsList() && len(extraAnnotations) > 0 {
                    pr.globalWarnings.GlobalWarningLn(context.Background(), fmt.Sprintf("werf annotations won't be applied to *List resource Kinds, including %s. We advise to replace *List resources with multiple separate resources of the same Kind", obj.GetKind()))
                } else if len(extraAnnotations) > 0 {
                    if err := appendExtraData(metadataNode, "annotations", extraAnnotations); err != nil {
                        pr.globalWarnings.GlobalWarningLn(context.Background(), fmt.Sprintf("werf annotations won't be applied to the %s/%s: an error have occurred during annotations injection: %s\n", strings.ToLower(obj.GetKind()), obj.GetName(), err))
                    }
                }

                if obj.IsList() && len(extraLabels) > 0 {
                    pr.globalWarnings.GlobalWarningLn(context.Background(), fmt.Sprintf("werf labels won't be applied to *List resource Kinds, including %s. We advise to replace *List resources with multiple separate resources of the same Kind", obj.GetKind()))
                } else if len(extraLabels) > 0 {
                    if err := appendExtraData(metadataNode, "labels", extraLabels); err != nil {
                        pr.globalWarnings.GlobalWarningLn(context.Background(), fmt.Sprintf("werf labels won't be applied to the %s/%s: an error have occurred during labels injection: %s\n", strings.ToLower(obj.GetKind()), obj.GetName(), err))
                    }
                }

                if annotationsNode := findNodeByKey(metadataNode, "annotations"); annotationsNode != nil {
                    validNodes, errors := validateMapStringStringNode(annotationsNode)
                    for _, err := range errors {
                        pr.globalWarnings.GlobalWarningLn(context.Background(), fmt.Sprintf("%s/%s annotations validation: %s", strings.ToLower(obj.GetKind()), obj.GetName(), err.Error()))
                    }
                    if pr.IgnoreInvalidAnnotationsAndLabels {
                        annotationsNode.Content = validNodes
                    }
                }

                if labelsNode := findNodeByKey(metadataNode, "labels"); labelsNode != nil {
                    validNodes, errors := validateMapStringStringNode(labelsNode)
                    for _, err := range errors {
                        pr.globalWarnings.GlobalWarningLn(context.Background(), fmt.Sprintf("%s/%s labels validation: %s\n", strings.ToLower(obj.GetKind()), obj.GetName(), err.Error()))
                    }
                    if pr.IgnoreInvalidAnnotationsAndLabels {
                        labelsNode.Content = validNodes
                    }
                }

            }
        }

        var modifiedManifestContent bytes.Buffer
        yamlEncoder := yaml_v3.NewEncoder(&modifiedManifestContent)
        yamlEncoder.SetIndent(2)

        if err := yamlEncoder.Encode(&objNode); err != nil {
            return nil, fmt.Errorf("unable to modify manifest: %w\n%s\n---\n", err, manifestContent)
        } else {
            splitModifiedManifests = append(splitModifiedManifests, modifiedManifestContent.String())

            if os.Getenv("WERF_HELM_V3_EXTRA_ANNOTATIONS_AND_LABELS_DEBUG") == "1" {
                fmt.Printf("ExtraAnnotationsAndLabelsPostRenderer -- modified manifest BEGIN\n")
                fmt.Printf("%s", modifiedManifestContent.String())
                fmt.Printf("ExtraAnnotationsAndLabelsPostRenderer -- modified manifest END\n")
            }
        }
    }

    modifiedManifests := bytes.NewBufferString(strings.Join(splitModifiedManifests, "---\n"))
    if os.Getenv("WERF_HELM_V3_EXTRA_ANNOTATIONS_AND_LABELS_DEBUG") == "1" {
        fmt.Printf("ExtraAnnotationsAndLabelsPostRenderer -- modified manifests RESULT BEGIN\n")
        fmt.Printf("%s\n", modifiedManifests.String())
        fmt.Printf("ExtraAnnotationsAndLabelsPostRenderer -- modified manifests RESULT END\n")
    }

    return modifiedManifests, nil
}

func (pr *ExtraAnnotationsAndLabelsPostRenderer) Add(extraAnnotations, extraLabels map[string]string) {
    if len(extraAnnotations) > 0 {
        if pr.ExtraAnnotations == nil {
            pr.ExtraAnnotations = make(map[string]string)
        }
        for k, v := range extraAnnotations {
            pr.ExtraAnnotations[k] = v
        }
    }

    if len(extraLabels) > 0 {
        if pr.ExtraLabels == nil {
            pr.ExtraLabels = make(map[string]string)
        }
        for k, v := range extraLabels {
            pr.ExtraLabels[k] = v
        }
    }
}