Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: normal creator #3114

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
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
2 changes: 1 addition & 1 deletion examples/manifests/zarf.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ components:
kustomizations:
# kustomizations can be specified relative to the `zarf.yaml` or as remoteBuild resources with the
# following syntax: https://github.com/kubernetes-sigs/kustomize/blob/master/examples/remoteBuild.md:
- github.com/stefanprodan/podinfo//kustomize?ref=6.4.0
- https://github.com/stefanprodan/podinfo//kustomize?ref=6.4.0
# while ?ref= is not a requirement, it is recommended to use a specific commit hash / git tag to
# ensure that the kustomization is not changed in a way that breaks your deployment.
# image discovery is supported in all manifests and charts using:
Expand Down
1 change: 0 additions & 1 deletion site/src/content/docs/commands/zarf_package_create.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ zarf package create [ DIRECTORY ] [flags]
-o, --output string Specify the output (either a directory or an oci:// URL) for the created Zarf package
--registry-override stringToString Specify a map of domains to override on package create when pulling images (e.g. --registry-override docker.io=dockerio-reg.enterprise.intranet) (default [])
--retries int Number of retries to perform for Zarf deploy operations like git/image pushes or Helm installs (default 3)
-s, --sbom View SBOM contents after creating the package
--sbom-out string Specify an output directory for the SBOMs from the created Zarf package
--set stringToString Specify package variables to set on the command line (KEY=value) (default [])
--signing-key string Path to private key file for signing packages
Expand Down
29 changes: 12 additions & 17 deletions src/cmd/package.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ import (
"github.com/zarf-dev/zarf/src/internal/dns"
"github.com/zarf-dev/zarf/src/internal/packager2"
"github.com/zarf-dev/zarf/src/pkg/cluster"
"github.com/zarf-dev/zarf/src/pkg/lint"
"github.com/zarf-dev/zarf/src/pkg/message"
"github.com/zarf-dev/zarf/src/pkg/packager"
"github.com/zarf-dev/zarf/src/pkg/packager/filters"
Expand Down Expand Up @@ -64,24 +63,21 @@ var packageCreateCmd = &cobra.Command{
pkgConfig.CreateOpts.SetVariables = helpers.TransformAndMergeMap(
v.GetStringMapString(common.VPkgCreateSet), pkgConfig.CreateOpts.SetVariables, strings.ToUpper)

pkgClient, err := packager.New(&pkgConfig,
packager.WithContext(cmd.Context()),
)
opt := packager2.CreateOptions{
Flavor: pkgConfig.CreateOpts.Flavor,
RegistryOverrides: pkgConfig.CreateOpts.RegistryOverrides,
SigningKeyPath: pkgConfig.CreateOpts.SigningKeyPath,
SigningKeyPassword: pkgConfig.CreateOpts.SigningKeyPassword,
SetVariables: pkgConfig.CreateOpts.SetVariables,
MaxPackageSizeMB: pkgConfig.CreateOpts.MaxPackageSizeMB,
SBOMOut: pkgConfig.CreateOpts.SBOMOutputDir,
SkipSBOM: pkgConfig.CreateOpts.SkipSBOM,
Output: pkgConfig.CreateOpts.Output,
}
err := packager2.Create(cmd.Context(), pkgConfig.CreateOpts.BaseDir, opt)
if err != nil {
return err
}
defer pkgClient.ClearTempPaths()

err = pkgClient.Create(cmd.Context())

// NOTE(mkcp): LintErrors are rendered with a table
var lintErr *lint.LintError
if errors.As(err, &lintErr) {
common.PrintFindings(lintErr)
}
if err != nil {
return fmt.Errorf("failed to create package: %w", err)
}
return nil
},
}
Expand Down Expand Up @@ -506,7 +502,6 @@ func bindCreateFlags(v *viper.Viper) {

createFlags.StringVar(&pkgConfig.CreateOpts.DifferentialPackagePath, "differential", v.GetString(common.VPkgCreateDifferential), lang.CmdPackageCreateFlagDifferential)
createFlags.StringToStringVar(&pkgConfig.CreateOpts.SetVariables, "set", v.GetStringMapString(common.VPkgCreateSet), lang.CmdPackageCreateFlagSet)
createFlags.BoolVarP(&pkgConfig.CreateOpts.ViewSBOM, "sbom", "s", v.GetBool(common.VPkgCreateSbom), lang.CmdPackageCreateFlagSbom)
createFlags.StringVar(&pkgConfig.CreateOpts.SBOMOutputDir, "sbom-out", v.GetString(common.VPkgCreateSbomOutput), lang.CmdPackageCreateFlagSbomOut)
createFlags.BoolVar(&pkgConfig.CreateOpts.SkipSBOM, "skip-sbom", v.GetBool(common.VPkgCreateSkipSbom), lang.CmdPackageCreateFlagSkipSbom)
createFlags.IntVarP(&pkgConfig.CreateOpts.MaxPackageSizeMB, "max-package-size", "m", v.GetInt(common.VPkgCreateMaxPackageSize), lang.CmdPackageCreateFlagMaxPackageSize)
Expand Down
307 changes: 307 additions & 0 deletions src/internal/packager2/actions/actions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,307 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2021-Present The Zarf Authors

// Package actions contains functions for running component actions within Zarf packages.
package actions

import (
"context"
"fmt"
"path/filepath"
"regexp"
"runtime"
"strings"
"time"

"github.com/defenseunicorns/pkg/helpers/v2"
"github.com/zarf-dev/zarf/src/api/v1alpha1"
"github.com/zarf-dev/zarf/src/internal/packager/template"
"github.com/zarf-dev/zarf/src/pkg/message"
"github.com/zarf-dev/zarf/src/pkg/utils"
"github.com/zarf-dev/zarf/src/pkg/utils/exec"
"github.com/zarf-dev/zarf/src/pkg/variables"
)

// Run runs all provided actions.
func Run(ctx context.Context, basePath string, defaultCfg v1alpha1.ZarfComponentActionDefaults, actions []v1alpha1.ZarfComponentAction, variableConfig *variables.VariableConfig) error {
if variableConfig == nil {
variableConfig = template.GetZarfVariableConfig(ctx)
}

for _, a := range actions {
if err := runAction(ctx, basePath, defaultCfg, a, variableConfig); err != nil {
return err
}
}
return nil
}

// Run commands that a component has provided.
func runAction(ctx context.Context, basePath string, defaultCfg v1alpha1.ZarfComponentActionDefaults, action v1alpha1.ZarfComponentAction, variableConfig *variables.VariableConfig) error {
var (
cmdEscaped string
out string
err error

cmd = action.Cmd
)

// If the action is a wait, convert it to a command.
if action.Wait != nil {
// If the wait has no timeout, set a default of 5 minutes.
if action.MaxTotalSeconds == nil {
fiveMin := 300
action.MaxTotalSeconds = &fiveMin
}

// Convert the wait to a command.
if cmd, err = convertWaitToCmd(ctx, *action.Wait, action.MaxTotalSeconds); err != nil {
return err
}

// Mute the output because it will be noisy.
t := true
action.Mute = &t

// Set the max retries to 0.
z := 0
action.MaxRetries = &z

// Not used for wait actions.
d := ""
action.Dir = &d
action.Env = []string{}
action.SetVariables = []v1alpha1.Variable{}
}

if action.Description != "" {
cmdEscaped = action.Description
} else {
cmdEscaped = helpers.Truncate(cmd, 60, false)
}

spinner := message.NewProgressSpinner("Running \"%s\"", cmdEscaped)
// Persist the spinner output so it doesn't get overwritten by the command output.
spinner.EnablePreserveWrites()

actionDefaults := actionGetCfg(ctx, defaultCfg, action, variableConfig.GetAllTemplates())
actionDefaults.Dir = filepath.Join(basePath, actionDefaults.Dir)

fmt.Println("base path", basePath)
fmt.Println("action path", actionDefaults.Dir)

if cmd, err = actionCmdMutation(ctx, cmd, actionDefaults.Shell); err != nil {
spinner.Errorf(err, "Error mutating command: %s", cmdEscaped)
}

duration := time.Duration(actionDefaults.MaxTotalSeconds) * time.Second
timeout := time.After(duration)

// Keep trying until the max retries is reached.
// TODO: Refactor using go-retry
retryCmd:
for remaining := actionDefaults.MaxRetries + 1; remaining > 0; remaining-- {
// Perform the action run.
tryCmd := func(ctx context.Context) error {
// Try running the command and continue the retry loop if it fails.
if out, err = actionRun(ctx, actionDefaults, cmd, actionDefaults.Shell, spinner); err != nil {
return err
}

out = strings.TrimSpace(out)

// If an output variable is defined, set it.
for _, v := range action.SetVariables {
variableConfig.SetVariable(v.Name, out, v.Sensitive, v.AutoIndent, v.Type)
if err := variableConfig.CheckVariablePattern(v.Name, v.Pattern); err != nil {
return err
}
}

// If the action has a wait, change the spinner message to reflect that on success.
if action.Wait != nil {
spinner.Successf("Wait for \"%s\" succeeded", cmdEscaped)
} else {
spinner.Successf("Completed \"%s\"", cmdEscaped)
}

// If the command ran successfully, continue to the next action.
return nil
}

// If no timeout is set, run the command and return or continue retrying.
if actionDefaults.MaxTotalSeconds < 1 {
spinner.Updatef("Waiting for \"%s\" (no timeout)", cmdEscaped)
//TODO (schristoff): Make it so tryCmd can take a normal ctx
if err := tryCmd(context.Background()); err != nil {
continue retryCmd
}

return nil
}

// Run the command on repeat until success or timeout.
spinner.Updatef("Waiting for \"%s\" (timeout: %ds)", cmdEscaped, actionDefaults.MaxTotalSeconds)
select {
// On timeout break the loop to abort.
case <-timeout:
break retryCmd

// Otherwise, try running the command.
default:
ctx, cancel := context.WithTimeout(ctx, duration)
defer cancel()
if err := tryCmd(ctx); err != nil {
continue retryCmd
}

return nil
}
}

select {
case <-timeout:
// If we reached this point, the timeout was reached or command failed with no retries.
if actionDefaults.MaxTotalSeconds < 1 {
return fmt.Errorf("command %q failed after %d retries", cmdEscaped, actionDefaults.MaxRetries)
} else {
return fmt.Errorf("command %q timed out after %d seconds", cmdEscaped, actionDefaults.MaxTotalSeconds)
}
default:
// If we reached this point, the retry limit was reached.
return fmt.Errorf("command %q failed after %d retries", cmdEscaped, actionDefaults.MaxRetries)
}
}

// convertWaitToCmd will return the wait command if it exists, otherwise it will return the original command.
func convertWaitToCmd(_ context.Context, wait v1alpha1.ZarfComponentActionWait, timeout *int) (string, error) {
// Build the timeout string.
timeoutString := fmt.Sprintf("--timeout %ds", *timeout)

// If the action has a wait, build a cmd from that instead.
cluster := wait.Cluster
if cluster != nil {
ns := cluster.Namespace
if ns != "" {
ns = fmt.Sprintf("-n %s", ns)
}

// Build a call to the zarf tools wait-for command.
return fmt.Sprintf("./zarf tools wait-for %s %s %s %s %s",
cluster.Kind, cluster.Name, cluster.Condition, ns, timeoutString), nil
}

network := wait.Network
if network != nil {
// Make sure the protocol is lower case.
network.Protocol = strings.ToLower(network.Protocol)

// If the protocol is http and no code is set, default to 200.
if strings.HasPrefix(network.Protocol, "http") && network.Code == 0 {
network.Code = 200
}

// Build a call to the zarf tools wait-for command.
return fmt.Sprintf("./zarf tools wait-for %s %s %d %s",
network.Protocol, network.Address, network.Code, timeoutString), nil
}

return "", fmt.Errorf("wait action is missing a cluster or network")
}

// Perform some basic string mutations to make commands more useful.
func actionCmdMutation(_ context.Context, cmd string, shellPref v1alpha1.Shell) (string, error) {
zarfCommand, err := utils.GetFinalExecutableCommand()
if err != nil {
return cmd, err
}

// Try to patch the zarf binary path in case the name isn't exactly "./zarf".
cmd = strings.ReplaceAll(cmd, "./zarf ", zarfCommand+" ")

// Make commands 'more' compatible with Windows OS PowerShell
if runtime.GOOS == "windows" && (exec.IsPowershell(shellPref.Windows) || shellPref.Windows == "") {
// Replace "touch" with "New-Item" on Windows as it's a common command, but not POSIX so not aliased by M$.
// See https://mathieubuisson.github.io/powershell-linux-bash/ &
// http://web.cs.ucla.edu/~miryung/teaching/EE461L-Spring2012/labs/posix.html for more details.
cmd = regexp.MustCompile(`^touch `).ReplaceAllString(cmd, `New-Item `)

// Convert any ${ZARF_VAR_*} or $ZARF_VAR_* to ${env:ZARF_VAR_*} or $env:ZARF_VAR_* respectively (also TF_VAR_*).
// https://regex101.com/r/xk1rkw/1
envVarRegex := regexp.MustCompile(`(?P<envIndicator>\${?(?P<varName>(ZARF|TF)_VAR_([a-zA-Z0-9_-])+)}?)`)
get, err := helpers.MatchRegex(envVarRegex, cmd)
if err == nil {
newCmd := strings.ReplaceAll(cmd, get("envIndicator"), fmt.Sprintf("$Env:%s", get("varName")))
message.Debugf("Converted command \"%s\" to \"%s\" t", cmd, newCmd)
cmd = newCmd
}
}

return cmd, nil
}

// Merge the ActionSet defaults with the action config.
func actionGetCfg(_ context.Context, cfg v1alpha1.ZarfComponentActionDefaults, a v1alpha1.ZarfComponentAction, vars map[string]*variables.TextTemplate) v1alpha1.ZarfComponentActionDefaults {
if a.Mute != nil {
cfg.Mute = *a.Mute
}

// Default is no timeout, but add a timeout if one is provided.
if a.MaxTotalSeconds != nil {
cfg.MaxTotalSeconds = *a.MaxTotalSeconds
}

if a.MaxRetries != nil {
cfg.MaxRetries = *a.MaxRetries
}

if a.Dir != nil {
cfg.Dir = *a.Dir
}

if len(a.Env) > 0 {
cfg.Env = append(cfg.Env, a.Env...)
}

if a.Shell != nil {
cfg.Shell = *a.Shell
}

// Add variables to the environment.
for k, v := range vars {
// Remove # from env variable name.
k = strings.ReplaceAll(k, "#", "")
// Make terraform variables available to the action as TF_VAR_lowercase_name.
k1 := strings.ReplaceAll(strings.ToLower(k), "zarf_var", "TF_VAR")
cfg.Env = append(cfg.Env, fmt.Sprintf("%s=%s", k, v.Value))
cfg.Env = append(cfg.Env, fmt.Sprintf("%s=%s", k1, v.Value))
}

return cfg
}

func actionRun(ctx context.Context, cfg v1alpha1.ZarfComponentActionDefaults, cmd string, shellPref v1alpha1.Shell, spinner *message.Spinner) (string, error) {
shell, shellArgs := exec.GetOSShell(shellPref)

message.Debugf("Running command in %s: %s", shell, cmd)

execCfg := exec.Config{
Env: cfg.Env,
Dir: cfg.Dir,
}

fmt.Println("exec cfg", execCfg.Dir)

if !cfg.Mute {
execCfg.Stdout = spinner
execCfg.Stderr = spinner
}

out, errOut, err := exec.CmdWithContext(ctx, execCfg, shell, append(shellArgs, cmd)...)
// Dump final complete output (respect mute to prevent sensitive values from hitting the logs).
if !cfg.Mute {
message.Debug(cmd, out, errOut)
}

return out, err
}
Loading