Skip to content

Commit 8984909

Browse files
authored
Merge branch 'main' into cj/385-push-image-fail
2 parents 48b94fa + 08bdb8d commit 8984909

File tree

10 files changed

+144
-13
lines changed

10 files changed

+144
-13
lines changed

cmd/envbuilder/main.go

+4
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,10 @@ func envbuilderCmd() serpent.Command {
7575
}
7676
}
7777

78+
if o.GitSSHPrivateKeyPath != "" && o.GitSSHPrivateKeyBase64 != "" {
79+
return errors.New("cannot have both GIT_SSH_PRIVATE_KEY_PATH and GIT_SSH_PRIVATE_KEY_BASE64 set")
80+
}
81+
7882
if o.GetCachedImage {
7983
img, err := envbuilder.RunCacheProbe(inv.Context(), o)
8084
if err != nil {

docs/env-variables.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@
2727
| `--git-clone-single-branch` | `ENVBUILDER_GIT_CLONE_SINGLE_BRANCH` | | Clone only a single branch of the Git repository. |
2828
| `--git-username` | `ENVBUILDER_GIT_USERNAME` | | The username to use for Git authentication. This is optional. |
2929
| `--git-password` | `ENVBUILDER_GIT_PASSWORD` | | The password to use for Git authentication. This is optional. |
30-
| `--git-ssh-private-key-path` | `ENVBUILDER_GIT_SSH_PRIVATE_KEY_PATH` | | Path to an SSH private key to be used for Git authentication. |
30+
| `--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. |
31+
| `--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. |
3132
| `--git-http-proxy-url` | `ENVBUILDER_GIT_HTTP_PROXY_URL` | | The URL for the HTTP proxy. This is optional. |
3233
| `--workspace-folder` | `ENVBUILDER_WORKSPACE_FOLDER` | | The path to the workspace folder that will be built. This is optional. |
3334
| `--ssl-cert-base64` | `ENVBUILDER_SSL_CERT_BASE64` | | The content of an SSL cert file. This is useful for self-signed certificates. |

envbuilder.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ func Run(ctx context.Context, opts options.Options, preExec ...func()) error {
120120
func run(ctx context.Context, opts options.Options, execArgs *execArgsInfo) error {
121121
defer options.UnsetEnv()
122122

123-
workingDir := workingdir.At(opts.MagicDirBase)
123+
workingDir := workingdir.At(opts.WorkingDirBase)
124124

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

967-
workingDir := workingdir.At(opts.MagicDirBase)
967+
workingDir := workingdir.At(opts.WorkingDirBase)
968968

969969
stageNumber := 0
970970
startStage := func(format string, args ...any) func(format string, args ...any) {

git/git.go

+28
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package git
22

33
import (
44
"context"
5+
"encoding/base64"
56
"errors"
67
"fmt"
78
"io"
@@ -181,6 +182,22 @@ func ReadPrivateKey(path string) (gossh.Signer, error) {
181182
return k, nil
182183
}
183184

185+
// DecodeBase64PrivateKey attempts to decode a base64 encoded private
186+
// key and returns an ssh.Signer
187+
func DecodeBase64PrivateKey(key string) (gossh.Signer, error) {
188+
bs, err := base64.StdEncoding.DecodeString(key)
189+
if err != nil {
190+
return nil, fmt.Errorf("decode base64: %w", err)
191+
}
192+
193+
k, err := gossh.ParsePrivateKey(bs)
194+
if err != nil {
195+
return nil, fmt.Errorf("parse private key: %w", err)
196+
}
197+
198+
return k, nil
199+
}
200+
184201
// LogHostKeyCallback is a HostKeyCallback that just logs host keys
185202
// and does nothing else.
186203
func LogHostKeyCallback(logger func(string, ...any)) gossh.HostKeyCallback {
@@ -273,6 +290,17 @@ func SetupRepoAuth(logf func(string, ...any), options *options.Options) transpor
273290
}
274291
}
275292

293+
// If no path was provided, fall back to the environment variable
294+
if options.GitSSHPrivateKeyBase64 != "" {
295+
s, err := DecodeBase64PrivateKey(options.GitSSHPrivateKeyBase64)
296+
if err != nil {
297+
logf("❌ Failed to decode base 64 private key: %s", err.Error())
298+
} else {
299+
logf("🔑 Using %s key!", s.PublicKey().Type())
300+
signer = s
301+
}
302+
}
303+
276304
// If no SSH key set, fall back to agent auth.
277305
if signer == nil {
278306
logf("🔑 No SSH key found, falling back to agent!")

git/git_test.go

+21
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package git_test
33
import (
44
"context"
55
"crypto/ed25519"
6+
"encoding/base64"
67
"fmt"
78
"io"
89
"net/http/httptest"
@@ -433,6 +434,22 @@ func TestSetupRepoAuth(t *testing.T) {
433434
require.Equal(t, actualSigner, pk.Signer)
434435
})
435436

437+
t.Run("SSH/Base64PrivateKey", func(t *testing.T) {
438+
opts := &options.Options{
439+
GitURL: "ssh://[email protected]:repo/path",
440+
GitSSHPrivateKeyBase64: base64EncodeTestPrivateKey(),
441+
}
442+
auth := git.SetupRepoAuth(t.Logf, opts)
443+
444+
pk, ok := auth.(*gitssh.PublicKeys)
445+
require.True(t, ok)
446+
require.NotNil(t, pk.Signer)
447+
448+
actualSigner, err := gossh.ParsePrivateKey([]byte(testKey))
449+
require.NoError(t, err)
450+
require.Equal(t, actualSigner, pk.Signer)
451+
})
452+
436453
t.Run("SSH/NoAuthMethods", func(t *testing.T) {
437454
opts := &options.Options{
438455
GitURL: "ssh://[email protected]:repo/path",
@@ -502,3 +519,7 @@ func writeTestPrivateKey(t *testing.T) string {
502519
require.NoError(t, os.WriteFile(kPath, []byte(testKey), 0o600))
503520
return kPath
504521
}
522+
523+
func base64EncodeTestPrivateKey() string {
524+
return base64.StdEncoding.EncodeToString([]byte(testKey))
525+
}

integration/integration_test.go

+61
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"bufio"
55
"bytes"
66
"context"
7+
"crypto/ed25519"
78
"encoding/base64"
89
"encoding/json"
910
"encoding/pem"
@@ -32,6 +33,8 @@ import (
3233
"github.com/coder/envbuilder/testutil/gittest"
3334
"github.com/coder/envbuilder/testutil/mwtest"
3435
"github.com/coder/envbuilder/testutil/registrytest"
36+
"github.com/go-git/go-billy/v5/osfs"
37+
gossh "golang.org/x/crypto/ssh"
3538

3639
clitypes "github.com/docker/cli/cli/config/types"
3740
"github.com/docker/docker/api/types"
@@ -58,6 +61,16 @@ const (
5861
testContainerLabel = "envbox-integration-test"
5962
testImageAlpine = "localhost:5000/envbuilder-test-alpine:latest"
6063
testImageUbuntu = "localhost:5000/envbuilder-test-ubuntu:latest"
64+
65+
// nolint:gosec // Throw-away key for testing. DO NOT REUSE.
66+
testSSHKey = `-----BEGIN OPENSSH PRIVATE KEY-----
67+
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
68+
QyNTUxOQAAACBXOGgAge/EbcejqASqZa6s8PFXZle56DiGEt0VYnljuwAAAKgM05mUDNOZ
69+
lAAAAAtzc2gtZWQyNTUxOQAAACBXOGgAge/EbcejqASqZa6s8PFXZle56DiGEt0VYnljuw
70+
AAAEDCawwtjrM4AGYXD1G6uallnbsgMed4cfkFsQ+mLZtOkFc4aACB78Rtx6OoBKplrqzw
71+
8VdmV7noOIYS3RVieWO7AAAAHmNpYW5AY2RyLW1icC1mdmZmdzBuOHEwNXAuaG9tZQECAw
72+
QFBgc=
73+
-----END OPENSSH PRIVATE KEY-----`
6174
)
6275

6376
func TestLogs(t *testing.T) {
@@ -382,6 +395,54 @@ func TestSucceedsGitAuth(t *testing.T) {
382395
require.Contains(t, gitConfig, srv.URL)
383396
}
384397

398+
func TestGitSSHAuth(t *testing.T) {
399+
t.Parallel()
400+
401+
base64Key := base64.StdEncoding.EncodeToString([]byte(testSSHKey))
402+
403+
t.Run("Base64/Success", func(t *testing.T) {
404+
signer, err := gossh.ParsePrivateKey([]byte(testSSHKey))
405+
require.NoError(t, err)
406+
require.NotNil(t, signer)
407+
408+
tmpDir := t.TempDir()
409+
srvFS := osfs.New(tmpDir, osfs.WithChrootOS())
410+
411+
_ = gittest.NewRepo(t, srvFS, gittest.Commit(t, "Dockerfile", "FROM "+testImageAlpine, "Initial commit"))
412+
tr := gittest.NewServerSSH(t, srvFS, signer.PublicKey())
413+
414+
_, err = runEnvbuilder(t, runOpts{env: []string{
415+
envbuilderEnv("DOCKERFILE_PATH", "Dockerfile"),
416+
envbuilderEnv("GIT_URL", tr.String()+"."),
417+
envbuilderEnv("GIT_SSH_PRIVATE_KEY_BASE64", base64Key),
418+
}})
419+
// TODO: Ensure it actually clones but this does mean we have
420+
// successfully authenticated.
421+
require.ErrorContains(t, err, "repository not found")
422+
})
423+
424+
t.Run("Base64/Failure", func(t *testing.T) {
425+
_, randomKey, err := ed25519.GenerateKey(nil)
426+
require.NoError(t, err)
427+
signer, err := gossh.NewSignerFromKey(randomKey)
428+
require.NoError(t, err)
429+
require.NotNil(t, signer)
430+
431+
tmpDir := t.TempDir()
432+
srvFS := osfs.New(tmpDir, osfs.WithChrootOS())
433+
434+
_ = gittest.NewRepo(t, srvFS, gittest.Commit(t, "Dockerfile", "FROM "+testImageAlpine, "Initial commit"))
435+
tr := gittest.NewServerSSH(t, srvFS, signer.PublicKey())
436+
437+
_, err = runEnvbuilder(t, runOpts{env: []string{
438+
envbuilderEnv("DOCKERFILE_PATH", "Dockerfile"),
439+
envbuilderEnv("GIT_URL", tr.String()+"."),
440+
envbuilderEnv("GIT_SSH_PRIVATE_KEY_BASE64", base64Key),
441+
}})
442+
require.ErrorContains(t, err, "handshake failed")
443+
})
444+
}
445+
385446
func TestSucceedsGitAuthInURL(t *testing.T) {
386447
t.Parallel()
387448
srv := gittest.CreateGitServer(t, gittest.Options{

options/defaults.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ func (o *Options) SetDefaults() {
6565
if o.BinaryPath == "" {
6666
o.BinaryPath = "/.envbuilder/bin/envbuilder"
6767
}
68-
if o.MagicDirBase == "" {
69-
o.MagicDirBase = workingdir.Default.Path()
68+
if o.WorkingDirBase == "" {
69+
o.WorkingDirBase = workingdir.Default.Path()
7070
}
7171
}

options/defaults_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ func TestOptions_SetDefaults(t *testing.T) {
141141
Filesystem: chmodfs.New(osfs.New("/")),
142142
GitURL: "",
143143
WorkspaceFolder: options.EmptyWorkspaceDir,
144-
MagicDirBase: "/.envbuilder",
144+
WorkingDirBase: "/.envbuilder",
145145
BinaryPath: "/.envbuilder/bin/envbuilder",
146146
}
147147

options/options.go

+17-6
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,9 @@ type Options struct {
108108
// GitSSHPrivateKeyPath is the path to an SSH private key to be used for
109109
// Git authentication.
110110
GitSSHPrivateKeyPath string
111+
// GitSSHPrivateKeyBase64 is the content of an SSH private key to be used
112+
// for Git authentication.
113+
GitSSHPrivateKeyBase64 string
111114
// GitHTTPProxyURL is the URL for the HTTP proxy. This is optional.
112115
GitHTTPProxyURL string
113116
// WorkspaceFolder is the path to the workspace folder that will be built.
@@ -162,10 +165,10 @@ type Options struct {
162165
// GetCachedImage is true.
163166
BinaryPath string
164167

165-
// MagicDirBase is the path to the directory where all envbuilder files should be
168+
// WorkingDirBase is the path to the directory where all envbuilder files should be
166169
// stored. By default, this is set to `/.envbuilder`. This is intentionally
167170
// excluded from the CLI options.
168-
MagicDirBase string
171+
WorkingDirBase string
169172
}
170173

171174
const envPrefix = "ENVBUILDER_"
@@ -358,10 +361,18 @@ func (o *Options) CLI() serpent.OptionSet {
358361
Description: "The password to use for Git authentication. This is optional.",
359362
},
360363
{
361-
Flag: "git-ssh-private-key-path",
362-
Env: WithEnvPrefix("GIT_SSH_PRIVATE_KEY_PATH"),
363-
Value: serpent.StringOf(&o.GitSSHPrivateKeyPath),
364-
Description: "Path to an SSH private key to be used for Git authentication.",
364+
Flag: "git-ssh-private-key-path",
365+
Env: WithEnvPrefix("GIT_SSH_PRIVATE_KEY_PATH"),
366+
Value: serpent.StringOf(&o.GitSSHPrivateKeyPath),
367+
Description: "Path to an SSH private key to be used for Git authentication." +
368+
" If this is set, then GIT_SSH_PRIVATE_KEY_BASE64 cannot be set.",
369+
},
370+
{
371+
Flag: "git-ssh-private-key-base64",
372+
Env: WithEnvPrefix("GIT_SSH_PRIVATE_KEY_BASE64"),
373+
Value: serpent.StringOf(&o.GitSSHPrivateKeyBase64),
374+
Description: "Base64 encoded SSH private key to be used for Git authentication." +
375+
" If this is set, then GIT_SSH_PRIVATE_KEY_PATH cannot be set.",
365376
},
366377
{
367378
Flag: "git-http-proxy-url",

options/testdata/options.golden

+6-1
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,13 @@ OPTIONS:
9494
--git-password string, $ENVBUILDER_GIT_PASSWORD
9595
The password to use for Git authentication. This is optional.
9696

97+
--git-ssh-private-key-base64 string, $ENVBUILDER_GIT_SSH_PRIVATE_KEY_BASE64
98+
Base64 encoded SSH private key to be used for Git authentication. If
99+
this is set, then GIT_SSH_PRIVATE_KEY_PATH cannot be set.
100+
97101
--git-ssh-private-key-path string, $ENVBUILDER_GIT_SSH_PRIVATE_KEY_PATH
98-
Path to an SSH private key to be used for Git authentication.
102+
Path to an SSH private key to be used for Git authentication. If this
103+
is set, then GIT_SSH_PRIVATE_KEY_BASE64 cannot be set.
99104

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

0 commit comments

Comments
 (0)