Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: migrate exec behavior for maru #80

Merged
merged 39 commits into from
May 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
e2ef520
chore: migrate variables and exec behavior for maru
Racer159 May 8, 2024
89a57bd
add the go mods
Racer159 May 8, 2024
431994c
add exec utils
Racer159 May 8, 2024
275cf1f
add cmd mutation
Racer159 May 8, 2024
878a5dc
remove err
Racer159 May 8, 2024
1864651
add GetSetVariables
Racer159 May 8, 2024
400c678
Update exec/shell.go
Racer159 May 13, 2024
8367d9b
Update exec/utils.go
Racer159 May 13, 2024
135da1b
Merge branch 'main' into migrate-exec-variables
Racer159 May 13, 2024
f0343ec
fix feedback
Racer159 May 13, 2024
c72e9b4
command print config
Racer159 May 13, 2024
0fda3af
refactor to errgroups
Racer159 May 13, 2024
401ca1d
add replaces
Racer159 May 13, 2024
989901f
add replaces
Racer159 May 13, 2024
37298e0
chore: update progress writer interface
Racer159 May 14, 2024
8d91a6b
lint
Racer159 May 14, 2024
1523191
comment
Racer159 May 14, 2024
8485433
change to failf
Racer159 May 14, 2024
21cb320
fix error
Racer159 May 14, 2024
7e071f0
Merge branch 'change-progress-writer-interface' into migrate-exec-var…
Racer159 May 14, 2024
f7de3bf
fix booboo
Racer159 May 14, 2024
5289da8
Merge branch 'change-progress-writer-interface' into migrate-exec-var…
Racer159 May 14, 2024
a77ce38
fix exec (need tests)
Racer159 May 14, 2024
1a5968d
cover >75% of exec statements
Racer159 May 14, 2024
3171601
add variables tests
Racer159 May 15, 2024
36842ea
fix lint
Racer159 May 15, 2024
79b6b08
fix test
Racer159 May 15, 2024
fcf7b85
Merge branch 'main' into migrate-exec-variables
Racer159 May 15, 2024
063d2d1
address feedback for template test
Racer159 May 17, 2024
4fb8779
remove variables entirely
Racer159 May 17, 2024
df5d332
add exec release workflow
Racer159 May 17, 2024
c4f1bcd
Merge branch 'main' into migrate-exec-variables
May 17, 2024
19bd4fe
add a way to get a cmd mutation by its key
Racer159 May 20, 2024
2dfc91c
Update .gitignore
Racer159 May 20, 2024
9abb507
change shell preference
Racer159 May 20, 2024
ce97d70
switch to sync map
Racer159 May 20, 2024
a903c44
feedback
Racer159 May 20, 2024
f24933e
feedback and more tests
Racer159 May 20, 2024
a38ae59
deconflict zarf and other tests
Racer159 May 20, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions .github/workflows/release-exec.yaml
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
128 changes: 128 additions & 0 deletions exec/exec.go
Racer159 marked this conversation as resolved.
Show resolved Hide resolved
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()
Racer159 marked this conversation as resolved.
Show resolved Hide resolved
}

return nil
}
61 changes: 61 additions & 0 deletions exec/exec_test.go
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)
}
}
16 changes: 16 additions & 0 deletions exec/go.mod
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
)
18 changes: 18 additions & 0 deletions exec/go.sum
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=
74 changes: 74 additions & 0 deletions exec/shell.go
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
}
Loading
Loading