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 }