Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions examples/REQUIREMENTS/manifests/namespace.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: requirements-test
49 changes: 49 additions & 0 deletions examples/REQUIREMENTS/requirements.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# YAML file that specifies prereqs for package to have a clean deploy
# This is a very minimal & functional example
# agent - specify utils & vars that must exist on the agent doing the zarf package deploy
agent:
tools:
- name: kubectl
version: ">=1.20.0"
reason: Required to apply manifests
# cluster - specify crds, k8s resources, or other zarf packages that must exist on the cluster being deployed into
cluster:
resources:
- apiVersion: v1
kind: Namespace
name: kube-system

# While this is more elaborate
#agent:
# tools:
# - name: yq
# version: ">= 4.40.5"
# - name: custom_binary
# version: ">= 3.14.0"
# versionCommand: /path/to/custom_binary --flag-for-version
# reason: "Used in our create onBefore scripts for X"
#
# env:
# - name: HTTPS_PROXY
# required: false
#
# Things that must be present on the cluster being deployed into
# Can specify crds, specific resources, or other zarf packages
#cluster:
# crds:
# - name: gateways.gateway.networking.k8s.io
# version: ">= 1.0.0"
# - name: certificates.cert-manager.io
# resources:
# - apiVersion: v1
# kind: Namespace
# name: cert-manager
# - apiVersion: apps/v1
# kind: Deployment
# namespace: zarf
# name: zarf-injector
# packages:
# - name: iffy-package
# version: ">=1.4.0, !=1.6.2, !=1.6.3"
# reason: "our package is incompatible with iffy-package 1.6.2–1.6.3"
#
16 changes: 16 additions & 0 deletions examples/REQUIREMENTS/zarf.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
kind: ZarfPackageConfig
metadata:
name: requirements-test
description: Simple package to test requirements handling
version: 0.0.1
architecture: amd64

