Skip to content

Commit 8f3606b

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

File tree

7 files changed

+438
-213
lines changed

7 files changed

+438
-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

+60-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,40 @@ 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+
//
145+
// Skipping clone here if we believe workspace repo matches remote repo
146+
// is a performance optimization.
147+
if fallbackErr == nil && !cloned && opts.RepoBuildMode == options.RepoBuildModeRemote {
148+
cloneOpts, err := git.CloneOptionsFromOptions(opts)
149+
if err != nil {
150+
return fmt.Errorf("git clone options: %w", err)
151+
}
152+
cloneOpts.Path = constants.MagicRemoteRepoDir
153+
154+
endStage := startStage("📦 Remote build mode enabled, cloning %s to %s for build context...",
155+
newColor(color.FgCyan).Sprintf(opts.GitURL),
156+
newColor(color.FgCyan).Sprintf(cloneOpts.Path),
157+
)
158+
159+
w := git.Logger(func(line string) { opts.Logger(log.LevelInfo, "#%d: %s", stageNumber, line) })
160+
defer w.Close()
161+
cloneOpts.Progress = w
162+
163+
fallbackErr = git.ShallowCloneRepo(ctx, cloneOpts)
164+
if fallbackErr == nil {
165+
endStage("📦 Cloned repository!")
166+
buildTimeWorkspaceFolder = cloneOpts.Path
167+
} else {
168+
opts.Logger(log.LevelError, "Failed to clone repository for remote repo mode: %s", err.Error())
169+
opts.Logger(log.LevelError, "Falling back to the default image...")
170+
}
171+
}
185172
}
186173

187174
defaultBuildParams := func() (*devcontainer.Compiled, error) {
@@ -222,7 +209,7 @@ func Run(ctx context.Context, opts options.Options) error {
222209
// devcontainer is a standard, so it's reasonable to be the default.
223210
var devcontainerDir string
224211
var err error
225-
devcontainerPath, devcontainerDir, err = findDevcontainerJSON(opts)
212+
devcontainerPath, devcontainerDir, err = findDevcontainerJSON(buildTimeWorkspaceFolder, opts)
226213
if err != nil {
227214
opts.Logger(log.LevelError, "Failed to locate devcontainer.json: %s", err.Error())
228215
opts.Logger(log.LevelError, "Falling back to the default image...")
@@ -614,10 +601,10 @@ ENTRYPOINT [%q]`, exePath, exePath, exePath)
614601
if err != nil {
615602
return fmt.Errorf("unmarshal metadata: %w", err)
616603
}
617-
opts.Logger(log.LevelInfo, "#3: 👀 Found devcontainer.json label metadata in image...")
604+
opts.Logger(log.LevelInfo, "#%d: 👀 Found devcontainer.json label metadata in image...", stageNumber)
618605
for _, container := range devContainer {
619606
if container.RemoteUser != "" {
620-
opts.Logger(log.LevelInfo, "#3: 🧑 Updating the user to %q!", container.RemoteUser)
607+
opts.Logger(log.LevelInfo, "#%d: 🧑 Updating the user to %q!", stageNumber, container.RemoteUser)
621608

622609
configFile.Config.User = container.RemoteUser
623610
}
@@ -724,7 +711,7 @@ ENTRYPOINT [%q]`, exePath, exePath, exePath)
724711
username = buildParams.User
725712
}
726713
if username == "" {
727-
opts.Logger(log.LevelWarn, "#3: no user specified, using root")
714+
opts.Logger(log.LevelWarn, "#%d: no user specified, using root", stageNumber)
728715
}
729716

730717
userInfo, err := getUser(username)
@@ -1030,7 +1017,11 @@ func newColor(value ...color.Attribute) *color.Color {
10301017
return c
10311018
}
10321019

1033-
func findDevcontainerJSON(options options.Options) (string, string, error) {
1020+
func findDevcontainerJSON(workspaceFolder string, options options.Options) (string, string, error) {
1021+
if workspaceFolder == "" {
1022+
workspaceFolder = options.WorkspaceFolder
1023+
}
1024+
10341025
// 0. Check if custom devcontainer directory or path is provided.
10351026
if options.DevcontainerDir != "" || options.DevcontainerJSONPath != "" {
10361027
devcontainerDir := options.DevcontainerDir
@@ -1040,7 +1031,7 @@ func findDevcontainerJSON(options options.Options) (string, string, error) {
10401031

10411032
// If `devcontainerDir` is not an absolute path, assume it is relative to the workspace folder.
10421033
if !filepath.IsAbs(devcontainerDir) {
1043-
devcontainerDir = filepath.Join(options.WorkspaceFolder, devcontainerDir)
1034+
devcontainerDir = filepath.Join(workspaceFolder, devcontainerDir)
10441035
}
10451036

10461037
// An absolute location always takes a precedence.
@@ -1059,20 +1050,20 @@ func findDevcontainerJSON(options options.Options) (string, string, error) {
10591050
return devcontainerPath, devcontainerDir, nil
10601051
}
10611052

1062-
// 1. Check `options.WorkspaceFolder`/.devcontainer/devcontainer.json.
1063-
location := filepath.Join(options.WorkspaceFolder, ".devcontainer", "devcontainer.json")
1053+
// 1. Check `workspaceFolder`/.devcontainer/devcontainer.json.
1054+
location := filepath.Join(workspaceFolder, ".devcontainer", "devcontainer.json")
10641055
if _, err := options.Filesystem.Stat(location); err == nil {
10651056
return location, filepath.Dir(location), nil
10661057
}
10671058

1068-
// 2. Check `options.WorkspaceFolder`/devcontainer.json.
1069-
location = filepath.Join(options.WorkspaceFolder, "devcontainer.json")
1059+
// 2. Check `workspaceFolder`/devcontainer.json.
1060+
location = filepath.Join(workspaceFolder, "devcontainer.json")
10701061
if _, err := options.Filesystem.Stat(location); err == nil {
10711062
return location, filepath.Dir(location), nil
10721063
}
10731064

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

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

0 commit comments

Comments
 (0)