diff --git a/internal/cli/machine.go b/internal/cli/machine.go index 3eacbfc0..a2103b0e 100644 --- a/internal/cli/machine.go +++ b/internal/cli/machine.go @@ -2,6 +2,7 @@ package cli import ( "context" + "encoding/base64" "errors" "fmt" "os" @@ -13,14 +14,11 @@ import ( "github.com/psviderski/uncloud/internal/cli/tui" "github.com/psviderski/uncloud/internal/machine/api/pb" "github.com/psviderski/uncloud/internal/sshexec" + "github.com/psviderski/uncloud/scripts" "google.golang.org/protobuf/types/known/emptypb" ) -const ( - // TODO: support pinning the script version to the CLI version. - installScriptURL = "https://raw.githubusercontent.com/psviderski/uncloud/refs/heads/main/scripts/install.sh" - rootUser = "root" -) +const rootUser = "root" type RemoteMachine struct { User string @@ -30,26 +28,30 @@ type RemoteMachine struct { UseSSHGo bool // Use Go's built-in SSH library instead of the system ssh CLI command. } -func installCmd(user string, version string) string { +// installCmd returns a shell command that decodes the base64-encoded install script and pipes it +// into bash, optionally via sudo and with UNCLOUD_* environment variables set. +func installCmd(scriptBase64, user, version string) string { sudoPrefix := "" var env []string // Add the SSH user (non-root) to the uncloud group to allow access to the Uncloud daemon unix socket. if user != rootUser { - sudoPrefix = "sudo" + sudoPrefix = "sudo " env = append(env, "UNCLOUD_GROUP_ADD_USER="+sshexec.Quote(user)) } if version != "" { env = append(env, "UNCLOUD_VERSION="+sshexec.Quote(version)) } - envCmd := strings.Join(env, " ") - curlBashCmd := fmt.Sprintf("curl -fsSL %s | %s %s bash", sshexec.Quote(installScriptURL), sudoPrefix, envCmd) + envPrefix := "" + if len(env) > 0 { + envPrefix = strings.Join(env, " ") + " " + } - return curlBashCmd + return fmt.Sprintf("printf '%%s' %s | base64 -d | %s%sbash", scriptBase64, sudoPrefix, envPrefix) } -// provisionMachine provisions the remote machine by downloading the Uncloud install script from GitHub and running it. +// provisionMachine provisions the remote machine by running the Uncloud install script embedded in the uc CLI. // If version is specified, it will be passed to the install script as UNCLOUD_VERSION environment variable. func provisionMachine(ctx context.Context, exec sshexec.Executor, version string) error { user, err := exec.Run(ctx, "whoami") @@ -88,13 +90,10 @@ func provisionMachine(ctx context.Context, exec sshexec.Executor, version string } } - cmd := installCmd(user, version) - - fmt.Println("Downloading Uncloud install script:", installScriptURL) - - cmd = sshexec.QuoteCommand("bash", "-c", "set -o pipefail; "+cmd) + scriptBase64 := base64.StdEncoding.EncodeToString([]byte(scripts.InstallScript)) + cmd := sshexec.QuoteCommand("bash", "-c", "set -o pipefail; "+installCmd(scriptBase64, user, version)) if err = exec.Stream(ctx, cmd, os.Stdout, os.Stderr); err != nil { - return fmt.Errorf("download and run install script: %w", err) + return fmt.Errorf("run install script: %w", err) } return nil } diff --git a/internal/cli/machine_test.go b/internal/cli/machine_test.go index b43717b4..10531c42 100644 --- a/internal/cli/machine_test.go +++ b/internal/cli/machine_test.go @@ -7,30 +7,42 @@ import ( ) func TestInstallCmd(t *testing.T) { - t.Run("root", func(t *testing.T) { - cmd := installCmd("root", "") - assert.NotContains(t, cmd, "sudo") - assert.NotContains(t, cmd, "UNCLOUD_GROUP_ADD_USER") - }) + const scriptB64 = "SCRIPT_BASE64_PLACEHOLDER" - // Test with version - t.Run("root with version", func(t *testing.T) { - cmd := installCmd("root", "v1.2.3") - assert.NotContains(t, cmd, "sudo") - assert.NotContains(t, cmd, "UNCLOUD_GROUP_ADD_USER") - assert.Contains(t, cmd, "UNCLOUD_VERSION=v1.2.3") - }) + tests := []struct { + name string + user string + version string + want string + }{ + { + name: "root", + user: "root", + want: "printf '%s' SCRIPT_BASE64_PLACEHOLDER | base64 -d | bash", + }, + { + name: "root with version", + user: "root", + version: "v1.2.3", + want: "printf '%s' SCRIPT_BASE64_PLACEHOLDER | base64 -d | UNCLOUD_VERSION=v1.2.3 bash", + }, + { + name: "nonroot", + user: "nonroot", + want: "printf '%s' SCRIPT_BASE64_PLACEHOLDER | base64 -d | sudo UNCLOUD_GROUP_ADD_USER=nonroot bash", + }, + { + name: "nonroot with version", + user: "nonroot", + version: "v1.2.3", + want: "printf '%s' SCRIPT_BASE64_PLACEHOLDER | base64 -d | " + + "sudo UNCLOUD_GROUP_ADD_USER=nonroot UNCLOUD_VERSION=v1.2.3 bash", + }, + } - t.Run("nonroot", func(t *testing.T) { - cmd := installCmd("nonroot", "") - assert.Contains(t, cmd, "sudo") - assert.Contains(t, cmd, "UNCLOUD_GROUP_ADD_USER=nonroot") - }) - - t.Run("nonroot with version", func(t *testing.T) { - cmd := installCmd("nonroot", "v1.2.3") - assert.Contains(t, cmd, "sudo") - assert.Contains(t, cmd, "UNCLOUD_GROUP_ADD_USER=nonroot") - assert.Contains(t, cmd, "UNCLOUD_VERSION=v1.2.3") - }) -} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, installCmd(scriptB64, tt.user, tt.version)) + }) + } +} \ No newline at end of file diff --git a/scripts/scripts.go b/scripts/scripts.go new file mode 100644 index 00000000..fdea848a --- /dev/null +++ b/scripts/scripts.go @@ -0,0 +1,8 @@ +// Package scripts embeds shell scripts shipped with the uc CLI so they can be +// executed on remote machines without being fetched from the network. +package scripts + +import _ "embed" + +//go:embed install.sh +var InstallScript string