-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
chore: migrate
exec
behavior for maru (#80)
## Description This migrates `exec` here in preparation of `maru` disconnecting itself from `zarf`. ## Related Issue Fixes #N/A --------- Co-authored-by: razzle <[email protected]> Co-authored-by: Lucas Rodriguez <[email protected]>
- Loading branch information
1 parent
549be52
commit a51bbe7
Showing
9 changed files
with
583 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
name: Release Exec | ||
|
||
on: | ||
push: | ||
branches: | ||
- main | ||
paths: | ||
- "exec/**" | ||
|
||
permissions: | ||
contents: read | ||
|
||
jobs: | ||
bump-version-and-release-notes: | ||
runs-on: ubuntu-latest | ||
outputs: | ||
new-version: ${{ steps.bump-version.outputs.new-version }} | ||
steps: | ||
- name: Checkout | ||
uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 | ||
with: | ||
fetch-depth: 0 | ||
|
||
- name: Bump Version and Generate Release Notes | ||
uses: ./.github/actions/bump-and-notes | ||
id: bump-version | ||
with: | ||
module: "exec" | ||
|
||
release: | ||
runs-on: ubuntu-latest | ||
needs: bump-version-and-release-notes | ||
# contents: write via the GH app | ||
environment: release | ||
steps: | ||
- name: Checkout | ||
uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 | ||
with: | ||
fetch-depth: 0 | ||
|
||
- name: Download Release Notes | ||
uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7 | ||
with: | ||
name: release-notes | ||
|
||
- name: Get pkg app token | ||
id: pkg-app-token | ||
uses: actions/create-github-app-token@a0de6af83968303c8c955486bf9739a57d23c7f1 # v1.10.0 | ||
with: | ||
app-id: ${{ vars.PKG_WORKFLOW_GITHUB_APP_ID }} | ||
private-key: ${{ secrets.PKG_WORKFLOW_GITHUB_APP_SECRET }} | ||
owner: defenseunicorns | ||
repositories: pkg | ||
|
||
- name: Release | ||
env: | ||
GH_TOKEN: ${{ steps.pkg-app-token.outputs.token }} | ||
NEW_VERSION: ${{ needs.bump-version-and-release-notes.outputs.new-version }} | ||
run: | | ||
gh release create "$NEW_VERSION" --title "$NEW_VERSION" --notes-file notes.md |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,128 @@ | ||
// SPDX-License-Identifier: Apache-2.0 | ||
// SPDX-FileCopyrightText: 2024-Present Defense Unicorns | ||
|
||
// Package exec provides a wrapper around the os/exec package | ||
package exec | ||
|
||
import ( | ||
"bytes" | ||
"context" | ||
"errors" | ||
"fmt" | ||
"io" | ||
"os" | ||
"os/exec" | ||
"runtime" | ||
"strings" | ||
|
||
"golang.org/x/sync/errgroup" | ||
) | ||
|
||
// Config is a struct for configuring the Cmd function. | ||
type Config struct { | ||
Print bool | ||
Dir string | ||
Env []string | ||
CommandPrinter func(format string, a ...any) | ||
Stdout io.Writer | ||
Stderr io.Writer | ||
} | ||
|
||
// Cmd executes a given command with given config. | ||
func Cmd(config Config, command string, args ...string) (string, string, error) { | ||
return CmdWithContext(context.TODO(), config, command, args...) | ||
} | ||
|
||
// CmdWithContext executes a given command with given config. | ||
func CmdWithContext(ctx context.Context, config Config, command string, args ...string) (string, string, error) { | ||
if command == "" { | ||
return "", "", errors.New("command is required") | ||
} | ||
|
||
// Set up the command. | ||
cmd := exec.CommandContext(ctx, command, args...) | ||
cmd.Dir = config.Dir | ||
cmd.Env = append(os.Environ(), config.Env...) | ||
|
||
// Capture the command outputs. | ||
cmdStdout, _ := cmd.StdoutPipe() | ||
cmdStderr, _ := cmd.StderrPipe() | ||
|
||
var ( | ||
stdoutBuf, stderrBuf bytes.Buffer | ||
) | ||
|
||
stdoutWriters := []io.Writer{ | ||
&stdoutBuf, | ||
} | ||
|
||
stdErrWriters := []io.Writer{ | ||
&stderrBuf, | ||
} | ||
|
||
// Add the writers if requested. | ||
if config.Stdout != nil { | ||
stdoutWriters = append(stdoutWriters, config.Stdout) | ||
} | ||
|
||
if config.Stderr != nil { | ||
stdErrWriters = append(stdErrWriters, config.Stderr) | ||
} | ||
|
||
// Print to stdout if requested. | ||
if config.Print { | ||
stdoutWriters = append(stdoutWriters, os.Stdout) | ||
stdErrWriters = append(stdErrWriters, os.Stderr) | ||
} | ||
|
||
// Bind all the writers. | ||
stdout := io.MultiWriter(stdoutWriters...) | ||
stderr := io.MultiWriter(stdErrWriters...) | ||
|
||
// If a CommandPrinter was provided print the command. | ||
if config.CommandPrinter != nil { | ||
config.CommandPrinter("%s %s", command, strings.Join(args, " ")) | ||
} | ||
|
||
// Start the command. | ||
if err := cmd.Start(); err != nil { | ||
return "", "", err | ||
} | ||
|
||
// Add to waitgroup for each goroutine. | ||
g := new(errgroup.Group) | ||
|
||
// Run a goroutine to capture the command's stdout live. | ||
g.Go(func() error { | ||
_, err := io.Copy(stdout, cmdStdout) | ||
return err | ||
}) | ||
|
||
// Run a goroutine to capture the command's stderr live. | ||
g.Go(func() error { | ||
_, err := io.Copy(stderr, cmdStderr) | ||
return err | ||
}) | ||
|
||
// Wait for the goroutines to finish and abort if there was an error capturing the command's outputs. | ||
if err := g.Wait(); err != nil { | ||
return "", "", fmt.Errorf("failed to capture the command output: %w", err) | ||
} | ||
|
||
// Return the buffered outputs, regardless of whether we printed them. | ||
return stdoutBuf.String(), stderrBuf.String(), cmd.Wait() | ||
} | ||
|
||
// LaunchURL opens a URL in the default browser. | ||
func LaunchURL(url string) error { | ||
switch runtime.GOOS { | ||
case "linux": | ||
return exec.Command("xdg-open", url).Start() | ||
case "windows": | ||
return exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start() | ||
case "darwin": | ||
return exec.Command("open", url).Start() | ||
} | ||
|
||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
// SPDX-License-Identifier: Apache-2.0 | ||
// SPDX-FileCopyrightText: 2024-Present Defense Unicorns | ||
|
||
package exec | ||
|
||
import ( | ||
"bytes" | ||
"errors" | ||
"testing" | ||
) | ||
|
||
func TestCmd(t *testing.T) { | ||
type test struct { | ||
config Config | ||
command string | ||
args []string | ||
wantStdOut string | ||
wantStdErr string | ||
wantErr error | ||
} | ||
|
||
var stdOutBuff bytes.Buffer | ||
var stdErrBuff bytes.Buffer | ||
|
||
tests := []test{ | ||
{wantErr: errors.New("command is required")}, | ||
{config: Config{}, command: "echo", args: []string{"hello kitteh"}, wantStdOut: "hello kitteh\n"}, | ||
{config: Config{Env: []string{"ARCH=amd64"}}, command: "printenv", args: []string{"ARCH"}, wantStdOut: "amd64\n"}, | ||
{config: Config{Dir: "/"}, command: "pwd", wantStdOut: "/\n"}, | ||
{config: Config{Stdout: &stdOutBuff}, command: "sh", args: []string{"-c", "echo \"hello kitteh out\""}, wantStdOut: "hello kitteh out\n"}, | ||
{config: Config{Stderr: &stdErrBuff}, command: "sh", args: []string{"-c", "echo \"hello kitteh err\" >&2"}, wantStdErr: "hello kitteh err\n"}, | ||
} | ||
|
||
// Run tests without registering command mutations | ||
for _, tc := range tests { | ||
gotStdOut, gotStdErr, gotErr := Cmd(tc.config, tc.command, tc.args...) | ||
if gotStdOut != tc.wantStdOut { | ||
t.Fatalf("wanted std out: %s, got std out: %s", tc.wantStdOut, gotStdOut) | ||
} | ||
if gotStdErr != tc.wantStdErr { | ||
t.Fatalf("wanted std err: %s, got std err: %s", tc.wantStdErr, gotStdErr) | ||
} | ||
if gotErr != nil && tc.wantErr != nil { | ||
if gotErr.Error() != tc.wantErr.Error() { | ||
t.Fatalf("wanted err: %s, got err: %s", tc.wantErr, gotErr) | ||
} | ||
} else if gotErr != nil { | ||
t.Fatalf("got unexpected err: %s", gotErr) | ||
} | ||
} | ||
|
||
stdOutBufferString := stdOutBuff.String() | ||
if stdOutBufferString != "hello kitteh out\n" { | ||
t.Fatalf("wanted std out buffer: hello kitteh out\n got std out buffer: %s", stdOutBufferString) | ||
} | ||
|
||
stdErrBufferString := stdErrBuff.String() | ||
if stdErrBufferString != "hello kitteh err\n" { | ||
t.Fatalf("wanted std err buffer: hello kitteh err\n got std err buffer: %s", stdErrBufferString) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
module github.com/defenseunicorns/pkg/exec | ||
|
||
go 1.21.8 | ||
|
||
replace github.com/defenseunicorns/pkg/helpers => ../helpers | ||
|
||
require ( | ||
github.com/defenseunicorns/pkg/helpers v1.1.1 | ||
golang.org/x/sync v0.6.0 | ||
) | ||
|
||
require ( | ||
github.com/otiai10/copy v1.14.0 // indirect | ||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect | ||
oras.land/oras-go/v2 v2.5.0 // indirect | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= | ||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||
github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU= | ||
github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w= | ||
github.com/otiai10/mint v1.5.1 h1:XaPLeE+9vGbuyEHem1JNk3bYc7KKqyI/na0/mLd/Kks= | ||
github.com/otiai10/mint v1.5.1/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM= | ||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | ||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | ||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= | ||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= | ||
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= | ||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= | ||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= | ||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= | ||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | ||
oras.land/oras-go/v2 v2.5.0 h1:o8Me9kLY74Vp5uw07QXPiitjsw7qNXi8Twd+19Zf02c= | ||
oras.land/oras-go/v2 v2.5.0/go.mod h1:z4eisnLP530vwIOUOJeBIj0aGI0L1C3d53atvCBqZHg= |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
// SPDX-License-Identifier: Apache-2.0 | ||
// SPDX-FileCopyrightText: 2024-Present Defense Unicorns | ||
|
||
package exec | ||
|
||
import "runtime" | ||
|
||
// ShellPreference represents the desired shell to use for a given command | ||
type ShellPreference 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"` | ||
} | ||
|
||
// IsPowerShell returns whether a shell name is PowerShell | ||
func IsPowerShell(shellName string) bool { | ||
return shellName == "powershell" || shellName == "pwsh" | ||
} | ||
|
||
// GetOSShell returns the shell and shellArgs based on the current OS | ||
func GetOSShell(shellPref ShellPreference) (string, []string) { | ||
return getOSShellForOS(shellPref, runtime.GOOS) | ||
} | ||
|
||
func getOSShellForOS(shellPref ShellPreference, operatingSystem string) (string, []string) { | ||
var shell string | ||
var shellArgs []string | ||
powershellShellArgs := []string{"-Command", "$ErrorActionPreference = 'Stop';"} | ||
shShellArgs := []string{"-e", "-c"} | ||
|
||
switch operatingSystem { | ||
case "windows": | ||
shell = "powershell" | ||
if shellPref.Windows != "" { | ||
shell = shellPref.Windows | ||
} | ||
|
||
shellArgs = powershellShellArgs | ||
if shell == "cmd" { | ||
// Change shellArgs to /c if cmd is chosen | ||
shellArgs = []string{"/c"} | ||
} else if !IsPowerShell(shell) { | ||
// Change shellArgs to -c if a real shell is chosen | ||
shellArgs = shShellArgs | ||
} | ||
case "darwin": | ||
shell = "sh" | ||
if shellPref.Darwin != "" { | ||
shell = shellPref.Darwin | ||
} | ||
|
||
shellArgs = shShellArgs | ||
if IsPowerShell(shell) { | ||
// Change shellArgs to -Command if pwsh is chosen | ||
shellArgs = powershellShellArgs | ||
} | ||
case "linux": | ||
shell = "sh" | ||
if shellPref.Linux != "" { | ||
shell = shellPref.Linux | ||
} | ||
|
||
shellArgs = shShellArgs | ||
if IsPowerShell(shell) { | ||
// Change shellArgs to -Command if pwsh is chosen | ||
shellArgs = powershellShellArgs | ||
} | ||
default: | ||
shell = "sh" | ||
shellArgs = shShellArgs | ||
} | ||
|
||
return shell, shellArgs | ||
} |
Oops, something went wrong.