components:
- name: create-namespace
description: Creates a test namespace
required: true
manifests:
- name: namespace
namespace: ""
files:
- manifests/namespace.yaml
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -629,7 +629,7 @@ require (
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
gorm.io/gorm v1.31.1 // indirect
k8s.io/apiextensions-apiserver v0.35.0 // indirect
k8s.io/apiextensions-apiserver v0.35.0
k8s.io/apiserver v0.35.0 // indirect
k8s.io/cli-runtime v0.35.1
k8s.io/component-helpers v0.35.1
Expand Down
1 change: 1 addition & 0 deletions site/src/content/docs/commands/zarf_package_deploy.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ zarf package deploy [ PACKAGE_SOURCE ] [flags]
--set-values stringToString Specify deployment package values to set on the command line (key.path=value). (default [])
--set-variables stringToString Specify deployment variables to set on the command line (KEY=value) (default [])
--shasum string Shasum of the package to deploy. Required if deploying a remote https package.
--skip-requirements-check Ignore the package's REQUIREMENTS when deploying
--timeout duration Timeout for health checks and Helm operations such as installs and rollbacks (default 15m0s)
-v, --values strings [alpha] Values files to use for templating and Helm overrides. Multiple files can be passed in as a comma separated list, and the flag can be provided multiple times.
--verify Verify the Zarf package signature
Expand Down
3 changes: 3 additions & 0 deletions src/cmd/package.go
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,7 @@ type packageDeployOptions struct {
verify bool
skipSignatureValidation bool
skipVersionCheck bool
skipRequirementsCheck bool
ociConcurrency int
publicKeyPath string
}
Expand Down Expand Up @@ -291,6 +292,7 @@ func newPackageDeployCommand(v *viper.Viper) *cobra.Command {
cmd.Flags().BoolVar(&o.skipSignatureValidation, "skip-signature-validation", false, lang.CmdPackageFlagSkipSignatureValidation)
cmd.Flags().BoolVar(&o.verify, "verify", v.GetBool(VPkgVerify), lang.CmdPackageFlagVerify)
cmd.Flags().BoolVar(&o.skipVersionCheck, "skip-version-check", false, "Ignore version requirements when deploying the package")
cmd.Flags().BoolVar(&o.skipRequirementsCheck, "skip-requirements-check", false, "Ignore the package's requirements.yaml when deploying")
_ = cmd.Flags().MarkHidden("skip-version-check")
errSig := cmd.Flags().MarkDeprecated("skip-signature-validation", "Signature verification now occurs on every execution, but is not enforced by default. Use --verify to enforce validation. This flag will be removed in Zarf v1.0.0.")
if errSig != nil {
Expand Down Expand Up @@ -381,6 +383,7 @@ func (o *packageDeployOptions) run(cmd *cobra.Command, args []string) (err error
RemoteOptions: defaultRemoteOptions(),
IsInteractive: !o.confirm,
SkipVersionCheck: o.skipVersionCheck,
SkipRequirementsCheck: o.skipRequirementsCheck,
}

deployedComponents, err := deploy(ctx, pkgLayout, deployOpts, o.setVariables, o.optionalComponents)
Expand Down
201 changes: 201 additions & 0 deletions src/internal/packager/requirements/agent.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2021-Present The Zarf Authors

package requirements

import (
"bytes"
"context"
"fmt"
"os"
"os/exec"
"regexp"
"strings"

"github.com/Masterminds/semver/v3"
)

type requirementsValidationError struct {
Failures []string
}

func (e *requirementsValidationError) Error() string {
return "REQUIREMENTS validation failed:\n - " + strings.Join(e.Failures, "\n - ")
}

func validateAgentRequirements(ctx context.Context, req agentRequirements) error {
var failures []string

// env checks
for _, e := range req.Env {
if !e.Required {
continue
}
if _, ok := os.LookupEnv(e.Name); !ok {
msg := fmt.Sprintf("agent env var %q is required but not set", e.Name)
if e.Reason != "" {
msg += fmt.Sprintf(" (reason: %s)", e.Reason)
}
failures = append(failures, msg)
}
}

// tool checks
for _, t := range req.Tools {
if err := validateTool(ctx, t); err != nil {
if t.Optional {
continue
}
failures = append(failures, err.Error())
}
}

if len(failures) > 0 {
return &requirementsValidationError{Failures: failures}
}
return nil
}

func validateTool(ctx context.Context, t toolRequirement) error {
if strings.TrimSpace(t.Name) == "" {
return fmt.Errorf("agent tool requirement has empty name")
}

path, err := exec.LookPath(t.Name)
if err != nil {
msg := fmt.Sprintf("agent tool %q is missing from PATH", t.Name)
if t.Reason != "" {
msg += fmt.Sprintf(" (reason: %s)", t.Reason)
}
return fmt.Errorf("%s", msg)
}

// If no version constraint provided, presence is enough.
if strings.TrimSpace(t.Version) == "" {
return nil
}

constraint, err := semver.NewConstraint(t.Version)
if err != nil {
return fmt.Errorf("invalid semver constraint for tool %q: %q: %w", t.Name, t.Version, err)
}

cmdline := t.VersionCommand
if strings.TrimSpace(cmdline) == "" {
// common defaults
cmdline = t.Name + " --version"
}

out, err := runShellish(ctx, cmdline)
if err != nil {
return fmt.Errorf("failed running version check for tool %q (%s): %w", t.Name, cmdline, err)
}

ver, err := extractSemver(out, t.VersionRegex)
if err != nil {
return fmt.Errorf("unable to parse version for tool %q from output %q: %w", t.Name, strings.TrimSpace(out), err)
}

if !constraint.Check(ver) {
msg := fmt.Sprintf("agent tool %q at %q does not satisfy constraint %q (resolved binary: %s)",
t.Name, ver.Original(), t.Version, path)
if t.Reason != "" {
msg += fmt.Sprintf(" (reason: %s)", t.Reason)
}
return fmt.Errorf("%s", msg)
}

return nil
}

// runShellish runs a command string in a conservative way: split on spaces unless quoted.
// Keeps it dependency-free (no bash required).
func runShellish(ctx context.Context, command string) (string, error) {
parts := splitArgs(command)
if len(parts) == 0 {
return "", fmt.Errorf("empty command")
}
cmd := exec.CommandContext(ctx, parts[0], parts[1:]...)
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
out := stdout.String()
if err != nil {
// include stderr for debugging
if s := strings.TrimSpace(stderr.String()); s != "" {
return out, fmt.Errorf("%w: %s", err, s)
}
return out, err
}
return out, nil
}

// extractSemver pulls the first semver-like token from output (supports leading "v").
// If regex is provided, it must contain either a named group "ver" or group 1.
func extractSemver(output string, versionRegex string) (*semver.Version, error) {
s := strings.TrimSpace(output)
if s == "" {
return nil, fmt.Errorf("empty output")
}

if versionRegex != "" {
re, err := regexp.Compile(versionRegex)
if err != nil {
return nil, fmt.Errorf("invalid versionRegex: %w", err)
}
m := re.FindStringSubmatch(s)
if len(m) == 0 {
return nil, fmt.Errorf("regex did not match")
}
// Named group?
if idx := re.SubexpIndex("ver"); idx > 0 && idx < len(m) {
return semver.NewVersion(strings.TrimPrefix(m[idx], "v"))
}
if len(m) >= 2 {
return semver.NewVersion(strings.TrimPrefix(m[1], "v"))
}
return nil, fmt.Errorf("regex matched but no capture group found")
}

// Default: find first semver token
// e.g. "yq (https://...) version v4.40.5"
re := regexp.MustCompile(`v?(\d+\.\d+\.\d+)([-+][0-9A-Za-z\.\-]+)?`)
m := re.FindString(s)
if m == "" {
return nil, fmt.Errorf("no semver token found")
}
return semver.NewVersion(strings.TrimPrefix(m, "v"))
}

// splitArgs is a tiny quoted-arg splitter (handles "..." and '...').
func splitArgs(in string) []string {
var out []string
var cur strings.Builder
var quote rune
flush := func() {
if cur.Len() > 0 {
out = append(out, cur.String())
cur.Reset()
}
}

for _, r := range strings.TrimSpace(in) {
switch {
case quote != 0:
if r == quote {
quote = 0
} else {
cur.WriteRune(r)
}
case r == '"' || r == '\'':
quote = r
case r == ' ' || r == '\t' || r == '\n':
flush()
default:
cur.WriteRune(r)
}
}
flush()
return out
}
Loading