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 6 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
146 changes: 146 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,146 @@
// 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"
"sync"
)

// 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
}

// PrintCfg is a helper function for returning a Config struct with Print set to true.
func PrintCfg() Config {
return Config{Print: true}
}

// Cmd executes a given command with given config.
func Cmd(command string, args ...string) (string, string, error) {
return CmdWithContext(context.TODO(), Config{}, command, args...)
}

// CmdWithPrint executes a given command with given config and prints the command.
func CmdWithPrint(command string, args ...string) error {
_, _, err := CmdWithContext(context.TODO(), PrintCfg(), command, args...)
return err
}

// CmdWithContext executes a given command with given config.
func CmdWithContext(ctx context.Context, config Config, command string, args ...string) (string, string, error) {
Racer159 marked this conversation as resolved.
Show resolved Hide resolved
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
errStdout, errStderr error
wg sync.WaitGroup
)

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 we're printing, print the command.
if config.Print && config.CommandPrinter != nil {
config.CommandPrinter("%s %s", command, strings.Join(args, " "))
}
Racer159 marked this conversation as resolved.
Show resolved Hide resolved

// Start the command.
if err := cmd.Start(); err != nil {
return "", "", err
}

// Add to waitgroup for each goroutine.
wg.Add(2)

// Run a goroutine to capture the command's stdout live.
go func() {
_, errStdout = io.Copy(stdout, cmdStdout)
wg.Done()
}()

// Run a goroutine to capture the command's stderr live.
go func() {
_, errStderr = io.Copy(stderr, cmdStderr)
wg.Done()
}()

// Wait for the goroutines to finish (if any).
wg.Wait()
Racer159 marked this conversation as resolved.
Show resolved Hide resolved

// Abort if there was an error capturing the command's outputs.
if errStdout != nil {
return "", "", fmt.Errorf("failed to capture the stdout command output: %w", errStdout)
}
if errStderr != nil {
return "", "", fmt.Errorf("failed to capture the stderr command output: %w", errStderr)
}

// Wait for the command to finish and 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
}
12 changes: 12 additions & 0 deletions exec/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
module github.com/defenseunicorns/pkg/exec

go 1.21.8

require github.com/defenseunicorns/pkg/helpers v1.1.1
Racer159 marked this conversation as resolved.
Show resolved Hide resolved

require (
github.com/otiai10/copy v1.14.0 // indirect
golang.org/x/sync v0.6.0 // indirect
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect
oras.land/oras-go/v2 v2.5.0 // indirect
)
20 changes: 20 additions & 0 deletions exec/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
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/defenseunicorns/pkg/helpers v1.1.1 h1:p3pKeK5SeFaoZUJZIX9sEsJqX1CGGMS8OpQMPgJtSqM=
github.com/defenseunicorns/pkg/helpers v1.1.1/go.mod h1:F4S5VZLDrlNWQKklzv4v9tFWjjZNhxJ1gT79j4XiLwk=
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=
71 changes: 71 additions & 0 deletions exec/shell.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// 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 "runtime"

// Shell represents the desired shell to use for a given command
type Shell 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"`
}
Racer159 marked this conversation as resolved.
Show resolved Hide resolved

