From 4683bd0f65837c75b6fe8bbcc5f73c1d883f43c7 Mon Sep 17 00:00:00 2001 From: Minimind <882485+jeff-mccoy@users.noreply.github.com> Date: Tue, 24 May 2022 09:48:08 -0500 Subject: [PATCH] implement component choice groups (#488) * implement component choice groups * fix test & handle default values w/--confirm again * add test case to ensure second component was not created when first is chosen --- Makefile | 7 + examples/component-choice/blank-file.txt | 1 + examples/component-choice/zarf.yaml | 18 ++ .../big-bang-core}/.images/helmreleases.png | Bin .../big-bang-core}/.images/pods.png | Bin .../big-bang-core}/README.md | 26 +- .../core-light/kustomization.yaml | 9 + .../kustomization/core-light/values.yaml | 222 ++++++++++++++++++ .../core-standard}/kustomization.yaml | 0 .../kustomization/core-standard}/values.yaml | 3 + .../big-bang-core}/zarf.yaml | 25 +- src/internal/packager/common.go | 133 +---------- src/internal/packager/components.go | 208 ++++++++++++++++ src/internal/packager/deploy.go | 21 +- src/internal/packager/scripts.go | 72 ++++++ src/internal/packager/validate/validate.go | 37 ++- src/internal/utils/yaml.go | 2 +- src/test/e2e/e2e_component_choice_test.go | 40 ++++ src/types/types.go | 4 + zarf.schema.json | 3 + 20 files changed, 662 insertions(+), 169 deletions(-) create mode 100644 examples/component-choice/blank-file.txt create mode 100644 examples/component-choice/zarf.yaml rename {examples/big-bang => packages/big-bang-core}/.images/helmreleases.png (100%) rename {examples/big-bang => packages/big-bang-core}/.images/pods.png (100%) rename {examples/big-bang => packages/big-bang-core}/README.md (71%) create mode 100644 packages/big-bang-core/kustomization/core-light/kustomization.yaml create mode 100644 packages/big-bang-core/kustomization/core-light/values.yaml rename {examples/big-bang/kustomization => packages/big-bang-core/kustomization/core-standard}/kustomization.yaml (100%) rename {examples/big-bang/kustomization => packages/big-bang-core/kustomization/core-standard}/values.yaml (99%) rename {examples/big-bang => packages/big-bang-core}/zarf.yaml (85%) create mode 100644 src/internal/packager/components.go create mode 100644 src/internal/packager/scripts.go create mode 100644 src/test/e2e/e2e_component_choice_test.go diff --git a/Makefile b/Makefile index 0c1f065eb5..1f3ccaf770 100644 --- a/Makefile +++ b/Makefile @@ -88,6 +88,10 @@ package-example-game: ## Create the Doom example package-example-component-scripts: ## Create component script example cd examples/component-scripts && ../../$(ZARF_BIN) package create --confirm && mv zarf-package-* ../../build/ +.PHONY: package-example-component-choice +package-example-component-choice: ## Create component choice example + cd examples/component-choice && ../../$(ZARF_BIN) package create --confirm && mv zarf-package-* ../../build/ + .PHONY: package-example-component-variables package-example-component-variables: ## Create component script example cd examples/component-variables && ../../$(ZARF_BIN) package create --confirm && mv zarf-package-* ../../build/ @@ -128,6 +132,9 @@ test-e2e: ## Run e2e tests. Will automatically build any required dependencies t @if [ ! -f zarf-package-component-scripts-$(ARCH).tar.zst ]; then\ $(MAKE) package-example-component-scripts;\ fi + @if [ ! -f zarf-package-component-choice-$(ARCH).tar.zst ]; then\ + $(MAKE) package-example-component-choice;\ + fi @if [ ! -f zarf-package-component-variables-$(ARCH).tar.zst ]; then\ $(MAKE) package-example-component-variables;\ fi diff --git a/examples/component-choice/blank-file.txt b/examples/component-choice/blank-file.txt new file mode 100644 index 0000000000..7fd28ffa23 --- /dev/null +++ b/examples/component-choice/blank-file.txt @@ -0,0 +1 @@ +Just some simple file.... \ No newline at end of file diff --git a/examples/component-choice/zarf.yaml b/examples/component-choice/zarf.yaml new file mode 100644 index 0000000000..4c1696bb1f --- /dev/null +++ b/examples/component-choice/zarf.yaml @@ -0,0 +1,18 @@ +kind: ZarfPackageConfig +metadata: + name: component-choice + description: "Test component to demonstrate grouping components for a user to choose from" + +components: + - name: first-choice + group: example-choice + files: + - source: blank-file.txt + target: first-choice-file.txt + + - name: second-choice + group: example-choice + default: true + files: + - source: blank-file.txt + target: second-choice-file.txt diff --git a/examples/big-bang/.images/helmreleases.png b/packages/big-bang-core/.images/helmreleases.png similarity index 100% rename from examples/big-bang/.images/helmreleases.png rename to packages/big-bang-core/.images/helmreleases.png diff --git a/examples/big-bang/.images/pods.png b/packages/big-bang-core/.images/pods.png similarity index 100% rename from examples/big-bang/.images/pods.png rename to packages/big-bang-core/.images/pods.png diff --git a/examples/big-bang/README.md b/packages/big-bang-core/README.md similarity index 71% rename from examples/big-bang/README.md rename to packages/big-bang-core/README.md index 881f185ecc..f44429cdcc 100644 --- a/examples/big-bang/README.md +++ b/packages/big-bang-core/README.md @@ -1,6 +1,6 @@ # Example: Big Bang Core -This example shows a deployment of [Big Bang Core](https://repo1.dso.mil/platform-one/big-bang/bigbang) using Zarf. +This package deploys [Big Bang Core](https://repo1.dso.mil/platform-one/big-bang/bigbang) using Zarf. ![pods](.images/pods.png) @@ -8,15 +8,10 @@ This example shows a deployment of [Big Bang Core](https://repo1.dso.mil/platfor ## Known Issues -- Inside the Vagrant VM the services are available on the standard port `443`. Outside the VM if you want to pull something up in your browser that traffic is being routed to port `8443` to avoid needing to be root when running the Vagrant box. -- Due to issues with Elasticsearch this example doesn't work yet in some distros. It does work in the Vagrant VM detailed below. Upcoming work to update to the latest version of Big Bang and swap the EFK stack out for the PLG stack (Promtail, Loki, Grafana) should resolve this issue -- Currently this example does the equivalent of `kustomize build | kubectl apply -f -`, which means Flux will be used to deploy everything, but it won't be watching a Git repository for changes. Upcoming work is planned to update the example so that you will be able to open up a Git repo in the private Gitea server inside the cluster, commit and push a change, and see that change get reflected in the deployment. +- Currently this package does the equivalent of `kustomize build | kubectl apply -f -`, which means Flux will be used to deploy everything, but it won't be watching a Git repository for changes. Upcoming work is planned to update the package so that you will be able to open up a Git repo in the private Gitea server inside the cluster, commit and push a change, and see that change get reflected in the deployment. -## Prerequisites - -1. Install [Vagrant](https://www.vagrantup.com/) -1. Install `make` -1. Install `sha256sum` (on Mac it's `brew install coreutils`) +> NOTE: +> Big Bang requires an AMD64 system to deploy as Iron Bank does not yet support ARM. You will need to deploy to a cluster that is running AMD64. Specifically, M1 Apple computers are not supported locally and you will need to provision a remote cluster to work with Big Bang currently. ## Instructions @@ -33,10 +28,6 @@ cd zarf/examples make fetch-release ``` -> NOTE: -> -> If you have any issues with `make fetch-release` you can try `make build-release` instead. It will build the files instead of downloading them. You'll need Golang installed. - ### Build the deploy package ```shell @@ -51,10 +42,6 @@ make package-example-big-bang make vm-init ``` -> NOTE: -> -> All subsequent commands should be happening INSIDE the Vagrant VM - ### Initialize Zarf ```shell @@ -99,10 +86,5 @@ make vm-destroy ## Troubleshooting -### Elasticsearch isn't working when I try to deploy the Big Bang package on KinD (or K3d, or any other distro other than K3s) -That's a known issue. This example is only supported right now when using the K3s cluster that Zarf is able to deploy when running `zarf init`. Updating to the latest version of Big Bang and swapping the EFK stack out for the PLG stack should fix this issue. It's on the roadmap™. -### I'm getting "Misdirected Request" when trying to get to any of the services in my browser -Run the `kubectl delete` command documented above to delete the buggy EnvoyFilter. Updating to the latest version of Big Bang will fix this issue. It's on the roadmap™. - ### My computer crashed! Close all those hundreds of chrome tabs, shut down all non-essential programs, and try again. Big Bang is a HOG. If you have less than 32GB of RAM you're in for a rough time. diff --git a/packages/big-bang-core/kustomization/core-light/kustomization.yaml b/packages/big-bang-core/kustomization/core-light/kustomization.yaml new file mode 100644 index 0000000000..955ac9df96 --- /dev/null +++ b/packages/big-bang-core/kustomization/core-light/kustomization.yaml @@ -0,0 +1,9 @@ +bases: + - ../core-prod + +configMapGenerator: + - name: environment + namespace: bigbang + behavior: merge + files: + - values.yaml diff --git a/packages/big-bang-core/kustomization/core-light/values.yaml b/packages/big-bang-core/kustomization/core-light/values.yaml new file mode 100644 index 0000000000..17daae9458 --- /dev/null +++ b/packages/big-bang-core/kustomization/core-light/values.yaml @@ -0,0 +1,222 @@ +clusterAuditor: + values: + resources: + requests: + cpu: "100m" + memory: "512Mi" + limits: + cpu: "500m" + memory: "512Mi" + +gatekeeper: + values: + controllerManager: + resources: + limits: + cpu: "1" + memory: "2Gi" + requests: + cpu: "175m" + memory: "512Mi" + audit: + resources: + limits: + cpu: "1.2" + memory: "2Gi" + requests: + cpu: "200m" + memory: "768Mi" + +istio: + ingressGateways: + public-ingressgateway: + kubernetesResourceSpec: + resources: + requests: + cpu: "100m" + memory: "512Mi" + limits: + cpu: "500m" + memory: "512Mi" + values: + istiod: + resources: + requests: + cpu: "100m" + memory: "1Gi" + limits: + cpu: "500m" + memory: "1Gi" + hpaSpec: + maxReplicas: 1 + +istiooperator: + values: + operator: + resources: + limits: + cpu: "500m" + memory: "256Mi" + requests: + cpu: "100m" + memory: "256Mi" + +jaeger: + values: + jaeger: + spec: + allInOne: + resources: + requests: + cpu: "100m" + memory: "128Mi" + limits: + cpu: "500m" + memory: "128Mi" + collector: + resources: + requests: + cpu: "100m" + memory: "128Mi" + limits: + cpu: "500m" + memory: "128Mi" + resources: + limits: + cpu: "500m" + memory: "128Mi" + requests: + cpu: "100m" + memory: "128Mi" + +kiali: + values: + resources: + requests: + cpu: "100m" + memory: "256Mi" + limits: + cpu: "500m" + memory: "256Mi" + cr: + spec: + deployment: + resources: + requests: + cpu: "100m" + memory: "368Mi" + limits: + cpu: "500m" + memory: "368Mi" + +monitoring: + values: + cleanUpgrade: + resources: + requests: + memory: "128Mi" + cpu: "100m" + limits: + memory: "256Mi" + cpu: "200m" + alertmanager: + alertmanagerSpec: + resources: + limits: + cpu: "500m" + memory: "256Mi" + requests: + cpu: "100m" + memory: "256Mi" + grafana: + resources: + limits: + cpu: "500m" + memory: "512Mi" + requests: + cpu: "100m" + memory: "256Mi" + sidecar: + resources: + limits: + cpu: "500m" + memory: "100Mi" + requests: + cpu: "50m" + memory: "50Mi" + downloadDashboards: + resources: + limits: + cpu: "20m" + memory: "20Mi" + requests: + cpu: "20m" + memory: "20Mi" + kube-state-metrics: + resources: + limits: + cpu: "500m" + memory: "128Mi" + requests: + cpu: "10m" + memory: "128Mi" + prometheus-node-exporter: + resources: + limits: + cpu: "500m" + memory: "128Mi" + requests: + cpu: "100m" + memory: "128Mi" + prometheusOperator: + admissionWebhooks: + patch: + resources: + limits: + cpu: "100m" + memory: "128Mi" + requests: + cpu: "50m" + memory: "128Mi" + cleanupProxy: + resources: + limits: + cpu: "100m" + memory: "128Mi" + requests: + cpu: "50m" + memory: "128Mi" + resources: + limits: + cpu: "500m" + memory: "512Mi" + requests: + cpu: "100m" + memory: "512Mi" + prometheusConfigReloader: + resources: + requests: + cpu: "50m" + memory: "128Mi" + limits: + cpu: "100m" + memory: "128Mi" + prometheus: + prometheusSpec: + resources: + limits: + cpu: "300m" + memory: "2Gi" + requests: + cpu: "100m" + memory: "2Gi" + +twistlock: + values: + resources: + limits: + memory: "1Gi" + cpu: "500m" + requests: + memory: "1Gi" + cpu: "100m" diff --git a/examples/big-bang/kustomization/kustomization.yaml b/packages/big-bang-core/kustomization/core-standard/kustomization.yaml similarity index 100% rename from examples/big-bang/kustomization/kustomization.yaml rename to packages/big-bang-core/kustomization/core-standard/kustomization.yaml diff --git a/examples/big-bang/kustomization/values.yaml b/packages/big-bang-core/kustomization/core-standard/values.yaml similarity index 99% rename from examples/big-bang/kustomization/values.yaml rename to packages/big-bang-core/kustomization/core-standard/values.yaml index 8364f3349c..0218b76c78 100644 --- a/examples/big-bang/kustomization/values.yaml +++ b/packages/big-bang-core/kustomization/core-standard/values.yaml @@ -15,6 +15,9 @@ networkPolicies: nodeCidr: "0.0.0.0/0" vpcCidr: "0.0.0.0/0" +kiali: + enabled: false + istio: enabled: true ingressGateways: diff --git a/examples/big-bang/zarf.yaml b/packages/big-bang-core/zarf.yaml similarity index 85% rename from examples/big-bang/zarf.yaml rename to packages/big-bang-core/zarf.yaml index 32cc89555e..3edf04f89e 100644 --- a/examples/big-bang/zarf.yaml +++ b/packages/big-bang-core/zarf.yaml @@ -1,7 +1,7 @@ kind: ZarfPackageConfig metadata: name: big-bang-core-demo - description: "Demo Zarf basic deployment of Big Bang Core" + description: "Deploy Big Bang Core" # Big Bang / Iron Bank are only amd64 architecture: amd64 @@ -11,12 +11,9 @@ components: import: path: ../../packages/flux-iron-bank - - name: big-bang + - name: big-bang-core-assets + description: "Git repositories and OCI images used by Big Bang Core" required: true - manifests: - - name: big-bang-config - kustomizations: - - "kustomization" repos: - https://repo1.dso.mil/platform-one/big-bang/bigbang.git@1.33.0 - https://repo1.dso.mil/platform-one/big-bang/apps/core/istio-controlplane.git@1.13.2-bb.1 @@ -78,3 +75,19 @@ components: # twistlock - registry1.dso.mil/ironbank/twistlock/console/console:22.01.840 - registry1.dso.mil/ironbank/twistlock/defender/defender:22.01.840 + + - name: big-bang-core-limited-resources + description: "Deploy a lightweight version of Big Bang Core using limited resources" + group: big-bang-variant + manifests: + - name: big-bang-config + kustomizations: + - "kustomization/core-light" + + - name: big-bang-core-standard + description: "Deploy Big Bang Core with a standard configuration" + group: big-bang-variant + manifests: + - name: big-bang-config + kustomizations: + - "kustomization/core-standard" diff --git a/src/internal/packager/common.go b/src/internal/packager/common.go index a1f2fdfa73..f09953c82a 100644 --- a/src/internal/packager/common.go +++ b/src/internal/packager/common.go @@ -1,7 +1,6 @@ package packager import ( - "context" "crypto/sha256" "encoding/hex" "fmt" @@ -12,11 +11,9 @@ import ( "os" "path/filepath" "strings" - "time" "github.com/defenseunicorns/zarf/src/types" - - "github.com/goccy/go-yaml" + "github.com/pterm/pterm" "github.com/AlecAivazis/survey/v2" "github.com/defenseunicorns/zarf/src/config" @@ -82,10 +79,11 @@ func confirmAction(configPath, userMessage string, sbomViewFiles []string) bool if err != nil { message.Fatal(err, "Unable to open the package config file") } - + // Convert []byte to string and print to screen text := string(content) - + + pterm.Println() utils.ColorPrintYAML(text) if len(sbomViewFiles) > 0 { @@ -95,6 +93,8 @@ func confirmAction(configPath, userMessage string, sbomViewFiles []string) bool message.Note(msg) } + pterm.Println() + // Display prompt if not auto-confirmed var confirmFlag bool if config.DeployOptions.Confirm { @@ -110,68 +110,6 @@ func confirmAction(configPath, userMessage string, sbomViewFiles []string) bool return confirmFlag } -func getValidComponents(allComponents []types.ZarfComponent, requestedComponentNames []string) []types.ZarfComponent { - var validComponentsList []types.ZarfComponent - confirmedComponents := make([]bool, len(requestedComponentNames)) - for _, component := range allComponents { - confirmComponent := component.Required - - // If the component is not required check if the user wants it deployed - if !confirmComponent { - // Check if this is one of the components that has been requested - if len(requestedComponentNames) > 0 || config.DeployOptions.Confirm { - for index, requestedComponent := range requestedComponentNames { - if strings.ToLower(requestedComponent) == component.Name { - confirmComponent = true - confirmedComponents[index] = true - } - } - } else { - confirmComponent = ConfirmOptionalComponent(component) - } - } - - if confirmComponent { - validComponentsList = append(validComponentsList, component) - // Make it easier to know we are running k3s - if config.IsZarfInitConfig() && component.Name == "k3s" { - config.DeployOptions.ApplianceMode = true - } - } - } - - // Verify that we were able to successfully identify all the requested components - var nonMatchedComponents []string - for requestedComponentIndex, componentMatched := range confirmedComponents { - if !componentMatched { - nonMatchedComponents = append(nonMatchedComponents, requestedComponentNames[requestedComponentIndex]) - } - } - - if len(nonMatchedComponents) > 0 { - message.Fatalf(nil, "Unable to find these components to deploy: %v.", nonMatchedComponents) - } - - return validComponentsList -} - -// Confirm optional component -func ConfirmOptionalComponent(component types.ZarfComponent) (confirmComponent bool) { - displayComponent := component - displayComponent.Description = "" - content, _ := yaml.Marshal(displayComponent) - utils.ColorPrintYAML(string(content)) - message.Question(fmt.Sprintf("%s: %s", component.Name, component.Description)) - - // Since no requested components were provided, prompt the user - prompt := &survey.Confirm{ - Message: "Deploy this component?", - Default: component.Default, - } - _ = survey.AskOne(prompt, &confirmComponent) - return confirmComponent -} - // HandleIfURL If provided package is a URL download it to a temp directory func HandleIfURL(packagePath string, shasum string, insecureDeploy bool) (string, func()) { // Check if the user gave us a remote package @@ -235,65 +173,6 @@ func isValidFileExtension(filename string) bool { return false } -func loopScriptUntilSuccess(script string, scripts types.ZarfComponentScripts) { - spinner := message.NewProgressSpinner("Waiting for command \"%s\"", script) - defer spinner.Stop() - - // Try to patch the zarf binary path in case the name isn't exactly "./zarf" - binaryPath, err := os.Executable() - if err != nil { - spinner.Errorf(err, "Unable to determine the current zarf binary path") - } else { - script = strings.ReplaceAll(script, "./zarf ", binaryPath+" ") - } - - var ctx context.Context - var cancel context.CancelFunc - - // Default timeout is 5 minutes - if scripts.TimeoutSeconds < 1 { - scripts.TimeoutSeconds = 300 - } - - duration := time.Duration(scripts.TimeoutSeconds) * time.Second - timeout := time.After(duration) - - spinner.Updatef("Waiting for command \"%s\" (timeout: %d seconds)", script, scripts.TimeoutSeconds) - - for { - select { - // On timeout abort - case <-timeout: - cancel() - spinner.Fatalf(nil, "Script \"%s\" timed out", script) - // Oherwise try running the script - default: - ctx, cancel = context.WithTimeout(context.Background(), duration) - output, errOut, err := utils.ExecCommandWithContext(ctx, scripts.ShowOutput, "sh", "-c", script) - defer cancel() - - if err != nil { - message.Debug(err, output, errOut) - // If retry, let the script run again - if scripts.Retry { - continue - } - // Otherwise fatal - spinner.Fatalf(err, "Script \"%s\" failed (%s)", script, err.Error()) - } - - // Dump the script output in debug if output not already streamed - if !scripts.ShowOutput { - message.Debug(output, errOut) - } - - // Close the function now that we are done - spinner.Success() - return - } - } -} - // removeDuplicates reduces a string slice to unique values only, https://www.dotnetperls.com/duplicates-go func removeDuplicates(elements []string) []string { seen := map[string]bool{} diff --git a/src/internal/packager/components.go b/src/internal/packager/components.go new file mode 100644 index 0000000000..3c9f113a29 --- /dev/null +++ b/src/internal/packager/components.go @@ -0,0 +1,208 @@ +package packager + +import ( + "fmt" + "strings" + + "github.com/AlecAivazis/survey/v2" + "github.com/defenseunicorns/zarf/src/config" + "github.com/defenseunicorns/zarf/src/internal/k8s" + "github.com/defenseunicorns/zarf/src/internal/message" + "github.com/defenseunicorns/zarf/src/internal/utils" + "github.com/defenseunicorns/zarf/src/types" + "github.com/pterm/pterm" + "gopkg.in/yaml.v2" +) + +const horizontalRule = "───────────────────────────────────────────────────────────────────────────────────────" + +func getValidComponents(allComponents []types.ZarfComponent, requestedComponentNames []string) []types.ZarfComponent { + var validComponentsList []types.ZarfComponent + var orderedKeys []string + var choiceComponents []string + + componentGroups := make(map[string][]types.ZarfComponent) + + // Break up components into choice groups + for _, component := range allComponents { + key := component.Group + // If not a choice group, then use the component name as the key + if key == "" { + key = component.Name + } else { + // Otherwise, add the component name to the choice group list for later validation + choiceComponents = appendIfNotExists(choiceComponents, component.Name) + } + + // Preserve component order + orderedKeys = appendIfNotExists(orderedKeys, key) + + // Append the component to the list of components in the group + componentGroups[key] = append(componentGroups[key], component) + } + + // Loop through each component group in original order and handle required, requested or user confirmation + for _, key := range orderedKeys { + + componentGroup := componentGroups[key] + + // Choice groups are handled differently for user confirmation + userChoicePrompt := len(componentGroup) > 1 + + // Loop through the components in the group + for _, component := range componentGroup { + // First check if the component is required or requested via CLI flag + requested := isRequiredOrRequested(component, requestedComponentNames) + + // If the user has not requested this component via CLI flag, then prompt them if not a choice group + if !requested && !userChoicePrompt { + requested = confirmOptionalComponent(component) + } + + if requested { + // Mark deployment as appliance mode if this is an init config and the k3s component is enabled + if component.Name == k8s.DistroIsK3s && config.IsZarfInitConfig() { + config.DeployOptions.ApplianceMode = true + } + // Add the component to the list of valid components + validComponentsList = append(validComponentsList, component) + // Ensure that the component is not requested again if in a choice group + userChoicePrompt = false + // Exit the inner loop on a match since groups should only have one requested component + break + } + } + + // If the user has requested a choice group, then prompt them + if userChoicePrompt { + selectedComponent := confirmChoiceGroup(componentGroup) + validComponentsList = append(validComponentsList, selectedComponent) + } + } + + // Ensure all user requested components are valid + if err := validateRequests(validComponentsList, requestedComponentNames, choiceComponents); err != nil { + message.Fatalf(err, "Invalid component argument, %s", err) + } + + return validComponentsList +} + +// Match on the first requested component that is not in the list of valid components and return the component name +func validateRequests(validComponentsList []types.ZarfComponent, requestedComponentNames, choiceComponents []string) error { + // Loop through each requested component names + for _, componentName := range requestedComponentNames { + found := false + // Match on the first requested component that is a valid component + for _, component := range validComponentsList { + if component.Name == componentName { + found = true + break + } + } + + // If the requested component was not found, then return an error + if !found { + // If the requested component is in a choice group, then warn the user they must choose only one + for _, component := range choiceComponents { + if component == componentName { + return fmt.Errorf("component %s is part of a group of components and only one may be chosen", componentName) + } + } + // Otherwise, return an error a gneral error + return fmt.Errorf("unable to find component %s", componentName) + } + } + + return nil +} + +func isRequiredOrRequested(component types.ZarfComponent, requestedComponentNames []string) bool { + // If the component is required, then just return true + if component.Required { + return true + } else { + // Otherwise,check if this is one of the components that has been requested + if len(requestedComponentNames) > 0 || config.DeployOptions.Confirm { + for _, requestedComponent := range requestedComponentNames { + // If the component name matches one of the requested components, then return true + if strings.ToLower(requestedComponent) == component.Name { + return true + } + } + } + } + + // All other cases, return false + return false +} + +// Confirm optional component +func confirmOptionalComponent(component types.ZarfComponent) (confirmComponent bool) { + // Confirm flag passed, just use defaults + if config.DeployOptions.Confirm { + return component.Default + } + + pterm.Println(horizontalRule) + + displayComponent := component + displayComponent.Description = "" + content, _ := yaml.Marshal(displayComponent) + utils.ColorPrintYAML(string(content)) + if component.Description != "" { + message.Question(component.Description) + } + + // Since no requested components were provided, prompt the user + prompt := &survey.Confirm{ + Message: fmt.Sprintf("Deploy the %s component?", component.Name), + Default: component.Default, + } + _ = survey.AskOne(prompt, &confirmComponent) + return confirmComponent +} + +func confirmChoiceGroup(componentGroup []types.ZarfComponent) types.ZarfComponent { + // Confirm flag passed, just use defaults + if config.DeployOptions.Confirm { + var componentNames []string + for _, component := range componentGroup { + // If the component is default, then return it + if component.Default { + return component + } + // Add each component name to the list + componentNames = append(componentNames, component.Name) + } + // If no default component was found, give up + message.Fatalf(nil, "You must specify at least one component from the group %v when using the --confirm flag.", componentNames) + } + + pterm.Println(horizontalRule) + + var chosen int + var options []string + + for _, component := range componentGroup { + text := fmt.Sprintf("Name: %s\n Description: %s\n", component.Name, component.Description) + options = append(options, text) + } + + prompt := &survey.Select{ + Message: "Select a component to deploy:", + Options: options, + } + _ = survey.AskOne(prompt, &chosen) + + return componentGroup[chosen] +} + +func appendIfNotExists(slice []string, item string) []string { + for _, s := range slice { + if s == item { + return slice + } + } + return append(slice, item) +} diff --git a/src/internal/packager/deploy.go b/src/internal/packager/deploy.go index db731eb026..8cdbaaa3c6 100644 --- a/src/internal/packager/deploy.go +++ b/src/internal/packager/deploy.go @@ -123,13 +123,24 @@ func Deploy() { pterm.Println() if config.IsZarfInitConfig() { - _ = pterm.DefaultTable.WithHasHeader().WithData(pterm.TableData{ + loginTable := pterm.TableData{ {" Application", "Username", "Password", "Connect"}, - {" Logging", "zarf-admin", config.GetSecret(config.StateLogging), "zarf connect logging"}, - {" Git", config.ZarfGitPushUser, config.GetSecret(config.StateGitPush), "zarf connect git"}, - {" Git (read-only)", config.ZarfGitReadUser, config.GetSecret(config.StateGitPull), "zarf connect git"}, {" Registry", "zarf-push-user", config.GetSecret(config.StateRegistryPush), "zarf connect registry"}, - }).Render() + } + for _, component := range componentsToDeploy { + // Show message if including logging stack + if component.Name == "logging" { + loginTable = append(loginTable, pterm.TableData{{" Logging", "zarf-admin", config.GetSecret(config.StateLogging), "zarf connect logging"}}...) + } + // Show message if including git-server + if component.Name == "git-server" { + loginTable = append(loginTable, pterm.TableData{ + {" Git", config.ZarfGitPushUser, config.GetSecret(config.StateGitPush), "zarf connect git"}, + {" Git (read-only)", config.ZarfGitReadUser, config.GetSecret(config.StateGitPull), "zarf connect git"}, + }...) + } + } + _ = pterm.DefaultTable.WithHasHeader().WithData(loginTable).Render() } // All done diff --git a/src/internal/packager/scripts.go b/src/internal/packager/scripts.go new file mode 100644 index 0000000000..a094844639 --- /dev/null +++ b/src/internal/packager/scripts.go @@ -0,0 +1,72 @@ +package packager + +import ( + "context" + "os" + "strings" + "time" + + "github.com/defenseunicorns/zarf/src/internal/message" + "github.com/defenseunicorns/zarf/src/internal/utils" + "github.com/defenseunicorns/zarf/src/types" +) + + +func loopScriptUntilSuccess(script string, scripts types.ZarfComponentScripts) { + spinner := message.NewProgressSpinner("Waiting for command \"%s\"", script) + defer spinner.Stop() + + // Try to patch the zarf binary path in case the name isn't exactly "./zarf" + binaryPath, err := os.Executable() + if err != nil { + spinner.Errorf(err, "Unable to determine the current zarf binary path") + } else { + script = strings.ReplaceAll(script, "./zarf ", binaryPath+" ") + } + + var ctx context.Context + var cancel context.CancelFunc + + // Default timeout is 5 minutes + if scripts.TimeoutSeconds < 1 { + scripts.TimeoutSeconds = 300 + } + + duration := time.Duration(scripts.TimeoutSeconds) * time.Second + timeout := time.After(duration) + + spinner.Updatef("Waiting for command \"%s\" (timeout: %d seconds)", script, scripts.TimeoutSeconds) + + for { + select { + // On timeout abort + case <-timeout: + cancel() + spinner.Fatalf(nil, "Script \"%s\" timed out", script) + // Oherwise try running the script + default: + ctx, cancel = context.WithTimeout(context.Background(), duration) + output, errOut, err := utils.ExecCommandWithContext(ctx, scripts.ShowOutput, "sh", "-c", script) + defer cancel() + + if err != nil { + message.Debug(err, output, errOut) + // If retry, let the script run again + if scripts.Retry { + continue + } + // Otherwise fatal + spinner.Fatalf(err, "Script \"%s\" failed (%s)", script, err.Error()) + } + + // Dump the script output in debug if output not already streamed + if !scripts.ShowOutput { + message.Debug(output, errOut) + } + + // Close the function now that we are done + spinner.Success() + return + } + } +} \ No newline at end of file diff --git a/src/internal/packager/validate/validate.go b/src/internal/packager/validate/validate.go index e012b08496..c9e13ea6c4 100644 --- a/src/internal/packager/validate/validate.go +++ b/src/internal/packager/validate/validate.go @@ -19,19 +19,40 @@ func Run() { message.Fatalf(err, "Invalid package name") } + uniqueNames := make(map[string]bool) + for _, component := range components { - for _, chart := range component.Charts { - if err := validateChart(chart); err != nil { - message.Fatalf(err, "Invalid chart definition in the %s component: %s", component.Name, err) - } + // ensure component name is unique + if _, ok := uniqueNames[component.Name]; ok { + message.Fatalf(nil, "Component names must be unique") } - for _, manifest := range component.Manifests { - if err := validateManifest(manifest); err != nil { - message.Fatalf(err, "Invalid manifest definition in the %s component: %s", component.Name, err) - } + uniqueNames[component.Name] = true + + validateComponent(component) + } + +} + +func validateComponent(component types.ZarfComponent) { + if component.Required { + if component.Default { + message.Fatalf(nil, "Component %s cannot be required and default", component.Name) + } + if component.Group != "" { + message.Fatalf(nil, "Component %s cannot be required and part of a choice group", component.Name) } } + for _, chart := range component.Charts { + if err := validateChart(chart); err != nil { + message.Fatalf(err, "Invalid chart definition in the %s component: %s", component.Name) + } + } + for _, manifest := range component.Manifests { + if err := validateManifest(manifest); err != nil { + message.Fatalf(err, "Invalid manifest definition in the %s component: %s", component.Name) + } + } } func validatePackageName(subject string) error { diff --git a/src/internal/utils/yaml.go b/src/internal/utils/yaml.go index 97692450ff..55dd8f0b13 100644 --- a/src/internal/utils/yaml.go +++ b/src/internal/utils/yaml.go @@ -62,7 +62,7 @@ func ColorPrintYAML(text string) { } } writer := colorable.NewColorableStdout() - _, err := writer.Write([]byte("\n" + p.PrintTokens(tokens) + "\n")) + _, err := writer.Write([]byte(p.PrintTokens(tokens) + "\n")) if err != nil { message.Error(err, "Unable to print the config yaml contents") } diff --git a/src/test/e2e/e2e_component_choice_test.go b/src/test/e2e/e2e_component_choice_test.go new file mode 100644 index 0000000000..e86a30a332 --- /dev/null +++ b/src/test/e2e/e2e_component_choice_test.go @@ -0,0 +1,40 @@ +package test + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestE2eComponentChoice(t *testing.T) { + defer e2e.cleanupAfterTest(t) + + path := fmt.Sprintf("../../../build/zarf-package-component-choice-%s.tar.zst", e2e.arch) + + // Try to deploy both and expect failure due to only one component allowed at a time + // We currently don't have a pattern to actually test the interactive prompt, so just testing automation for now + output, err := e2e.execZarfCommand("package", "deploy", path, "--confirm", "--components=first-choice,second-choice") + require.Error(t, err, output) + + // Deploy a single choice and expect success + output, err = e2e.execZarfCommand("package", "deploy", path, "--confirm", "--components=first-choice") + require.NoError(t, err, output) + + // Verify the file was created + expectedFile := "first-choice-file.txt" + require.FileExists(t, expectedFile) + // Verify the second choice file was not created + expectedFile = "second-choice-file.txt" + require.NoFileExists(t, expectedFile) + e2e.filesToRemove = append(e2e.filesToRemove, expectedFile) + + // Deploy using default choice + output, err = e2e.execZarfCommand("package", "deploy", path, "--confirm") + require.NoError(t, err, output) + + // Verify the file was created + expectedFile = "second-choice-file.txt" + require.FileExists(t, expectedFile) + e2e.filesToRemove = append(e2e.filesToRemove, expectedFile) +} diff --git a/src/types/types.go b/src/types/types.go index 75668e11f3..727e322b1e 100644 --- a/src/types/types.go +++ b/src/types/types.go @@ -68,6 +68,10 @@ type ZarfComponent struct { // Dynamic template values for K8s resources Variables ZarfComponentVariables `yaml:"variables,omitempty"` + + // Key to match other components to produce a user selector field, used to create a BOOLEAN XOR for a set of components + // Note: ignores default and required flags + Group string `yaml:"group,omitempty"` } // ZarfComponentVariables are variables that can be used to dynaically template K8s resources diff --git a/zarf.schema.json b/zarf.schema.json index 0303d9acd1..62c53f02d2 100644 --- a/zarf.schema.json +++ b/zarf.schema.json @@ -138,6 +138,9 @@ } }, "type": "object" + }, + "group": { + "type": "string" } }, "additionalProperties": false,