From 79b432fe0fd47757fcaff524f339f34b46b19411 Mon Sep 17 00:00:00 2001 From: Megamind <882485+jeff-mccoy@users.noreply.github.com> Date: Fri, 16 Dec 2022 12:04:47 -0600 Subject: [PATCH] Add YOLO mode (#1105) Provides the ability to create Zarf packages intended to work in online-only environments, i.e., without the internal Zarf airgap components typically provided by `zarf init`. Also adds early FAQ. ## Related Issue Fixes #1051 Fixes #1046 Related to #1134 Co-authored-by: unclegedd Co-authored-by: Wayne Starr Co-authored-by: MxNxPx --- Makefile | 2 + adr/0009-yolo-mode.md | 19 ++++ .../0-using-zarf-package-create.md | 3 +- .../2-zarf-packages/2-zarf-components.md | 2 +- .../3-the-zarf-init-package.md | 4 +- docs/4-user-guide/3-zarf-schema.md | 28 ++++-- docs/6-developer-guide/2-testing.md | 3 +- docs/6-developer-guide/3-nerd-notes.md | 1 - docs/9-faq.md | 36 +++++++- examples/yolo/README.md | 43 +++++++++ examples/yolo/zarf.yaml | 15 ++++ src/config/lang/english.go | 64 +++++++++---- src/internal/packager/helm/post-render.go | 5 ++ src/internal/packager/validate/validate.go | 89 ++++++++++++------- src/pkg/packager/deploy.go | 23 ++++- src/test/e2e/99_yolo_test.go | 46 ++++++++++ src/types/component.go | 12 +-- src/types/package.go | 1 + src/ui/lib/api-types.ts | 22 +++-- zarf.schema.json | 16 ++-- 20 files changed, 348 insertions(+), 86 deletions(-) create mode 100644 adr/0009-yolo-mode.md create mode 100644 examples/yolo/README.md create mode 100644 examples/yolo/zarf.yaml create mode 100644 src/test/e2e/99_yolo_test.go diff --git a/Makefile b/Makefile index 90d1a63b68..7607455aab 100644 --- a/Makefile +++ b/Makefile @@ -145,6 +145,8 @@ build-examples: ## Build all of the example packages @test -s ./build/zarf-package-test-helm-wait-$(ARCH).tar.zst || $(ZARF_BIN) package create examples/helm-no-wait -o build -a $(ARCH) --confirm + @test -s ./build/zarf-package-yolo-$(ARCH).tar.zst || $(ZARF_BIN) package create examples/yolo -o build -a $(ARCH) --confirm + ## Run e2e tests. Will automatically build any required dependencies that aren't present. ## Requires an existing cluster for the env var APPLIANCE_MODE=true .PHONY: test-e2e diff --git a/adr/0009-yolo-mode.md b/adr/0009-yolo-mode.md new file mode 100644 index 0000000000..c999c63228 --- /dev/null +++ b/adr/0009-yolo-mode.md @@ -0,0 +1,19 @@ +# 9. YOLO Mode + +Date: 2022-12-14 + +## Status + +Accepted + +## Context + +Zarf was rooted in the idea of declarative K8s deployments for disconnected environments. Many of the design decisions made in Zarf are based on this idea. However, in certain connected environments, Zarf can still be leveraged as a way to define declarative deployments and upgrades without the constraints of disconnected environments. To that end, providing a declarative way to deploy Zarf packages without the need for a Zarf init package would be useful in such environments. + +## Decision + +YOLO mode is an optional boolean config set in the `metadata` section of the Zarf package manifest. Setting `metadata.yolo=true` will deploy the Zarf package "as is" without needing the Zarf state to exist or the Zarf Agent mutating webhook. Zarf packages with YOLO mode enabled are not allowed to specify components with container images or Git repos and validation will prevent the package from being created. + +## Consequences + +YOLO mode provides a way for existing, connected clusters to use Zarf for declarative deployments and upgrades because there is no need to perform any Zarf bootstrapping in order to deploy Zarf-packaged workloads. The addition of the `metadata.yolo` config should not affect existing Zarf users as it is entirely optional. Additionally, requiring the `metadata.yolo` config to be set to `true` and not allowing a runtime flag to override it makes it very clear both in `package create` and `package deploy` the intent and usage of the package. diff --git a/docs/13-walkthroughs/0-using-zarf-package-create.md b/docs/13-walkthroughs/0-using-zarf-package-create.md index b9e3b3e179..fa4c8ce34f 100644 --- a/docs/13-walkthroughs/0-using-zarf-package-create.md +++ b/docs/13-walkthroughs/0-using-zarf-package-create.md @@ -9,8 +9,7 @@ When creating a Zarf package, you will need to have Internet connection so that 1. The [Zarf](https://github.com/defenseunicorns/zarf) repository cloned: ([git clone instructions](https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository)) 2. Zarf binary installed on your $PATH: ([install instructions](../3-getting-started.md#installing-zarf)) -3. A copy of the injector's `zarf-registry` binary in the `build/` directory: ([build instructions](../3-getting-started.md#building-the-cli-from-scratch)) -4. The Zarf agent image name and tag you would like to use (the default can be found in the project's [`Makefile`](https://github.com/defenseunicorns/zarf/blob/master/Makefile) under `AGENT_IMAGE ?= `) + ## Building the init-package diff --git a/docs/4-user-guide/2-zarf-packages/2-zarf-components.md b/docs/4-user-guide/2-zarf-packages/2-zarf-components.md index fe837a7a06..c8960a6e51 100644 --- a/docs/4-user-guide/2-zarf-packages/2-zarf-components.md +++ b/docs/4-user-guide/2-zarf-packages/2-zarf-components.md @@ -46,7 +46,7 @@ components: name: flux-v1.0.0 ``` -> Note: When importing a component, Zarf will copy all of the values from the original component expect for the `required` key. In addition, while Zarf will copy the values, you have the ability to override the value for the `description` key. +> Note: When importing a component, Zarf will copy all of the values from the original component except for the `required` key. In addition, while Zarf will copy the values, you have the ability to override the value for the `description` key. Checkout the [composable-packages](https://github.com/defenseunicorns/zarf/blob/master/examples/composable-packages/zarf.yaml) example to see this in action. diff --git a/docs/4-user-guide/2-zarf-packages/3-the-zarf-init-package.md b/docs/4-user-guide/2-zarf-packages/3-the-zarf-init-package.md index 94b1c9432c..ad2812d890 100644 --- a/docs/4-user-guide/2-zarf-packages/3-the-zarf-init-package.md +++ b/docs/4-user-guide/2-zarf-packages/3-the-zarf-init-package.md @@ -25,7 +25,7 @@ In addition to those that are always installed, Zarf's optional components provi These optional components for the init package are listed below along with the "magic strings" you pass to `zarf init --components` to pull them in: -| --components | Description | +| components | Description | | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | | k3s | REQUIRES ROOT. Installs a lightweight Kubernetes Cluster on the local host—[k3s](https://k3s.io/)—and configures it to start up on boot. | | logging | Adds a log monitoring stack—[promtail / loki / graphana (a.k.a. PLG)](https://github.com/grafana/loki)—into the cluster. | @@ -39,6 +39,6 @@ There are two ways to deploy optional components, you can either pass a comma se # What Makes the Init Package Special -Deploying onto air-gapped environments is a [hard problem](../../1-understand-the-basics.md#what-is-the-air-gap), especially when the k8s environment you're deploying to doesn't have a container registry running for you to put your images into. This leads to a classic 'chicken or the egg' problem since the container registry image needs to make its way into the cluster but there is on container registry running on the cluster to push to yet because the image isn't in the cluster yet. In order to remain distro agnostic, we had to come up with a unique solution to seed the container registry into the cluster. +Deploying onto air-gapped environments is a [hard problem](../../1-understand-the-basics.md#what-is-the-air-gap), especially when the k8s environment you're deploying to doesn't have a container registry running for you to put your images into. This leads to a classic 'chicken or the egg' problem since the container registry image needs to make its way into the cluster but there is no container registry running on the cluster to push to yet because the image isn't in the cluster yet. In order to remain distro agnostic, we had to come up with a unique solution to seed the container registry into the cluster. The `zarf-injector` [component](https://github.com/defenseunicorns/zarf/blob/master/packages/zarf-injector/zarf.yaml) within the init-package solves this problem by injecting a single rust binary (statically compiled) and a series of configmap chunks of a `registry:2` image into an ephemeral pod based on an existing image in the cluster. diff --git a/docs/4-user-guide/3-zarf-schema.md b/docs/4-user-guide/3-zarf-schema.md index 662a26233f..24a0ce864e 100644 --- a/docs/4-user-guide/3-zarf-schema.md +++ b/docs/4-user-guide/3-zarf-schema.md @@ -164,6 +164,22 @@ Must be one of: +
+ yolo + + +  +
+ +**Description:** Yaml OnLy Online (YOLO): True enables deploying a Zarf package without first running zarf init against the cluster. This is ideal for connected environments where you want to use existing VCS and container registries. + +| | | +| -------- | --------- | +| **Type** | `boolean` | + +
+
+ @@ -923,7 +939,7 @@ Must be one of: ![Required](https://img.shields.io/badge/Required-red) -**Description:** The name of the chart to deploy +**Description:** The name of the chart to deploy; this should be the name of the chart as it is installed in the helm repo | | | | -------- | -------- | @@ -939,7 +955,7 @@ Must be one of:  
-**Description:** The name of the release to create +**Description:** The name of the release to create; defaults to the name of the chart | | | | -------- | -------- | @@ -973,7 +989,7 @@ Must be one of: ![Required](https://img.shields.io/badge/Required-red) -**Description:** The version of the chart to deploy +**Description:** The version of the chart to deploy; for git-based charts this is also the tag of the git repo | | | | -------- | -------- | @@ -1007,7 +1023,7 @@ Must be one of:  
-**Description:** List of values files to include in the package +**Description:** List of values files to include in the package; these will be merged together | | | | -------- | ----------------- | @@ -1037,7 +1053,7 @@ Must be one of:  
-**Description:** If using a git repo +**Description:** The path to the chart in the repo if using a git repo instead of a helm repo | | | | -------- | -------- | @@ -1117,7 +1133,7 @@ Must be one of: ![Required](https://img.shields.io/badge/Required-red) -**Description:** A name to give this collection of manifests +**Description:** A name to give this collection of manifests; this will become the name of the dynamically-created helm chart | | | | -------- | -------- | diff --git a/docs/6-developer-guide/2-testing.md b/docs/6-developer-guide/2-testing.md index cac3d42fd1..bf4eb1140f 100644 --- a/docs/6-developer-guide/2-testing.md +++ b/docs/6-developer-guide/2-testing.md @@ -72,7 +72,8 @@ Due to resource constraints in public github runners, K8s tests are only perform - 20 is reserved for `zarf init` - 21 is reserved for logging tests so they can be removed first (they take the most resources in the cluster) - 22 is reserved for tests required the git-server, which is removed at the end of the test -- 23-99 are for the remaining tests that only require a basic zarf cluster without logging for the git-server +- 23-98 are for the remaining tests that only require a basic zarf cluster without logging for the git-server +- 99 is reserved for the `zarf destroy` and [YOLO Mode](../../examples/yolo/README.md) test ## Running Unit Tests Locally diff --git a/docs/6-developer-guide/3-nerd-notes.md b/docs/6-developer-guide/3-nerd-notes.md index 9b6031f0a3..0c84b31f04 100644 --- a/docs/6-developer-guide/3-nerd-notes.md +++ b/docs/6-developer-guide/3-nerd-notes.md @@ -20,7 +20,6 @@ Zarf is written entirely in [go](https://go.dev/), except for a single 868Kb bin - The container then starts and runs the rust binary to host the registry image in an static docker registry - After this, the main docker registry chart is deployed, pulls the image from the ephemeral pod, and finally destroys the created configmaps, pod, and service - ## Zarf Architecture ![Architecture Diagram](../.images/architecture.drawio.svg) diff --git a/docs/9-faq.md b/docs/9-faq.md index 662839e6d7..a2deced60d 100644 --- a/docs/9-faq.md +++ b/docs/9-faq.md @@ -1,5 +1,35 @@ # FAQ -:::caution Hard Hat Area -This page is still being developed. More content will be added soon! -::: \ No newline at end of file +## Do I have to use [Homebrew](https://brew.sh/) to install Zarf? + +No, the Zarf binary and init package can be downloaded from the [Releases Page](https://github.com/defenseunicorns/zarf/releases). Zarf does not need to be installed or available to all users on the system, but it does need to be executable for the current user (i.e. `chmod +x zarf` for Linux/Mac). + +## What dependencies does Zarf have? + +Zarf is statically compiled and written in [Go](https://golang.org/) and [Rust](https://www.rust-lang.org/), so it has no external dependencies. For Linux, Zarf can bring a Kubernetes cluster using [K3s](https://k3s.io/). For Mac and Windows, Zarf can leverage any avaiilable local or remote cluster the user has access to. Currently, the K3s installation Zarf performs does require a [Systemd](https://en.wikipedia.org/wiki/Systemd) based system and root access. + +## What is the Zarf Agent? + +The Zarf Agent is a [Kubernetes Mutating Webhook](https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/#mutatingadmissionwebhook) that is installed into the cluster during the `zarf init` operation. The Agent is responsible for modifying [Kubernetes PodSpec](https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#PodSpec) objects [Image](https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#Container.Image) fields to point to the Zarf Registry. This allows the cluster to pull images from the Zarf Registry instead of the internet without having to modify the original image references. The Agent also modifies [Flux GitRepository](https://fluxcd.io/docs/components/source/gitrepositories/) objects to point to the local Git Server. + +## Why doesn't the Zarf Agent create secrets it needs in the cluster? + +During early discussions and [subsequent decision](../adr/0005-mutating-webhook.md) to use a Mutating Webhook, we decided to not have the Agent create any secrets in the cluster. This is to avoid the Agent having to have more priveleges than it needs as well as avoid collisions with Helm. The Agent today simply repsonds to requests to patch PodSpec and GitRepository objects. + +The Agent does not need to create any secrets in the cluster. Instead, during `zarf init` and `zarf package deploy`, secrets are automatically created as [Helm Postrender Hook](https://helm.sh/docs/topics/advanced/#post-rendering) for any namespaces Zarf sees. If you have resources managed by [Flux](https://fluxcd.io/) that are not in a namespace managed by Zarf, you can either create the secrets manually or include a manifest to create the namespace in your package and let Zarf create the secrets for you. + +## How can a Kubernetes resource be excluded from the Zarf Agent? + +Resources can be exluded at the namespace or resources level by adding the `zarf.dev/agent: ignore` label. + +## What happens to resources that exist in the cluster before `zarf init`? + +During the `zarf init` operation, the Zarf Agent will patch any existing namespaces with the `zarf.dev/agent: ignore` label to prevent the Agent from modifying any resources in that namespace. This is done because there is no way to guarantee the images used by pods in existing namespaces are available in the Zarf Registry. + +## What is YOLO Mode and why would I use it? + +YOLO Mode is a special package metatdata designation that be added to a package prior to `zarf package create` to allow the package to be installed without the need for a `zarf init` operation. In most cases this will not be used, but it can be useful for testing or for environments that manage their own registries and Git servers completely outside of Zarf. This can also be used as a way to transition slowly to using Zarf without having to do a full migration. + +:::note +Typically you should not deploy a Zarf package in YOLO mode if the cluster has already been initialized with Zarf. This could lead to an [ImagePullBackOff](https://kubernetes.io/docs/concepts/containers/images/#imagepullbackoff) if the resources in the package do not include the `zarf.dev/agent: ignore` label and are not already available in the Zarf Registry. +::: diff --git a/examples/yolo/README.md b/examples/yolo/README.md new file mode 100644 index 0000000000..6bb0de0687 --- /dev/null +++ b/examples/yolo/README.md @@ -0,0 +1,43 @@ +# YOLO Mode +This example demonstrates YOLO mode, an optional mode for using Zarf in a fully connected environment where users can bring their own external container registry and Git server. + +:::info + +To view the example source code, select the `Edit this page` link below the article and select the parent folder. + +::: + + +## Prerequisites +- A running K8s cluster. _Note that the cluster does not need to have the Zarf init package installed or any other Zarf-related bootstrapping._ + +## Instructions +Create the package: +```sh +zarf package create +``` + +### Deploy the package + +```sh +# Run the following command to deploy the created package to the cluster +zarf package deploy + +# Choose the yolo package from the list +? Choose or type the package file [tab for suggestions] +> zarf-package-yolo-.tar.zst + +# Confirm the deployment +? Deploy this Zarf package? (y/N) + +# Wait a few seconds for the cluster to deploy the package; you should +# see the following output when the package has been finished deploying: + Connect Command | Description + zarf connect doom | Play doom!!! + zarf connect games | Play some old dos games 🦄 + +# Run the specified `zarf connect ` command to connect to the deployed +# workload (ie. kill some demons). Note that the typical Zarf registry, +# Gitea server and Zarf agent pods are not present in the cluster. This means +# that the game's container image was pulled directly from the public registry and the URL was not mutated by Zarf. +``` diff --git a/examples/yolo/zarf.yaml b/examples/yolo/zarf.yaml new file mode 100644 index 0000000000..5624a81381 --- /dev/null +++ b/examples/yolo/zarf.yaml @@ -0,0 +1,15 @@ +kind: ZarfPackageConfig +metadata: + name: yolo + yolo: true + description: "Game example in YOLO (online-only) mode that can be deployed without a Zarf cluster" + +components: + - name: yolo-games + required: true + manifests: + - name: multi-games + namespace: zarf-yolo-example + files: + - ../game/manifests/deployment.yaml + - ../game/manifests/service.yaml diff --git a/src/config/lang/english.go b/src/config/lang/english.go index f9f5376e9a..2ccc4d7631 100644 --- a/src/config/lang/english.go +++ b/src/config/lang/english.go @@ -15,12 +15,12 @@ import "errors" // Debug messages will not be a part of the language strings since they are not intended to be user facing // Include sprintf formatting directives in the string if needed const ( + ErrLoadingConfig = "failed to load config: %w" + ErrLoadState = "Failed to load the Zarf State from the Kubernetes cluster." + ErrMarshal = "failed to marshal file: %w" ErrNoClusterConnection = "Failed to connect to the Kubernetes cluster." ErrTunnelFailed = "Failed to create a tunnel to the Kubernetes cluster." - ErrLoadState = "Failed to load the Zarf State from the Kubernetes cluster." ErrUnmarshal = "failed to unmarshal file: %w" - ErrMarshal = "failed to marshal file: %w" - ErrLoadingConfig = "failed to load config: %w" ErrWritingFile = "failed to write the file %s: %s" ) @@ -181,20 +181,52 @@ const ( AgentInfoShutdown = "Shutdown gracefully..." AgentInfoPort = "Server running in port: %s" - AgentErrStart = "Failed to start the web server" - AgentErrShutdown = "unable to properly shutdown the web server" - AgentErrNilReq = "malformed admission review: request is nil" - AgentErrMarshalResponse = "unable to marshal the response" - AgentErrMarshallJSONPatch = "unable to marshall the json patch" - AgentErrInvalidType = "only content type 'application/json' is supported" - AgentErrInvalidOp = "invalid operation: %s" - AgentErrInvalidMethod = "invalid method only POST requests are allowed" - AgentErrImageSwap = "Unable to swap the host for (%s)" - AgentErrHostnameMatch = "failed to complete hostname matching: %w" - AgentErrGetState = "failed to load zarf state from file: %w" - AgentErrCouldNotDeserializeReq = "could not deserialize request: %s" - AgentErrBindHandler = "Unable to bind the webhook handler" AgentErrBadRequest = "could not read request body: %s" + AgentErrBindHandler = "Unable to bind the webhook handler" + AgentErrCouldNotDeserializeReq = "could not deserialize request: %s" + AgentErrGetState = "failed to load zarf state from file: %w" + AgentErrHostnameMatch = "failed to complete hostname matching: %w" + AgentErrImageSwap = "Unable to swap the host for (%s)" + AgentErrInvalidMethod = "invalid method only POST requests are allowed" + AgentErrInvalidOp = "invalid operation: %s" + AgentErrInvalidType = "only content type 'application/json' is supported" + AgentErrMarshallJSONPatch = "unable to marshall the json patch" + AgentErrMarshalResponse = "unable to marshal the response" + AgentErrNilReq = "malformed admission review: request is nil" + AgentErrShutdown = "unable to properly shutdown the web server" + AgentErrStart = "Failed to start the web server" +) + +// src/internal/packager/validate +const ( + PkgValidateErrChart = "invalid chart definition: %w" + PkgValidateErrChartName = "chart %s exceed the maximum length of %d characters" + PkgValidateErrChartNameMissing = "chart %s must include a name" + PkgValidateErrChartNamespaceMissing = "chart %s must include a namespace" + PkgValidateErrChartUrlOrPath = "chart %s must only have a url or localPath" + PkgValidateErrChartVersion = "chart %s must include a chart version" + PkgValidateErrCompenantNameNotUnique = "component name '%s' is not unique" + PkgValidateErrComponent = "invalid component: %w" + PkgValidateErrComponentReqDefault = "component %s cannot be both required and default" + PkgValidateErrComponentReqGrouped = "component %s cannot be both required and grouped" + PkgValidateErrComponentYOLO = "component %s incompatible with the online-only package flag (metadata.yolo): %w" + PkgValidateErrConstant = "invalid package constant: %w" + PkgValidateErrImportPathInvalid = "invalid file path \"%s\" provided directory must contain a valid zarf.yaml file" + PkgValidateErrImportPathMissing = "imported package %s must include a path" + PkgValidateErrInitNoYOLO = "sorry, you can't YOLO an init package" + PkgValidateErrManifest = "invalid manifest definition: %w" + PkgValidateErrManifestFileOrKustomize = "manifest %s must have at least one file or kustomization" + PkgValidateErrManifestNameLength = "manifest %s exceed the maximum length of %d characters" + PkgValidateErrManifestNameMissing = "manifest %s must include a name" + PkgValidateErrName = "invalid package name: %w" + PkgValidateErrPkgConstantName = "constant name '%s' must be all uppercase and contain no special characters except _" + PkgValidateErrPkgName = "package name '%s' must be all lowercase and contain no special characters except -" + PkgValidateErrPkgVariableName = "variable name '%s' must be all uppercase and contain no special characters except _" + PkgValidateErrVariable = "invalid package variable: %w" + PkgValidateErrYOLONoArch = "cluster architecture not allowed" + PkgValidateErrYOLONoDistro = "cluster distros not allowed" + PkgValidateErrYOLONoGit = "git repos not allowed" + PkgValidateErrYOLONoOCI = "OCI images not allowed" ) // ErrInitNotFound diff --git a/src/internal/packager/helm/post-render.go b/src/internal/packager/helm/post-render.go index eeaeb8a686..ef2d337f44 100644 --- a/src/internal/packager/helm/post-render.go +++ b/src/internal/packager/helm/post-render.go @@ -170,6 +170,11 @@ func (r *renderer) Run(renderedManifests *bytes.Buffer) (*bytes.Buffer, error) { } } + // If the package is marked as YOLO and the state is empty, skip the secret creation + if r.options.Cfg.Pkg.Metadata.YOLO && r.options.Cfg.State.Distro == "YOLO" { + break + } + // Create the secret validSecret, err := c.GenerateRegistryPullCreds(name, config.ZarfImagePullSecretName) if err != nil { diff --git a/src/internal/packager/validate/validate.go b/src/internal/packager/validate/validate.go index 3d1f2c712f..8d69b2dde9 100644 --- a/src/internal/packager/validate/validate.go +++ b/src/internal/packager/validate/validate.go @@ -12,25 +12,30 @@ import ( "strings" "github.com/defenseunicorns/zarf/src/config" + "github.com/defenseunicorns/zarf/src/config/lang" "github.com/defenseunicorns/zarf/src/pkg/utils" "github.com/defenseunicorns/zarf/src/types" ) // Run performs config validations func Run(pkg types.ZarfPackage) error { + if pkg.Kind == "ZarfInitConfig" && pkg.Metadata.YOLO { + return fmt.Errorf(lang.PkgValidateErrInitNoYOLO) + } + if err := validatePackageName(pkg.Metadata.Name); err != nil { - return fmt.Errorf("invalid package name: %w", err) + return fmt.Errorf(lang.PkgValidateErrName, err) } for _, variable := range pkg.Variables { if err := validatePackageVariable(variable); err != nil { - return fmt.Errorf("invalid package variable: %w", err) + return fmt.Errorf(lang.PkgValidateErrVariable, err) } } for _, constant := range pkg.Constants { if err := validatePackageConstant(constant); err != nil { - return fmt.Errorf("invalid package constant: %w", err) + return fmt.Errorf(lang.PkgValidateErrConstant, err) } } @@ -39,12 +44,12 @@ func Run(pkg types.ZarfPackage) error { for _, component := range pkg.Components { // ensure component name is unique if _, ok := uniqueNames[component.Name]; ok { - return fmt.Errorf("component name '%s' is not unique", component.Name) + return fmt.Errorf(lang.PkgValidateErrCompenantNameNotUnique, component.Name) } uniqueNames[component.Name] = true - if err := validateComponent(component); err != nil { - return fmt.Errorf("invalid component: %w", err) + if err := validateComponent(pkg, component); err != nil { + return fmt.Errorf(lang.PkgValidateErrComponent, err) } } @@ -53,12 +58,11 @@ func Run(pkg types.ZarfPackage) error { // ImportPackage validates the package trying to be imported. func ImportPackage(composedComponent *types.ZarfComponent) error { - intro := fmt.Sprintf("imported package %s", composedComponent.Name) path := composedComponent.Import.Path // ensure path exists if !(len(path) > 0) { - return fmt.Errorf("%s must include a path", intro) + return fmt.Errorf(lang.PkgValidateErrImportPathMissing, composedComponent.Name) } // remove zarf.yaml from path if path has zarf.yaml suffix @@ -73,7 +77,7 @@ func ImportPackage(composedComponent *types.ZarfComponent) error { // ensure there is a zarf.yaml in provided path if utils.InvalidPath(path + config.ZarfYAML) { - return fmt.Errorf("invalid file path \"%s\" provided directory must contain a valid zarf.yaml file", composedComponent.Import.Path) + return fmt.Errorf(lang.PkgValidateErrImportPathInvalid, composedComponent.Import.Path) } return nil @@ -87,36 +91,63 @@ func oneIfNotEmpty(testString string) int { return 1 } -func validateComponent(component types.ZarfComponent) error { +func validateComponent(pkg types.ZarfPackage, component types.ZarfComponent) error { if component.Required { if component.Default { - return fmt.Errorf("component %s cannot be both required and default", component.Name) + return fmt.Errorf(lang.PkgValidateErrComponentReqDefault, component.Name) } if component.Group != "" { - return fmt.Errorf("component %s cannot be both required and grouped", component.Name) + return fmt.Errorf(lang.PkgValidateErrComponentReqGrouped, component.Name) } } for _, chart := range component.Charts { if err := validateChart(chart); err != nil { - return fmt.Errorf("invalid chart definition: %w", err) + return fmt.Errorf(lang.PkgValidateErrChart, err) } } + for _, manifest := range component.Manifests { if err := validateManifest(manifest); err != nil { - return fmt.Errorf("invalid manifest definition: %w", err) + return fmt.Errorf(lang.PkgValidateErrManifest, err) + } + } + + if pkg.Metadata.YOLO { + if err := validateYOLO(component); err != nil { + return fmt.Errorf(lang.PkgValidateErrComponentYOLO, component.Name, err) } } return nil } +func validateYOLO(component types.ZarfComponent) error { + if len(component.Images) > 0 { + return fmt.Errorf(lang.PkgValidateErrYOLONoOCI) + } + + if len(component.Repos) > 0 { + return fmt.Errorf(lang.PkgValidateErrYOLONoGit) + } + + if component.Only.Cluster.Architecture != "" { + return fmt.Errorf(lang.PkgValidateErrYOLONoArch) + } + + if len(component.Only.Cluster.Distros) > 0 { + return fmt.Errorf(lang.PkgValidateErrYOLONoDistro) + } + + return nil +} + func validatePackageName(subject string) error { // https://regex101.com/r/vpi8a8/1 isValid := regexp.MustCompile(`^[a-z0-9\-]+$`).MatchString if !isValid(subject) { - return fmt.Errorf("package name '%s' must be all lowercase and contain no special characters except -", subject) + return fmt.Errorf(lang.PkgValidateErrPkgName, subject) } return nil @@ -127,7 +158,7 @@ func validatePackageVariable(subject types.ZarfPackageVariable) error { // ensure the variable name is only capitals and underscores if !isAllCapsUnderscore(subject.Name) { - return fmt.Errorf("variable name '%s' must be all uppercase and contain no special characters except _", subject.Name) + return fmt.Errorf(lang.PkgValidateErrPkgVariableName, subject.Name) } return nil @@ -138,64 +169,56 @@ func validatePackageConstant(subject types.ZarfPackageConstant) error { // ensure the constant name is only capitals and underscores if !isAllCapsUnderscore(subject.Name) { - return fmt.Errorf("constant name '%s' must be all uppercase and contain no special characters except _", subject.Name) + return fmt.Errorf(lang.PkgValidateErrPkgConstantName, subject.Name) } return nil } func validateChart(chart types.ZarfChart) error { - intro := fmt.Sprintf("chart %s", chart.Name) - // Don't allow empty names if chart.Name == "" { - return fmt.Errorf("%s must include a name", intro) + return fmt.Errorf(lang.PkgValidateErrChartNameMissing, chart.Name) } // Helm max release name if len(chart.Name) > config.ZarfMaxChartNameLength { - return fmt.Errorf("%s exceed the maximum length of %d characters", - intro, - config.ZarfMaxChartNameLength) + return fmt.Errorf(lang.PkgValidateErrChartName, chart.Name, config.ZarfMaxChartNameLength) } // Must have a namespace if chart.Namespace == "" { - return fmt.Errorf("%s must include a namespace", intro) + return fmt.Errorf(lang.PkgValidateErrChartNamespaceMissing, chart.Name) } // Must only have a url or localPath count := oneIfNotEmpty(chart.Url) + oneIfNotEmpty(chart.LocalPath) if count != 1 { - return fmt.Errorf("%s must only have a url or localPath", intro) + return fmt.Errorf(lang.PkgValidateErrChartUrlOrPath, chart.Name) } // Must have a version if chart.Version == "" { - return fmt.Errorf("%s must include a chart version", intro) + return fmt.Errorf(lang.PkgValidateErrChartVersion, chart.Name) } return nil } func validateManifest(manifest types.ZarfManifest) error { - intro := fmt.Sprintf("chart %s", manifest.Name) - // Don't allow empty names if manifest.Name == "" { - return fmt.Errorf("%s must include a name", intro) + return fmt.Errorf(lang.PkgValidateErrManifestNameMissing, manifest.Name) } // Helm max release name if len(manifest.Name) > config.ZarfMaxChartNameLength { - return fmt.Errorf("%s exceed the maximum length of %d characters", - intro, - config.ZarfMaxChartNameLength) + return fmt.Errorf(lang.PkgValidateErrManifestNameLength, manifest.Name, config.ZarfMaxChartNameLength) } // Require files in manifest if len(manifest.Files) < 1 && len(manifest.Kustomizations) < 1 { - return fmt.Errorf("%s must have at least one file or kustomization", intro) + return fmt.Errorf(lang.PkgValidateErrManifestFileOrKustomize, manifest.Name) } return nil diff --git a/src/pkg/packager/deploy.go b/src/pkg/packager/deploy.go index 73832ac0a5..a00106090d 100644 --- a/src/pkg/packager/deploy.go +++ b/src/pkg/packager/deploy.go @@ -304,10 +304,26 @@ func (p *Packager) getUpdatedValueTemplate(component types.ZarfComponent) (value defer spinner.Stop() state, err := p.cluster.LoadZarfState() + // Return on error if we are not in YOLO mode + if err != nil && !p.cfg.Pkg.Metadata.YOLO { + return values, fmt.Errorf("unable to load the Zarf State from the Kubernetes cluster: %w", err) + } - // If no distro the zarf secret did not load properly - if err != nil || state.Distro == "" { - return values, err + // Check if the state is empty + if state.Distro == "" { + // If we are in YOLO mode, return an error + if !p.cfg.Pkg.Metadata.YOLO { + return values, fmt.Errorf("unable to load the Zarf State from the Kubernetes cluster: %w", err) + } + + // YOLO mode, so no state needed + state.Distro = "YOLO" + } + + if p.cfg.Pkg.Metadata.YOLO && state.Distro != "YOLO" { + message.Warn("This package is in YOLO mode, but the cluster was already initialized with 'zarf init'. " + + "This may cause issues if the package does not exclude any charts or manifests from the Zarf Agent using " + + "the pod or namespace label `zarf.dev/agent: ignore'.") } p.cfg.State = state @@ -318,6 +334,7 @@ func (p *Packager) getUpdatedValueTemplate(component types.ZarfComponent) (value return values, err } + // Only check the architecture if the package has images if len(component.Images) > 0 && state.Architecture != p.arch { // If the package has images but the architectures don't match warn the user to avoid ugly hidden errors with image push/pull return values, fmt.Errorf("this package architecture is %s, but this cluster seems to be initialized with the %s architecture", diff --git a/src/test/e2e/99_yolo_test.go b/src/test/e2e/99_yolo_test.go new file mode 100644 index 0000000000..82dd50b4f1 --- /dev/null +++ b/src/test/e2e/99_yolo_test.go @@ -0,0 +1,46 @@ +package test + +import ( + "fmt" + "net/http" + "testing" + + "github.com/defenseunicorns/zarf/src/internal/cluster" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestYOLOMode(t *testing.T) { + t.Log("E2E: YOLO Mode") + + // Don't run this test in appliance mode + if e2e.applianceMode { + return + } + + e2e.setupWithCluster(t) + defer e2e.teardown(t) + + // Destroy the cluster to test Zarf cleaning up after itself + stdOut, stdErr, err := e2e.execZarfCommand("destroy", "--confirm", "--remove-components") + require.NoError(t, err, stdOut, stdErr) + + path := fmt.Sprintf("build/zarf-package-yolo-%s.tar.zst", e2e.arch) + + // Deploy the YOLO package + stdOut, stdErr, err = e2e.execZarfCommand("package", "deploy", path, "--confirm") + require.NoError(t, err, stdOut, stdErr) + + tunnel, err := cluster.NewZarfTunnel() + require.NoError(t, err) + tunnel.Connect("doom", false) + defer tunnel.Close() + + // Check that 'curl' returns something. + resp, err := http.Get(tunnel.HttpEndpoint()) + assert.NoError(t, err, resp) + assert.Equal(t, 200, resp.StatusCode) + + stdOut, stdErr, err = e2e.execZarfCommand("package", "remove", "yolo", "--confirm") + require.NoError(t, err, stdOut, stdErr) +} diff --git a/src/types/component.go b/src/types/component.go index 90673f374b..8749839b54 100644 --- a/src/types/component.go +++ b/src/types/component.go @@ -76,20 +76,20 @@ type ZarfFile struct { // ZarfChart defines a helm chart to be deployed. type ZarfChart struct { - Name string `json:"name" jsonschema:"description=The name of the chart to deploy, this should be the name of the chart as it is installed in the helm repo"` - ReleaseName string `json:"releaseName,omitempty" jsonschema:"description=The name of the release to create, defaults to the name of the chart"` + Name string `json:"name" jsonschema:"description=The name of the chart to deploy; this should be the name of the chart as it is installed in the helm repo"` + ReleaseName string `json:"releaseName,omitempty" jsonschema:"description=The name of the release to create; defaults to the name of the chart"` Url string `json:"url,omitempty" jsonschema:"oneof_required=url,description=The URL of the chart repository or git url if the chart is using a git repo instead of helm repo"` - Version string `json:"version" jsonschema:"description=The version of the chart to deploy, for git-based charts this is also the tag of the git repo"` + Version string `json:"version" jsonschema:"description=The version of the chart to deploy; for git-based charts this is also the tag of the git repo"` Namespace string `json:"namespace" jsonschema:"description=The namespace to deploy the chart to"` - ValuesFiles []string `json:"valuesFiles,omitempty" jsonschema:"description=List of values files to include in the package, these will be merged together"` - GitPath string `json:"gitPath,omitempty" jsonschema:"description=If using a git repo, the path to the chart in the repo"` + ValuesFiles []string `json:"valuesFiles,omitempty" jsonschema:"description=List of values files to include in the package; these will be merged together"` + GitPath string `json:"gitPath,omitempty" jsonschema:"description=The path to the chart in the repo if using a git repo instead of a helm repo"` LocalPath string `json:"localPath,omitempty" jsonschema:"oneof_required=localPath,description=The path to the chart folder"` NoWait bool `json:"noWait,omitempty" jsonschema:"description=Wait for chart resources to be ready before continuing"` } // ZarfManifest defines raw manifests Zarf will deploy as a helm chart type ZarfManifest struct { - Name string `json:"name" jsonschema:"description=A name to give this collection of manifests, this will become the name of the dynamically-created helm chart"` + Name string `json:"name" jsonschema:"description=A name to give this collection of manifests; this will become the name of the dynamically-created helm chart"` Namespace string `json:"namespace,omitempty" jsonschema:"description=The namespace to deploy the manifests to"` Files []string `json:"files,omitempty" jsonschema:"description=List of individual K8s YAML files to deploy (in order)"` KustomizeAllowAnyDirectory bool `json:"kustomizeAllowAnyDirectory,omitempty" jsonschema:"description=Allow traversing directory above the current directory if needed for kustomization"` diff --git a/src/types/package.go b/src/types/package.go index a07547281b..61b2a005f8 100644 --- a/src/types/package.go +++ b/src/types/package.go @@ -23,6 +23,7 @@ type ZarfMetadata struct { Image string `json:"image,omitempty" jsonschema:"description=An image URL to embed in this package for future Zarf UI listing"` Uncompressed bool `json:"uncompressed,omitempty" jsonschema:"description=Disable compression of this package"` Architecture string `json:"architecture,omitempty" jsonschema:"description=The target cluster architecture of this package"` + YOLO bool `json:"yolo,omitempty" jsonschema:"description=Yaml OnLy Online (YOLO): True enables deploying a Zarf package without first running zarf init against the cluster. This is ideal for connected environments where you want to use existing VCS and container registries."` } // ZarfBuildData is written during the packager.Create() operation to track details of the created package. diff --git a/src/ui/lib/api-types.ts b/src/ui/lib/api-types.ts index 081f305378..066bbcbf32 100644 --- a/src/ui/lib/api-types.ts +++ b/src/ui/lib/api-types.ts @@ -125,7 +125,7 @@ export interface ZarfComponent { export interface ZarfChart { /** - * If using a git repo + * The path to the chart in the repo if using a git repo instead of a helm repo */ gitPath?: string; /** @@ -133,7 +133,8 @@ export interface ZarfChart { */ localPath?: string; /** - * The name of the chart to deploy + * The name of the chart to deploy; this should be the name of the chart as it is installed + * in the helm repo */ name: string; /** @@ -145,7 +146,7 @@ export interface ZarfChart { */ noWait?: boolean; /** - * The name of the release to create + * The name of the release to create; defaults to the name of the chart */ releaseName?: string; /** @@ -154,11 +155,12 @@ export interface ZarfChart { */ url?: string; /** - * List of values files to include in the package + * List of values files to include in the package; these will be merged together */ valuesFiles?: string[]; /** - * The version of the chart to deploy + * The version of the chart to deploy; for git-based charts this is also the tag of the git + * repo */ version: string; } @@ -246,7 +248,8 @@ export interface ZarfManifest { */ kustomizeAllowAnyDirectory?: boolean; /** - * A name to give this collection of manifests + * A name to give this collection of manifests; this will become the name of the + * dynamically-created helm chart */ name: string; /** @@ -390,6 +393,12 @@ export interface ZarfMetadata { * Generic string to track the package version by a package author */ version?: string; + /** + * Yaml OnLy Online (YOLO): True enables deploying a Zarf package without first running zarf + * init against the cluster. This is ideal for connected environments where you want to use + * existing VCS and container registries. + */ + yolo?: boolean; } export interface ZarfPackageVariable { @@ -920,6 +929,7 @@ const typeMap: any = { { json: "uncompressed", js: "uncompressed", typ: u(undefined, true) }, { json: "url", js: "url", typ: u(undefined, "") }, { json: "version", js: "version", typ: u(undefined, "") }, + { json: "yolo", js: "yolo", typ: u(undefined, true) }, ], false), "ZarfPackageVariable": o([ { json: "default", js: "default", typ: u(undefined, "") }, diff --git a/zarf.schema.json b/zarf.schema.json index 30edae1dd9..457d5d2e6d 100644 --- a/zarf.schema.json +++ b/zarf.schema.json @@ -39,11 +39,11 @@ "properties": { "name": { "type": "string", - "description": "The name of the chart to deploy" + "description": "The name of the chart to deploy; this should be the name of the chart as it is installed in the helm repo" }, "releaseName": { "type": "string", - "description": "The name of the release to create" + "description": "The name of the release to create; defaults to the name of the chart" }, "url": { "type": "string", @@ -51,7 +51,7 @@ }, "version": { "type": "string", - "description": "The version of the chart to deploy" + "description": "The version of the chart to deploy; for git-based charts this is also the tag of the git repo" }, "namespace": { "type": "string", @@ -62,11 +62,11 @@ "type": "string" }, "type": "array", - "description": "List of values files to include in the package" + "description": "List of values files to include in the package; these will be merged together" }, "gitPath": { "type": "string", - "description": "If using a git repo" + "description": "The path to the chart in the repo if using a git repo instead of a helm repo" }, "localPath": { "type": "string", @@ -375,7 +375,7 @@ "properties": { "name": { "type": "string", - "description": "A name to give this collection of manifests" + "description": "A name to give this collection of manifests; this will become the name of the dynamically-created helm chart" }, "namespace": { "type": "string", @@ -440,6 +440,10 @@ "architecture": { "type": "string", "description": "The target cluster architecture of this package" + }, + "yolo": { + "type": "boolean", + "description": "Yaml OnLy Online (YOLO): True enables deploying a Zarf package without first running zarf init against the cluster. This is ideal for connected environments where you want to use existing VCS and container registries." } }, "additionalProperties": false,