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

Merged
merged 3 commits into from
Jan 13, 2025
Merged
Show file tree
Hide file tree
Changes from 2 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
phillebaba marked this conversation as resolved.
Show resolved Hide resolved
# 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
23 changes: 13 additions & 10 deletions src/cmd/package.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,16 +139,19 @@ func (o *PackageCreateOptions) Run(cmd *cobra.Command, args []string) error {
pkgConfig.CreateOpts.SetVariables = helpers.TransformAndMergeMap(
v.GetStringMapString(common.VPkgCreateSet), pkgConfig.CreateOpts.SetVariables, strings.ToUpper)

pkgClient, err := packager.New(&pkgConfig,
packager.WithContext(ctx),
)
if err != nil {
return err
}
defer pkgClient.ClearTempPaths()

err = pkgClient.Create(ctx)

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,
DifferentialPackagePath: pkgConfig.CreateOpts.DifferentialPackagePath,
}
err := packager2.Create(cmd.Context(), pkgConfig.CreateOpts.BaseDir, opt)
// NOTE(mkcp): LintErrors are rendered with a table
var lintErr *lint.LintError
if errors.As(err, &lintErr) {
Expand Down
320 changes: 320 additions & 0 deletions src/internal/packager2/actions/actions.go
AustinAbro321 marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,320 @@
// 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/logger"
"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
var err error
cmd := action.Cmd
l := logger.From(ctx)
start := time.Now()

// 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)
}

// TODO(mkcp): Remove message on logger release
spinner := message.NewProgressSpinner("Running \"%s\"", cmdEscaped)
// Persist the spinner output so it doesn't get overwritten by the command output.
spinner.EnablePreserveWrites()
l.Info("running command", "cmd", cmdEscaped)

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

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

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.
stdout, stderr, err := actionRun(ctx, actionDefaults, cmd, spinner)
if err != nil {
if !actionDefaults.Mute {
l.Warn("action failed", "cmd", cmdEscaped, "stdout", stdout, "stderr", stderr)
}
return err
}
if !actionDefaults.Mute {
l.Info("action succeeded", "cmd", cmdEscaped, "stdout", stdout, "stderr", stderr)
}

outTrimmed := strings.TrimSpace(stdout)

// If an output variable is defined, set it.
for _, v := range action.SetVariables {
variableConfig.SetVariable(v.Name, outTrimmed, 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)
l.Debug("wait for action succeeded", "cmd", cmdEscaped, "duration", time.Since(start))
return nil
}

spinner.Successf("Completed \"%s\"", cmdEscaped)
l.Debug("completed action", "cmd", cmdEscaped, "duration", time.Since(start))

// 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)
l.Info("waiting for action (no timeout)", "cmd", cmdEscaped)
if err := tryCmd(ctx); 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)
l.Info("waiting for action", "cmd", cmdEscaped, "timeout", 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(ctx 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")))
// TODO(mkcp): Remove message on logger release
message.Debugf("Converted command \"%s\" to \"%s\" t", cmd, newCmd)
logger.From(ctx).Debug("converted command", "cmd", cmd, "newCmd", 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, spinner *message.Spinner) (string, string, error) {
l := logger.From(ctx)
shell, shellArgs := exec.GetOSShell(cfg.Shell)

// TODO(mkcp): Remove message on logger release
message.Debugf("Running command in %s: %s", shell, cmd)
l.Debug("running command", "shell", shell, "cmd", cmd)

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

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

stdout, stderr, 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 {
// TODO(mkcp): Remove message on logger release
message.Debug(cmd, stdout, stderr)
}
return stdout, stderr, err
}
Loading
Loading