github.com/Racer159/jackal@v0.32.7-0.20240401174413-0bd2339e4f2e/src/extensions/bigbang/flux.go (about)

     1  // SPDX-License-Identifier: Apache-2.0
     2  // SPDX-FileCopyrightText: 2021-Present The Jackal Authors
     3  
     4  // Package bigbang contains the logic for installing Big Bang and Flux
     5  package bigbang
     6  
     7  import (
     8  	"fmt"
     9  	"os"
    10  	"path"
    11  	"path/filepath"
    12  
    13  	"github.com/Racer159/jackal/src/internal/packager/kustomize"
    14  	"github.com/Racer159/jackal/src/pkg/utils"
    15  	"github.com/Racer159/jackal/src/types"
    16  	"github.com/Racer159/jackal/src/types/extensions"
    17  	"github.com/defenseunicorns/pkg/helpers"
    18  	fluxHelmCtrl "github.com/fluxcd/helm-controller/api/v2beta1"
    19  	"helm.sh/helm/v3/pkg/chartutil"
    20  	v1 "k8s.io/api/apps/v1"
    21  	corev1 "k8s.io/api/core/v1"
    22  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    23  	"k8s.io/apimachinery/pkg/runtime"
    24  	krustytypes "sigs.k8s.io/kustomize/api/types"
    25  )
    26  
    27  // HelmReleaseDependency is a struct that represents a Flux Helm Release from an HR DependsOn list.
    28  type HelmReleaseDependency struct {
    29  	Metadata               metav1.ObjectMeta
    30  	NamespacedDependencies []string
    31  	NamespacedSource       string
    32  	ValuesFrom             []fluxHelmCtrl.ValuesReference
    33  }
    34  
    35  // Name returns a namespaced name for the HelmRelease for dependency sorting.
    36  func (h HelmReleaseDependency) Name() string {
    37  	return getNamespacedNameFromMeta(h.Metadata)
    38  }
    39  
    40  // Dependencies returns a list of namespaced dependencies for the HelmRelease for dependency sorting.
    41  func (h HelmReleaseDependency) Dependencies() []string {
    42  	return h.NamespacedDependencies
    43  }
    44  
    45  // getFlux Creates a component to deploy Flux.
    46  func getFlux(baseDir string, cfg *extensions.BigBang) (manifest types.JackalManifest, images []string, err error) {
    47  	localPath := path.Join(baseDir, "bb-ext-flux.yaml")
    48  	kustomizePath := path.Join(baseDir, "kustomization.yaml")
    49  
    50  	if cfg.Repo == "" {
    51  		cfg.Repo = bbRepo
    52  	}
    53  
    54  	remotePath := fmt.Sprintf("%s//base/flux?ref=%s", cfg.Repo, cfg.Version)
    55  
    56  	fluxKustomization := krustytypes.Kustomization{
    57  		Resources: []string{remotePath},
    58  	}
    59  
    60  	for _, path := range cfg.FluxPatchFiles {
    61  		absFluxPatchPath, _ := filepath.Abs(path)
    62  		fluxKustomization.Patches = append(fluxKustomization.Patches, krustytypes.Patch{Path: absFluxPatchPath})
    63  	}
    64  
    65  	if err := utils.WriteYaml(kustomizePath, fluxKustomization, helpers.ReadWriteUser); err != nil {
    66  		return manifest, images, fmt.Errorf("unable to write kustomization: %w", err)
    67  	}
    68  
    69  	// Perform Kustomization now to get the flux.yaml file.
    70  	if err := kustomize.Build(baseDir, localPath, true); err != nil {
    71  		return manifest, images, fmt.Errorf("unable to build kustomization: %w", err)
    72  	}
    73  
    74  	// Add the flux.yaml file to the component manifests.
    75  	manifest = types.JackalManifest{
    76  		Name:      "flux-system",
    77  		Namespace: "flux-system",
    78  		Files:     []string{localPath},
    79  	}
    80  
    81  	// Read the flux.yaml file to get the images.
    82  	if images, err = readFluxImages(localPath); err != nil {
    83  		return manifest, images, fmt.Errorf("unable to read flux images: %w", err)
    84  	}
    85  
    86  	return manifest, images, nil
    87  }
    88  
    89  // readFluxImages finds the images Flux needs to deploy
    90  func readFluxImages(localPath string) (images []string, err error) {
    91  	contents, err := os.ReadFile(localPath)
    92  	if err != nil {
    93  		return images, fmt.Errorf("unable to read flux manifest: %w", err)
    94  	}
    95  
    96  	// Break the manifest into separate resources.
    97  	yamls, _ := utils.SplitYAML(contents)
    98  
    99  	// Loop through each resource and find the images.
   100  	for _, yaml := range yamls {
   101  		// Flux controllers are Deployments.
   102  		if yaml.GetKind() == "Deployment" {
   103  			deployment := v1.Deployment{}
   104  			content := yaml.UnstructuredContent()
   105  
   106  			// Convert the unstructured content into a Deployment.
   107  			if err := runtime.DefaultUnstructuredConverter.FromUnstructured(content, &deployment); err != nil {
   108  				return nil, fmt.Errorf("could not parse deployment: %w", err)
   109  			}
   110  
   111  			// Get the pod spec.
   112  			pod := deployment.Spec.Template.Spec
   113  
   114  			// Flux controllers do not have init containers today, but this is future proofing.
   115  			for _, container := range pod.InitContainers {
   116  				images = append(images, container.Image)
   117  			}
   118  
   119  			// Add the main containers.
   120  			for _, container := range pod.Containers {
   121  				images = append(images, container.Image)
   122  			}
   123  
   124  		}
   125  	}
   126  
   127  	return images, nil
   128  }
   129  
   130  // composeValues composes values from a Flux HelmRelease and Secrets Map
   131  // (loosely based on upstream https://github.com/fluxcd/helm-controller/blob/main/controllers/helmrelease_controller.go#L551)
   132  func composeValues(hr HelmReleaseDependency, secrets map[string]corev1.Secret, configMaps map[string]corev1.ConfigMap) (valuesMap chartutil.Values, err error) {
   133  	valuesMap = chartutil.Values{}
   134  
   135  	for _, v := range hr.ValuesFrom {
   136  		var valuesData string
   137  		namespacedName := getNamespacedNameFromStr(hr.Metadata.Namespace, v.Name)
   138  
   139  		switch v.Kind {
   140  		case "ConfigMap":
   141  			cm, ok := configMaps[namespacedName]
   142  			if !ok {
   143  				return nil, fmt.Errorf("could not find values %s '%s'", v.Kind, namespacedName)
   144  			}
   145  
   146  			valuesData, ok = cm.Data[v.GetValuesKey()]
   147  			if !ok {
   148  				return nil, fmt.Errorf("missing key '%s' in %s '%s'", v.GetValuesKey(), v.Kind, namespacedName)
   149  			}
   150  		case "Secret":
   151  			sec, ok := secrets[namespacedName]
   152  			if !ok {
   153  				return nil, fmt.Errorf("could not find values %s '%s'", v.Kind, namespacedName)
   154  			}
   155  
   156  			valuesData, ok = sec.StringData[v.GetValuesKey()]
   157  			if !ok {
   158  				return nil, fmt.Errorf("missing key '%s' in %s '%s'", v.GetValuesKey(), v.Kind, namespacedName)
   159  			}
   160  		default:
   161  			return nil, fmt.Errorf("unsupported ValuesReference kind '%s'", v.Kind)
   162  		}
   163  
   164  		values, err := chartutil.ReadValues([]byte(valuesData))
   165  		if err != nil {
   166  			return nil, fmt.Errorf("unable to read values from key '%s' in %s '%s': %w", v.GetValuesKey(), v.Kind, hr.Name(), err)
   167  		}
   168  
   169  		valuesMap = helpers.MergeMapRecursive(valuesMap, values)
   170  	}
   171  
   172  	return valuesMap, nil
   173  }
   174  
   175  func getNamespacedNameFromMeta(o metav1.ObjectMeta) string {
   176  	return getNamespacedNameFromStr(o.Namespace, o.Name)
   177  }
   178  
   179  func getNamespacedNameFromStr(namespace, name string) string {
   180  	return fmt.Sprintf("%s.%s", namespace, name)
   181  }