@@ -12,7 +12,6 @@ import (
12
12
"github.com/coder/envbuilder/options"
13
13
14
14
giturls "github.com/chainguard-dev/git-urls"
15
- "github.com/coder/envbuilder/log"
16
15
"github.com/go-git/go-billy/v5"
17
16
"github.com/go-git/go-git/v5"
18
17
"github.com/go-git/go-git/v5/plumbing"
@@ -47,11 +46,12 @@ type CloneRepoOptions struct {
47
46
// be cloned again.
48
47
//
49
48
// 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 ) {
51
50
parsed , err := giturls .Parse (opts .RepoURL )
52
51
if err != nil {
53
52
return false , fmt .Errorf ("parse url %q: %w" , opts .RepoURL , err )
54
53
}
54
+ logf ("Parsed Git URL as %q" , parsed .Redacted ())
55
55
if parsed .Hostname () == "dev.azure.com" {
56
56
// Azure DevOps requires capabilities multi_ack / multi_ack_detailed,
57
57
// which are not fully implemented and by default are included in
@@ -73,6 +73,7 @@ func CloneRepo(ctx context.Context, opts CloneRepoOptions) (bool, error) {
73
73
transport .UnsupportedCapabilities = []capability.Capability {
74
74
capability .ThinPack ,
75
75
}
76
+ logf ("Workaround for Azure DevOps: marking thin-pack as unsupported" )
76
77
}
77
78
78
79
err = opts .Storage .MkdirAll (opts .Path , 0o755 )
@@ -131,7 +132,7 @@ func CloneRepo(ctx context.Context, opts CloneRepoOptions) (bool, error) {
131
132
// clone will not be performed.
132
133
//
133
134
// 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 {
135
136
opts .Depth = 1
136
137
opts .SingleBranch = true
137
138
@@ -150,7 +151,7 @@ func ShallowCloneRepo(ctx context.Context, opts CloneRepoOptions) error {
150
151
}
151
152
}
152
153
153
- cloned , err := CloneRepo (ctx , opts )
154
+ cloned , err := CloneRepo (ctx , logf , opts )
154
155
if err != nil {
155
156
return err
156
157
}
@@ -182,14 +183,14 @@ func ReadPrivateKey(path string) (gossh.Signer, error) {
182
183
183
184
// LogHostKeyCallback is a HostKeyCallback that just logs host keys
184
185
// and does nothing else.
185
- func LogHostKeyCallback (logger log. Func ) gossh.HostKeyCallback {
186
+ func LogHostKeyCallback (logger func ( string , ... any ) ) gossh.HostKeyCallback {
186
187
return func (hostname string , remote net.Addr , key gossh.PublicKey ) error {
187
188
var sb strings.Builder
188
189
_ = knownhosts .WriteKnownHost (& sb , hostname , remote , key )
189
190
// skeema/knownhosts uses a fake public key to determine the host key
190
191
// algorithms. Ignore this one.
191
192
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 ))
193
194
}
194
195
return nil
195
196
}
@@ -203,6 +204,8 @@ func LogHostKeyCallback(logger log.Func) gossh.HostKeyCallback {
203
204
// | https?://host.tld/repo | Not Set | Set | HTTP Basic |
204
205
// | https?://host.tld/repo | Set | Not Set | HTTP Basic |
205
206
// | https?://host.tld/repo | Set | Set | HTTP Basic |
207
+ // | file://path/to/repo | - | - | None |
208
+ // | path/to/repo | - | - | None |
206
209
// | All other formats | - | - | SSH |
207
210
//
208
211
// For SSH authentication, the default username is "git" but will honour
@@ -214,58 +217,73 @@ func LogHostKeyCallback(logger log.Func) gossh.HostKeyCallback {
214
217
// If SSH_KNOWN_HOSTS is not set, the SSH auth method will be configured
215
218
// to accept and log all host keys. Otherwise, host key checking will be
216
219
// performed as usual.
217
- func SetupRepoAuth (options * options.Options ) transport.AuthMethod {
220
+ func SetupRepoAuth (logf func ( string , ... any ), options * options.Options ) transport.AuthMethod {
218
221
if options .GitURL == "" {
219
- options . Logger ( log . LevelInfo , "#1: ❔ No Git URL supplied!" )
222
+ logf ( " ❔ No Git URL supplied!" )
220
223
return nil
221
224
}
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" {
223
232
// Special case: no auth
224
233
if options .GitUsername == "" && options .GitPassword == "" {
225
- options . Logger ( log . LevelInfo , "#1: 👤 Using no authentication!" )
234
+ logf ( " 👤 Using no authentication!" )
226
235
return nil
227
236
}
228
237
// Basic Auth
229
238
// NOTE: we previously inserted the credentials into the repo URL.
230
239
// 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!" )
232
241
return & githttp.BasicAuth {
233
242
Username : options .GitUsername ,
234
243
Password : options .GitPassword ,
235
244
}
236
245
}
237
246
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
+
238
256
// Generally git clones over SSH use the 'git' user, but respect
239
257
// GIT_USERNAME if set.
240
258
if options .GitUsername == "" {
241
259
options .GitUsername = "git"
242
260
}
243
261
244
262
// Assume SSH auth for all other formats.
245
- options . Logger ( log . LevelInfo , "#1: 🔑 Using SSH authentication!" )
263
+ logf ( " 🔑 Using SSH authentication!" )
246
264
247
265
var signer ssh.Signer
248
266
if options .GitSSHPrivateKeyPath != "" {
249
267
s , err := ReadPrivateKey (options .GitSSHPrivateKeyPath )
250
268
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 ())
252
270
} else {
253
- options . Logger ( log . LevelInfo , "#1: 🔑 Using %s key!" , s .PublicKey ().Type ())
271
+ logf ( " 🔑 Using %s key!" , s .PublicKey ().Type ())
254
272
signer = s
255
273
}
256
274
}
257
275
258
276
// If no SSH key set, fall back to agent auth.
259
277
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!" )
261
279
auth , err := gitssh .NewSSHAgentAuth (options .GitUsername )
262
280
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 ())
264
282
return nil // nothing else we can do
265
283
}
266
284
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 )
269
287
}
270
288
return auth
271
289
}
@@ -283,19 +301,20 @@ func SetupRepoAuth(options *options.Options) transport.AuthMethod {
283
301
284
302
// Duplicated code due to Go's type system.
285
303
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 )
288
306
}
289
307
return auth
290
308
}
291
309
292
- func CloneOptionsFromOptions (options options.Options ) (CloneRepoOptions , error ) {
310
+ func CloneOptionsFromOptions (logf func ( string , ... any ), options options.Options ) (CloneRepoOptions , error ) {
293
311
caBundle , err := options .CABundle ()
294
312
if err != nil {
295
313
return CloneRepoOptions {}, err
296
314
}
297
315
298
316
cloneOpts := CloneRepoOptions {
317
+ RepoURL : options .GitURL ,
299
318
Path : options .WorkspaceFolder ,
300
319
Storage : options .Filesystem ,
301
320
Insecure : options .Insecure ,
@@ -304,13 +323,12 @@ func CloneOptionsFromOptions(options options.Options) (CloneRepoOptions, error)
304
323
CABundle : caBundle ,
305
324
}
306
325
307
- cloneOpts .RepoAuth = SetupRepoAuth (& options )
326
+ cloneOpts .RepoAuth = SetupRepoAuth (logf , & options )
308
327
if options .GitHTTPProxyURL != "" {
309
328
cloneOpts .ProxyOptions = transport.ProxyOptions {
310
329
URL : options .GitHTTPProxyURL ,
311
330
}
312
331
}
313
- cloneOpts .RepoURL = options .GitURL
314
332
315
333
return cloneOpts , nil
316
334
}
@@ -331,7 +349,7 @@ func (w *progressWriter) Close() error {
331
349
return err2
332
350
}
333
351
334
- func ProgressWriter (write func (line string )) io.WriteCloser {
352
+ func ProgressWriter (write func (line string , args ... any )) io.WriteCloser {
335
353
reader , writer := io .Pipe ()
336
354
done := make (chan struct {})
337
355
go func () {
@@ -347,6 +365,8 @@ func ProgressWriter(write func(line string)) io.WriteCloser {
347
365
if line == "" {
348
366
continue
349
367
}
368
+ // Escape % signs so that they don't get interpreted as format specifiers
369
+ line = strings .Replace (line , "%" , "%%" , - 1 )
350
370
write (strings .TrimSpace (line ))
351
371
}
352
372
}
0 commit comments