Skip to content

Commit 022d9d4

Browse files
committed
feat: implement repo-mode
Fixes #218
1 parent 039314e commit 022d9d4

File tree

7 files changed

+435
-213
lines changed

7 files changed

+435
-213
lines changed

constants/constants.go

+4
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,8 @@ var (
2828
// to skip building when a container is restarting.
2929
// e.g. docker stop -> docker start
3030
MagicFile = filepath.Join(MagicDir, "built")
31+
32+
// MagicFile is the location of the build context when
33+
// using remote build mode.
34+
MagicRemoteRepoDir = filepath.Join(MagicDir, "repo")
3135
)

envbuilder.go

+57-69
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"bufio"
55
"bytes"
66
"context"
7-
"crypto/x509"
87
"encoding/base64"
98
"encoding/json"
109
"errors"
@@ -41,7 +40,6 @@ import (
4140
_ "github.com/distribution/distribution/v3/registry/storage/driver/filesystem"
4241
"github.com/docker/cli/cli/config/configfile"
4342
"github.com/fatih/color"
44-
"github.com/go-git/go-git/v5/plumbing/transport"
4543
v1 "github.com/google/go-containerregistry/pkg/v1"
4644
"github.com/google/go-containerregistry/pkg/v1/remote"
4745
"github.com/kballard/go-shellquote"
@@ -88,23 +86,6 @@ func Run(ctx context.Context, opts options.Options) error {
8886

8987
opts.Logger(log.LevelInfo, "%s - Build development environments from repositories in a container", newColor(color.Bold).Sprintf("envbuilder"))
9088

91-
var caBundle []byte
92-
if opts.SSLCertBase64 != "" {
93-
certPool, err := x509.SystemCertPool()
94-
if err != nil {
95-
return xerrors.Errorf("get global system cert pool: %w", err)
96-
}
97-
data, err := base64.StdEncoding.DecodeString(opts.SSLCertBase64)
98-
if err != nil {
99-
return xerrors.Errorf("base64 decode ssl cert: %w", err)
100-
}
101-
ok := certPool.AppendCertsFromPEM(data)
102-
if !ok {
103-
return xerrors.Errorf("failed to append the ssl cert to the global pool: %s", data)
104-
}
105-
caBundle = data
106-
}
107-
10889
if opts.DockerConfigBase64 != "" {
10990
decoded, err := base64.StdEncoding.DecodeString(opts.DockerConfigBase64)
11091
if err != nil {
@@ -125,51 +106,23 @@ func Run(ctx context.Context, opts options.Options) error {
125106
}
126107
}
127108

109+
buildTimeWorkspaceFolder := opts.WorkspaceFolder
128110
var fallbackErr error
129111
var cloned bool
130112
if opts.GitURL != "" {
113+
cloneOpts, err := git.CloneOptionsFromOptions(opts)
114+
if err != nil {
115+
return fmt.Errorf("git clone options: %w", err)
116+
}
117+
131118
endStage := startStage("📦 Cloning %s to %s...",
132119
newColor(color.FgCyan).Sprintf(opts.GitURL),
133-
newColor(color.FgCyan).Sprintf(opts.WorkspaceFolder),
120+
newColor(color.FgCyan).Sprintf(cloneOpts.Path),
134121
)
135122

136-
reader, writer := io.Pipe()
137-
defer reader.Close()
138-
defer writer.Close()
139-
go func() {
140-
data := make([]byte, 4096)
141-
for {
142-
read, err := reader.Read(data)
143-
if err != nil {
144-
return
145-
}
146-
content := data[:read]
147-
for _, line := range strings.Split(string(content), "\r") {
148-
if line == "" {
149-
continue
150-
}
151-
opts.Logger(log.LevelInfo, "#1: %s", strings.TrimSpace(line))
152-
}
153-
}
154-
}()
155-
156-
cloneOpts := git.CloneRepoOptions{
157-
Path: opts.WorkspaceFolder,
158-
Storage: opts.Filesystem,
159-
Insecure: opts.Insecure,
160-
Progress: writer,
161-
SingleBranch: opts.GitCloneSingleBranch,
162-
Depth: int(opts.GitCloneDepth),
163-
CABundle: caBundle,
164-
}
165-
166-
cloneOpts.RepoAuth = git.SetupRepoAuth(&opts)
167-
if opts.GitHTTPProxyURL != "" {
168-
cloneOpts.ProxyOptions = transport.ProxyOptions{
169-
URL: opts.GitHTTPProxyURL,
170-
}
171-
}
172-
cloneOpts.RepoURL = opts.GitURL
123+
w := git.Logger(func(line string) { opts.Logger(log.LevelInfo, "#%d: %s", stageNumber, line) })
124+
defer w.Close()
125+
cloneOpts.Progress = w
173126

174127
cloned, fallbackErr = git.CloneRepo(ctx, cloneOpts)
175128
if fallbackErr == nil {
@@ -182,6 +135,37 @@ func Run(ctx context.Context, opts options.Options) error {
182135
opts.Logger(log.LevelError, "Failed to clone repository: %s", fallbackErr.Error())
183136
opts.Logger(log.LevelError, "Falling back to the default image...")
184137
}
138+
139+
// The repo wasn't cloned (i.e. may not be up-to-date with remote), so
140+
// we need to clone it to get the right build context. This is only
141+
// necessary when the repo is in remote build mode. If the repo is in
142+
// local build mode, the build context is already available on the host
143+
// filesystem.
144+
if fallbackErr == nil && !cloned && opts.RepoBuildMode == options.RepoBuildModeRemote {
145+
cloneOpts, err := git.CloneOptionsFromOptions(opts)
146+
if err != nil {
147+
return fmt.Errorf("git clone options: %w", err)
148+
}
149+
cloneOpts.Path = constants.MagicRemoteRepoDir
150+
151+
endStage := startStage("📦 Remote build mode enabled, cloning %s to %s for build context...",
152+
newColor(color.FgCyan).Sprintf(opts.GitURL),
153+
newColor(color.FgCyan).Sprintf(cloneOpts.Path),
154+
)
155+
156+
w := git.Logger(func(line string) { opts.Logger(log.LevelInfo, "#%d: %s", stageNumber, line) })
157+
defer w.Close()
158+
cloneOpts.Progress = w
159+
160+
fallbackErr = git.ShallowCloneRepo(ctx, cloneOpts)
161+
if fallbackErr == nil {
162+
endStage("📦 Cloned repository!")
163+
buildTimeWorkspaceFolder = cloneOpts.Path
164+
} else {
165+
opts.Logger(log.LevelError, "Failed to clone repository for remote repo mode: %s", err.Error())
166+
opts.Logger(log.LevelError, "Falling back to the default image...")
167+
}
168+
}
185169
}
186170

187171
defaultBuildParams := func() (*devcontainer.Compiled, error) {
@@ -222,7 +206,7 @@ func Run(ctx context.Context, opts options.Options) error {
222206
// devcontainer is a standard, so it's reasonable to be the default.
223207
var devcontainerDir string
224208
var err error
225-
devcontainerPath, devcontainerDir, err = findDevcontainerJSON(opts)
209+
devcontainerPath, devcontainerDir, err = findDevcontainerJSON(buildTimeWorkspaceFolder, opts)
226210
if err != nil {
227211
opts.Logger(log.LevelError, "Failed to locate devcontainer.json: %s", err.Error())
228212
opts.Logger(log.LevelError, "Falling back to the default image...")
@@ -614,10 +598,10 @@ ENTRYPOINT [%q]`, exePath, exePath, exePath)
614598
if err != nil {
615599
return fmt.Errorf("unmarshal metadata: %w", err)
616600
}
617-
opts.Logger(log.LevelInfo, "#3: 👀 Found devcontainer.json label metadata in image...")
601+
opts.Logger(log.LevelInfo, "#%d: 👀 Found devcontainer.json label metadata in image...", stageNumber)
618602
for _, container := range devContainer {
619603
if container.RemoteUser != "" {
620-
opts.Logger(log.LevelInfo, "#3: 🧑 Updating the user to %q!", container.RemoteUser)
604+
opts.Logger(log.LevelInfo, "#%d: 🧑 Updating the user to %q!", stageNumber, container.RemoteUser)
621605

622606
configFile.Config.User = container.RemoteUser
623607
}
@@ -724,7 +708,7 @@ ENTRYPOINT [%q]`, exePath, exePath, exePath)
724708
username = buildParams.User
725709
}
726710
if username == "" {
727-
opts.Logger(log.LevelWarn, "#3: no user specified, using root")
711+
opts.Logger(log.LevelWarn, "#%d: no user specified, using root", stageNumber)
728712
}
729713

730714
userInfo, err := getUser(username)
@@ -1030,7 +1014,11 @@ func newColor(value ...color.Attribute) *color.Color {
10301014
return c
10311015
}
10321016

1033-
func findDevcontainerJSON(options options.Options) (string, string, error) {
1017+
func findDevcontainerJSON(workspaceFolder string, options options.Options) (string, string, error) {
1018+
if workspaceFolder == "" {
1019+
workspaceFolder = options.WorkspaceFolder
1020+
}
1021+
10341022
// 0. Check if custom devcontainer directory or path is provided.
10351023
if options.DevcontainerDir != "" || options.DevcontainerJSONPath != "" {
10361024
devcontainerDir := options.DevcontainerDir
@@ -1040,7 +1028,7 @@ func findDevcontainerJSON(options options.Options) (string, string, error) {
10401028

10411029
// If `devcontainerDir` is not an absolute path, assume it is relative to the workspace folder.
10421030
if !filepath.IsAbs(devcontainerDir) {
1043-
devcontainerDir = filepath.Join(options.WorkspaceFolder, devcontainerDir)
1031+
devcontainerDir = filepath.Join(workspaceFolder, devcontainerDir)
10441032
}
10451033

10461034
// An absolute location always takes a precedence.
@@ -1059,20 +1047,20 @@ func findDevcontainerJSON(options options.Options) (string, string, error) {
10591047
return devcontainerPath, devcontainerDir, nil
10601048
}
10611049

1062-
// 1. Check `options.WorkspaceFolder`/.devcontainer/devcontainer.json.
1063-
location := filepath.Join(options.WorkspaceFolder, ".devcontainer", "devcontainer.json")
1050+
// 1. Check `workspaceFolder`/.devcontainer/devcontainer.json.
1051+
location := filepath.Join(workspaceFolder, ".devcontainer", "devcontainer.json")
10641052
if _, err := options.Filesystem.Stat(location); err == nil {
10651053
return location, filepath.Dir(location), nil
10661054
}
10671055

1068-
// 2. Check `options.WorkspaceFolder`/devcontainer.json.
1069-
location = filepath.Join(options.WorkspaceFolder, "devcontainer.json")
1056+
// 2. Check `workspaceFolder`/devcontainer.json.
1057+
location = filepath.Join(workspaceFolder, "devcontainer.json")
10701058
if _, err := options.Filesystem.Stat(location); err == nil {
10711059
return location, filepath.Dir(location), nil
10721060
}
10731061

1074-
// 3. Check every folder: `options.WorkspaceFolder`/.devcontainer/<folder>/devcontainer.json.
1075-
devcontainerDir := filepath.Join(options.WorkspaceFolder, ".devcontainer")
1062+
// 3. Check every folder: `workspaceFolder`/.devcontainer/<folder>/devcontainer.json.
1063+
devcontainerDir := filepath.Join(workspaceFolder, ".devcontainer")
10761064

10771065
fileInfos, err := options.Filesystem.ReadDir(devcontainerDir)
10781066
if err != nil {

0 commit comments

Comments
 (0)