Skip to content

Commit

Permalink
feat(cli): Add options to pass dotfile script env vars
Browse files Browse the repository at this point in the history
  • Loading branch information
charlysotelo committed Dec 22, 2024
1 parent fe07d22 commit fdd5ae8
Show file tree
Hide file tree
Showing 2 changed files with 93 additions and 25 deletions.
113 changes: 88 additions & 25 deletions cmd/up.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"os"
"os/exec"
"path/filepath"
"slices"
"strconv"
"strings"

Expand Down Expand Up @@ -64,8 +65,10 @@ type UpCmd struct {

SSHConfigPath string

DotfilesSource string
DotfilesScript string
DotfilesSource string
DotfilesScript string
DotfilesScriptEnv []string // Key=Value to pass to install script
DotfilesScriptEnvFile []string // Paths to files containing Key=Value pairs to pass to install script
}

// NewUpCmd creates a new up command
Expand Down Expand Up @@ -98,6 +101,8 @@ func NewUpCmd(f *flags.GlobalFlags) *cobra.Command {
upCmd.Flags().StringVar(&cmd.SSHConfigPath, "ssh-config", "", "The path to the ssh config to modify, if empty will use ~/.ssh/config")
upCmd.Flags().StringVar(&cmd.DotfilesSource, "dotfiles", "", "The path or url to the dotfiles to use in the container")
upCmd.Flags().StringVar(&cmd.DotfilesScript, "dotfiles-script", "", "The path in dotfiles directory to use to install the dotfiles, if empty will try to guess")
upCmd.Flags().StringSliceVar(&cmd.DotfilesScriptEnv, "dotfiles-script-env", []string{}, "Extra environment variables to put into the dotfiles install script. E.g. MY_ENV_VAR=MY_VALUE")
upCmd.Flags().StringSliceVar(&cmd.DotfilesScriptEnvFile, "dotfiles-script-env-file", []string{}, "The path to files containing environment variables to set for the dotfiles install script")
upCmd.Flags().StringArrayVar(&cmd.IDEOptions, "ide-option", []string{}, "IDE option in the form KEY=VALUE")
upCmd.Flags().StringVar(&cmd.DevContainerImage, "devcontainer-image", "", "The container image to use, this will override the devcontainer.json value in the project")
upCmd.Flags().StringVar(&cmd.DevContainerPath, "devcontainer-path", "", "The path to the devcontainer.json relative to the project")
Expand Down Expand Up @@ -202,7 +207,7 @@ func (cmd *UpCmd) Run(
}

// setup dotfiles in the container
err = setupDotfiles(cmd.DotfilesSource, cmd.DotfilesScript, client, devPodConfig, log)
err = setupDotfiles(cmd.DotfilesSource, cmd.DotfilesScript, cmd.DotfilesScriptEnvFile, cmd.DotfilesScriptEnv, client, devPodConfig, log)
if err != nil {
return err
}
Expand Down Expand Up @@ -993,6 +998,7 @@ func createSSHCommand(

func setupDotfiles(
dotfiles, script string,
envFiles, envKeyValuePairs []string,
client client2.BaseWorkspaceClient,
devPodConfig *config.Config,
log log.Logger,
Expand All @@ -1015,11 +1021,32 @@ func setupDotfiles(
log.Infof("Dotfiles git repository %s specified", dotfilesRepo)
log.Debug("Cloning dotfiles into the devcontainer...")

execPath, err := os.Executable()
dotCmd, err := buildDotCmd(dotfilesRepo, dotfilesScript, envFiles, envKeyValuePairs, devPodConfig, client, log)
if err != nil {
return err
}
if log.GetLevel() == logrus.DebugLevel {
dotCmd.Args = append(dotCmd.Args, "--debug")
}

log.Debugf("Running command: %v", dotCmd.Args)

writer := log.Writer(logrus.InfoLevel, false)

dotCmd.Stdout = writer
dotCmd.Stderr = writer

err = dotCmd.Run()
if err != nil {
return err
}

log.Infof("Done setting up dotfiles into the devcontainer")

return nil
}

func buildDotCmdAgentArguments(dotfilesRepo, dotfilesScript string, log log.Logger) []string {
agentArguments := []string{
"agent",
"workspace",
Expand All @@ -1034,21 +1061,42 @@ func setupDotfiles(

if dotfilesScript != "" {
log.Infof("Dotfiles script %s specified", dotfilesScript)
agentArguments = append(agentArguments, "--install-script", dotfilesScript)
}

return agentArguments
}

func buildDotCmd(dotfilesRepo, dotfilesScript string, envFiles, envKeyValuePairs []string, devPodConfig *config.Config, client client2.BaseWorkspaceClient, log log.Logger) (*exec.Cmd, error) {
sshCmd := []string{
"ssh",
"--agent-forwarding=true",
"--start-services=true",
}

// Merge context and CLI options env files and collect key-value pairs
dotfilesScriptEnvFilesStr := devPodConfig.ContextOption(config.ContextOptionDotfilesScriptEnvFiles)
envFiles = append(envFiles, strings.Split(dotfilesScriptEnvFilesStr, ",")...)
envFilesKeyValuePairs, err := collectDotfilesScriptEnvKeyvaluePairs(envFiles)
if err != nil {
return nil, err
}

agentArguments = append(agentArguments, "--install-script")
agentArguments = append(agentArguments, dotfilesScript)
// Collect file-based and CLI options env variables names (aka keys) and
// configure ssh env var passthrough with send-env
allEnvKeyValuesPairs := slices.Concat(envFilesKeyValuePairs, envKeyValuePairs)
allEnvKeys := extractKeysFromEnvKeyValuePairs(allEnvKeyValuesPairs)
for _, envKey := range allEnvKeys {
sshCmd = append(sshCmd, "--send-env", envKey)
}

remoteUser, err := devssh.GetUser(client.WorkspaceConfig().ID, client.WorkspaceConfig().SSHConfigPath)
if err != nil {
remoteUser = "root"
}

dotCmd := exec.Command(
execPath,
"ssh",
"--agent-forwarding=true",
"--start-services=true",
agentArguments := buildDotCmdAgentArguments(dotfilesRepo, dotfilesScript, log)
sshCmd = append(sshCmd,
"--user",
remoteUser,
"--context",
Expand All @@ -1058,26 +1106,41 @@ func setupDotfiles(
"--command",
agent.ContainerDevPodHelperLocation+" "+strings.Join(agentArguments, " "),
)

if log.GetLevel() == logrus.DebugLevel {
dotCmd.Args = append(dotCmd.Args, "--debug")
execPath, err := os.Executable()
if err != nil {
return nil, err
}

log.Debugf("Running command: %v", dotCmd.Args)

writer := log.Writer(logrus.InfoLevel, false)
dotCmd := exec.Command(
execPath,
sshCmd...,
)

dotCmd.Stdout = writer
dotCmd.Stderr = writer
dotCmd.Env = append(os.Environ(), allEnvKeyValuesPairs...)
return dotCmd, nil
}

err = dotCmd.Run()
if err != nil {
return err
func extractKeysFromEnvKeyValuePairs(envKeyValuePairs []string) []string {
keys := []string{}
for _, env := range envKeyValuePairs {
keyValue := strings.SplitN(env, "=", 2)
if len(keyValue) == 2 {
keys = append(keys, keyValue[0])
}
}
return keys
}

log.Infof("Done setting up dotfiles into the devcontainer")

return nil
func collectDotfilesScriptEnvKeyvaluePairs(envFiles []string) ([]string, error) {
keyValues := []string{}
for _, file := range envFiles {
envFromFile, err := config2.ParseKeyValueFile(file)
if err != nil {
return nil, err
}
keyValues = append(keyValues, envFromFile...)
}
return keyValues, nil
}

func setupGitSSHSignature(signingKey string, client client2.BaseWorkspaceClient, log log.Logger) error {
Expand Down
5 changes: 5 additions & 0 deletions pkg/config/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const (
ContextOptionAgentURL = "AGENT_URL"
ContextOptionDotfilesURL = "DOTFILES_URL"
ContextOptionDotfilesScript = "DOTFILES_SCRIPT"
ContextOptionDotfilesScriptEnvFiles = "DOTFILES_SCRIPT_ENV_FILES"
ContextOptionSSHAgentForwarding = "SSH_AGENT_FORWARDING"
ContextOptionSSHConfigPath = "SSH_CONFIG_PATH"
ContextOptionAgentInjectTimeout = "AGENT_INJECT_TIMEOUT"
Expand Down Expand Up @@ -84,6 +85,10 @@ var ContextOptions = []ContextOption{
Name: ContextOptionDotfilesScript,
Description: "Specifies the script to run after cloning dotfiles repo to install them",
},
{
Name: ContextOptionDotfilesScriptEnvFiles,
Description: "Specifies the comma-separated paths to local .env files to source environment variables from to pass to the dotfiles install script",
},
{
Name: ContextOptionSSHConfigPath,
Description: "Specifies the path where the ssh config should be written to",
Expand Down

0 comments on commit fdd5ae8

Please sign in to comment.