github.com/Racer159/jackal@v0.32.7-0.20240401174413-0bd2339e4f2e/src/extensions/bigbang/bigbang.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  	"strings"
    13  	"time"
    14  
    15  	"github.com/Masterminds/semver/v3"
    16  	"github.com/Racer159/jackal/src/internal/packager/helm"
    17  	"github.com/Racer159/jackal/src/pkg/layout"
    18  	"github.com/Racer159/jackal/src/pkg/message"
    19  	"github.com/Racer159/jackal/src/pkg/utils"
    20  	"github.com/Racer159/jackal/src/types"
    21  	"github.com/Racer159/jackal/src/types/extensions"
    22  	"github.com/defenseunicorns/pkg/helpers"
    23  	fluxHelmCtrl "github.com/fluxcd/helm-controller/api/v2beta1"
    24  	fluxSrcCtrl "github.com/fluxcd/source-controller/api/v1beta2"
    25  	"helm.sh/helm/v3/pkg/chartutil"
    26  	corev1 "k8s.io/api/core/v1"
    27  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    28  	"sigs.k8s.io/yaml"
    29  )
    30  
    31  // Default location for pulling Big Bang.
    32  const (
    33  	bb                   = "bigbang"
    34  	bbRepo               = "https://repo1.dso.mil/big-bang/bigbang.git"
    35  	bbMinRequiredVersion = "1.54.0"
    36  )
    37  
    38  var tenMins = metav1.Duration{
    39  	Duration: 10 * time.Minute,
    40  }
    41  
    42  // Run mutates a component that should deploy Big Bang to a set of manifests
    43  // that contain the flux deployment of Big Bang
    44  func Run(YOLO bool, tmpPaths *layout.ComponentPaths, c types.JackalComponent) (types.JackalComponent, error) {
    45  	cfg := c.Extensions.BigBang
    46  	manifests := []types.JackalManifest{}
    47  
    48  	validVersionResponse, err := isValidVersion(cfg.Version)
    49  
    50  	if err != nil {
    51  		return c, fmt.Errorf("invalid Big Bang version: %s, parsing issue %s", cfg.Version, err)
    52  	}
    53  
    54  	// Make sure the version is valid.
    55  	if !validVersionResponse {
    56  		return c, fmt.Errorf("invalid Big Bang version: %s, must be at least %s", cfg.Version, bbMinRequiredVersion)
    57  	}
    58  
    59  	// Print the banner for Big Bang.
    60  	printBanner()
    61  
    62  	// If no repo is provided, use the default.
    63  	if cfg.Repo == "" {
    64  		cfg.Repo = bbRepo
    65  	}
    66  
    67  	// By default, we want to deploy flux.
    68  	if !cfg.SkipFlux {
    69  		fluxManifest, images, err := getFlux(tmpPaths.Temp, cfg)
    70  		if err != nil {
    71  			return c, err
    72  		}
    73  
    74  		// Add the flux manifests to the list of manifests to be pulled down by Jackal.
    75  		manifests = append(manifests, fluxManifest)
    76  
    77  		if !YOLO {
    78  			// Add the images to the list of images to be pulled down by Jackal.
    79  			c.Images = append(c.Images, images...)
    80  		}
    81  	}
    82  
    83  	bbRepo := fmt.Sprintf("%s@%s", cfg.Repo, cfg.Version)
    84  
    85  	// Configure helm to pull down the Big Bang chart.
    86  	helmCfg := helm.New(
    87  		types.JackalChart{
    88  			Name:        bb,
    89  			Namespace:   bb,
    90  			URL:         bbRepo,
    91  			Version:     cfg.Version,
    92  			ValuesFiles: cfg.ValuesFiles,
    93  			GitPath:     "./chart",
    94  		},
    95  		path.Join(tmpPaths.Temp, bb),
    96  		path.Join(tmpPaths.Temp, bb, "values"),
    97  		helm.WithPackageConfig(&types.PackagerConfig{}),
    98  	)
    99  
   100  	// Download the chart from Git and save it to a temporary directory.
   101  	err = helmCfg.PackageChartFromGit(c.DeprecatedCosignKeyPath)
   102  	if err != nil {
   103  		return c, fmt.Errorf("unable to download Big Bang Chart: %w", err)
   104  	}
   105  
   106  	// Template the chart so we can see what GitRepositories are being referenced in the
   107  	// manifests created with the provided Helm.
   108  	template, _, err := helmCfg.TemplateChart()
   109  	if err != nil {
   110  		return c, fmt.Errorf("unable to template Big Bang Chart: %w", err)
   111  	}
   112  
   113  	// Add the Big Bang repo to the list of repos to be pulled down by Jackal.
   114  	if !YOLO {
   115  		bbRepo := fmt.Sprintf("%s@%s", cfg.Repo, cfg.Version)
   116  		c.Repos = append(c.Repos, bbRepo)
   117  	}
   118  	// Parse the template for GitRepository objects and add them to the list of repos to be pulled down by Jackal.
   119  	gitRepos, hrDependencies, hrValues, err := findBBResources(template)
   120  	if err != nil {
   121  		return c, fmt.Errorf("unable to find Big Bang resources: %w", err)
   122  	}
   123  	if !YOLO {
   124  		for _, gitRepo := range gitRepos {
   125  			c.Repos = append(c.Repos, gitRepo)
   126  		}
   127  	}
   128  
   129  	// Generate a list of HelmReleases that need to be deployed in order.
   130  	dependencies := []utils.Dependency{}
   131  	for _, hrDep := range hrDependencies {
   132  		dependencies = append(dependencies, hrDep)
   133  	}
   134  	namespacedHelmReleaseNames, err := utils.SortDependencies(dependencies)
   135  	if err != nil {
   136  		return c, fmt.Errorf("unable to sort Big Bang HelmReleases: %w", err)
   137  	}
   138  
   139  	// ten minutes in seconds
   140  	maxTotalSeconds := 10 * 60
   141  
   142  	defaultMaxTotalSeconds := c.Actions.OnDeploy.Defaults.MaxTotalSeconds
   143  	if defaultMaxTotalSeconds > maxTotalSeconds {
   144  		maxTotalSeconds = defaultMaxTotalSeconds
   145  	}
   146  
   147  	// Add wait actions for each of the helm releases in generally the order they should be deployed.
   148  	for _, hrNamespacedName := range namespacedHelmReleaseNames {
   149  		hr := hrDependencies[hrNamespacedName]
   150  		action := types.JackalComponentAction{
   151  			Description:     fmt.Sprintf("Big Bang Helm Release `%s` to be ready", hrNamespacedName),
   152  			MaxTotalSeconds: &maxTotalSeconds,
   153  			Wait: &types.JackalComponentActionWait{
   154  				Cluster: &types.JackalComponentActionWaitCluster{
   155  					Kind:       "HelmRelease",
   156  					Identifier: hr.Metadata.Name,
   157  					Namespace:  hr.Metadata.Namespace,
   158  					Condition:  "ready",
   159  				},
   160  			},
   161  		}
   162  
   163  		// In Big Bang the metrics-server is a special case that only deploy if needed.
   164  		// The check it, we need to look for the existence of APIService instead of the HelmRelease, which
   165  		// may not ever be created. See links below for more details.
   166  		// https://repo1.dso.mil/big-bang/bigbang/-/blob/1.54.0/chart/templates/metrics-server/helmrelease.yaml
   167  		if hr.Metadata.Name == "metrics-server" {
   168  			action.Description = "K8s metric server to exist or be deployed by Big Bang"
   169  			action.Wait.Cluster = &types.JackalComponentActionWaitCluster{
   170  				Kind: "APIService",
   171  				// https://github.com/kubernetes-sigs/metrics-server#compatibility-matrix
   172  				Identifier: "v1beta1.metrics.k8s.io",
   173  			}
   174  		}
   175  
   176  		c.Actions.OnDeploy.OnSuccess = append(c.Actions.OnDeploy.OnSuccess, action)
   177  	}
   178  
   179  	t := true
   180  	failureGeneral := []string{
   181  		"get nodes -o wide",
   182  		"get hr -n bigbang",
   183  		"get gitrepo -n bigbang",
   184  		"get pods -A",
   185  	}
   186  	failureDebug := []string{
   187  		"describe hr -n bigbang",
   188  		"describe gitrepo -n bigbang",
   189  		"describe pods -A",
   190  		"describe nodes",
   191  		"get events -A",
   192  	}
   193  
   194  	// Add onFailure actions with additional troubleshooting information.
   195  	for _, cmd := range failureGeneral {
   196  		c.Actions.OnDeploy.OnFailure = append(c.Actions.OnDeploy.OnFailure, types.JackalComponentAction{
   197  			Cmd: fmt.Sprintf("./jackal tools kubectl %s", cmd),
   198  		})
   199  	}
   200  
   201  	for _, cmd := range failureDebug {
   202  		c.Actions.OnDeploy.OnFailure = append(c.Actions.OnDeploy.OnFailure, types.JackalComponentAction{
   203  			Mute:        &t,
   204  			Description: "Storing debug information to the log for troubleshooting.",
   205  			Cmd:         fmt.Sprintf("./jackal tools kubectl %s", cmd),
   206  		})
   207  	}
   208  
   209  	// Add a pre-remove action to suspend the Big Bang HelmReleases to prevent reconciliation during removal.
   210  	c.Actions.OnRemove.Before = append(c.Actions.OnRemove.Before, types.JackalComponentAction{
   211  		Description: "Suspend Big Bang HelmReleases to prevent reconciliation during removal.",
   212  		Cmd:         `./jackal tools kubectl patch helmrelease -n bigbang bigbang --type=merge -p '{"spec":{"suspend":true}}'`,
   213  	})
   214  
   215  	// Select the images needed to support the repos for this configuration of Big Bang.
   216  	if !YOLO {
   217  		for _, hr := range hrDependencies {
   218  			namespacedName := getNamespacedNameFromMeta(hr.Metadata)
   219  			gitRepo := gitRepos[hr.NamespacedSource]
   220  			values := hrValues[namespacedName]
   221  
   222  			images, err := findImagesforBBChartRepo(gitRepo, values)
   223  			if err != nil {
   224  				return c, fmt.Errorf("unable to find images for chart repo: %w", err)
   225  			}
   226  
   227  			c.Images = append(c.Images, images...)
   228  		}
   229  
   230  		// Make sure the list of images is unique.
   231  		c.Images = helpers.Unique(c.Images)
   232  	}
   233  
   234  	// Create the flux wrapper around Big Bang for deployment.
   235  	manifest, err := addBigBangManifests(YOLO, tmpPaths.Temp, cfg)
   236  	if err != nil {
   237  		return c, err
   238  	}
   239  
   240  	// Add the Big Bang manifests to the list of manifests to be pulled down by Jackal.
   241  	manifests = append(manifests, manifest)
   242  
   243  	// Prepend the Big Bang manifests to the list of manifests to be pulled down by Jackal.
   244  	// This is done so that the Big Bang manifests are deployed first.
   245  	c.Manifests = append(manifests, c.Manifests...)
   246  
   247  	return c, nil
   248  }
   249  
   250  // Skeletonize mutates a component so that the valuesFiles can be contained inside a skeleton package
   251  func Skeletonize(tmpPaths *layout.ComponentPaths, c types.JackalComponent) (types.JackalComponent, error) {
   252  	for valuesIdx, valuesFile := range c.Extensions.BigBang.ValuesFiles {
   253  		// Get the base file name for this file.
   254  		baseName := filepath.Base(valuesFile)
   255  
   256  		// Define the name as the file name without the extension.
   257  		baseName = strings.TrimSuffix(baseName, filepath.Ext(baseName))
   258  
   259  		// Add the skeleton name prefix.
   260  		skelName := fmt.Sprintf("bb-skel-vals-%d-%s.yaml", valuesIdx, baseName)
   261  
   262  		rel := filepath.Join(layout.TempDir, skelName)
   263  		dst := filepath.Join(tmpPaths.Base, rel)
   264  
   265  		if err := helpers.CreatePathAndCopy(valuesFile, dst); err != nil {
   266  			return c, err
   267  		}
   268  
   269  		c.Extensions.BigBang.ValuesFiles[valuesIdx] = rel
   270  	}
   271  
   272  	for fluxPatchFileIdx, fluxPatchFile := range c.Extensions.BigBang.FluxPatchFiles {
   273  		// Get the base file name for this file.
   274  		baseName := filepath.Base(fluxPatchFile)
   275  
   276  		// Define the name as the file name without the extension.
   277  		baseName = strings.TrimSuffix(baseName, filepath.Ext(baseName))
   278  
   279  		// Add the skeleton name prefix.
   280  		skelName := fmt.Sprintf("bb-skel-flux-patch-%d-%s.yaml", fluxPatchFileIdx, baseName)
   281  
   282  		rel := filepath.Join(layout.TempDir, skelName)
   283  		dst := filepath.Join(tmpPaths.Base, rel)
   284  
   285  		if err := helpers.CreatePathAndCopy(fluxPatchFile, dst); err != nil {
   286  			return c, err
   287  		}
   288  
   289  		c.Extensions.BigBang.FluxPatchFiles[fluxPatchFileIdx] = rel
   290  	}
   291  
   292  	return c, nil
   293  }
   294  
   295  // Compose mutates a component so that its local paths are relative to the provided path
   296  //
   297  // additionally, it will merge any overrides
   298  func Compose(c *types.JackalComponent, override types.JackalComponent, relativeTo string) {
   299  	// perform any overrides
   300  	if override.Extensions.BigBang != nil {
   301  		for valuesIdx, valuesFile := range override.Extensions.BigBang.ValuesFiles {
   302  			if helpers.IsURL(valuesFile) {
   303  				continue
   304  			}
   305  
   306  			fixed := filepath.Join(relativeTo, valuesFile)
   307  			override.Extensions.BigBang.ValuesFiles[valuesIdx] = fixed
   308  		}
   309  
   310  		for fluxPatchFileIdx, fluxPatchFile := range override.Extensions.BigBang.FluxPatchFiles {
   311  			if helpers.IsURL(fluxPatchFile) {
   312  				continue
   313  			}
   314  
   315  			fixed := filepath.Join(relativeTo, fluxPatchFile)
   316  			override.Extensions.BigBang.FluxPatchFiles[fluxPatchFileIdx] = fixed
   317  		}
   318  
   319  		if c.Extensions.BigBang == nil {
   320  			c.Extensions.BigBang = override.Extensions.BigBang
   321  		} else {
   322  			c.Extensions.BigBang.ValuesFiles = append(c.Extensions.BigBang.ValuesFiles, override.Extensions.BigBang.ValuesFiles...)
   323  			c.Extensions.BigBang.FluxPatchFiles = append(c.Extensions.BigBang.FluxPatchFiles, override.Extensions.BigBang.FluxPatchFiles...)
   324  		}
   325  	}
   326  }
   327  
   328  // isValidVersion check if the version is 1.54.0 or greater.
   329  func isValidVersion(version string) (bool, error) {
   330  	specifiedVersion, err := semver.NewVersion(version)
   331  
   332  	if err != nil {
   333  		return false, err
   334  	}
   335  
   336  	minRequiredVersion, _ := semver.NewVersion(bbMinRequiredVersion)
   337  
   338  	// Evaluating pre-releases too
   339  	c, _ := semver.NewConstraint(fmt.Sprintf(">= %s-0", minRequiredVersion))
   340  
   341  	// This extension requires BB 1.54.0 or greater.
   342  	return c.Check(specifiedVersion), nil
   343  }
   344  
   345  // findBBResources takes a list of yaml objects (as a string) and
   346  // parses it for GitRepository objects that it then parses
   347  // to return the list of git repos and tags needed.
   348  func findBBResources(t string) (gitRepos map[string]string, helmReleaseDeps map[string]HelmReleaseDependency, helmReleaseValues map[string]map[string]interface{}, err error) {
   349  	// Break the template into separate resources.
   350  	yamls, _ := utils.SplitYAMLToString([]byte(t))
   351  
   352  	gitRepos = map[string]string{}
   353  	helmReleaseDeps = map[string]HelmReleaseDependency{}
   354  	helmReleaseValues = map[string]map[string]interface{}{}
   355  	secrets := map[string]corev1.Secret{}
   356  	configMaps := map[string]corev1.ConfigMap{}
   357  
   358  	for _, y := range yamls {
   359  		var (
   360  			h fluxHelmCtrl.HelmRelease
   361  			g fluxSrcCtrl.GitRepository
   362  			s corev1.Secret
   363  			c corev1.ConfigMap
   364  		)
   365  
   366  		if err := yaml.Unmarshal([]byte(y), &h); err != nil {
   367  			continue
   368  		}
   369  
   370  		// If the resource is a HelmRelease, parse it for the dependencies.
   371  		if h.Kind == fluxHelmCtrl.HelmReleaseKind {
   372  			var deps []string
   373  			for _, d := range h.Spec.DependsOn {
   374  				depNamespacedName := getNamespacedNameFromStr(d.Namespace, d.Name)
   375  				deps = append(deps, depNamespacedName)
   376  			}
   377  
   378  			namespacedName := getNamespacedNameFromMeta(h.ObjectMeta)
   379  			srcNamespacedName := getNamespacedNameFromStr(h.Spec.Chart.Spec.SourceRef.Namespace,
   380  				h.Spec.Chart.Spec.SourceRef.Name)
   381  
   382  			helmReleaseDeps[namespacedName] = HelmReleaseDependency{
   383  				Metadata:               h.ObjectMeta,
   384  				NamespacedDependencies: deps,
   385  				NamespacedSource:       srcNamespacedName,
   386  				ValuesFrom:             h.Spec.ValuesFrom,
   387  			}
   388  
   389  			// Skip the rest as this is not a GitRepository.
   390  			continue
   391  		}
   392  
   393  		if err := yaml.Unmarshal([]byte(y), &g); err != nil {
   394  			continue
   395  		}
   396  
   397  		// If the resource is a GitRepository, parse it for the URL and tag.
   398  		if g.Kind == fluxSrcCtrl.GitRepositoryKind && g.Spec.URL != "" {
   399  			ref := "master"
   400  
   401  			switch {
   402  			case g.Spec.Reference.Commit != "":
   403  				ref = g.Spec.Reference.Commit
   404  
   405  			case g.Spec.Reference.SemVer != "":
   406  				ref = g.Spec.Reference.SemVer
   407  
   408  			case g.Spec.Reference.Tag != "":
   409  				ref = g.Spec.Reference.Tag
   410  
   411  			case g.Spec.Reference.Branch != "":
   412  				ref = g.Spec.Reference.Branch
   413  			}
   414  
   415  			// Set the URL and tag in the repo map
   416  			namespacedName := getNamespacedNameFromMeta(g.ObjectMeta)
   417  			gitRepos[namespacedName] = fmt.Sprintf("%s@%s", g.Spec.URL, ref)
   418  		}
   419  
   420  		if err := yaml.Unmarshal([]byte(y), &s); err != nil {
   421  			continue
   422  		}
   423  
   424  		// If the resource is a Secret, parse it so it can be used later for value templating.
   425  		if s.Kind == "Secret" {
   426  			namespacedName := getNamespacedNameFromMeta(s.ObjectMeta)
   427  			secrets[namespacedName] = s
   428  		}
   429  
   430  		if err := yaml.Unmarshal([]byte(y), &c); err != nil {
   431  			continue
   432  		}
   433  
   434  		// If the resource is a Secret, parse it so it can be used later for value templating.
   435  		if c.Kind == "ConfigMap" {
   436  			namespacedName := getNamespacedNameFromMeta(c.ObjectMeta)
   437  			configMaps[namespacedName] = c
   438  		}
   439  	}
   440  
   441  	for _, hr := range helmReleaseDeps {
   442  		namespacedName := getNamespacedNameFromMeta(hr.Metadata)
   443  		values, err := composeValues(hr, secrets, configMaps)
   444  		if err != nil {
   445  			return nil, nil, nil, err
   446  		}
   447  		helmReleaseValues[namespacedName] = values
   448  	}
   449  
   450  	return gitRepos, helmReleaseDeps, helmReleaseValues, nil
   451  }
   452  
   453  // addBigBangManifests creates the manifests component for deploying Big Bang.
   454  func addBigBangManifests(YOLO bool, manifestDir string, cfg *extensions.BigBang) (types.JackalManifest, error) {
   455  	// Create a manifest component that we add to the jackal package for bigbang.
   456  	manifest := types.JackalManifest{
   457  		Name:      bb,
   458  		Namespace: bb,
   459  	}
   460  
   461  	// Helper function to marshal and write a manifest and add it to the component.
   462  	addManifest := func(name string, data any) error {
   463  		path := path.Join(manifestDir, name)
   464  		out, err := yaml.Marshal(data)
   465  		if err != nil {
   466  			return err
   467  		}
   468  
   469  		if err := os.WriteFile(path, out, helpers.ReadWriteUser); err != nil {
   470  			return err
   471  		}
   472  
   473  		manifest.Files = append(manifest.Files, path)
   474  		return nil
   475  	}
   476  
   477  	// Create the GitRepository manifest.
   478  	if err := addManifest("bb-ext-gitrepository.yaml", manifestGitRepo(cfg)); err != nil {
   479  		return manifest, err
   480  	}
   481  
   482  	var hrValues []fluxHelmCtrl.ValuesReference
   483  
   484  	// If YOLO mode is enabled, do not include the jackal-credentials secret
   485  	if !YOLO {
   486  		// Create the jackal-credentials secret manifest.
   487  		if err := addManifest("bb-ext-jackal-credentials.yaml", manifestJackalCredentials(cfg.Version)); err != nil {
   488  			return manifest, err
   489  		}
   490  
   491  		// Create the list of values manifests starting with jackal-credentials.
   492  		hrValues = []fluxHelmCtrl.ValuesReference{{
   493  			Kind: "Secret",
   494  			Name: "jackal-credentials",
   495  		}}
   496  	}
   497  
   498  	// Loop through the valuesFrom list and create a manifest for each.
   499  	for valuesIdx, valuesFile := range cfg.ValuesFiles {
   500  		data, err := manifestValuesFile(valuesIdx, valuesFile)
   501  		if err != nil {
   502  			return manifest, err
   503  		}
   504  
   505  		path := fmt.Sprintf("%s.yaml", data.Name)
   506  		if err := addManifest(path, data); err != nil {
   507  			return manifest, err
   508  		}
   509  
   510  		// Add it to the list of valuesFrom for the HelmRelease
   511  		hrValues = append(hrValues, fluxHelmCtrl.ValuesReference{
   512  			Kind: "Secret",
   513  			Name: data.Name,
   514  		})
   515  	}
   516  
   517  	if err := addManifest("bb-ext-helmrelease.yaml", manifestHelmRelease(hrValues)); err != nil {
   518  		return manifest, err
   519  	}
   520  
   521  	return manifest, nil
   522  }
   523  
   524  // findImagesforBBChartRepo finds and returns the images for the Big Bang chart repo
   525  func findImagesforBBChartRepo(repo string, values chartutil.Values) (images []string, err error) {
   526  	matches := strings.Split(repo, "@")
   527  	if len(matches) < 2 {
   528  		return images, fmt.Errorf("cannot convert git repo %s to helm chart without a version tag", repo)
   529  	}
   530  
   531  	spinner := message.NewProgressSpinner("Discovering images in %s", repo)
   532  	defer spinner.Stop()
   533  
   534  	gitPath, err := helm.DownloadChartFromGitToTemp(repo, spinner)
   535  	if err != nil {
   536  		return images, err
   537  	}
   538  	defer os.RemoveAll(gitPath)
   539  
   540  	// Set the directory for the chart
   541  	chartPath := filepath.Join(gitPath, "chart")
   542  
   543  	images, err = helm.FindAnnotatedImagesForChart(chartPath, values)
   544  	if err != nil {
   545  		return images, err
   546  	}
   547  
   548  	spinner.Success()
   549  
   550  	return images, err
   551  }