// GetOSShell returns the shell and shellArgs based on the current OS
func GetOSShell(shellPref Shell) (string, []string) {
var shell string
var shellArgs []string
powershellShellArgs := []string{"-Command", "$ErrorActionPreference = 'Stop';"}
shShellArgs := []string{"-e", "-c"}

switch runtime.GOOS {
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
}

// IsPowershell returns whether a shell name is powershell
func IsPowershell(shellName string) bool {
Racer159 marked this conversation as resolved.
Show resolved Hide resolved
return shellName == "powershell" || shellName == "pwsh"
}
61 changes: 61 additions & 0 deletions exec/utils.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 provides a wrapper around the os/exec package
Racer159 marked this conversation as resolved.
Show resolved Hide resolved
package exec

import (
"fmt"
"os"
"path/filepath"
"regexp"
"runtime"
"strings"

"github.com/defenseunicorns/pkg/helpers"
)

var registeredCmdMutations = map[string]string{}

// GetFinalExecutablePath returns the absolute path to the current executable, following any symlinks along the way.
func GetFinalExecutablePath() (string, error) {
binaryPath, err := os.Executable()
if err != nil {
return "", err
}

// In case the binary is symlinked somewhere else, get the final destination
linkedPath, err := filepath.EvalSymlinks(binaryPath)
return linkedPath, err
Racer159 marked this conversation as resolved.
Show resolved Hide resolved
}

// RegisterCmdMutation registers local ./ commands that should change to the specified cmdLocation
func RegisterCmdMutation(cmdKey string, cmdLocation string) {
registeredCmdMutations[fmt.Sprintf("./%s ", cmdKey)] = fmt.Sprintf("%s ", cmdLocation)
}

// MutateCommand performs some basic string mutations to make commands more useful.
func MutateCommand(cmd string, shellPref Shell) string {
for cmdKey, cmdLocation := range registeredCmdMutations {
cmd = strings.ReplaceAll(cmd, cmdKey, cmdLocation)
}

// Make commands 'more' compatible with Windows OS PowerShell
if runtime.GOOS == "windows" && (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.
cmd = regexp.MustCompile(`^touch `).ReplaceAllString(cmd, `New-Item `)

// Convert any ${ENV_*} or $ENV_* to ${Env:ENV_*} or $Env:ENV_* respectively.
// https://regex101.com/r/bBDfW2/1
envVarRegex := regexp.MustCompile(`(?P<envIndicator>\${?(?P<varName>([^E{]|E[^n]|En[^v]|Env[^:\s])([a-zA-Z0-9_-])+)}?)`)
get, err := helpers.MatchRegex(envVarRegex, cmd)
if err == nil {
newCmd := strings.ReplaceAll(cmd, get("envIndicator"), fmt.Sprintf("$Env:%s", get("varName")))
cmd = newCmd
}
}

return cmd
}
44 changes: 44 additions & 0 deletions variables/common.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2024-Present Defense Unicorns

// Package variables contains functions for interacting with variables
package variables

import (
"log/slog"
)

// VariableConfig represents a value to be templated into a text file.
type VariableConfig struct {
templatePrefix string
deprecatedKeys map[string]string

applicationTemplates map[string]*TextTemplate
setVariableMap SetVariableMap
constants []Constant

prompt func(variable InteractiveVariable) (value string, err error)
logger *slog.Logger
}

// New creates a new VariableConfig
func New(templatePrefix string, deprecatedKeys map[string]string, prompt func(variable InteractiveVariable) (value string, err error), logger *slog.Logger) *VariableConfig {
return &VariableConfig{
templatePrefix: templatePrefix,
deprecatedKeys: deprecatedKeys,
applicationTemplates: make(map[string]*TextTemplate),
setVariableMap: make(SetVariableMap),
prompt: prompt,
logger: logger,
}
}

// SetApplicationTemplates sets the application-specific templates for the variable config (i.e. ZARF_REGISTRY for Zarf)
func (vc *VariableConfig) SetApplicationTemplates(applicationTemplates map[string]*TextTemplate) {
vc.applicationTemplates = applicationTemplates
}

// SetConstants sets the constants for a variable config (templated as PREFIX_CONST_NAME)
func (vc *VariableConfig) SetConstants(constants []Constant) {
vc.constants = constants
}
12 changes: 12 additions & 0 deletions variables/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
module github.com/defenseunicorns/pkg/variables

go 1.21.8

require github.com/defenseunicorns/pkg/helpers v1.1.1

require (
github.com/otiai10/copy v1.14.0 // indirect
golang.org/x/sync v0.6.0 // indirect
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect
oras.land/oras-go/v2 v2.5.0 // indirect
)
20 changes: 20 additions & 0 deletions variables/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
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/defenseunicorns/pkg/helpers v1.1.1 h1:p3pKeK5SeFaoZUJZIX9sEsJqX1CGGMS8OpQMPgJtSqM=
github.com/defenseunicorns/pkg/helpers v1.1.1/go.mod h1:F4S5VZLDrlNWQKklzv4v9tFWjjZNhxJ1gT79j4XiLwk=
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=
Loading
Loading