github.com/Racer159/jackal@v0.32.7-0.20240401174413-0bd2339e4f2e/src/internal/packager/helm/chart.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 "errors" 9 "fmt" 10 "time" 11 12 "github.com/defenseunicorns/pkg/helpers" 13 14 "github.com/Masterminds/semver/v3" 15 "github.com/Racer159/jackal/src/config" 16 "github.com/Racer159/jackal/src/types" 17 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 18 "sigs.k8s.io/yaml" 19 20 "github.com/Racer159/jackal/src/pkg/message" 21 "helm.sh/helm/v3/pkg/action" 22 "helm.sh/helm/v3/pkg/chartutil" 23 "helm.sh/helm/v3/pkg/releaseutil" 24 25 "helm.sh/helm/v3/pkg/chart" 26 "helm.sh/helm/v3/pkg/release" 27 "helm.sh/helm/v3/pkg/storage/driver" 28 ) 29 30 // InstallOrUpgradeChart performs a helm install of the given chart. 31 func (h *Helm) InstallOrUpgradeChart() (types.ConnectStrings, string, error) { 32 fromMessage := h.chart.URL 33 if fromMessage == "" { 34 fromMessage = "Jackal-generated helm chart" 35 } 36 spinner := message.NewProgressSpinner("Processing helm chart %s:%s from %s", 37 h.chart.Name, 38 h.chart.Version, 39 fromMessage) 40 defer spinner.Stop() 41 42 // If no release name is specified, use the chart name. 43 if h.chart.ReleaseName == "" { 44 h.chart.ReleaseName = h.chart.Name 45 } 46 47 // Do not wait for the chart to be ready if data injections are present. 48 if len(h.component.DataInjections) > 0 { 49 spinner.Updatef("Data injections detected, not waiting for chart to be ready") 50 h.chart.NoWait = true 51 } 52 53 // Setup K8s connection. 54 err := h.createActionConfig(h.chart.Namespace, spinner) 55 if err != nil { 56 return nil, "", fmt.Errorf("unable to initialize the K8s client: %w", err) 57 } 58 59 postRender, err := h.newRenderer() 60 if err != nil { 61 return nil, "", fmt.Errorf("unable to create helm renderer: %w", err) 62 } 63 64 histClient := action.NewHistory(h.actionConfig) 65 tryHelm := func() error { 66 var err error 67 var output *release.Release 68 69 releases, histErr := histClient.Run(h.chart.ReleaseName) 70 71 spinner.Updatef("Checking for existing helm deployment") 72 73 if errors.Is(histErr, driver.ErrReleaseNotFound) { 74 // No prior release, try to install it. 75 spinner.Updatef("Attempting chart installation") 76 77 output, err = h.installChart(postRender) 78 } else if histErr == nil && len(releases) > 0 { 79 // Otherwise, there is a prior release so upgrade it. 80 spinner.Updatef("Attempting chart upgrade") 81 82 lastRelease := releases[len(releases)-1] 83 84 output, err = h.upgradeChart(lastRelease, postRender) 85 } else { 86 // 😠things aren't working 87 return fmt.Errorf("unable to verify the chart installation status: %w", histErr) 88 } 89 90 if err != nil { 91 return fmt.Errorf("unable to complete the helm chart install/upgrade: %w", err) 92 } 93 94 message.Debug(output.Info.Description) 95 spinner.Success() 96 return nil 97 } 98 99 err = helpers.Retry(tryHelm, h.retries, 5*time.Second, message.Warnf) 100 if err != nil { 101 // Try to rollback any deployed releases 102 releases, _ := histClient.Run(h.chart.ReleaseName) 103 previouslyDeployedVersion := 0 104 105 // Check for previous releases that successfully deployed 106 for _, release := range releases { 107 if release.Info.Status == "deployed" { 108 previouslyDeployedVersion = release.Version 109 } 110 } 111 112 // On total failure try to rollback (if there was a previously deployed version) or uninstall. 113 if previouslyDeployedVersion > 0 { 114 spinner.Updatef("Performing chart rollback") 115 116 err = h.rollbackChart(h.chart.ReleaseName, previouslyDeployedVersion) 117 if err != nil { 118 return nil, "", fmt.Errorf("unable to upgrade chart after %d attempts and unable to rollback: %w", h.retries, err) 119 } 120 121 return nil, "", fmt.Errorf("unable to upgrade chart after %d attempts", h.retries) 122 } 123 124 spinner.Updatef("Performing chart uninstall") 125 _, err = h.uninstallChart(h.chart.ReleaseName) 126 if err != nil { 127 return nil, "", fmt.Errorf("unable to install chart after %d attempts and unable to uninstall: %w", h.retries, err) 128 } 129 130 return nil, "", fmt.Errorf("unable to install chart after %d attempts", h.retries) 131 } 132 133 // return any collected connect strings for jackal connect. 134 return postRender.connectStrings, h.chart.ReleaseName, nil 135 } 136 137 // TemplateChart generates a helm template from a given chart. 138 func (h *Helm) TemplateChart() (manifest string, chartValues chartutil.Values, err error) { 139 message.Debugf("helm.TemplateChart()") 140 spinner := message.NewProgressSpinner("Templating helm chart %s", h.chart.Name) 141 defer spinner.Stop() 142 143 err = h.createActionConfig(h.chart.Namespace, spinner) 144 145 // Setup K8s connection. 146 if err != nil { 147 return "", nil, fmt.Errorf("unable to initialize the K8s client: %w", err) 148 } 149 150 // Bind the helm action. 151 client := action.NewInstall(h.actionConfig) 152 153 client.DryRun = true 154 client.Replace = true // Skip the name check. 155 client.ClientOnly = true 156 client.IncludeCRDs = true 157 // TODO: Further research this with regular/OCI charts 158 client.Verify = false 159 client.InsecureSkipTLSverify = config.CommonOptions.Insecure 160 if h.kubeVersion != "" { 161 parsedKubeVersion, err := chartutil.ParseKubeVersion(h.kubeVersion) 162 if err != nil { 163 return "", nil, fmt.Errorf("invalid kube version '%s': %s", h.kubeVersion, err) 164 } 165 client.KubeVersion = parsedKubeVersion 166 } 167 client.ReleaseName = h.chart.ReleaseName 168 169 // If no release name is specified, use the chart name. 170 if client.ReleaseName == "" { 171 client.ReleaseName = h.chart.Name 172 } 173 174 // Namespace must be specified. 175 client.Namespace = h.chart.Namespace 176 177 loadedChart, chartValues, err := h.loadChartData() 178 if err != nil { 179 return "", nil, fmt.Errorf("unable to load chart data: %w", err) 180 } 181 182 client.PostRenderer, err = h.newRenderer() 183 if err != nil { 184 return "", nil, fmt.Errorf("unable to create helm renderer: %w", err) 185 } 186 187 // Perform the loadedChart installation. 188 templatedChart, err := client.Run(loadedChart, chartValues) 189 if err != nil { 190 return "", nil, fmt.Errorf("error generating helm chart template: %w", err) 191 } 192 193 manifest = templatedChart.Manifest 194 195 for _, hook := range templatedChart.Hooks { 196 manifest += fmt.Sprintf("\n---\n%s", hook.Manifest) 197 } 198 199 spinner.Success() 200 201 return manifest, chartValues, nil 202 } 203 204 // RemoveChart removes a chart from the cluster. 205 func (h *Helm) RemoveChart(namespace string, name string, spinner *message.Spinner) error { 206 // Establish a new actionConfig for the namespace. 207 _ = h.createActionConfig(namespace, spinner) 208 // Perform the uninstall. 209 response, err := h.uninstallChart(name) 210 message.Debug(response) 211 return err 212 } 213 214 // UpdateReleaseValues updates values for a given chart release 215 // (note: this only works on single-deep charts, charts with dependencies (like loki-stack) will not work) 216 func (h *Helm) UpdateReleaseValues(updatedValues map[string]interface{}) error { 217 spinner := message.NewProgressSpinner("Updating values for helm release %s", h.chart.ReleaseName) 218 defer spinner.Stop() 219 220 err := h.createActionConfig(h.chart.Namespace, spinner) 221 if err != nil { 222 return fmt.Errorf("unable to initialize the K8s client: %w", err) 223 } 224 225 postRender, err := h.newRenderer() 226 if err != nil { 227 return fmt.Errorf("unable to create helm renderer: %w", err) 228 } 229 230 histClient := action.NewHistory(h.actionConfig) 231 histClient.Max = 1 232 releases, histErr := histClient.Run(h.chart.ReleaseName) 233 if histErr == nil && len(releases) > 0 { 234 lastRelease := releases[len(releases)-1] 235 236 // Setup a new upgrade action 237 client := action.NewUpgrade(h.actionConfig) 238 239 // Let each chart run for the default timeout. 240 client.Timeout = h.timeout 241 242 client.SkipCRDs = true 243 244 // Namespace must be specified. 245 client.Namespace = h.chart.Namespace 246 247 // Post-processing our manifests to apply vars and run jackal helm logic in cluster 248 client.PostRenderer = postRender 249 250 // Set reuse values to only override the values we are explicitly given 251 client.ReuseValues = true 252 253 // Wait for the update operation to successfully complete 254 client.Wait = true 255 256 // Perform the loadedChart upgrade. 257 _, err = client.Run(h.chart.ReleaseName, lastRelease.Chart, updatedValues) 258 if err != nil { 259 return err 260 } 261 262 spinner.Success() 263 264 return nil 265 } 266 267 return fmt.Errorf("unable to find the %s helm release", h.chart.ReleaseName) 268 } 269 270 func (h *Helm) installChart(postRender *renderer) (*release.Release, error) { 271 // Bind the helm action. 272 client := action.NewInstall(h.actionConfig) 273 274 // Let each chart run for the default timeout. 275 client.Timeout = h.timeout 276 277 // Default helm behavior for Jackal is to wait for the resources to deploy, NoWait overrides that for special cases (such as data-injection). 278 client.Wait = !h.chart.NoWait 279 280 // We need to include CRDs or operator installations will fail spectacularly. 281 client.SkipCRDs = false 282 283 // Must be unique per-namespace and < 53 characters. @todo: restrict helm loadedChart name to this. 284 client.ReleaseName = h.chart.ReleaseName 285 286 // Namespace must be specified. 287 client.Namespace = h.chart.Namespace 288 289 // Post-processing our manifests to apply vars and run jackal helm logic in cluster 290 client.PostRenderer = postRender 291 292 loadedChart, chartValues, err := h.loadChartData() 293 if err != nil { 294 return nil, fmt.Errorf("unable to load chart data: %w", err) 295 } 296 297 // Perform the loadedChart installation. 298 return client.Run(loadedChart, chartValues) 299 } 300 301 func (h *Helm) upgradeChart(lastRelease *release.Release, postRender *renderer) (*release.Release, error) { 302 // Migrate any deprecated APIs (if applicable) 303 err := h.migrateDeprecatedAPIs(lastRelease) 304 if err != nil { 305 return nil, fmt.Errorf("unable to check for API deprecations: %w", err) 306 } 307 308 // Setup a new upgrade action 309 client := action.NewUpgrade(h.actionConfig) 310 311 // Let each chart run for the default timeout. 312 client.Timeout = h.timeout 313 314 // Default helm behavior for Jackal is to wait for the resources to deploy, NoWait overrides that for special cases (such as data-injection). 315 client.Wait = !h.chart.NoWait 316 317 client.SkipCRDs = true 318 319 // Namespace must be specified. 320 client.Namespace = h.chart.Namespace 321 322 // Post-processing our manifests to apply vars and run jackal helm logic in cluster 323 client.PostRenderer = postRender 324 325 loadedChart, chartValues, err := h.loadChartData() 326 if err != nil { 327 return nil, fmt.Errorf("unable to load chart data: %w", err) 328 } 329 330 // Perform the loadedChart upgrade. 331 return client.Run(h.chart.ReleaseName, loadedChart, chartValues) 332 } 333 334 func (h *Helm) rollbackChart(name string, version int) error { 335 message.Debugf("helm.rollbackChart(%s)", name) 336 client := action.NewRollback(h.actionConfig) 337 client.CleanupOnFail = true 338 client.Force = true 339 client.Wait = true 340 client.Timeout = h.timeout 341 client.Version = version 342 return client.Run(name) 343 } 344 345 func (h *Helm) uninstallChart(name string) (*release.UninstallReleaseResponse, error) { 346 message.Debugf("helm.uninstallChart(%s)", name) 347 client := action.NewUninstall(h.actionConfig) 348 client.KeepHistory = false 349 client.Wait = true 350 client.Timeout = h.timeout 351 return client.Run(name) 352 } 353 354 func (h *Helm) loadChartData() (*chart.Chart, chartutil.Values, error) { 355 message.Debugf("helm.loadChartData()") 356 var ( 357 loadedChart *chart.Chart 358 chartValues chartutil.Values 359 err error 360 ) 361 362 if h.chartOverride == nil { 363 // If there is no override, get the chart and values info. 364 loadedChart, err = h.loadChartFromTarball() 365 if err != nil { 366 return nil, nil, fmt.Errorf("unable to load chart tarball: %w", err) 367 } 368 369 chartValues, err = h.parseChartValues() 370 if err != nil { 371 return loadedChart, nil, fmt.Errorf("unable to parse chart values: %w", err) 372 } 373 } else { 374 // Otherwise, use the overrides instead. 375 loadedChart = h.chartOverride 376 chartValues = h.valuesOverrides 377 } 378 379 return loadedChart, chartValues, nil 380 } 381 382 func (h *Helm) migrateDeprecatedAPIs(latestRelease *release.Release) error { 383 // Get the Kubernetes version from the current cluster 384 kubeVersion, err := h.cluster.GetServerVersion() 385 if err != nil { 386 return err 387 } 388 389 kubeGitVersion, err := semver.NewVersion(kubeVersion) 390 if err != nil { 391 return err 392 } 393 394 // Use helm to re-split the manifest bytes (same call used by helm to pass this data to postRender) 395 _, resources, err := releaseutil.SortManifests(map[string]string{"manifest": latestRelease.Manifest}, nil, releaseutil.InstallOrder) 396 397 if err != nil { 398 return fmt.Errorf("error re-rendering helm output: %w", err) 399 } 400 401 modifiedManifest := "" 402 modified := false 403 404 // Loop over the resources from the lastRelease manifest to check for deprecations 405 for _, resource := range resources { 406 // parse to unstructured to have access to more data than just the name 407 rawData := &unstructured.Unstructured{} 408 if err := yaml.Unmarshal([]byte(resource.Content), rawData); err != nil { 409 return fmt.Errorf("failed to unmarshal manifest: %#v", err) 410 } 411 412 rawData, manifestModified, _ := h.cluster.HandleDeprecations(rawData, *kubeGitVersion) 413 manifestContent, err := yaml.Marshal(rawData) 414 if err != nil { 415 return fmt.Errorf("failed to marshal raw manifest after deprecation check: %#v", err) 416 } 417 418 // If this is not a bad object, place it back into the manifest 419 modifiedManifest += fmt.Sprintf("---\n# Source: %s\n%s\n", resource.Name, manifestContent) 420 421 if manifestModified { 422 modified = true 423 } 424 } 425 426 // If the release was modified in the above loop, save it back to the cluster 427 if modified { 428 message.Warnf("Jackal detected deprecated APIs for the '%s' helm release. Attempting automatic upgrade.", latestRelease.Name) 429 430 // Update current release version to be superseded (same as the helm mapkubeapis plugin) 431 latestRelease.Info.Status = release.StatusSuperseded 432 if err := h.actionConfig.Releases.Update(latestRelease); err != nil { 433 return err 434 } 435 436 // Use a shallow copy of current release version to update the object with the modification 437 // and then store this new version (same as the helm mapkubeapis plugin) 438 var newRelease = latestRelease 439 newRelease.Manifest = modifiedManifest 440 newRelease.Info.Description = "Kubernetes deprecated API upgrade - DO NOT rollback from this version" 441 newRelease.Info.LastDeployed = h.actionConfig.Now() 442 newRelease.Version = latestRelease.Version + 1 443 newRelease.Info.Status = release.StatusDeployed 444 if err := h.actionConfig.Releases.Create(newRelease); err != nil { 445 return err 446 } 447 } 448 449 return nil 450 }