diff --git a/site/src/content/docs/commands/zarf_tools.md b/site/src/content/docs/commands/zarf_tools.md index 65a9e64ca0..eff8e62227 100644 --- a/site/src/content/docs/commands/zarf_tools.md +++ b/site/src/content/docs/commands/zarf_tools.md @@ -33,6 +33,7 @@ Collection of additional tools to make airgap easier ### SEE ALSO * [zarf](/commands/zarf/) - The Airgap Native Packager Manager for Kubernetes +* [zarf tools aliases](/commands/zarf_tools_aliases/) - Print POSIX shell aliases for bundled tools * [zarf tools archiver](/commands/zarf_tools_archiver/) - Compresses/Decompresses generic archives, including Zarf packages * [zarf tools clear-cache](/commands/zarf_tools_clear-cache/) - Clears the configured git and image cache directory * [zarf tools download-init](/commands/zarf_tools_download-init/) - Downloads the init package for the current Zarf version into the specified directory diff --git a/site/src/content/docs/commands/zarf_tools_aliases.md b/site/src/content/docs/commands/zarf_tools_aliases.md new file mode 100644 index 0000000000..028f4549e7 --- /dev/null +++ b/site/src/content/docs/commands/zarf_tools_aliases.md @@ -0,0 +1,40 @@ +--- +title: zarf tools aliases +description: Zarf CLI command reference for zarf tools aliases. +tableOfContents: false +--- + + + +## zarf tools aliases + +Print POSIX shell aliases for bundled tools + +``` +zarf tools aliases [flags] +``` + +### Options + +``` + -h, --help help for aliases +``` + +### Options inherited from parent commands + +``` + -a, --architecture string Architecture for OCI images and Zarf packages + --cache string Specify the location of the Zarf cache directory (default "~/.zarf-cache") + --features stringToString Provide a comma-separated list of feature names to bools to enable or disable. Ex. --features "foo=true,bar=false,baz=true" (default []) + --insecure-skip-tls-verify Skip checking server's certificate for validity. This flag should only be used if you have a specific reason and accept the reduced security posture. + --log-format string Select a logging format. Defaults to 'console'. Valid options are: 'console', 'json', 'dev'. (default "console") + -l, --log-level string Log level when running Zarf. Valid options are: warn, info, debug, trace (default "info") + --no-color Disable terminal color codes in logging and stdout prints. + --plain-http Force the connections over HTTP instead of HTTPS. This flag should only be used if you have a specific reason and accept the reduced security posture. + --tmpdir string Specify the temporary directory to use for intermediate files +``` + +### SEE ALSO + +* [zarf tools](/commands/zarf_tools/) - Collection of additional tools to make airgap easier + diff --git a/site/src/content/docs/ref/tools.mdx b/site/src/content/docs/ref/tools.mdx index af68dde649..c11c7565bc 100644 --- a/site/src/content/docs/ref/tools.mdx +++ b/site/src/content/docs/ref/tools.mdx @@ -17,6 +17,23 @@ TODO: finish this page for all embedded CLIs Zarf vendors in [`yq`](https://github.com/mikefarah/yq/) to provide a way to interact with YAML files. `yq` is a lightweight and portable command-line YAML processor. It can be used to extract specific parts of a YAML file, replace or delete values, and more. +## Shell aliases + +> command: [`zarf tools aliases`](/commands/zarf_tools_aliases) + +Source the generated aliases to use Zarf's bundled `kubectl`, `helm`, `yq`, `k9s`, and `syft` as standalone commands: + +```sh +source <(zarf tools aliases) +``` + +When using Bash, enable alias expansion before sourcing aliases from a package manifest or another noninteractive context: + +```sh +shopt -s expand_aliases +source <(zarf tools aliases) +``` + ## archiver > command: [`zarf tools archiver`](/commands/zarf_tools_archiver) diff --git a/src/cmd/tools.go b/src/cmd/tools.go index 2362b13c46..f2ae79f380 100644 --- a/src/cmd/tools.go +++ b/src/cmd/tools.go @@ -21,6 +21,7 @@ func newToolsCommand() *cobra.Command { v := getViper() cmd.AddCommand(newArchiverCommand()) + cmd.AddCommand(newToolsAliasesCommand()) cmd.AddCommand(newRegistryCommand()) cmd.AddCommand(newDeprecatedCraneCommand()) cmd.AddCommand(newHelmCommand()) diff --git a/src/cmd/tools_aliases.go b/src/cmd/tools_aliases.go new file mode 100644 index 0000000000..0d60413f7c --- /dev/null +++ b/src/cmd/tools_aliases.go @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2021-Present The Zarf Authors + +// Package cmd contains the CLI commands for Zarf. +package cmd + +import ( + "errors" + "fmt" + "os" + "runtime" + "strings" + "unicode" + + "github.com/spf13/cobra" + "github.com/zarf-dev/zarf/src/config" + "github.com/zarf-dev/zarf/src/config/lang" + "github.com/zarf-dev/zarf/src/pkg/utils" +) + +var ( + aliasedTools = []struct { + alias string + target string + }{ + {"kubectl", "kubectl"}, + {"helm", "helm"}, + {"yq", "yq"}, + {"k9s", "monitor"}, + {"syft", "sbom"}, + } + doubleQuoteEscaper = strings.NewReplacer( + `\`, `\\`, + `$`, `\$`, + "`", "\\`", + `"`, `\"`, + ) + toolsAliasesUsage = "aliases" +) + +func newToolsAliasesCommand() *cobra.Command { + return &cobra.Command{ + Use: toolsAliasesUsage, + Args: cobra.NoArgs, + Short: lang.CmdToolsAliasesShort, + RunE: func(cmd *cobra.Command, _ []string) error { + if runtime.GOOS == "windows" { + return errors.New(lang.CmdToolsAliasesErrWindows) + } + + executablePath, err := utils.GetFinalExecutablePath() + if err != nil { + return fmt.Errorf("resolving final executable path: %w", err) + } + + _, err = fmt.Fprint(cmd.OutOrStdout(), toolsAliases(executablePath, os.Args)) + + return err + }, + } +} + +func toolsAliases(executablePath string, args []string) string { + aliasComponents := []string{escape(executablePath)} + if config.ActionsCommandZarfPrefix != "" { + aliasComponents = append(aliasComponents, config.ActionsCommandZarfPrefix) + } + + for i, arg := range args { + if i == 0 { + continue + } + + if arg == toolsAliasesUsage { + break + } + + aliasComponents = append(aliasComponents, escape(arg)) + } + + aliasPrefix := strings.Join(aliasComponents, " ") + + var output strings.Builder + for _, tool := range aliasedTools { + fmt.Fprintf(&output, "alias %s=%s\n", tool.alias, quote(aliasPrefix+" "+tool.target)) + } + + return output.String() +} + +func escape(value string) string { + var escaped strings.Builder + + for _, r := range value { + if r == '\n' { + escaped.WriteString("'\n'") + continue + } + + if !isShellSafe(r) { + escaped.WriteByte('\\') + } + + escaped.WriteRune(r) + } + + return escaped.String() +} + +func isShellSafe(r rune) bool { + return unicode.IsLetter(r) || unicode.IsDigit(r) || strings.ContainsRune("_@%+=:,./-", r) +} + +func quote(value string) string { + if !strings.Contains(value, "'") { + return "'" + value + "'" + } + + return `"` + doubleQuoteEscaper.Replace(value) + `"` +} diff --git a/src/cmd/tools_aliases_test.go b/src/cmd/tools_aliases_test.go new file mode 100644 index 0000000000..96a0b10ccd --- /dev/null +++ b/src/cmd/tools_aliases_test.go @@ -0,0 +1,141 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2021-Present The Zarf Authors + +// Package cmd contains the CLI commands for Zarf. +package cmd + +import ( + "bytes" + "runtime" + "strings" + "testing" + + "github.com/stretchr/testify/require" + "github.com/zarf-dev/zarf/src/config" + "github.com/zarf-dev/zarf/src/config/lang" +) + +func TestToolsAliasesIntegration(t *testing.T) { + cmd := newToolsAliasesCommand() + + stdout := new(bytes.Buffer) + stderr := new(bytes.Buffer) + + cmd.SetOut(stdout) + cmd.SetErr(stderr) + + err := cmd.Execute() + + if runtime.GOOS == "windows" { + require.ErrorContains(t, err, lang.CmdToolsAliasesErrWindows) + return + } + + require.NoError(t, err) + + for i, alias := range strings.Split(strings.TrimSuffix(stdout.String(), "\n"), "\n") { + parts := strings.Split(alias, " ") + cmd := parts[len(parts)-1] + require.Contains(t, cmd, aliasedTools[i].target) + } +} + +func TestToolsAliasesOutput(t *testing.T) { + tests := map[string]struct { + before func() func() + executablePath string + args []string + expected string + }{ + "standalone executable": { + executablePath: "/path/zarf", + args: []string{"/path/zarf", "tools", "aliases"}, + expected: "" + + "alias kubectl='/path/zarf tools kubectl'\n" + + "alias helm='/path/zarf tools helm'\n" + + "alias yq='/path/zarf tools yq'\n" + + "alias k9s='/path/zarf tools monitor'\n" + + "alias syft='/path/zarf tools sbom'\n", + }, + "embedded executable": { + executablePath: "/path/uds", + args: []string{"/path/uds", "zarf", "tools", "aliases"}, + expected: "" + + "alias kubectl='/path/uds zarf tools kubectl'\n" + + "alias helm='/path/uds zarf tools helm'\n" + + "alias yq='/path/uds zarf tools yq'\n" + + "alias k9s='/path/uds zarf tools monitor'\n" + + "alias syft='/path/uds zarf tools sbom'\n", + }, + "escapes whitespace and shell metacharacters": { + executablePath: "/path with spaces/$zarf", + args: []string{"/path with spaces/$zarf", "tools", "aliases"}, + expected: "" + + `alias kubectl='/path\ with\ spaces/\$zarf tools kubectl'` + "\n" + + `alias helm='/path\ with\ spaces/\$zarf tools helm'` + "\n" + + `alias yq='/path\ with\ spaces/\$zarf tools yq'` + "\n" + + `alias k9s='/path\ with\ spaces/\$zarf tools monitor'` + "\n" + + `alias syft='/path\ with\ spaces/\$zarf tools sbom'` + "\n", + }, + "escapes double-quote-unfriendly characters": { + executablePath: "/linus' $special$ k8s tools/zarf", + args: []string{"/linus' $special$ k8s tools/zarf", "tools", "aliases"}, + expected: "" + + `alias kubectl="/linus\\'\\ \\\$special\\\$\\ k8s\\ tools/zarf tools kubectl"` + "\n" + + `alias helm="/linus\\'\\ \\\$special\\\$\\ k8s\\ tools/zarf tools helm"` + "\n" + + `alias yq="/linus\\'\\ \\\$special\\\$\\ k8s\\ tools/zarf tools yq"` + "\n" + + `alias k9s="/linus\\'\\ \\\$special\\\$\\ k8s\\ tools/zarf tools monitor"` + "\n" + + `alias syft="/linus\\'\\ \\\$special\\\$\\ k8s\\ tools/zarf tools sbom"` + "\n", + }, + "multi-token embedded prefix": { + executablePath: "/path/uds", + args: []string{"/path/uds", "wrapper", "zarf", "tools", "aliases"}, + expected: "" + + "alias kubectl='/path/uds wrapper zarf tools kubectl'\n" + + "alias helm='/path/uds wrapper zarf tools helm'\n" + + "alias yq='/path/uds wrapper zarf tools yq'\n" + + "alias k9s='/path/uds wrapper zarf tools monitor'\n" + + "alias syft='/path/uds wrapper zarf tools sbom'\n", + }, + "with custom zarf prefix": { + before: func() func() { + oldPrefix := config.ActionsCommandZarfPrefix + config.ActionsCommandZarfPrefix = "thisisatest" + return func() { + config.ActionsCommandZarfPrefix = oldPrefix + } + }, + executablePath: "/path/uds", + args: []string{"/path/uds", "zarf", "tools", "aliases"}, + expected: "" + + "alias kubectl='/path/uds thisisatest zarf tools kubectl'\n" + + "alias helm='/path/uds thisisatest zarf tools helm'\n" + + "alias yq='/path/uds thisisatest zarf tools yq'\n" + + "alias k9s='/path/uds thisisatest zarf tools monitor'\n" + + "alias syft='/path/uds thisisatest zarf tools sbom'\n", + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + if test.before != nil { + after := test.before() + defer after() + } + require.Equal(t, test.expected, toolsAliases(test.executablePath, test.args)) + }) + } +} + +func TestToolsAliasesTargetsExistInCommandTree(t *testing.T) { + rootCmd := NewZarfCommand() + + for _, tool := range aliasedTools { + t.Run(tool.alias, func(t *testing.T) { + cmd, _, err := rootCmd.Find([]string{"tools", tool.target}) + + require.NoError(t, err) + require.Equal(t, tool.target, cmd.Name()) + }) + } +} diff --git a/src/config/lang/english.go b/src/config/lang/english.go index 5f2951d2b8..441fa64043 100644 --- a/src/config/lang/english.go +++ b/src/config/lang/english.go @@ -552,7 +552,9 @@ $ zarf package pull oci://ghcr.io/zarf-dev/packages/dos-games:1.3.0 -a skeleton` CmdDevLintLong = "Verifies the package schema, checks if any variables won't be evaluated, and checks for unpinned images/repos/files" // zarf tools - CmdToolsShort = "Collection of additional tools to make airgap easier" + CmdToolsShort = "Collection of additional tools to make airgap easier" + CmdToolsAliasesShort = "Print POSIX shell aliases for bundled tools" + CmdToolsAliasesErrWindows = "zarf tools aliases is only supported on Linux and Unix systems" CmdToolsArchiverShort = "Compresses/Decompresses generic archives, including Zarf packages" CmdToolsArchiverCompressShort = "Compresses a collection of sources based off of the destination file extension." diff --git a/src/test/e2e/00_use_cli_test.go b/src/test/e2e/00_use_cli_test.go index 1a7cfa3704..c1d34dadb1 100644 --- a/src/test/e2e/00_use_cli_test.go +++ b/src/test/e2e/00_use_cli_test.go @@ -19,6 +19,7 @@ import ( "github.com/otiai10/copy" "github.com/sigstore/sigstore-go/pkg/root" "github.com/stretchr/testify/require" + "github.com/zarf-dev/zarf/src/config/lang" "github.com/zarf-dev/zarf/src/pkg/logger" "github.com/zarf-dev/zarf/src/pkg/packager/layout" "github.com/zarf-dev/zarf/src/test" @@ -358,6 +359,25 @@ components: }) } +func TestToolsAliases(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip(lang.CmdToolsAliasesErrWindows) + } + t.Parallel() + + stdOut, stdErr, err := e2e.Zarf(t, "tools", "aliases") + + require.NoError(t, err, stdOut, stdErr) + + expected := "" + + "alias kubectl='" + e2e.ZarfBinPath + " tools kubectl'\n" + + "alias helm='" + e2e.ZarfBinPath + " tools helm'\n" + + "alias yq='" + e2e.ZarfBinPath + " tools yq'\n" + + "alias k9s='" + e2e.ZarfBinPath + " tools monitor'\n" + + "alias syft='" + e2e.ZarfBinPath + " tools sbom'\n" + require.Equal(t, expected, stdOut) +} + func TestBuildMachineInfo(t *testing.T) { t.Parallel()