Skip to content

Commit 0a14ee3

Browse files
committed
refactor: normal creator
Signed-off-by: Philip Laine <[email protected]>
1 parent 56f62ea commit 0a14ee3

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

55 files changed

+4545
-51
lines changed

examples/manifests/zarf.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ components:
3939
kustomizations:
4040
# kustomizations can be specified relative to the `zarf.yaml` or as remoteBuild resources with the
4141
# following syntax: https://github.com/kubernetes-sigs/kustomize/blob/master/examples/remoteBuild.md:
42-
- github.com/stefanprodan/podinfo//kustomize?ref=6.4.0
42+
- https://github.com/stefanprodan/podinfo//kustomize?ref=6.4.0
4343
# while ?ref= is not a requirement, it is recommended to use a specific commit hash / git tag to
4444
# ensure that the kustomization is not changed in a way that breaks your deployment.
4545
# image discovery is supported in all manifests and charts using:

src/cmd/package.go

+13-10
Original file line numberDiff line numberDiff line change
@@ -139,16 +139,19 @@ func (o *PackageCreateOptions) Run(cmd *cobra.Command, args []string) error {
139139
pkgConfig.CreateOpts.SetVariables = helpers.TransformAndMergeMap(
140140
v.GetStringMapString(common.VPkgCreateSet), pkgConfig.CreateOpts.SetVariables, strings.ToUpper)
141141

142-
pkgClient, err := packager.New(&pkgConfig,
143-
packager.WithContext(ctx),
144-
)
145-
if err != nil {
146-
return err
147-
}
148-
defer pkgClient.ClearTempPaths()
149-
150-
err = pkgClient.Create(ctx)
151-
142+
opt := packager2.CreateOptions{
143+
Flavor: pkgConfig.CreateOpts.Flavor,
144+
RegistryOverrides: pkgConfig.CreateOpts.RegistryOverrides,
145+
SigningKeyPath: pkgConfig.CreateOpts.SigningKeyPath,
146+
SigningKeyPassword: pkgConfig.CreateOpts.SigningKeyPassword,
147+
SetVariables: pkgConfig.CreateOpts.SetVariables,
148+
MaxPackageSizeMB: pkgConfig.CreateOpts.MaxPackageSizeMB,
149+
SBOMOut: pkgConfig.CreateOpts.SBOMOutputDir,
150+
SkipSBOM: pkgConfig.CreateOpts.SkipSBOM,
151+
Output: pkgConfig.CreateOpts.Output,
152+
DifferentialPackagePath: pkgConfig.CreateOpts.DifferentialPackagePath,
153+
}
154+
err := packager2.Create(cmd.Context(), pkgConfig.CreateOpts.BaseDir, opt)
152155
// NOTE(mkcp): LintErrors are rendered with a table
153156
var lintErr *lint.LintError
154157
if errors.As(err, &lintErr) {
+304
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,304 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// SPDX-FileCopyrightText: 2021-Present The Zarf Authors
3+
4+
// Package actions contains functions for running component actions within Zarf packages.
5+
package actions
6+
7+
import (
8+
"context"
9+
"fmt"
10+
"path/filepath"
11+
"regexp"
12+
"runtime"
13+
"strings"
14+
"time"
15+
16+
"github.com/defenseunicorns/pkg/helpers/v2"
17+
"github.com/zarf-dev/zarf/src/api/v1alpha1"
18+
"github.com/zarf-dev/zarf/src/internal/packager/template"
19+
"github.com/zarf-dev/zarf/src/pkg/message"
20+
"github.com/zarf-dev/zarf/src/pkg/utils"
21+
"github.com/zarf-dev/zarf/src/pkg/utils/exec"
22+
"github.com/zarf-dev/zarf/src/pkg/variables"
23+
)
24+
25+
// Run runs all provided actions.
26+
func Run(ctx context.Context, basePath string, defaultCfg v1alpha1.ZarfComponentActionDefaults, actions []v1alpha1.ZarfComponentAction, variableConfig *variables.VariableConfig) error {
27+
if variableConfig == nil {
28+
variableConfig = template.GetZarfVariableConfig(ctx)
29+
}
30+
31+
for _, a := range actions {
32+
if err := runAction(ctx, basePath, defaultCfg, a, variableConfig); err != nil {
33+
return err
34+
}
35+
}
36+
return nil
37+
}
38+
39+
// Run commands that a component has provided.
40+
func runAction(ctx context.Context, basePath string, defaultCfg v1alpha1.ZarfComponentActionDefaults, action v1alpha1.ZarfComponentAction, variableConfig *variables.VariableConfig) error {
41+
var (
42+
cmdEscaped string
43+
out string
44+
err error
45+
46+
cmd = action.Cmd
47+
)
48+
49+
// If the action is a wait, convert it to a command.
50+
if action.Wait != nil {
51+
// If the wait has no timeout, set a default of 5 minutes.
52+
if action.MaxTotalSeconds == nil {
53+
fiveMin := 300
54+
action.MaxTotalSeconds = &fiveMin
55+
}
56+
57+
// Convert the wait to a command.
58+
if cmd, err = convertWaitToCmd(ctx, *action.Wait, action.MaxTotalSeconds); err != nil {
59+
return err
60+
}
61+
62+
// Mute the output because it will be noisy.
63+
t := true
64+
action.Mute = &t
65+
66+
// Set the max retries to 0.
67+
z := 0
68+
action.MaxRetries = &z
69+
70+
// Not used for wait actions.
71+
d := ""
72+
action.Dir = &d
73+
action.Env = []string{}
74+
action.SetVariables = []v1alpha1.Variable{}
75+
}
76+
77+
if action.Description != "" {
78+
cmdEscaped = action.Description
79+
} else {
80+
cmdEscaped = helpers.Truncate(cmd, 60, false)
81+
}
82+
83+
spinner := message.NewProgressSpinner("Running \"%s\"", cmdEscaped)
84+
// Persist the spinner output so it doesn't get overwritten by the command output.
85+
spinner.EnablePreserveWrites()
86+
87+
actionDefaults := actionGetCfg(ctx, defaultCfg, action, variableConfig.GetAllTemplates())
88+
actionDefaults.Dir = filepath.Join(basePath, actionDefaults.Dir)
89+
90+
if cmd, err = actionCmdMutation(ctx, cmd, actionDefaults.Shell); err != nil {
91+
spinner.Errorf(err, "Error mutating command: %s", cmdEscaped)
92+
}
93+
94+
duration := time.Duration(actionDefaults.MaxTotalSeconds) * time.Second
95+
timeout := time.After(duration)
96+
97+
// Keep trying until the max retries is reached.
98+
// TODO: Refactor using go-retry
99+
retryCmd:
100+
for remaining := actionDefaults.MaxRetries + 1; remaining > 0; remaining-- {
101+
// Perform the action run.
102+
tryCmd := func(ctx context.Context) error {
103+
// Try running the command and continue the retry loop if it fails.
104+
if out, err = actionRun(ctx, actionDefaults, cmd, actionDefaults.Shell, spinner); err != nil {
105+
return err
106+
}
107+
108+
out = strings.TrimSpace(out)
109+
110+
// If an output variable is defined, set it.
111+
for _, v := range action.SetVariables {
112+
variableConfig.SetVariable(v.Name, out, v.Sensitive, v.AutoIndent, v.Type)
113+
if err := variableConfig.CheckVariablePattern(v.Name, v.Pattern); err != nil {
114+
return err
115+
}
116+
}
117+
118+
// If the action has a wait, change the spinner message to reflect that on success.
119+
if action.Wait != nil {
120+
spinner.Successf("Wait for \"%s\" succeeded", cmdEscaped)
121+
} else {
122+
spinner.Successf("Completed \"%s\"", cmdEscaped)
123+
}
124+
125+
// If the command ran successfully, continue to the next action.
126+
return nil
127+
}
128+
129+
// If no timeout is set, run the command and return or continue retrying.
130+
if actionDefaults.MaxTotalSeconds < 1 {
131+
spinner.Updatef("Waiting for \"%s\" (no timeout)", cmdEscaped)
132+
//TODO (schristoff): Make it so tryCmd can take a normal ctx
133+
if err := tryCmd(context.Background()); err != nil {
134+
continue retryCmd
135+
}
136+
137+
return nil
138+
}
139+
140+
// Run the command on repeat until success or timeout.
141+
spinner.Updatef("Waiting for \"%s\" (timeout: %ds)", cmdEscaped, actionDefaults.MaxTotalSeconds)
142+
select {
143+
// On timeout break the loop to abort.
144+
case <-timeout:
145+
break retryCmd
146+
147+
// Otherwise, try running the command.
148+
default:
149+
ctx, cancel := context.WithTimeout(ctx, duration)
150+
defer cancel()
151+
if err := tryCmd(ctx); err != nil {
152+
continue retryCmd
153+
}
154+
155+
return nil
156+
}
157+
}
158+
159+
select {
160+
case <-timeout:
161+
// If we reached this point, the timeout was reached or command failed with no retries.
162+
if actionDefaults.MaxTotalSeconds < 1 {
163+
return fmt.Errorf("command %q failed after %d retries", cmdEscaped, actionDefaults.MaxRetries)
164+
} else {
165+
return fmt.Errorf("command %q timed out after %d seconds", cmdEscaped, actionDefaults.MaxTotalSeconds)
166+
}
167+
default:
168+
// If we reached this point, the retry limit was reached.
169+
return fmt.Errorf("command %q failed after %d retries", cmdEscaped, actionDefaults.MaxRetries)
170+
}
171+
}
172+
173+
// convertWaitToCmd will return the wait command if it exists, otherwise it will return the original command.
174+
func convertWaitToCmd(_ context.Context, wait v1alpha1.ZarfComponentActionWait, timeout *int) (string, error) {
175+
// Build the timeout string.
176+
timeoutString := fmt.Sprintf("--timeout %ds", *timeout)
177+
178+
// If the action has a wait, build a cmd from that instead.
179+
cluster := wait.Cluster
180+
if cluster != nil {
181+
ns := cluster.Namespace
182+
if ns != "" {
183+
ns = fmt.Sprintf("-n %s", ns)
184+
}
185+
186+
// Build a call to the zarf tools wait-for command.
187+
return fmt.Sprintf("./zarf tools wait-for %s %s %s %s %s",
188+
cluster.Kind, cluster.Name, cluster.Condition, ns, timeoutString), nil
189+
}
190+
191+
network := wait.Network
192+
if network != nil {
193+
// Make sure the protocol is lower case.
194+
network.Protocol = strings.ToLower(network.Protocol)
195+
196+
// If the protocol is http and no code is set, default to 200.
197+
if strings.HasPrefix(network.Protocol, "http") && network.Code == 0 {
198+
network.Code = 200
199+
}
200+
201+
// Build a call to the zarf tools wait-for command.
202+
return fmt.Sprintf("./zarf tools wait-for %s %s %d %s",
203+
network.Protocol, network.Address, network.Code, timeoutString), nil
204+
}
205+
206+
return "", fmt.Errorf("wait action is missing a cluster or network")
207+
}
208+
209+
// Perform some basic string mutations to make commands more useful.
210+
func actionCmdMutation(_ context.Context, cmd string, shellPref v1alpha1.Shell) (string, error) {
211+
zarfCommand, err := utils.GetFinalExecutableCommand()
212+
if err != nil {
213+
return cmd, err
214+
}
215+
216+
// Try to patch the zarf binary path in case the name isn't exactly "./zarf".
217+
cmd = strings.ReplaceAll(cmd, "./zarf ", zarfCommand+" ")
218+
219+
// Make commands 'more' compatible with Windows OS PowerShell
220+
if runtime.GOOS == "windows" && (exec.IsPowershell(shellPref.Windows) || shellPref.Windows == "") {
221+
// Replace "touch" with "New-Item" on Windows as it's a common command, but not POSIX so not aliased by M$.
222+
// See https://mathieubuisson.github.io/powershell-linux-bash/ &
223+
// http://web.cs.ucla.edu/~miryung/teaching/EE461L-Spring2012/labs/posix.html for more details.
224+
cmd = regexp.MustCompile(`^touch `).ReplaceAllString(cmd, `New-Item `)
225+
226+
// Convert any ${ZARF_VAR_*} or $ZARF_VAR_* to ${env:ZARF_VAR_*} or $env:ZARF_VAR_* respectively (also TF_VAR_*).
227+
// https://regex101.com/r/xk1rkw/1
228+
envVarRegex := regexp.MustCompile(`(?P<envIndicator>\${?(?P<varName>(ZARF|TF)_VAR_([a-zA-Z0-9_-])+)}?)`)
229+
get, err := helpers.MatchRegex(envVarRegex, cmd)
230+
if err == nil {
231+
newCmd := strings.ReplaceAll(cmd, get("envIndicator"), fmt.Sprintf("$Env:%s", get("varName")))
232+
message.Debugf("Converted command \"%s\" to \"%s\" t", cmd, newCmd)
233+
cmd = newCmd
234+
}
235+
}
236+
237+
return cmd, nil
238+
}
239+
240+
// Merge the ActionSet defaults with the action config.
241+
func actionGetCfg(_ context.Context, cfg v1alpha1.ZarfComponentActionDefaults, a v1alpha1.ZarfComponentAction, vars map[string]*variables.TextTemplate) v1alpha1.ZarfComponentActionDefaults {
242+
if a.Mute != nil {
243+
cfg.Mute = *a.Mute
244+
}
245+
246+
// Default is no timeout, but add a timeout if one is provided.
247+
if a.MaxTotalSeconds != nil {
248+
cfg.MaxTotalSeconds = *a.MaxTotalSeconds
249+
}
250+
251+
if a.MaxRetries != nil {
252+
cfg.MaxRetries = *a.MaxRetries
253+
}
254+
255+
if a.Dir != nil {
256+
cfg.Dir = *a.Dir
257+
}
258+
259+
if len(a.Env) > 0 {
260+
cfg.Env = append(cfg.Env, a.Env...)
261+
}
262+
263+
if a.Shell != nil {
264+
cfg.Shell = *a.Shell
265+
}
266+
267+
// Add variables to the environment.
268+
for k, v := range vars {
269+
// Remove # from env variable name.
270+
k = strings.ReplaceAll(k, "#", "")
271+
// Make terraform variables available to the action as TF_VAR_lowercase_name.
272+
k1 := strings.ReplaceAll(strings.ToLower(k), "zarf_var", "TF_VAR")
273+
cfg.Env = append(cfg.Env, fmt.Sprintf("%s=%s", k, v.Value))
274+
cfg.Env = append(cfg.Env, fmt.Sprintf("%s=%s", k1, v.Value))
275+
}
276+
277+
return cfg
278+
}
279+
280+
func actionRun(ctx context.Context, cfg v1alpha1.ZarfComponentActionDefaults, cmd string, shellPref v1alpha1.Shell, spinner *message.Spinner) (string, error) {
281+
shell, shellArgs := exec.GetOSShell(shellPref)
282+
283+
message.Debugf("Running command in %s: %s", shell, cmd)
284+
285+
execCfg := exec.Config{
286+
Env: cfg.Env,
287+
Dir: cfg.Dir,
288+
}
289+
290+
fmt.Println("exec cfg", execCfg.Dir)
291+
292+
if !cfg.Mute {
293+
execCfg.Stdout = spinner
294+
execCfg.Stderr = spinner
295+
}
296+
297+
out, errOut, err := exec.CmdWithContext(ctx, execCfg, shell, append(shellArgs, cmd)...)
298+
// Dump final complete output (respect mute to prevent sensitive values from hitting the logs).
299+
if !cfg.Mute {
300+
message.Debug(cmd, out, errOut)
301+
}
302+
303+
return out, err
304+
}

0 commit comments

Comments
 (0)