Skip to content

Commit

Permalink
Merge branch 'main' into cj/385-push-image-fail
Browse files Browse the repository at this point in the history
  • Loading branch information
johnstcn authored Oct 28, 2024
2 parents 48b94fa + 08bdb8d commit 8984909
Show file tree
Hide file tree
Showing 10 changed files with 144 additions and 13 deletions.
4 changes: 4 additions & 0 deletions cmd/envbuilder/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@ func envbuilderCmd() serpent.Command {
}
}

if o.GitSSHPrivateKeyPath != "" && o.GitSSHPrivateKeyBase64 != "" {
return errors.New("cannot have both GIT_SSH_PRIVATE_KEY_PATH and GIT_SSH_PRIVATE_KEY_BASE64 set")
}

if o.GetCachedImage {
img, err := envbuilder.RunCacheProbe(inv.Context(), o)
if err != nil {
Expand Down
3 changes: 2 additions & 1 deletion docs/env-variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@
| `--git-clone-single-branch` | `ENVBUILDER_GIT_CLONE_SINGLE_BRANCH` | | Clone only a single branch of the Git repository. |
| `--git-username` | `ENVBUILDER_GIT_USERNAME` | | The username to use for Git authentication. This is optional. |
| `--git-password` | `ENVBUILDER_GIT_PASSWORD` | | The password to use for Git authentication. This is optional. |
| `--git-ssh-private-key-path` | `ENVBUILDER_GIT_SSH_PRIVATE_KEY_PATH` | | Path to an SSH private key to be used for Git authentication. |
| `--git-ssh-private-key-path` | `ENVBUILDER_GIT_SSH_PRIVATE_KEY_PATH` | | Path to an SSH private key to be used for Git authentication. If this is set, then GIT_SSH_PRIVATE_KEY_BASE64 cannot be set. |
| `--git-ssh-private-key-base64` | `ENVBUILDER_GIT_SSH_PRIVATE_KEY_BASE64` | | Base64 encoded SSH private key to be used for Git authentication. If this is set, then GIT_SSH_PRIVATE_KEY_PATH cannot be set. |
| `--git-http-proxy-url` | `ENVBUILDER_GIT_HTTP_PROXY_URL` | | The URL for the HTTP proxy. This is optional. |
| `--workspace-folder` | `ENVBUILDER_WORKSPACE_FOLDER` | | The path to the workspace folder that will be built. This is optional. |
| `--ssl-cert-base64` | `ENVBUILDER_SSL_CERT_BASE64` | | The content of an SSL cert file. This is useful for self-signed certificates. |
Expand Down
4 changes: 2 additions & 2 deletions envbuilder.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ func Run(ctx context.Context, opts options.Options, preExec ...func()) error {
func run(ctx context.Context, opts options.Options, execArgs *execArgsInfo) error {
defer options.UnsetEnv()

workingDir := workingdir.At(opts.MagicDirBase)
workingDir := workingdir.At(opts.WorkingDirBase)

stageNumber := 0
startStage := func(format string, args ...any) func(format string, args ...any) {
Expand Down Expand Up @@ -964,7 +964,7 @@ func RunCacheProbe(ctx context.Context, opts options.Options) (v1.Image, error)
return nil, fmt.Errorf("--cache-repo must be set when using --get-cached-image")
}

workingDir := workingdir.At(opts.MagicDirBase)
workingDir := workingdir.At(opts.WorkingDirBase)

stageNumber := 0
startStage := func(format string, args ...any) func(format string, args ...any) {
Expand Down
28 changes: 28 additions & 0 deletions git/git.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package git

import (
"context"
"encoding/base64"
"errors"
"fmt"
"io"
Expand Down Expand Up @@ -181,6 +182,22 @@ func ReadPrivateKey(path string) (gossh.Signer, error) {
return k, nil
}

// DecodeBase64PrivateKey attempts to decode a base64 encoded private
// key and returns an ssh.Signer
func DecodeBase64PrivateKey(key string) (gossh.Signer, error) {
bs, err := base64.StdEncoding.DecodeString(key)
if err != nil {
return nil, fmt.Errorf("decode base64: %w", err)
}

k, err := gossh.ParsePrivateKey(bs)
if err != nil {
return nil, fmt.Errorf("parse private key: %w", err)
}

return k, nil
}

// LogHostKeyCallback is a HostKeyCallback that just logs host keys
// and does nothing else.
func LogHostKeyCallback(logger func(string, ...any)) gossh.HostKeyCallback {
Expand Down Expand Up @@ -273,6 +290,17 @@ func SetupRepoAuth(logf func(string, ...any), options *options.Options) transpor
}
}

// If no path was provided, fall back to the environment variable
if options.GitSSHPrivateKeyBase64 != "" {
s, err := DecodeBase64PrivateKey(options.GitSSHPrivateKeyBase64)
if err != nil {
logf("❌ Failed to decode base 64 private key: %s", err.Error())
} else {
logf("🔑 Using %s key!", s.PublicKey().Type())
signer = s
}
}

// If no SSH key set, fall back to agent auth.
if signer == nil {
logf("🔑 No SSH key found, falling back to agent!")
Expand Down
21 changes: 21 additions & 0 deletions git/git_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package git_test
import (
"context"
"crypto/ed25519"
"encoding/base64"
"fmt"
"io"
"net/http/httptest"
Expand Down Expand Up @@ -433,6 +434,22 @@ func TestSetupRepoAuth(t *testing.T) {
require.Equal(t, actualSigner, pk.Signer)
})

t.Run("SSH/Base64PrivateKey", func(t *testing.T) {
opts := &options.Options{
GitURL: "ssh://[email protected]:repo/path",
GitSSHPrivateKeyBase64: base64EncodeTestPrivateKey(),
}
auth := git.SetupRepoAuth(t.Logf, opts)

pk, ok := auth.(*gitssh.PublicKeys)
require.True(t, ok)
require.NotNil(t, pk.Signer)

actualSigner, err := gossh.ParsePrivateKey([]byte(testKey))
require.NoError(t, err)
require.Equal(t, actualSigner, pk.Signer)
})

t.Run("SSH/NoAuthMethods", func(t *testing.T) {
opts := &options.Options{
GitURL: "ssh://[email protected]:repo/path",
Expand Down Expand Up @@ -502,3 +519,7 @@ func writeTestPrivateKey(t *testing.T) string {
require.NoError(t, os.WriteFile(kPath, []byte(testKey), 0o600))
return kPath
}

func base64EncodeTestPrivateKey() string {
return base64.StdEncoding.EncodeToString([]byte(testKey))
}
61 changes: 61 additions & 0 deletions integration/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bufio"
"bytes"
"context"
"crypto/ed25519"
"encoding/base64"
"encoding/json"
"encoding/pem"
Expand Down Expand Up @@ -32,6 +33,8 @@ import (
"github.com/coder/envbuilder/testutil/gittest"
"github.com/coder/envbuilder/testutil/mwtest"
"github.com/coder/envbuilder/testutil/registrytest"
"github.com/go-git/go-billy/v5/osfs"
gossh "golang.org/x/crypto/ssh"

clitypes "github.com/docker/cli/cli/config/types"
"github.com/docker/docker/api/types"
Expand All @@ -58,6 +61,16 @@ const (
testContainerLabel = "envbox-integration-test"
testImageAlpine = "localhost:5000/envbuilder-test-alpine:latest"
testImageUbuntu = "localhost:5000/envbuilder-test-ubuntu:latest"

// nolint:gosec // Throw-away key for testing. DO NOT REUSE.
testSSHKey = `-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACBXOGgAge/EbcejqASqZa6s8PFXZle56DiGEt0VYnljuwAAAKgM05mUDNOZ
lAAAAAtzc2gtZWQyNTUxOQAAACBXOGgAge/EbcejqASqZa6s8PFXZle56DiGEt0VYnljuw
AAAEDCawwtjrM4AGYXD1G6uallnbsgMed4cfkFsQ+mLZtOkFc4aACB78Rtx6OoBKplrqzw
8VdmV7noOIYS3RVieWO7AAAAHmNpYW5AY2RyLW1icC1mdmZmdzBuOHEwNXAuaG9tZQECAw
QFBgc=
-----END OPENSSH PRIVATE KEY-----`
)

func TestLogs(t *testing.T) {
Expand Down Expand Up @@ -382,6 +395,54 @@ func TestSucceedsGitAuth(t *testing.T) {
require.Contains(t, gitConfig, srv.URL)
}

func TestGitSSHAuth(t *testing.T) {
t.Parallel()

base64Key := base64.StdEncoding.EncodeToString([]byte(testSSHKey))

t.Run("Base64/Success", func(t *testing.T) {
signer, err := gossh.ParsePrivateKey([]byte(testSSHKey))
require.NoError(t, err)
require.NotNil(t, signer)

tmpDir := t.TempDir()
srvFS := osfs.New(tmpDir, osfs.WithChrootOS())

_ = gittest.NewRepo(t, srvFS, gittest.Commit(t, "Dockerfile", "FROM "+testImageAlpine, "Initial commit"))
tr := gittest.NewServerSSH(t, srvFS, signer.PublicKey())

_, err = runEnvbuilder(t, runOpts{env: []string{
envbuilderEnv("DOCKERFILE_PATH", "Dockerfile"),
envbuilderEnv("GIT_URL", tr.String()+"."),
envbuilderEnv("GIT_SSH_PRIVATE_KEY_BASE64", base64Key),
}})
// TODO: Ensure it actually clones but this does mean we have
// successfully authenticated.
require.ErrorContains(t, err, "repository not found")
})

t.Run("Base64/Failure", func(t *testing.T) {
_, randomKey, err := ed25519.GenerateKey(nil)
require.NoError(t, err)
signer, err := gossh.NewSignerFromKey(randomKey)
require.NoError(t, err)
require.NotNil(t, signer)

tmpDir := t.TempDir()
srvFS := osfs.New(tmpDir, osfs.WithChrootOS())

_ = gittest.NewRepo(t, srvFS, gittest.Commit(t, "Dockerfile", "FROM "+testImageAlpine, "Initial commit"))
tr := gittest.NewServerSSH(t, srvFS, signer.PublicKey())

_, err = runEnvbuilder(t, runOpts{env: []string{
envbuilderEnv("DOCKERFILE_PATH", "Dockerfile"),
envbuilderEnv("GIT_URL", tr.String()+"."),
envbuilderEnv("GIT_SSH_PRIVATE_KEY_BASE64", base64Key),
}})
require.ErrorContains(t, err, "handshake failed")
})
}

func TestSucceedsGitAuthInURL(t *testing.T) {
t.Parallel()
srv := gittest.CreateGitServer(t, gittest.Options{
Expand Down
4 changes: 2 additions & 2 deletions options/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ func (o *Options) SetDefaults() {
if o.BinaryPath == "" {
o.BinaryPath = "/.envbuilder/bin/envbuilder"
}
if o.MagicDirBase == "" {
o.MagicDirBase = workingdir.Default.Path()
if o.WorkingDirBase == "" {
o.WorkingDirBase = workingdir.Default.Path()
}
}
2 changes: 1 addition & 1 deletion options/defaults_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ func TestOptions_SetDefaults(t *testing.T) {
Filesystem: chmodfs.New(osfs.New("/")),
GitURL: "",
WorkspaceFolder: options.EmptyWorkspaceDir,
MagicDirBase: "/.envbuilder",
WorkingDirBase: "/.envbuilder",
BinaryPath: "/.envbuilder/bin/envbuilder",
}

Expand Down
23 changes: 17 additions & 6 deletions options/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,9 @@ type Options struct {
// GitSSHPrivateKeyPath is the path to an SSH private key to be used for
// Git authentication.
GitSSHPrivateKeyPath string
// GitSSHPrivateKeyBase64 is the content of an SSH private key to be used
// for Git authentication.
GitSSHPrivateKeyBase64 string
// GitHTTPProxyURL is the URL for the HTTP proxy. This is optional.
GitHTTPProxyURL string
// WorkspaceFolder is the path to the workspace folder that will be built.
Expand Down Expand Up @@ -162,10 +165,10 @@ type Options struct {
// GetCachedImage is true.
BinaryPath string

// MagicDirBase is the path to the directory where all envbuilder files should be
// WorkingDirBase is the path to the directory where all envbuilder files should be
// stored. By default, this is set to `/.envbuilder`. This is intentionally
// excluded from the CLI options.
MagicDirBase string
WorkingDirBase string
}

const envPrefix = "ENVBUILDER_"
Expand Down Expand Up @@ -358,10 +361,18 @@ func (o *Options) CLI() serpent.OptionSet {
Description: "The password to use for Git authentication. This is optional.",
},
{
Flag: "git-ssh-private-key-path",
Env: WithEnvPrefix("GIT_SSH_PRIVATE_KEY_PATH"),
Value: serpent.StringOf(&o.GitSSHPrivateKeyPath),
Description: "Path to an SSH private key to be used for Git authentication.",
Flag: "git-ssh-private-key-path",
Env: WithEnvPrefix("GIT_SSH_PRIVATE_KEY_PATH"),
Value: serpent.StringOf(&o.GitSSHPrivateKeyPath),
Description: "Path to an SSH private key to be used for Git authentication." +
" If this is set, then GIT_SSH_PRIVATE_KEY_BASE64 cannot be set.",
},
{
Flag: "git-ssh-private-key-base64",
Env: WithEnvPrefix("GIT_SSH_PRIVATE_KEY_BASE64"),
Value: serpent.StringOf(&o.GitSSHPrivateKeyBase64),
Description: "Base64 encoded SSH private key to be used for Git authentication." +
" If this is set, then GIT_SSH_PRIVATE_KEY_PATH cannot be set.",
},
{
Flag: "git-http-proxy-url",
Expand Down
7 changes: 6 additions & 1 deletion options/testdata/options.golden
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,13 @@ OPTIONS:
--git-password string, $ENVBUILDER_GIT_PASSWORD
The password to use for Git authentication. This is optional.

--git-ssh-private-key-base64 string, $ENVBUILDER_GIT_SSH_PRIVATE_KEY_BASE64
Base64 encoded SSH private key to be used for Git authentication. If
this is set, then GIT_SSH_PRIVATE_KEY_PATH cannot be set.

--git-ssh-private-key-path string, $ENVBUILDER_GIT_SSH_PRIVATE_KEY_PATH
Path to an SSH private key to be used for Git authentication.
Path to an SSH private key to be used for Git authentication. If this
is set, then GIT_SSH_PRIVATE_KEY_BASE64 cannot be set.

--git-url string, $ENVBUILDER_GIT_URL
The URL of a Git repository containing a Devcontainer or Docker image
Expand Down

0 comments on commit 8984909

Please sign in to comment.