mirror of https://github.com/fluxcd/flux2.git
				
				
				
			
			You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			773 lines
		
	
	
		
			20 KiB
		
	
	
	
		
			Go
		
	
			
		
		
	
	
			773 lines
		
	
	
		
			20 KiB
		
	
	
	
		
			Go
		
	
/*
 | 
						|
Copyright 2022 The Flux authors
 | 
						|
 | 
						|
Licensed under the Apache License, Version 2.0 (the "License");
 | 
						|
you may not use this file except in compliance with the License.
 | 
						|
You may obtain a copy of the License at
 | 
						|
 | 
						|
    http://www.apache.org/licenses/LICENSE-2.0
 | 
						|
 | 
						|
Unless required by applicable law or agreed to in writing, software
 | 
						|
distributed under the License is distributed on an "AS IS" BASIS,
 | 
						|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
						|
See the License for the specific language governing permissions and
 | 
						|
limitations under the License.
 | 
						|
*/
 | 
						|
 | 
						|
package build
 | 
						|
 | 
						|
import (
 | 
						|
	"bytes"
 | 
						|
	"context"
 | 
						|
	"encoding/base64"
 | 
						|
	"encoding/json"
 | 
						|
	"errors"
 | 
						|
	"fmt"
 | 
						|
	"io"
 | 
						|
	"os"
 | 
						|
	"path/filepath"
 | 
						|
	"strings"
 | 
						|
	"sync"
 | 
						|
	"time"
 | 
						|
 | 
						|
	"github.com/theckman/yacspin"
 | 
						|
	apierrors "k8s.io/apimachinery/pkg/api/errors"
 | 
						|
	"k8s.io/apimachinery/pkg/api/meta"
 | 
						|
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 | 
						|
	"k8s.io/apimachinery/pkg/runtime"
 | 
						|
	"k8s.io/apimachinery/pkg/types"
 | 
						|
	k8syaml "k8s.io/apimachinery/pkg/util/yaml"
 | 
						|
	"k8s.io/cli-runtime/pkg/genericclioptions"
 | 
						|
	"sigs.k8s.io/controller-runtime/pkg/client"
 | 
						|
	"sigs.k8s.io/kustomize/api/resmap"
 | 
						|
	"sigs.k8s.io/kustomize/api/resource"
 | 
						|
	"sigs.k8s.io/kustomize/kyaml/yaml"
 | 
						|
 | 
						|
	kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1"
 | 
						|
	"github.com/fluxcd/pkg/kustomize"
 | 
						|
	runclient "github.com/fluxcd/pkg/runtime/client"
 | 
						|
	ssautil "github.com/fluxcd/pkg/ssa/utils"
 | 
						|
	"sigs.k8s.io/kustomize/kyaml/filesys"
 | 
						|
 | 
						|
	"github.com/fluxcd/flux2/v2/internal/utils"
 | 
						|
)
 | 
						|
 | 
						|
const (
 | 
						|
	controllerName       = "kustomize-controller"
 | 
						|
	controllerGroup      = "kustomize.toolkit.fluxcd.io"
 | 
						|
	mask                 = "**SOPS**"
 | 
						|
	dockercfgSecretType  = "kubernetes.io/dockerconfigjson"
 | 
						|
	typeField            = "type"
 | 
						|
	dataField            = "data"
 | 
						|
	stringDataField      = "stringData"
 | 
						|
	spinnerDryRunMessage = "running dry-run"
 | 
						|
)
 | 
						|
 | 
						|
var defaultTimeout = 80 * time.Second
 | 
						|
 | 
						|
// Builder builds yaml manifests
 | 
						|
// It retrieves the kustomization object from the k8s cluster
 | 
						|
// and overlays the manifests with the resources specified in the resourcesPath
 | 
						|
