github.com/Racer159/jackal@v0.32.7-0.20240401174413-0bd2339e4f2e/src/internal/packager/helm/post-render.go (about)

     1  // SPDX-License-Identifier: Apache-2.0
     2  // SPDX-FileCopyrightText: 2021-Present The Jackal Authors
     3  
     4  // Package helm contains operations for working with helm charts.
     5  package helm
     6  
     7  import (
     8  	"bytes"
     9  	"fmt"
    10  	"os"
    11  	"path/filepath"
    12  	"reflect"
    13  
    14  	"github.com/Racer159/jackal/src/config"
    15  	"github.com/Racer159/jackal/src/internal/packager/template"
    16  	"github.com/Racer159/jackal/src/pkg/message"
    17  	"github.com/Racer159/jackal/src/pkg/utils"
    18  	"github.com/Racer159/jackal/src/types"
    19  	"github.com/defenseunicorns/pkg/helpers"
    20  	"helm.sh/helm/v3/pkg/releaseutil"
    21  	corev1 "k8s.io/api/core/v1"
    22  	"sigs.k8s.io/yaml"
    23  
    24  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    25  	"k8s.io/apimachinery/pkg/runtime"
    26  )
    27  
    28  type renderer struct {
    29  	*Helm
    30  	connectStrings types.ConnectStrings
    31  	namespaces     map[string]*corev1.Namespace
    32  	values         template.Values
    33  }
    34  
    35  func (h *Helm) newRenderer() (*renderer, error) {
    36  	message.Debugf("helm.NewRenderer()")
    37  
    38  	valueTemplate, err := template.Generate(h.cfg)
    39  	if err != nil {
    40  		return nil, err
    41  	}
    42  
    43  	// TODO (@austinabro321) this should be cleaned up after https://github.com/Racer159/jackal/pull/2276 gets merged
    44  	if h.cfg.State == nil {
    45  		valueTemplate.SetState(&types.JackalState{})
    46  	}
    47  
    48  	namespaces := make(map[string]*corev1.Namespace)
    49  	if h.cluster != nil {
    50  		namespaces[h.chart.Namespace] = h.cluster.NewJackalManagedNamespace(h.chart.Namespace)
    51  	}
    52  
    53  	return &renderer{
    54  		Helm:           h,
    55  		connectStrings: make(types.ConnectStrings),
    56  		namespaces:     namespaces,
    57  		values:         *valueTemplate,
    58  	}, nil
    59  }
    60  
    61  func (r *renderer) Run(renderedManifests *bytes.Buffer) (*bytes.Buffer, error) {
    62  	// This is very low cost and consistent for how we replace elsewhere, also good for debugging
    63  	tempDir, err := utils.MakeTempDir(r.chartPath)
    64  	if err != nil {
    65  		return nil, fmt.Errorf("unable to create tmpdir:  %w", err)
    66  	}
    67  	path := filepath.Join(tempDir, "chart.yaml")
    68  
    69  	if err := os.WriteFile(path, renderedManifests.Bytes(), helpers.ReadWriteUser); err != nil {
    70  		return nil, fmt.Errorf("unable to write the post-render file for the helm chart")
    71  	}
    72  
    73  	// Run the template engine against the chart output
    74  	if _, err := template.ProcessYamlFilesInPath(tempDir, r.component, r.values); err != nil {
    75  		return nil, fmt.Errorf("error templating the helm chart: %w", err)
    76  	}
    77  
    78  	// Read back the templated file contents
    79  	buff, err := os.ReadFile(path)
    80  	if err != nil {
    81  		return nil, fmt.Errorf("error reading temporary post-rendered helm chart: %w", err)
    82  	}
    83  
    84  	// Use helm to re-split the manifest byte (same call used by helm to pass this data to postRender)
    85  	_, resources, err := releaseutil.SortManifests(map[string]string{path: string(buff)},
    86  		r.actionConfig.Capabilities.APIVersions,
    87  		releaseutil.InstallOrder,
    88  	)
    89  
    90  	if err != nil {
    91  		return nil, fmt.Errorf("error re-rendering helm output: %w", err)
    92  	}
    93  
    94  	finalManifestsOutput := bytes.NewBuffer(nil)
    95  
    96  	// Otherwise, loop over the resources,
    97  	if r.cluster != nil {
    98  		if err := r.editHelmResources(resources, finalManifestsOutput); err != nil {
    99  			return nil, err
   100  		}
   101  
   102  		if err := r.adoptAndUpdateNamespaces(); err != nil {
   103  			return nil, err
   104  		}
   105  	} else {
   106  		for _, resource := range resources {
   107  			fmt.Fprintf(finalManifestsOutput, "---\n# Source: %s\n%s\n", resource.Name, resource.Content)
   108  		}
   109  	}
   110  
   111  	// Send the bytes back to helm
   112  	return finalManifestsOutput, nil
   113  }
   114  
   115  func (r *renderer) adoptAndUpdateNamespaces() error {
   116  	c := r.cluster
   117  	existingNamespaces, _ := c.GetNamespaces()
   118  	for name, namespace := range r.namespaces {
   119  
   120  		// Check to see if this namespace already exists
   121  		var existingNamespace bool
   122  		for _, serverNamespace := range existingNamespaces.Items {
   123  			if serverNamespace.Name == name {
   124  				existingNamespace = true
   125  			}
   126  		}
   127  
   128  		if !existingNamespace {
   129  			// This is a new namespace, add it
   130  			if _, err := c.CreateNamespace(namespace); err != nil {
   131  				return fmt.Errorf("unable to create the missing namespace %s", name)
   132  			}
   133  		} else if r.cfg.DeployOpts.AdoptExistingResources {
   134  			if r.cluster.IsInitialNamespace(name) {
   135  				// If this is a K8s initial namespace, refuse to adopt it
   136  				message.Warnf("Refusing to adopt the initial namespace: %s", name)
   137  			} else {
   138  				// This is an existing namespace to adopt
   139  				if _, err := c.UpdateNamespace(namespace); err != nil {
   140  					return fmt.Errorf("unable to adopt the existing namespace %s", name)
   141  				}
   142  			}
   143  		}
   144  
   145  		// If the package is marked as YOLO and the state is empty, skip the secret creation for this namespace
   146  		if r.cfg.Pkg.Metadata.YOLO && r.cfg.State.Distro == "YOLO" {
   147  			continue
   148  		}
   149  
   150  		// Create the secret
   151  		validRegistrySecret := c.GenerateRegistryPullCreds(name, config.JackalImagePullSecretName, r.cfg.State.RegistryInfo)
   152  
   153  		// Try to get a valid existing secret
   154  		currentRegistrySecret, _ := c.GetSecret(name, config.JackalImagePullSecretName)
   155  		if currentRegistrySecret.Name != config.JackalImagePullSecretName || !reflect.DeepEqual(currentRegistrySecret.Data, validRegistrySecret.Data) {
   156  			// Create or update the jackal registry secret
   157  			if _, err := c.CreateOrUpdateSecret(validRegistrySecret); err != nil {
   158  				message.WarnErrf(err, "Problem creating registry secret for the %s namespace", name)
   159  			}
   160  
   161  			// Generate the git server secret
   162  			gitServerSecret := c.GenerateGitPullCreds(name, config.JackalGitServerSecretName, r.cfg.State.GitServer)
   163  
   164  			// Create or update the jackal git server secret
   165  			if _, err := c.CreateOrUpdateSecret(gitServerSecret); err != nil {
   166  				message.WarnErrf(err, "Problem creating git server secret for the %s namespace", name)
   167  			}
   168  		}
   169  	}
   170  	return nil
   171  }
   172  
   173  func (r *renderer) editHelmResources(resources []releaseutil.Manifest, finalManifestsOutput *bytes.Buffer) error {
   174  	for _, resource := range resources {
   175  		// parse to unstructured to have access to more data than just the name
   176  		rawData := &unstructured.Unstructured{}
   177  		if err := yaml.Unmarshal([]byte(resource.Content), rawData); err != nil {
   178  			return fmt.Errorf("failed to unmarshal manifest: %#v", err)
   179  		}
   180  
   181  		switch rawData.GetKind() {
   182  		case "Namespace":
   183  			var namespace corev1.Namespace
   184  			// parse the namespace resource so it can be applied out-of-band by jackal instead of helm to avoid helm ns shenanigans
   185  			if err := runtime.DefaultUnstructuredConverter.FromUnstructured(rawData.UnstructuredContent(), &namespace); err != nil {
   186  				message.WarnErrf(err, "could not parse namespace %s", rawData.GetName())
   187  			} else {
   188  				message.Debugf("Matched helm namespace %s for jackal annotation", namespace.Name)
   189  				if namespace.Labels == nil {
   190  					// Ensure label map exists to avoid nil panic
   191  					namespace.Labels = make(map[string]string)
   192  				}
   193  				// Now track this namespace by jackal
   194  				namespace.Labels[config.JackalManagedByLabel] = "jackal"
   195  				namespace.Labels["jackal-helm-release"] = r.chart.ReleaseName
   196  
   197  				// Add it to the stack
   198  				r.namespaces[namespace.Name] = &namespace
   199  			}
   200  			// skip so we can strip namespaces from helm's brain
   201  			continue
   202  
   203  		case "Service":
   204  			// Check service resources for the jackal-connect label
   205  			labels := rawData.GetLabels()
   206  			annotations := rawData.GetAnnotations()
   207  
   208  			if key, keyExists := labels[config.JackalConnectLabelName]; keyExists {
   209  				// If there is a jackal-connect label
   210  				message.Debugf("Match helm service %s for jackal connection %s", rawData.GetName(), key)
   211  
   212  				// Add the connectString for processing later in the deployment
   213  				r.connectStrings[key] = types.ConnectString{
   214  					Description: annotations[config.JackalConnectAnnotationDescription],
   215  					URL:         annotations[config.JackalConnectAnnotationURL],
   216  				}
   217  			}
   218  		}
   219  
   220  		namespace := rawData.GetNamespace()
   221  		if _, exists := r.namespaces[namespace]; !exists && namespace != "" {
   222  			// if this is the first time seeing this ns, we need to track that to create it as well
   223  			r.namespaces[namespace] = r.cluster.NewJackalManagedNamespace(namespace)
   224  		}
   225  
   226  		// If we have been asked to adopt existing resources, process those now as well
   227  		if r.cfg.DeployOpts.AdoptExistingResources {
   228  			deployedNamespace := namespace
   229  			if deployedNamespace == "" {
   230  				deployedNamespace = r.chart.Namespace
   231  			}
   232  
   233  			helmLabels := map[string]string{"app.kubernetes.io/managed-by": "Helm"}
   234  			helmAnnotations := map[string]string{
   235  				"meta.helm.sh/release-name":      r.chart.ReleaseName,
   236  				"meta.helm.sh/release-namespace": r.chart.Namespace,
   237  			}
   238  
   239  			if err := r.cluster.AddLabelsAndAnnotations(deployedNamespace, rawData.GetName(), rawData.GroupVersionKind().GroupKind(), helmLabels, helmAnnotations); err != nil {
   240  				// Print a debug message since this could just be because the resource doesn't exist
   241  				message.Debugf("Unable to adopt resource %s: %s", rawData.GetName(), err.Error())
   242  			}
   243  		}
   244  		// Finally place this back onto the output buffer
   245  		fmt.Fprintf(finalManifestsOutput, "---\n# Source: %s\n%s\n", resource.Name, resource.Content)
   246  	}
   247  	return nil
   248  }