Skip to content

Commit

Permalink
refactor: normal creator
Browse files Browse the repository at this point in the history
Signed-off-by: Philip Laine <[email protected]>
  • Loading branch information
phillebaba committed Nov 25, 2024
1 parent 44854ac commit 9141561
Show file tree
Hide file tree
Showing 55 changed files with 4,644 additions and 164 deletions.
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
23 changes: 14 additions & 9 deletions src/cmd/package.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"runtime"
"strings"

"github.com/zarf-dev/zarf/src/pkg/lint"
"github.com/zarf-dev/zarf/src/pkg/logger"

"github.com/AlecAivazis/survey/v2"
Expand All @@ -28,7 +29,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 @@ -65,16 +65,22 @@ 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(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)
if err != nil {
return err
}
defer pkgClient.ClearTempPaths()

err = pkgClient.Create(ctx)

// NOTE(mkcp): LintErrors are rendered with a table
var lintErr *lint.LintError
if errors.As(err, &lintErr) {
Expand Down Expand Up @@ -511,7 +517,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
304 changes: 304 additions & 0 deletions src/internal/packager2/actions/actions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,304 @@
// 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)

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

0 comments on commit 9141561

Please sign in to comment.