type Builder struct {
 | 
						|
	client            client.WithWatch
 | 
						|
	restMapper        meta.RESTMapper
 | 
						|
	name              string
 | 
						|
	namespace         string
 | 
						|
	resourcesPath     string
 | 
						|
	kustomizationFile string
 | 
						|
	ignore            []string
 | 
						|
	// mu is used to synchronize access to the kustomization file
 | 
						|
	mu            sync.Mutex
 | 
						|
	action        kustomize.Action
 | 
						|
	kustomization *kustomizev1.Kustomization
 | 
						|
	timeout       time.Duration
 | 
						|
	spinner       *yacspin.Spinner
 | 
						|
	dryRun        bool
 | 
						|
	strictSubst   bool
 | 
						|
	recursive     bool
 | 
						|
	localSources  map[string]string
 | 
						|
	// diff needs to handle kustomizations one by one
 | 
						|
	singleKustomization bool
 | 
						|
}
 | 
						|
 | 
						|
// BuilderOptionFunc is a function that configures a Builder
 | 
						|
type BuilderOptionFunc func(b *Builder) error
 | 
						|
 | 
						|
// WithKustomizationFile sets the kustomization file
 | 
						|
func WithKustomizationFile(file string) BuilderOptionFunc {
 | 
						|
	return func(b *Builder) error {
 | 
						|
		b.kustomizationFile = file
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// WithTimeout sets the timeout for the builder
 | 
						|
func WithTimeout(timeout time.Duration) BuilderOptionFunc {
 | 
						|
	return func(b *Builder) error {
 | 
						|
		b.timeout = timeout
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func WithProgressBar() BuilderOptionFunc {
 | 
						|
	return func(b *Builder) error {
 | 
						|
		// Add a spinner
 | 
						|
		cfg := yacspin.Config{
 | 
						|
			Frequency:       100 * time.Millisecond,
 | 
						|
			CharSet:         yacspin.CharSets[59],
 | 
						|
			Suffix:          "Kustomization diffing...",
 | 
						|
			SuffixAutoColon: true,
 | 
						|
			Message:         spinnerDryRunMessage,
 | 
						|
			StopCharacter:   "✓",
 | 
						|
			StopColors:      []string{"fgGreen"},
 | 
						|
		}
 | 
						|
		spinner, err := yacspin.New(cfg)
 | 
						|
		if err != nil {
 | 
						|
			return fmt.Errorf("failed to create spinner: %w", err)
 | 
						|
		}
 | 
						|
		b.spinner = spinner
 | 
						|
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// WithClientConfig sets the client configuration
 | 
						|
func WithClientConfig(rcg *genericclioptions.ConfigFlags, clientOpts *runclient.Options) BuilderOptionFunc {
 | 
						|
	return func(b *Builder) error {
 | 
						|
		kubeClient, err := utils.KubeClient(rcg, clientOpts)
 | 
						|
		if err != nil {
 | 
						|
			return err
 | 
						|
		}
 | 
						|
 | 
						|
		restMapper, err := rcg.ToRESTMapper()
 | 
						|
		if err != nil {
 | 
						|
			return err
 | 
						|
		}
 | 
						|
		b.client = kubeClient
 | 
						|
		b.restMapper = restMapper
 | 
						|
		b.namespace = *rcg.Namespace
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// WithNamespace sets the namespace
 | 
						|
func WithNamespace(namespace string) BuilderOptionFunc {
 | 
						|
	return func(b *Builder) error {
 | 
						|
		b.namespace = namespace
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// WithDryRun sets the dry-run flag
 | 
						|
func WithDryRun(dryRun bool) BuilderOptionFunc {
 | 
						|
	return func(b *Builder) error {
 | 
						|
		b.dryRun = dryRun
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// WithStrictSubstitute sets the strict substitute flag
 | 
						|
func WithStrictSubstitute(strictSubstitute bool) BuilderOptionFunc {
 | 
						|
	return func(b *Builder) error {
 | 
						|
		b.strictSubst = strictSubstitute
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// WithIgnore sets ignore field
 | 
						|
func WithIgnore(ignore []string) BuilderOptionFunc {
 | 
						|
	return func(b *Builder) error {
 | 
						|
		b.ignore = ignore
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// WithRecursive sets the recursive field
 | 
						|
func WithRecursive(recursive bool) BuilderOptionFunc {
 | 
						|
	return func(b *Builder) error {
 | 
						|
		b.recursive = recursive
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// WithLocalSources sets the local sources field
 | 
						|
func WithLocalSources(localSources map[string]string) BuilderOptionFunc {
 | 
						|
	return func(b *Builder) error {
 | 
						|
		b.localSources = localSources
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// WithSingleKustomization sets the single kustomization field to true
 | 
						|
func WithSingleKustomization() BuilderOptionFunc {
 | 
						|
	return func(b *Builder) error {
 | 
						|
		b.singleKustomization = true
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// withClientConfigFrom copies client and restMapper fields
 | 
						|
func withClientConfigFrom(in *Builder) BuilderOptionFunc {
 | 
						|
	return func(b *Builder) error {
 | 
						|
		b.client = in.client
 | 
						|
		b.restMapper = in.restMapper
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// withClientConfigFrom copies spinner field
 | 
						|
func withSpinnerFrom(in *Builder) BuilderOptionFunc {
 | 
						|
	return func(b *Builder) error {
 | 
						|
		b.spinner = in.spinner
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// withKustomization sets the kustomization field
 | 
						|
func withKustomization(k *kustomizev1.Kustomization) BuilderOptionFunc {
 | 
						|
	return func(b *Builder) error {
 | 
						|
		b.kustomization = k
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// NewBuilder returns a new Builder
 | 
						|
// It takes a kustomization name and a path to the resources
 | 
						|
// It also takes a list of BuilderOptionFunc to configure the builder
 | 
						|
// One of the options is WithClientConfig, that must be provided for the builder to work
 | 
						|
// with the k8s cluster
 | 
						|
// One other option is WithKustomizationFile, that must be provided for the builder to work
 | 
						|
// with a local kustomization file. If the kustomization file is not provided, the builder
 | 
						|
// will try to retrieve the kustomization object from the k8s cluster.
 | 
						|
// WithDryRun sets the dry-run flag, and needs to be provided if the builder is used for
 | 
						|
// a dry-run. This flag works in conjunction with WithKustomizationFile, because the
 | 
						|
// kustomization object is not retrieved from the k8s cluster when the dry-run flag is set.
 | 
						|
func NewBuilder(name, resources string, opts ...BuilderOptionFunc) (*Builder, error) {
 | 
						|
	b := &Builder{
 | 
						|
		name:          name,
 | 
						|
		resourcesPath: resources,
 | 
						|
	}
 | 
						|
 | 
						|
	for _, opt := range opts {
 | 
						|
		if err := opt(b); err != nil {
 | 
						|
			return nil, err
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	if b.timeout == 0 {
 | 
						|
		b.timeout = defaultTimeout
 | 
						|
	}
 | 
						|
 | 
						|
	if b.dryRun && b.kustomizationFile == "" && b.kustomization == nil {
 | 
						|
		return nil, fmt.Errorf("kustomization file is required for dry-run")
 | 
						|
	}
 | 
						|
 | 
						|
	if !b.dryRun && b.client == nil {
 | 
						|
		return nil, fmt.Errorf("client is required for live run")
 | 
						|
	}
 | 
						|
 | 
						|
	return b, nil
 | 
						|
}
 | 
						|
 | 
						|
func (b *Builder) resolveKustomization(liveKus *kustomizev1.Kustomization) (k *kustomizev1.Kustomization, err error) {
 | 
						|
	// local kustomization file takes precedence over live kustomization
 | 
						|
	if b.kustomizationFile != "" {
 | 
						|
		k, err = b.unMarshallKustomization()
 | 
						|
		if err != nil {
 | 
						|
			return
 | 
						|
		}
 | 
						|
		if !b.dryRun && liveKus != nil && liveKus.Status.Inventory != nil {
 | 
						|
			// merge the live kustomization status with the local kustomization in order to get the
 | 
						|
			// live resources status
 | 
						|
			k.Status = *liveKus.Status.DeepCopy()
 | 
						|
		}
 | 
						|
	} else {
 | 
						|
		k = liveKus
 | 
						|
	}
 | 
						|
	return
 | 
						|
}
 | 
						|
 | 
						|
func (b *Builder) getKustomization(ctx context.Context) (*kustomizev1.Kustomization, error) {
 | 
						|
	liveKus := &kustomizev1.Kustomization{}
 | 
						|
	namespacedName := types.NamespacedName{
 | 
						|
		Namespace: b.namespace,
 | 
						|
		Name:      b.name,
 | 
						|
	}
 | 
						|
	err := b.client.Get(ctx, namespacedName, liveKus)
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	return liveKus, nil
 | 
						|
}
 | 
						|
 | 
						|
// Build builds the yaml manifests from the kustomization object
 | 
						|
// and overlays the manifests with the resources specified in the resourcesPath
 | 
						|
// It expects a kustomization.yaml file in the resourcesPath, and it will
 | 
						|
// generate a kustomization.yaml file if it doesn't exist
 | 
						|
func (b *Builder) Build() ([]*unstructured.Unstructured, error) {
 | 
						|
	m, err := b.build()
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	resources, err := m.AsYaml()
 | 
						|
	if err != nil {
 | 
						|
		return nil, fmt.Errorf("kustomize build failed: %w", err)
 | 
						|
	}
 | 
						|
 | 
						|
	objects, err := ssautil.ReadObjects(bytes.NewReader(resources))
 | 
						|
	if err != nil {
 | 
						|
		return nil, fmt.Errorf("kustomize build failed: %w", err)
 | 
						|
	}
 | 
						|
 | 
						|
	if m := b.kustomization.Spec.CommonMetadata; m != nil {
 | 
						|
		ssautil.SetCommonMetadata(objects, m.Labels, m.Annotations)
 | 
						|
	}
 | 
						|
 | 
						|
	if b.recursive && !b.singleKustomization {
 | 
						|
		var objectsToAdd []*unstructured.Unstructured
 | 
						|
		for _, obj := range objects {
 | 
						|
			if isKustomization(obj) {
 | 
						|
				k, err := toKustomization(obj)
 | 
						|
				if err != nil {
 | 
						|
					return nil, err
 | 
						|
				}
 | 
						|
 | 
						|
				if !kustomizationsEqual(k, b.kustomization) {
 | 
						|
					subObjects, err := b.kustomizationBuild(k)
 | 
						|
					if err != nil {
 | 
						|
						return nil, err
 | 
						|
					}
 | 
						|
					objectsToAdd = append(objectsToAdd, subObjects...)
 | 
						|
				}
 | 
						|
			}
 | 
						|
		}
 | 
						|
		objects = append(objects, objectsToAdd...)
 | 
						|
	}
 | 
						|
 | 
						|
	return objects, nil
 | 
						|
}
 | 
						|
 | 
						|
func (b *Builder) build() (m resmap.ResMap, err error) {
 | 
						|
	ctx, cancel := context.WithTimeout(context.Background(), b.timeout)
 | 
						|
	defer cancel()
 | 
						|
 | 
						|
	// Get the kustomization object
 | 
						|
	liveKus := &kustomizev1.Kustomization{}
 | 
						|
	if b.dryRun {
 | 
						|
		liveKus = b.kustomization
 | 
						|
	} else {
 | 
						|
		liveKus, err = b.getKustomization(ctx)
 | 
						|
		if err != nil {
 | 
						|
			if !apierrors.IsNotFound(err) || b.kustomization == nil {
 | 
						|
				return nil, fmt.Errorf("failed to get kustomization object: %w", err)
 | 
						|
			}
 | 
						|
			// use provided Kustomization
 | 
						|
			liveKus = b.kustomization
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	k, err := b.resolveKustomization(liveKus)
 | 
						|
	if err != nil {
 | 
						|
		err = fmt.Errorf("failed to get kustomization object: %w", err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	// store the kustomization object
 | 
						|
	b.kustomization = k
 | 
						|
 | 
						|
	// generate kustomization.yaml if needed
 | 
						|
	action, er := b.generate(*k, b.resourcesPath)
 | 
						|
	if er != nil {
 | 
						|
		errf := kustomize.CleanDirectory(b.resourcesPath, action)
 | 
						|
		err = fmt.Errorf("failed to generate kustomization.yaml: %w", fmt.Errorf("%v %v", er, errf))
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	b.action = action
 | 
						|
 | 
						|
	defer func() {
 | 
						|
		errf := b.Cancel()
 | 
						|
		if err == nil {
 | 
						|
			err = errf
 | 
						|
		}
 | 
						|
	}()
 | 
						|
 | 
						|
	// build the kustomization
 | 
						|
	m, err = b.do(ctx, *k, b.resourcesPath)
 | 
						|
	if err != nil {
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	for _, res := range m.Resources() {
 | 
						|
		// set owner labels
 | 
						|
		err = b.setOwnerLabels(res)
 | 
						|
		if err != nil {
 | 
						|
			return
 | 
						|
		}
 | 
						|
 | 
						|
		// make sure secrets are masked
 | 
						|
		err = maskSopsData(res)
 | 
						|
		if err != nil {
 | 
						|
			return
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	return
 | 
						|
 | 
						|
}
 | 
						|
 | 
						|
func (b *Builder) kustomizationBuild(k *kustomizev1.Kustomization) ([]*unstructured.Unstructured, error) {
 | 
						|
	resourcesPath, err := b.kustomizationPath(k)
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	subBuilder, err := NewBuilder(k.Name, resourcesPath,
 | 
						|
		// use same client
 | 
						|
		withClientConfigFrom(b),
 | 
						|
		// kustomization will be used if there is no live kustomization
 | 
						|
		withKustomization(k),
 | 
						|
		WithTimeout(b.timeout),
 | 
						|
		WithNamespace(k.Namespace),
 | 
						|
		WithIgnore(b.ignore),
 | 
						|
		WithStrictSubstitute(b.strictSubst),
 | 
						|
		WithRecursive(b.recursive),
 | 
						|
		WithLocalSources(b.localSources),
 | 
						|
		WithDryRun(b.dryRun),
 | 
						|
	)
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	return subBuilder.Build()
 | 
						|
}
 | 
						|
 | 
						|
func (b *Builder) kustomizationPath(k *kustomizev1.Kustomization) (string, error) {
 | 
						|
	sourceRef := k.Spec.SourceRef.DeepCopy()
 | 
						|
	if sourceRef.Namespace == "" {
 | 
						|
		sourceRef.Namespace = k.Namespace
 | 
						|
	}
 | 
						|
 | 
						|
	sourceKey := sourceRef.String()
 | 
						|
	localPath, ok := b.localSources[sourceKey]
 | 
						|
	if !ok {
 | 
						|
		return "", fmt.Errorf("cannot get local path for %s of kustomization %s", sourceKey, k.Name)
 | 
						|
	}
 | 
						|
 | 
						|
	return filepath.Join(localPath, k.Spec.Path), nil
 | 
						|
}
 | 
						|
 | 
						|
func (b *Builder) unMarshallKustomization() (*kustomizev1.Kustomization, error) {
 | 
						|
	data, err := os.ReadFile(b.kustomizationFile)
 | 
						|
	if err != nil {
 | 
						|
		return nil, fmt.Errorf("failed to read kustomization file %s: %w", b.kustomizationFile, err)
 | 
						|
	}
 | 
						|
	k := &kustomizev1.Kustomization{}
 | 
						|
	decoder := k8syaml.NewYAMLOrJSONDecoder(bytes.NewBuffer(data), len(data))
 | 
						|
	// check for kustomization in yaml with the same name and namespace
 | 
						|
	for {
 | 
						|
		// ensure the target struct is emptied before decoding
 | 
						|
		k = &kustomizev1.Kustomization{}
 | 
						|
		err = decoder.Decode(k)
 | 
						|
		if err != nil {
 | 
						|
			if err == io.EOF {
 | 
						|
				return nil, fmt.Errorf("failed find kustomization with name '%s' and namespace '%s' in file '%s'",
 | 
						|
					b.name, b.namespace, b.kustomizationFile)
 | 
						|
			} else {
 | 
						|
				return nil, fmt.Errorf("failed to unmarshall kustomization file %s: %w", b.kustomizationFile, err)
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		if strings.HasPrefix(k.APIVersion, kustomizev1.GroupVersion.Group+"/") &&
 | 
						|
			k.Kind == kustomizev1.KustomizationKind &&
 | 
						|
			k.Name == b.name &&
 | 
						|
			(k.Namespace == b.namespace || k.Namespace == "") {
 | 
						|
			break
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return k, nil
 | 
						|
}
 | 
						|
 | 
						|
func (b *Builder) generate(kustomization kustomizev1.Kustomization, dirPath string) (kustomize.Action, error) {
 | 
						|
	data, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&kustomization)
 | 
						|
	if err != nil {
 | 
						|
		return "", err
 | 
						|
	}
 | 
						|
 | 
						|
	// a scanner will be used down the line to parse the list
 | 
						|
	// so we have to make sure to include newlines
 | 
						|
	ignoreList := strings.Join(b.ignore, "\n")
 | 
						|
	gen := kustomize.NewGeneratorWithIgnore("", ignoreList, unstructured.Unstructured{Object: data})
 | 
						|
 | 
						|
	// acquire the lock
 | 
						|
	b.mu.Lock()
 | 
						|
	defer b.mu.Unlock()
 | 
						|
 | 
						|
	return gen.WriteFile(dirPath, kustomize.WithSaveOriginalKustomization())
 | 
						|
}
 | 
						|
 | 
						|
func (b *Builder) do(ctx context.Context, kustomization kustomizev1.Kustomization, dirPath string) (resmap.ResMap, error) {
 | 
						|
	fs := filesys.MakeFsOnDisk()
 | 
						|
 | 
						|
	// acquire the lock
 | 
						|
	b.mu.Lock()
 | 
						|
	defer b.mu.Unlock()
 | 
						|
 | 
						|
	m, err := kustomize.Build(fs, dirPath)
 | 
						|
	if err != nil {
 | 
						|
		return nil, fmt.Errorf("kustomize build failed: %w", err)
 | 
						|
	}
 | 
						|
 | 
						|
	if kustomization.Spec.PostBuild == nil {
 | 
						|
		return m, nil
 | 
						|
	}
 | 
						|
 | 
						|
	data, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&kustomization)
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
	for _, res := range m.Resources() {
 | 
						|
		// run variable substitutions
 | 
						|
		outRes, err := kustomize.SubstituteVariables(ctx,
 | 
						|
			b.client,
 | 
						|
			unstructured.Unstructured{Object: data},
 | 
						|
			res,
 | 
						|
			kustomize.SubstituteWithDryRun(b.dryRun),
 | 
						|
			kustomize.SubstituteWithStrict(b.strictSubst),
 | 
						|
		)
 | 
						|
		if err != nil {
 | 
						|
			return nil, fmt.Errorf("var substitution failed for '%s': %w", res.GetName(), err)
 | 
						|
		}
 | 
						|
 | 
						|
		if outRes != nil {
 | 
						|
			_, err = m.Replace(res)
 | 
						|
			if err != nil {
 | 
						|
				return nil, err
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	return m, nil
 | 
						|
}
 | 
						|
 | 
						|
func isKustomization(object *unstructured.Unstructured) bool {
 | 
						|
	return strings.HasPrefix(object.GetAPIVersion(), kustomizev1.GroupVersion.Group+"/") &&
 | 
						|
		object.GetKind() == kustomizev1.KustomizationKind
 | 
						|
}
 | 
						|
 | 
						|
func toKustomization(object *unstructured.Unstructured) (*kustomizev1.Kustomization, error) {
 | 
						|
	obj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(object)
 | 
						|
	if err != nil {
 | 
						|
		return nil, fmt.Errorf("failed to convert to unstructured: %w", err)
 | 
						|
	}
 | 
						|
	k := &kustomizev1.Kustomization{}
 | 
						|
	err = runtime.DefaultUnstructuredConverter.FromUnstructured(obj, k)
 | 
						|
	if err != nil {
 | 
						|
		return nil, fmt.Errorf("failed to convert to kustomization: %w", err)
 | 
						|
	}
 | 
						|
	return k, nil
 | 
						|
}
 | 
						|
 | 
						|
func kustomizationsEqual(k1 *kustomizev1.Kustomization, k2 *kustomizev1.Kustomization) bool {
 | 
						|
	return k1.Name == k2.Name && k1.Namespace == k2.Namespace
 | 
						|
}
 | 
						|
 | 
						|
func (b *Builder) setOwnerLabels(res *resource.Resource) error {
 | 
						|
	labels := res.GetLabels()
 | 
						|
 | 
						|
	labels[controllerGroup+"/name"] = b.kustomization.GetName()
 | 
						|
	labels[controllerGroup+"/namespace"] = b.kustomization.GetNamespace()
 | 
						|
 | 
						|
	err := res.SetLabels(labels)
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
func maskSopsData(res *resource.Resource) error {
 | 
						|
	// sopsMess is the base64 encoded mask
 | 
						|
	sopsMess := base64.StdEncoding.EncodeToString([]byte(mask))
 | 
						|
 | 
						|
	if res.GetKind() == "Secret" {
 | 
						|
		// get both data and stringdata maps as a secret can have both
 | 
						|
		dataMap := res.GetDataMap()
 | 
						|
		stringDataMap := getStringDataMap(res)
 | 
						|
		asYaml, err := res.AsYAML()
 | 
						|
		if err != nil {
 | 
						|
			return fmt.Errorf("failed to mask secret %s sops data: %w", res.GetName(), err)
 | 
						|
		}
 | 
						|
 | 
						|
		// delete any sops data as we don't want to expose it
 | 
						|
		// assume that both data and stringdata are encrypted
 | 
						|
		if bytes.Contains(asYaml, []byte("sops:")) && bytes.Contains(asYaml, []byte("mac: ENC[")) {
 | 
						|
			// delete the sops object
 | 
						|
			res.PipeE(yaml.FieldClearer{Name: "sops"})
 | 
						|
 | 
						|
			secretType, err := res.GetFieldValue(typeField)
 | 
						|
			// If the intended type is Opaque, then it can be omitted from the manifest, since it's the default
 | 
						|
			// Ref: https://kubernetes.io/docs/concepts/configuration/secret/#opaque-secrets
 | 
						|
			if errors.As(err, &yaml.NoFieldError{}) {
 | 
						|
				secretType = "Opaque"
 | 
						|
			} else if err != nil {
 | 
						|
				return fmt.Errorf("failed to mask secret %s sops data: %w", res.GetName(), err)
 | 
						|
			}
 | 
						|
 | 
						|
			if v, ok := secretType.(string); ok && v == dockercfgSecretType {
 | 
						|
				// if the secret is a json docker config secret, we need to mask the data with a json object
 | 
						|
				err := maskDockerconfigjsonSopsData(dataMap, true)
 | 
						|
				if err != nil {
 | 
						|
					return fmt.Errorf("failed to mask secret %s sops data: %w", res.GetName(), err)
 | 
						|
				}
 | 
						|
 | 
						|
				err = maskDockerconfigjsonSopsData(stringDataMap, false)
 | 
						|
				if err != nil {
 | 
						|
					return fmt.Errorf("failed to mask secret %s sops data: %w", res.GetName(), err)
 | 
						|
				}
 | 
						|
 | 
						|
			} else {
 | 
						|
				for k := range dataMap {
 | 
						|
					dataMap[k] = sopsMess
 | 
						|
				}
 | 
						|
 | 
						|
				for k := range stringDataMap {
 | 
						|
					stringDataMap[k] = mask
 | 
						|
				}
 | 
						|
			}
 | 
						|
		} else {
 | 
						|
			err := maskBase64EncryptedSopsData(dataMap, sopsMess)
 | 
						|
			if err != nil {
 | 
						|
				return fmt.Errorf("failed to mask secret %s sops data: %w", res.GetName(), err)
 | 
						|
			}
 | 
						|
 | 
						|
			err = maskSopsDataInStringDataSecret(stringDataMap, sopsMess)
 | 
						|
			if err != nil {
 | 
						|
				return fmt.Errorf("failed to mask secret %s sops data: %w", res.GetName(), err)
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		// set the data and stringdata maps
 | 
						|
		res.SetDataMap(dataMap)
 | 
						|
 | 
						|
		if len(stringDataMap) > 0 {
 | 
						|
			err = res.SetMapField(yaml.NewMapRNode(&stringDataMap), stringDataField)
 | 
						|
			if err != nil {
 | 
						|
				return fmt.Errorf("failed to mask secret %s sops data: %w", res.GetName(), err)
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
func getStringDataMap(rn *resource.Resource) map[string]string {
 | 
						|
	n, err := rn.Pipe(yaml.Lookup(stringDataField))
 | 
						|
	if err != nil {
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
	result := map[string]string{}
 | 
						|
	_ = n.VisitFields(func(node *yaml.MapNode) error {
 | 
						|
		result[yaml.GetValue(node.Key)] = yaml.GetValue(node.Value)
 | 
						|
		return nil
 | 
						|
	})
 | 
						|
	return result
 | 
						|
}
 | 
						|
 | 
						|
func maskDockerconfigjsonSopsData(dataMap map[string]string, encode bool) error {
 | 
						|
	sopsMess := struct {
 | 
						|
		Mask string `json:"mask"`
 | 
						|
	}{
 | 
						|
		Mask: mask,
 | 
						|
	}
 | 
						|
 | 
						|
	maskJson, err := json.Marshal(sopsMess)
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	if encode {
 | 
						|
		for k := range dataMap {
 | 
						|
			dataMap[k] = base64.StdEncoding.EncodeToString(maskJson)
 | 
						|
		}
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
 | 
						|
	for k := range dataMap {
 | 
						|
		dataMap[k] = string(maskJson)
 | 
						|
	}
 | 
						|
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
func maskBase64EncryptedSopsData(dataMap map[string]string, mask string) error {
 | 
						|
	for k, v := range dataMap {
 | 
						|
		data, err := base64.StdEncoding.DecodeString(v)
 | 
						|
		if corruptErr := base64.CorruptInputError(0); errors.As(err, &corruptErr) {
 | 
						|
			return corruptErr
 | 
						|
		}
 | 
						|
 | 
						|
		if bytes.Contains(data, []byte("sops")) && bytes.Contains(data, []byte("ENC[")) {
 | 
						|
			dataMap[k] = mask
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
func maskSopsDataInStringDataSecret(stringDataMap map[string]string, mask string) error {
 | 
						|
	for k, v := range stringDataMap {
 | 
						|
		if bytes.Contains([]byte(v), []byte("sops")) && bytes.Contains([]byte(v), []byte("ENC[")) {
 | 
						|
			stringDataMap[k] = mask
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
// Cancel cancels the build
 | 
						|
// It restores a clean repository
 | 
						|
func (b *Builder) Cancel() error {
 | 
						|
	// acquire the lock
 | 
						|
	b.mu.Lock()
 | 
						|
	defer b.mu.Unlock()
 | 
						|
 | 
						|
	err := kustomize.CleanDirectory(b.resourcesPath, b.action)
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
func (b *Builder) StartSpinner() error {
 | 
						|
	if b.spinner == nil {
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
 | 
						|
	err := b.spinner.Start()
 | 
						|
	if err != nil {
 | 
						|
		return fmt.Errorf("failed to start spinner: %w", err)
 | 
						|
	}
 | 
						|
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
func (b *Builder) StopSpinner() error {
 | 
						|
	if b.spinner == nil {
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
 | 
						|
	status := b.spinner.Status()
 | 
						|
	if status == yacspin.SpinnerRunning || status == yacspin.SpinnerPaused {
 | 
						|
		err := b.spinner.Stop()
 | 
						|
		if err != nil {
 | 
						|
			return fmt.Errorf("failed to stop spinner: %w", err)
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	return nil
 | 
						|
}
 |