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 }