diff --git a/docs/4-user-guide/3-zarf-schema.md b/docs/4-user-guide/3-zarf-schema.md index f9ad04f416..4577812f25 100644 --- a/docs/4-user-guide/3-zarf-schema.md +++ b/docs/4-user-guide/3-zarf-schema.md @@ -852,6 +852,89 @@ Must be one of: +
+ + shell + +  +
+ + ## components > actions > onCreate > defaults > shell + +**Description:** (cmd only) Indicates a preference for a shell for the provided cmd to be executed in on supported operating systems + +| | | +| ------------------------- | -------------------------------------------------------------------------------------------------------- | +| **Type** | `object` | +| **Additional properties** | [![Not allowed](https://img.shields.io/badge/Not%20allowed-red)](# "Additional Properties not allowed.") | +| **Defined in** | #/definitions/ZarfComponentActionShell | + +
+ + windows + +  +
+ +**Description:** (default 'powershell') Indicates a preference for the shell to use on Windows systems (note that choosing 'cmd' will turn off migrations like touch -> New-Item) + +| | | +| -------- | -------- | +| **Type** | `string` | + +**Examples:** + + +"powershell", "cmd", "pwsh", "sh", "bash", "gsh" + +
+
+ +
+ + linux + +  +
+ +**Description:** (default 'sh') Indicates a preference for the shell to use on Linux systems + +| | | +| -------- | -------- | +| **Type** | `string` | + +**Examples:** + + +"sh", "bash", "fish", "zsh", "pwsh" + +
+
+ +
+ + darwin + +  +
+ +**Description:** (default 'sh') Indicates a preference for the shell to use on macOS systems + +| | | +| -------- | -------- | +| **Type** | `string` | + +**Examples:** + + +"sh", "bash", "fish", "zsh", "pwsh" + +
+
+ +
+
+ @@ -990,6 +1073,26 @@ Must be one of: +
+ + shell + +  +
+ + ## components > actions > onCreate > before > shell + +**Description:** (cmd only) Indicates a preference for a shell for the provided cmd to be executed in on supported operating systems + +| | | +| ------------------------- | -------------------------------------------------------------------------------------------------------- | +| **Type** | `object` | +| **Additional properties** | [![Not allowed](https://img.shields.io/badge/Not%20allowed-red)](# "Additional Properties not allowed.") | +| **Same definition as** | [shell](#components_items_actions_onCreate_defaults_shell) | + +
+
+
setVariables diff --git a/examples/component-actions/zarf.yaml b/examples/component-actions/zarf.yaml index 8ec106b02e..f84b407d41 100644 --- a/examples/component-actions/zarf.yaml +++ b/examples/component-actions/zarf.yaml @@ -39,6 +39,9 @@ components: - cmd: touch test-create-after.txt - cmd: sleep 1 - cmd: echo "I can print!" + # prefer to run the above command in "cmd" instead of "powershell" on Windows + shell: + windows: cmd - cmd: sleep 1 - cmd: | echo "multiline!" diff --git a/src/extensions/bigbang/bigbang.go b/src/extensions/bigbang/bigbang.go index eeab77575f..e93c2e92af 100644 --- a/src/extensions/bigbang/bigbang.go +++ b/src/extensions/bigbang/bigbang.go @@ -43,7 +43,7 @@ func Run(tmpPaths types.ComponentPaths, c types.ZarfComponent) (types.ZarfCompon cfg := c.Extensions.BigBang manifests := []types.ZarfManifest{} - err, validVersionResponse := isValidVersion(cfg.Version) + validVersionResponse, err := isValidVersion(cfg.Version) if err != nil { return c, fmt.Errorf("invalid Big Bang version: %s, parsing issue %s", cfg.Version, err) @@ -233,11 +233,11 @@ func Run(tmpPaths types.ComponentPaths, c types.ZarfComponent) (types.ZarfCompon } // isValidVersion check if the version is 1.54.0 or greater. -func isValidVersion(version string) (error, bool) { +func isValidVersion(version string) (bool, error) { specifiedVersion, err := semver.NewVersion(version) if err != nil { - return err, false + return false, err } minRequiredVersion, _ := semver.NewVersion(bbMinRequiredVersion) @@ -246,7 +246,7 @@ func isValidVersion(version string) (error, bool) { c, _ := semver.NewConstraint(fmt.Sprintf(">= %s-0", minRequiredVersion)) // This extension requires BB 1.54.0 or greater. - return nil, c.Check(specifiedVersion) + return c.Check(specifiedVersion), nil } // findBBResources takes a list of yaml objects (as a string) and diff --git a/src/extensions/bigbang/bigbang_test.go b/src/extensions/bigbang/bigbang_test.go index e6d1167089..6f8ec30f9c 100644 --- a/src/extensions/bigbang/bigbang_test.go +++ b/src/extensions/bigbang/bigbang_test.go @@ -1,33 +1,34 @@ package bigbang import ( - "github.com/stretchr/testify/assert" "testing" + + "github.com/stretchr/testify/assert" ) func TestRequiredBigBangVersions(t *testing.T) { // Support 1.54.0 and beyond - err, vv := isValidVersion("1.54.0") + vv, err := isValidVersion("1.54.0") assert.Equal(t, err, nil) assert.Equal(t, vv, true) // Do not support earlier than 1.54.0 - err, vv = isValidVersion("1.53.0") + vv, err = isValidVersion("1.53.0") assert.Equal(t, err, nil) assert.Equal(t, vv, false) // Support for Big Bang release candidates - err, vv = isValidVersion("1.57.0-rc.0") + vv, err = isValidVersion("1.57.0-rc.0") assert.Equal(t, err, nil) assert.Equal(t, vv, true) // Support for Big Bang 2.0.0 - err, vv = isValidVersion("2.0.0") + vv, err = isValidVersion("2.0.0") assert.Equal(t, err, nil) assert.Equal(t, vv, true) // Fail on non-semantic versions - err, vv = isValidVersion("1.57b") + vv, err = isValidVersion("1.57b") Expected := "Invalid Semantic Version" if err.Error() != Expected { t.Errorf("Error actual = %v, and Expected = %v.", err, Expected) diff --git a/src/internal/cluster/data.go b/src/internal/cluster/data.go index af09a79efa..d1767343c2 100644 --- a/src/internal/cluster/data.go +++ b/src/internal/cluster/data.go @@ -44,7 +44,7 @@ func (c *Cluster) HandleDataInjection(wg *sync.WaitGroup, data types.ZarfDataInj } // Get the OS shell to execute commands in - shell, shellArgs := exec.GetOSShell() + shell, shellArgs := exec.GetOSShell(types.ZarfComponentActionShell{Windows: "cmd"}) if _, _, err := exec.Cmd(shell, shellArgs, "tar --version"); err != nil { message.Error(err, "Unable to execute tar on this system. Please ensure it is installed and on your $PATH.") diff --git a/src/pkg/packager/actions.go b/src/pkg/packager/actions.go index f3e469b93f..66887890b0 100644 --- a/src/pkg/packager/actions.go +++ b/src/pkg/packager/actions.go @@ -89,7 +89,7 @@ func (p *Packager) runAction(defaultCfg types.ZarfComponentActionDefaults, actio cfg := actionGetCfg(defaultCfg, action, vars) - if cmd, err = actionCmdMutation(cmd); err != nil { + if cmd, err = actionCmdMutation(cmd, cfg.Shell); err != nil { spinner.Errorf(err, "Error mutating command: %s", cmdEscaped) } @@ -102,7 +102,7 @@ func (p *Packager) runAction(defaultCfg types.ZarfComponentActionDefaults, actio // 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, cfg, cmd, spinner); err != nil { + if out, err = actionRun(ctx, cfg, cmd, cfg.Shell, spinner); err != nil { return err } @@ -193,7 +193,7 @@ func convertWaitToCmd(wait types.ZarfComponentActionWait, timeout *int) (string, } // Perform some basic string mutations to make commands more useful. -func actionCmdMutation(cmd string) (string, error) { +func actionCmdMutation(cmd string, shellPref types.ZarfComponentActionShell) (string, error) { binaryPath, err := os.Executable() if err != nil { return cmd, err @@ -203,7 +203,7 @@ func actionCmdMutation(cmd string) (string, error) { cmd = strings.ReplaceAll(cmd, "./zarf ", binaryPath+" ") // Make commands 'more' compatible with Windows OS PowerShell - if runtime.GOOS == "windows" { + 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. @@ -246,6 +246,10 @@ func actionGetCfg(cfg types.ZarfComponentActionDefaults, a types.ZarfComponentAc 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. @@ -259,10 +263,10 @@ func actionGetCfg(cfg types.ZarfComponentActionDefaults, a types.ZarfComponentAc return cfg } -func actionRun(ctx context.Context, cfg types.ZarfComponentActionDefaults, cmd string, spinner *message.Spinner) (string, error) { - shell, shellArgs := exec.GetOSShell() +func actionRun(ctx context.Context, cfg types.ZarfComponentActionDefaults, cmd string, shellPref types.ZarfComponentActionShell, spinner *message.Spinner) (string, error) { + shell, shellArgs := exec.GetOSShell(shellPref) - message.Debug("Running command in %s: %s", shell, cmd) + message.Debugf("Running command in %s: %s", shell, cmd) execCfg := exec.Config{ Env: cfg.Env, diff --git a/src/pkg/utils/exec/exec.go b/src/pkg/utils/exec/exec.go index c1c000a8e8..09ca2a4794 100644 --- a/src/pkg/utils/exec/exec.go +++ b/src/pkg/utils/exec/exec.go @@ -14,6 +14,8 @@ import ( "os/exec" "runtime" "sync" + + "github.com/defenseunicorns/zarf/src/types" ) // Change terminal colors. @@ -152,17 +154,56 @@ func LaunchURL(url string) error { } // GetOSShell returns the shell and shellArgs based on the current OS -func GetOSShell() (string, string) { +func GetOSShell(shellPref types.ZarfComponentActionShell) (string, string) { var shell string var shellArgs string - if runtime.GOOS == "windows" { + switch runtime.GOOS { + case "windows": shell = "powershell" + if shellPref.Windows != "" { + shell = shellPref.Windows + } + shellArgs = "-Command" - } else { + if shell == "cmd" { + // Change shellArgs to /c if cmd is chosen + shellArgs = "/c" + } else if !IsPowershell(shell) { + // Change shellArgs to -c if a real shell is chosen + shellArgs = "-c" + } + case "darwin": + shell = "sh" + if shellPref.Darwin != "" { + shell = shellPref.Darwin + } + + shellArgs = "-c" + if IsPowershell(shell) { + // Change shellArgs to -Command if pwsh is chosen + shellArgs = "-Command" + } + case "linux": + shell = "sh" + if shellPref.Linux != "" { + shell = shellPref.Linux + } + + shellArgs = "-c" + if IsPowershell(shell) { + // Change shellArgs to -Command if pwsh is chosen + shellArgs = "-Command" + } + default: shell = "sh" shellArgs = "-c" } return shell, shellArgs } + +// IsPowershell returns whether a shell name is powershell +func IsPowershell(shellName string) bool { + return shellName == "powershell" || shellName == "pwsh" +} diff --git a/src/types/component.go b/src/types/component.go index 5f1faaa425..53cf7d7813 100644 --- a/src/types/component.go +++ b/src/types/component.go @@ -133,11 +133,19 @@ type ZarfComponentActionSet struct { // ZarfComponentActionDefaults sets the default configs for child actions type ZarfComponentActionDefaults struct { - Mute bool `json:"mute,omitempty" jsonschema:"description=Hide the output of commands during execution (default false)"` - MaxTotalSeconds int `json:"maxTotalSeconds,omitempty" jsonschema:"description=Default timeout in seconds for commands (default to 0, no timeout)"` - MaxRetries int `json:"maxRetries,omitempty" jsonschema:"description=Retry commands given number of times if they fail (default 0)"` - Dir string `json:"dir,omitempty" jsonschema:"description=Working directory for commands (default CWD)"` - Env []string `json:"env,omitempty" jsonschema:"description=Additional environment variables for commands"` + Mute bool `json:"mute,omitempty" jsonschema:"description=Hide the output of commands during execution (default false)"` + MaxTotalSeconds int `json:"maxTotalSeconds,omitempty" jsonschema:"description=Default timeout in seconds for commands (default to 0, no timeout)"` + MaxRetries int `json:"maxRetries,omitempty" jsonschema:"description=Retry commands given number of times if they fail (default 0)"` + Dir string `json:"dir,omitempty" jsonschema:"description=Working directory for commands (default CWD)"` + Env []string `json:"env,omitempty" jsonschema:"description=Additional environment variables for commands"` + Shell ZarfComponentActionShell `json:"shell,omitempty" jsonschema:"description=(cmd only) Indicates a preference for a shell for the provided cmd to be executed in on supported operating systems"` +} + +// ZarfComponentActionShell represents the desired shell to use for a given command +type ZarfComponentActionShell struct { + Windows string `json:"windows,omitempty" jsonschema:"description=(default 'powershell') Indicates a preference for the shell to use on Windows systems (note that choosing 'cmd' will turn off migrations like touch -> New-Item),example=powershell,example=cmd,example=pwsh,example=sh,example=bash,example=gsh"` + Linux string `json:"linux,omitempty" jsonschema:"description=(default 'sh') Indicates a preference for the shell to use on Linux systems,example=sh,example=bash,example=fish,example=zsh,example=pwsh"` + Darwin string `json:"darwin,omitempty" jsonschema:"description=(default 'sh') Indicates a preference for the shell to use on macOS systems,example=sh,example=bash,example=fish,example=zsh,example=pwsh"` } // ZarfComponentAction represents a single action to run during a zarf package operation @@ -148,6 +156,7 @@ type ZarfComponentAction struct { Dir *string `json:"dir,omitempty" jsonschema:"description=The working directory to run the command in (default is CWD)"` Env []string `json:"env,omitempty" jsonschema:"description=Additional environment variables to set for the command"` Cmd string `json:"cmd,omitempty" jsonschema:"description=The command to run. Must specify either cmd or wait for the action to do anything."` + Shell *ZarfComponentActionShell `json:"shell,omitempty" jsonschema:"description=(cmd only) Indicates a preference for a shell for the provided cmd to be executed in on supported operating systems"` DeprecatedSetVariable string `json:"setVariable,omitempty" jsonschema:"description=[Deprecated] (replaced by setVariables) (onDeploy/cmd only) The name of a variable to update with the output of the command. This variable will be available to all remaining actions and components in the package.,pattern=^[A-Z0-9_]+$"` SetVariables []ZarfComponentActionSetVariable `json:"setVariables,omitempty" jsonschema:"description=(onDeploy/cmd only) An array of variables to update with the output of the command. These variables will be available to all remaining actions and components in the package."` Description string `json:"description,omitempty" jsonschema:"description=Description of the action to be displayed during package execution instead of the command"` diff --git a/src/ui/lib/api-types.ts b/src/ui/lib/api-types.ts index 15bdf0041f..89c2b3ccbb 100644 --- a/src/ui/lib/api-types.ts +++ b/src/ui/lib/api-types.ts @@ -400,6 +400,11 @@ export interface ZarfComponentAction { * variables will be available to all remaining actions and components in the package. */ setVariables?: ZarfComponentActionSetVariable[]; + /** + * (cmd only) Indicates a preference for a shell for the provided cmd to be executed in on + * supported operating systems + */ + shell?: ZarfComponentActionShell; /** * Wait for a condition to be met before continuing. Must specify either cmd or wait for the * action. See the 'zarf tools wait-for' command for more info. @@ -423,6 +428,26 @@ export interface ZarfComponentActionSetVariable { sensitive?: boolean; } +/** + * (cmd only) Indicates a preference for a shell for the provided cmd to be executed in on + * supported operating systems + */ +export interface ZarfComponentActionShell { + /** + * (default 'sh') Indicates a preference for the shell to use on macOS systems + */ + darwin?: string; + /** + * (default 'sh') Indicates a preference for the shell to use on Linux systems + */ + linux?: string; + /** + * (default 'powershell') Indicates a preference for the shell to use on Windows systems + * (note that choosing 'cmd' will turn off migrations like touch -> New-Item) + */ + windows?: string; +} + /** * Wait for a condition to be met before continuing. Must specify either cmd or wait for the * action. See the 'zarf tools wait-for' command for more info. @@ -515,6 +540,11 @@ export interface ZarfComponentActionDefaults { * Hide the output of commands during execution (default false) */ mute?: boolean; + /** + * (cmd only) Indicates a preference for a shell for the provided cmd to be executed in on + * supported operating systems + */ + shell?: ZarfComponentActionShell; } export interface ZarfChart { @@ -1300,6 +1330,7 @@ const typeMap: any = { { json: "mute", js: "mute", typ: u(undefined, true) }, { json: "setVariable", js: "setVariable", typ: u(undefined, "") }, { json: "setVariables", js: "setVariables", typ: u(undefined, a(r("ZarfComponentActionSetVariable"))) }, + { json: "shell", js: "shell", typ: u(undefined, r("ZarfComponentActionShell")) }, { json: "wait", js: "wait", typ: u(undefined, r("ZarfComponentActionWait")) }, ], false), "ZarfComponentActionSetVariable": o([ @@ -1307,6 +1338,11 @@ const typeMap: any = { { json: "name", js: "name", typ: "" }, { json: "sensitive", js: "sensitive", typ: u(undefined, true) }, ], false), + "ZarfComponentActionShell": o([ + { json: "darwin", js: "darwin", typ: u(undefined, "") }, + { json: "linux", js: "linux", typ: u(undefined, "") }, + { json: "windows", js: "windows", typ: u(undefined, "") }, + ], false), "ZarfComponentActionWait": o([ { json: "cluster", js: "cluster", typ: u(undefined, r("ZarfComponentActionWaitCluster")) }, { json: "network", js: "network", typ: u(undefined, r("ZarfComponentActionWaitNetwork")) }, @@ -1328,6 +1364,7 @@ const typeMap: any = { { json: "maxRetries", js: "maxRetries", typ: u(undefined, 0) }, { json: "maxTotalSeconds", js: "maxTotalSeconds", typ: u(undefined, 0) }, { json: "mute", js: "mute", typ: u(undefined, true) }, + { json: "shell", js: "shell", typ: u(undefined, r("ZarfComponentActionShell")) }, ], false), "ZarfChart": o([ { json: "gitPath", js: "gitPath", typ: u(undefined, "") }, diff --git a/zarf.schema.json b/zarf.schema.json index f786474c9a..66aa0a2474 100644 --- a/zarf.schema.json +++ b/zarf.schema.json @@ -316,6 +316,10 @@ "type": "string", "description": "The command to run. Must specify either cmd or wait for the action to do anything." }, + "shell": { + "$ref": "#/definitions/ZarfComponentActionShell", + "description": "(cmd only) Indicates a preference for a shell for the provided cmd to be executed in on supported operating systems" + }, "setVariable": { "pattern": "^[A-Z0-9_]+$", "type": "string", @@ -366,6 +370,11 @@ }, "type": "array", "description": "Additional environment variables for commands" + }, + "shell": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/ZarfComponentActionShell", + "description": "(cmd only) Indicates a preference for a shell for the provided cmd to be executed in on supported operating systems" } }, "additionalProperties": false, @@ -433,6 +442,46 @@ "additionalProperties": false, "type": "object" }, + "ZarfComponentActionShell": { + "properties": { + "windows": { + "type": "string", + "description": "(default 'powershell') Indicates a preference for the shell to use on Windows systems (note that choosing 'cmd' will turn off migrations like touch -\u003e New-Item)", + "examples": [ + "powershell", + "cmd", + "pwsh", + "sh", + "bash", + "gsh" + ] + }, + "linux": { + "type": "string", + "description": "(default 'sh') Indicates a preference for the shell to use on Linux systems", + "examples": [ + "sh", + "bash", + "fish", + "zsh", + "pwsh" + ] + }, + "darwin": { + "type": "string", + "description": "(default 'sh') Indicates a preference for the shell to use on macOS systems", + "examples": [ + "sh", + "bash", + "fish", + "zsh", + "pwsh" + ] + } + }, + "additionalProperties": false, + "type": "object" + }, "ZarfComponentActionWait": { "properties": { "cluster": {