Skip to content

Commit e14b952

Browse files
authored
feat(git): log parsed gitURL and warn if local (#345)
1 parent df8ea67 commit e14b952

File tree

3 files changed

+118
-89
lines changed

3 files changed

+118
-89
lines changed

Diff for: envbuilder.go

+30-26
Original file line numberDiff line numberDiff line change
@@ -112,22 +112,25 @@ func Run(ctx context.Context, opts options.Options) error {
112112
var fallbackErr error
113113
var cloned bool
114114
if opts.GitURL != "" {
115-
cloneOpts, err := git.CloneOptionsFromOptions(opts)
116-
if err != nil {
117-
return fmt.Errorf("git clone options: %w", err)
118-
}
119-
120115
endStage := startStage("📦 Cloning %s to %s...",
121116
newColor(color.FgCyan).Sprintf(opts.GitURL),
122-
newColor(color.FgCyan).Sprintf(cloneOpts.Path),
117+
newColor(color.FgCyan).Sprintf(opts.WorkspaceFolder),
123118
)
124-
125119
stageNum := stageNumber
126-
w := git.ProgressWriter(func(line string) { opts.Logger(log.LevelInfo, "#%d: %s", stageNum, line) })
120+
logStage := func(format string, args ...any) {
121+
opts.Logger(log.LevelInfo, "#%d: %s", stageNum, fmt.Sprintf(format, args...))
122+
}
123+
124+
cloneOpts, err := git.CloneOptionsFromOptions(logStage, opts)
125+
if err != nil {
126+
return fmt.Errorf("git clone options: %w", err)
127+
}
128+
129+
w := git.ProgressWriter(logStage)
127130
defer w.Close()
128131
cloneOpts.Progress = w
129132

130-
cloned, fallbackErr = git.CloneRepo(ctx, cloneOpts)
133+
cloned, fallbackErr = git.CloneRepo(ctx, logStage, cloneOpts)
131134
if fallbackErr == nil {
132135
if cloned {
133136
endStage("📦 Cloned repository!")
@@ -144,7 +147,7 @@ func Run(ctx context.Context, opts options.Options) error {
144147
// Always clone the repo in remote repo build mode into a location that
145148
// we control that isn't affected by the users changes.
146149
if opts.RemoteRepoBuildMode {
147-
cloneOpts, err := git.CloneOptionsFromOptions(opts)
150+
cloneOpts, err := git.CloneOptionsFromOptions(logStage, opts)
148151
if err != nil {
149152
return fmt.Errorf("git clone options: %w", err)
150153
}
@@ -155,12 +158,11 @@ func Run(ctx context.Context, opts options.Options) error {
155158
newColor(color.FgCyan).Sprintf(cloneOpts.Path),
156159
)
157160

158-
stageNum := stageNumber
159-
w := git.ProgressWriter(func(line string) { opts.Logger(log.LevelInfo, "#%d: %s", stageNum, line) })
161+
w := git.ProgressWriter(logStage)
160162
defer w.Close()
161163
cloneOpts.Progress = w
162164

163-
fallbackErr = git.ShallowCloneRepo(ctx, cloneOpts)
165+
fallbackErr = git.ShallowCloneRepo(ctx, logStage, cloneOpts)
164166
if fallbackErr == nil {
165167
endStage("📦 Cloned repository!")
166168
buildTimeWorkspaceFolder = cloneOpts.Path
@@ -891,25 +893,28 @@ func RunCacheProbe(ctx context.Context, opts options.Options) (v1.Image, error)
891893
var fallbackErr error
892894
var cloned bool
893895
if opts.GitURL != "" {
896+
endStage := startStage("📦 Cloning %s to %s...",
897+
newColor(color.FgCyan).Sprintf(opts.GitURL),
898+
newColor(color.FgCyan).Sprintf(opts.WorkspaceFolder),
899+
)
900+
stageNum := stageNumber
901+
logStage := func(format string, args ...any) {
902+
opts.Logger(log.LevelInfo, "#%d: %s", stageNum, fmt.Sprintf(format, args...))
903+
}
904+
894905
// In cache probe mode we should only attempt to clone the full
895906
// repository if remote repo build mode isn't enabled.
896907
if !opts.RemoteRepoBuildMode {
897-
cloneOpts, err := git.CloneOptionsFromOptions(opts)
908+
cloneOpts, err := git.CloneOptionsFromOptions(logStage, opts)
898909
if err != nil {
899910
return nil, fmt.Errorf("git clone options: %w", err)
900911
}
901912

902-
endStage := startStage("📦 Cloning %s to %s...",
903-
newColor(color.FgCyan).Sprintf(opts.GitURL),
904-
newColor(color.FgCyan).Sprintf(cloneOpts.Path),
905-
)
906-
907-
stageNum := stageNumber
908-
w := git.ProgressWriter(func(line string) { opts.Logger(log.LevelInfo, "#%d: %s", stageNum, line) })
913+
w := git.ProgressWriter(logStage)
909914
defer w.Close()
910915
cloneOpts.Progress = w
911916

912-
cloned, fallbackErr = git.CloneRepo(ctx, cloneOpts)
917+
cloned, fallbackErr = git.CloneRepo(ctx, logStage, cloneOpts)
913918
if fallbackErr == nil {
914919
if cloned {
915920
endStage("📦 Cloned repository!")
@@ -923,7 +928,7 @@ func RunCacheProbe(ctx context.Context, opts options.Options) (v1.Image, error)
923928

924929
_ = w.Close()
925930
} else {
926-
cloneOpts, err := git.CloneOptionsFromOptions(opts)
931+
cloneOpts, err := git.CloneOptionsFromOptions(logStage, opts)
927932
if err != nil {
928933
return nil, fmt.Errorf("git clone options: %w", err)
929934
}
@@ -934,12 +939,11 @@ func RunCacheProbe(ctx context.Context, opts options.Options) (v1.Image, error)
934939
newColor(color.FgCyan).Sprintf(cloneOpts.Path),
935940
)
936941

937-
stageNum := stageNumber
938-
w := git.ProgressWriter(func(line string) { opts.Logger(log.LevelInfo, "#%d: %s", stageNum, line) })
942+
w := git.ProgressWriter(logStage)
939943
defer w.Close()
940944
cloneOpts.Progress = w
941945

942-
fallbackErr = git.ShallowCloneRepo(ctx, cloneOpts)
946+
fallbackErr = git.ShallowCloneRepo(ctx, logStage, cloneOpts)
943947
if fallbackErr == nil {
944948
endStage("📦 Cloned repository!")
945949
buildTimeWorkspaceFolder = cloneOpts.Path

Diff for: git/git.go

+44-24
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import (
1212
"github.com/coder/envbuilder/options"
1313

1414
giturls "github.com/chainguard-dev/git-urls"
15-
"github.com/coder/envbuilder/log"
1615
"github.com/go-git/go-billy/v5"
1716
"github.com/go-git/go-git/v5"
1817
"github.com/go-git/go-git/v5/plumbing"
@@ -47,11 +46,12 @@ type CloneRepoOptions struct {
4746
// be cloned again.
4847
//
4948
// The bool returned states whether the repository was cloned or not.
50-
func CloneRepo(ctx context.Context, opts CloneRepoOptions) (bool, error) {
49+
func CloneRepo(ctx context.Context, logf func(string, ...any), opts CloneRepoOptions) (bool, error) {
5150
parsed, err := giturls.Parse(opts.RepoURL)
5251
if err != nil {
5352
return false, fmt.Errorf("parse url %q: %w", opts.RepoURL, err)
5453
}
54+
logf("Parsed Git URL as %q", parsed.Redacted())
5555
if parsed.Hostname() == "dev.azure.com" {
5656
// Azure DevOps requires capabilities multi_ack / multi_ack_detailed,
5757
// which are not fully implemented and by default are included in
@@ -73,6 +73,7 @@ func CloneRepo(ctx context.Context, opts CloneRepoOptions) (bool, error) {
7373
transport.UnsupportedCapabilities = []capability.Capability{
7474
capability.ThinPack,
7575
}
76+
logf("Workaround for Azure DevOps: marking thin-pack as unsupported")
7677
}
7778

7879
err = opts.Storage.MkdirAll(opts.Path, 0o755)
@@ -131,7 +132,7 @@ func CloneRepo(ctx context.Context, opts CloneRepoOptions) (bool, error) {
131132
// clone will not be performed.
132133
//
133134
// The bool returned states whether the repository was cloned or not.
134-
func ShallowCloneRepo(ctx context.Context, opts CloneRepoOptions) error {
135+
func ShallowCloneRepo(ctx context.Context, logf func(string, ...any), opts CloneRepoOptions) error {
135136
opts.Depth = 1
136137
opts.SingleBranch = true
137138

@@ -150,7 +151,7 @@ func ShallowCloneRepo(ctx context.Context, opts CloneRepoOptions) error {
150151
}
151152
}
152153

153-
cloned, err := CloneRepo(ctx, opts)
154+
cloned, err := CloneRepo(ctx, logf, opts)
154155
if err != nil {
155156
return err
156157
}
@@ -182,14 +183,14 @@ func ReadPrivateKey(path string) (gossh.Signer, error) {
182183

183184
// LogHostKeyCallback is a HostKeyCallback that just logs host keys
184185
// and does nothing else.
185-
func LogHostKeyCallback(logger log.Func) gossh.HostKeyCallback {
186+
func LogHostKeyCallback(logger func(string, ...any)) gossh.HostKeyCallback {
186187
return func(hostname string, remote net.Addr, key gossh.PublicKey) error {
187188
var sb strings.Builder
188189
_ = knownhosts.WriteKnownHost(&sb, hostname, remote, key)
189190
// skeema/knownhosts uses a fake public key to determine the host key
190191
// algorithms. Ignore this one.
191192
if s := sb.String(); !strings.Contains(s, "fake-public-key ZmFrZSBwdWJsaWMga2V5") {
192-
logger(log.LevelInfo, "#1: 🔑 Got host key: %s", strings.TrimSpace(s))
193+
logger("🔑 Got host key: %s", strings.TrimSpace(s))
193194
}
194195
return nil
195196
}
@@ -203,6 +204,8 @@ func LogHostKeyCallback(logger log.Func) gossh.HostKeyCallback {
203204
// | https?://host.tld/repo | Not Set | Set | HTTP Basic |
204205
// | https?://host.tld/repo | Set | Not Set | HTTP Basic |
205206
// | https?://host.tld/repo | Set | Set | HTTP Basic |
207+
// | file://path/to/repo | - | - | None |
208+
// | path/to/repo | - | - | None |
206209
// | All other formats | - | - | SSH |
207210
//
208211
// For SSH authentication, the default username is "git" but will honour
@@ -214,58 +217,73 @@ func LogHostKeyCallback(logger log.Func) gossh.HostKeyCallback {
214217
// If SSH_KNOWN_HOSTS is not set, the SSH auth method will be configured
215218
// to accept and log all host keys. Otherwise, host key checking will be
216219
// performed as usual.
217-
func SetupRepoAuth(options *options.Options) transport.AuthMethod {
220+
func SetupRepoAuth(logf func(string, ...any), options *options.Options) transport.AuthMethod {
218221
if options.GitURL == "" {
219-
options.Logger(log.LevelInfo, "#1: ❔ No Git URL supplied!")
222+
logf("❔ No Git URL supplied!")
220223
return nil
221224
}
222-
if strings.HasPrefix(options.GitURL, "http://") || strings.HasPrefix(options.GitURL, "https://") {
225+
parsedURL, err := giturls.Parse(options.GitURL)
226+
if err != nil {
227+
logf("❌ Failed to parse Git URL: %s", err.Error())
228+
return nil
229+
}
230+
231+
if parsedURL.Scheme == "http" || parsedURL.Scheme == "https" {
223232
// Special case: no auth
224233
if options.GitUsername == "" && options.GitPassword == "" {
225-
options.Logger(log.LevelInfo, "#1: 👤 Using no authentication!")
234+
logf("👤 Using no authentication!")
226235
return nil
227236
}
228237
// Basic Auth
229238
// NOTE: we previously inserted the credentials into the repo URL.
230239
// This was removed in https://github.com/coder/envbuilder/pull/141
231-
options.Logger(log.LevelInfo, "#1: 🔒 Using HTTP basic authentication!")
240+
logf("🔒 Using HTTP basic authentication!")
232241
return &githttp.BasicAuth{
233242
Username: options.GitUsername,
234243
Password: options.GitPassword,
235244
}
236245
}
237246

247+
if parsedURL.Scheme == "file" {
248+
// go-git will try to fallback to using the `git` command for local
249+
// filesystem clones. However, it's more likely than not that the
250+
// `git` command is not present in the container image. Log a warning
251+
// but continue. Also, no auth.
252+
logf("🚧 Using local filesystem clone! This requires the git executable to be present!")
253+
return nil
254+
}
255+
238256
// Generally git clones over SSH use the 'git' user, but respect
239257
// GIT_USERNAME if set.
240258
if options.GitUsername == "" {
241259
options.GitUsername = "git"
242260
}
243261

244262
// Assume SSH auth for all other formats.
245-
options.Logger(log.LevelInfo, "#1: 🔑 Using SSH authentication!")
263+
logf("🔑 Using SSH authentication!")
246264

247265
var signer ssh.Signer
248266
if options.GitSSHPrivateKeyPath != "" {
249267
s, err := ReadPrivateKey(options.GitSSHPrivateKeyPath)
250268
if err != nil {
251-
options.Logger(log.LevelError, "#1: ❌ Failed to read private key from %s: %s", options.GitSSHPrivateKeyPath, err.Error())
269+
logf("❌ Failed to read private key from %s: %s", options.GitSSHPrivateKeyPath, err.Error())
252270
} else {
253-
options.Logger(log.LevelInfo, "#1: 🔑 Using %s key!", s.PublicKey().Type())
271+
logf("🔑 Using %s key!", s.PublicKey().Type())
254272
signer = s
255273
}
256274
}
257275

258276
// If no SSH key set, fall back to agent auth.
259277
if signer == nil {
260-
options.Logger(log.LevelError, "#1: 🔑 No SSH key found, falling back to agent!")
278+
logf("🔑 No SSH key found, falling back to agent!")
261279
auth, err := gitssh.NewSSHAgentAuth(options.GitUsername)
262280
if err != nil {
263-
options.Logger(log.LevelError, "#1: ❌ Failed to connect to SSH agent: %s", err.Error())
281+
logf("❌ Failed to connect to SSH agent: " + err.Error())
264282
return nil // nothing else we can do
265283
}
266284
if os.Getenv("SSH_KNOWN_HOSTS") == "" {
267-
options.Logger(log.LevelWarn, "#1: 🔓 SSH_KNOWN_HOSTS not set, accepting all host keys!")
268-
auth.HostKeyCallback = LogHostKeyCallback(options.Logger)
285+
logf("🔓 SSH_KNOWN_HOSTS not set, accepting all host keys!")
286+
auth.HostKeyCallback = LogHostKeyCallback(logf)
269287
}
270288
return auth
271289
}
@@ -283,19 +301,20 @@ func SetupRepoAuth(options *options.Options) transport.AuthMethod {
283301

284302
// Duplicated code due to Go's type system.
285303
if os.Getenv("SSH_KNOWN_HOSTS") == "" {
286-
options.Logger(log.LevelWarn, "#1: 🔓 SSH_KNOWN_HOSTS not set, accepting all host keys!")
287-
auth.HostKeyCallback = LogHostKeyCallback(options.Logger)
304+
logf("🔓 SSH_KNOWN_HOSTS not set, accepting all host keys!")
305+
auth.HostKeyCallback = LogHostKeyCallback(logf)
288306
}
289307
return auth
290308
}
291309

292-
func CloneOptionsFromOptions(options options.Options) (CloneRepoOptions, error) {
310+
func CloneOptionsFromOptions(logf func(string, ...any), options options.Options) (CloneRepoOptions, error) {
293311
caBundle, err := options.CABundle()
294312
if err != nil {
295313
return CloneRepoOptions{}, err
296314
}
297315

298316
cloneOpts := CloneRepoOptions{
317+
RepoURL: options.GitURL,
299318
Path: options.WorkspaceFolder,
300319
Storage: options.Filesystem,
301320
Insecure: options.Insecure,
@@ -304,13 +323,12 @@ func CloneOptionsFromOptions(options options.Options) (CloneRepoOptions, error)
304323
CABundle: caBundle,
305324
}
306325

307-
cloneOpts.RepoAuth = SetupRepoAuth(&options)
326+
cloneOpts.RepoAuth = SetupRepoAuth(logf, &options)
308327
if options.GitHTTPProxyURL != "" {
309328
cloneOpts.ProxyOptions = transport.ProxyOptions{
310329
URL: options.GitHTTPProxyURL,
311330
}
312331
}
313-
cloneOpts.RepoURL = options.GitURL
314332

315333
return cloneOpts, nil
316334
}
@@ -331,7 +349,7 @@ func (w *progressWriter) Close() error {
331349
return err2
332350
}
333351

334-
func ProgressWriter(write func(line string)) io.WriteCloser {
352+
func ProgressWriter(write func(line string, args ...any)) io.WriteCloser {
335353
reader, writer := io.Pipe()
336354
done := make(chan struct{})
337355
go func() {
@@ -347,6 +365,8 @@ func ProgressWriter(write func(line string)) io.WriteCloser {
347365
if line == "" {
348366
continue
349367
}
368+
// Escape % signs so that they don't get interpreted as format specifiers
369+
line = strings.Replace(line, "%", "%%", -1)
350370
write(strings.TrimSpace(line))
351371
}
352372
}

0 commit comments

Comments
 (0)