4
4
package provider
5
5
6
6
import (
7
+ "archive/tar"
7
8
"context"
8
9
"fmt"
10
+ "io"
9
11
"net/http"
10
12
"os"
11
13
"path/filepath"
@@ -16,6 +18,9 @@ import (
16
18
eblog "github.com/coder/envbuilder/log"
17
19
eboptions "github.com/coder/envbuilder/options"
18
20
"github.com/go-git/go-billy/v5/osfs"
21
+ "github.com/google/go-containerregistry/pkg/authn"
22
+ "github.com/google/go-containerregistry/pkg/name"
23
+ "github.com/google/go-containerregistry/pkg/v1/remote"
19
24
20
25
"github.com/hashicorp/terraform-plugin-framework/datasource"
21
26
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
@@ -62,6 +67,7 @@ type CachedImageDataSourceModel struct {
62
67
Insecure types.Bool `tfsdk:"insecure"`
63
68
SSLCertBase64 types.String `tfsdk:"ssl_cert_base64"`
64
69
Verbose types.Bool `tfsdk:"verbose"`
70
+ WorkspaceFolder types.String `tfsdk:"workspace_folder"`
65
71
// Computed "outputs".
66
72
Env types.List `tfsdk:"env"`
67
73
Exists types.Bool `tfsdk:"exists"`
@@ -179,6 +185,10 @@ func (d *CachedImageDataSource) Schema(ctx context.Context, req datasource.Schem
179
185
MarkdownDescription : "(Envbuilder option) Enable verbose output." ,
180
186
Optional : true ,
181
187
},
188
+ "workspace_folder" : schema.StringAttribute {
189
+ MarkdownDescription : "(Envbuilder option) path to the workspace folder that will be built. This is optional." ,
190
+ Optional : true ,
191
+ },
182
192
183
193
// Computed "outputs".
184
194
// TODO(mafredri): Map vs List? Support both?
@@ -248,9 +258,10 @@ func (d *CachedImageDataSource) Read(ctx context.Context, req datasource.ReadReq
248
258
}
249
259
defer func () {
250
260
if err := os .RemoveAll (tmpDir ); err != nil {
251
- tflog .Error (ctx , "failed to clean up tmpDir" , map [string ]any {"tmpDir" : tmpDir , "err" : err . Error () })
261
+ tflog .Error (ctx , "failed to clean up tmpDir" , map [string ]any {"tmpDir" : tmpDir , "err" : err })
252
262
}
253
263
}()
264
+
254
265
oldKanikoDir := kconfig .KanikoDir
255
266
tmpKanikoDir := filepath .Join (tmpDir , constants .MagicDir )
256
267
// Normally you would set the KANIKO_DIR environment variable, but we are importing kaniko directly.
@@ -262,6 +273,22 @@ func (d *CachedImageDataSource) Read(ctx context.Context, req datasource.ReadReq
262
273
}()
263
274
if err := os .MkdirAll (tmpKanikoDir , 0o755 ); err != nil {
264
275
tflog .Error (ctx , "failed to create kaniko dir: " + err .Error ())
276
+ return
277
+ }
278
+
279
+ // In order to correctly reproduce the final layer of the cached image, we
280
+ // need the envbuilder binary used to originally build the image!
281
+ envbuilderPath := filepath .Join (tmpDir , "envbuilder" )
282
+ if err := extractEnvbuilderFromImage (ctx , data .BuilderImage .ValueString (), envbuilderPath ); err != nil {
283
+ tflog .Error (ctx , "failed to fetch envbuilder binary from builder image" , map [string ]any {"err" : err })
284
+ resp .Diagnostics .AddError ("Internal Error" , fmt .Sprintf ("Failed to fetch the envbuilder binary from the builder image: %s" , err .Error ()))
285
+ return
286
+ }
287
+
288
+ workspaceFolder := data .WorkspaceFolder .ValueString ()
289
+ if workspaceFolder == "" {
290
+ workspaceFolder = filepath .Join (tmpDir , "workspace" )
291
+ tflog .Debug (ctx , "workspace_folder not specified, using temp dir" , map [string ]any {"workspace_folder" : workspaceFolder })
265
292
}
266
293
267
294
// TODO: check if this is a "plan" or "apply", and only run envbuilder on "apply".
@@ -274,7 +301,7 @@ func (d *CachedImageDataSource) Read(ctx context.Context, req datasource.ReadReq
274
301
GetCachedImage : true , // always!
275
302
Logger : tfLogFunc (ctx ),
276
303
Verbose : data .Verbose .ValueBool (),
277
- WorkspaceFolder : tmpDir ,
304
+ WorkspaceFolder : workspaceFolder ,
278
305
279
306
// Options related to compiling the devcontainer
280
307
BuildContextPath : data .BuildContextPath .ValueString (),
@@ -297,6 +324,7 @@ func (d *CachedImageDataSource) Read(ctx context.Context, req datasource.ReadReq
297
324
298
325
// Other options
299
326
BaseImageCacheDir : data .BaseImageCacheDir .ValueString (),
327
+ BinaryPath : envbuilderPath , // needed to reproduce the final layer.
300
328
ExitOnBuildFailure : data .ExitOnBuildFailure .ValueBool (), // may wish to do this instead of fallback image?
301
329
Insecure : data .Insecure .ValueBool (), // might have internal CAs?
302
330
IgnorePaths : tfListToStringSlice (data .IgnorePaths ), // may need to be specified?
@@ -310,7 +338,7 @@ func (d *CachedImageDataSource) Read(ctx context.Context, req datasource.ReadReq
310
338
InitScript : "" ,
311
339
LayerCacheDir : "" ,
312
340
PostStartScriptPath : "" ,
313
- PushImage : false ,
341
+ PushImage : false , // This is only relevant when building.
314
342
SetupScript : "" ,
315
343
SkipRebuild : false ,
316
344
}
@@ -401,3 +429,79 @@ func tfListToStringSlice(l types.List) []string {
401
429
}
402
430
return ss
403
431
}
432
+
433
+ // extractEnvbuilderFromImage reads the image located at imgRef and extracts
434
+ // MagicBinaryLocation to destPath.
435
+ func extractEnvbuilderFromImage (ctx context.Context , imgRef , destPath string ) error {
436
+ needle := filepath .Clean (constants .MagicBinaryLocation )[1 :] // skip leading '/'
437
+ ref , err := name .ParseReference (imgRef )
438
+ if err != nil {
439
+ return fmt .Errorf ("parse reference: %w" , err )
440
+ }
441
+
442
+ img , err := remote .Image (ref , remote .WithAuthFromKeychain (authn .DefaultKeychain ))
443
+ if err != nil {
444
+ return fmt .Errorf ("check remote image: %w" , err )
445
+ }
446
+
447
+ layers , err := img .Layers ()
448
+ if err != nil {
449
+ return fmt .Errorf ("get image layers: %w" , err )
450
+ }
451
+
452
+ // Check the layers in reverse order. The last layers are more likely to
453
+ // include the binary.
454
+ for i := len (layers ) - 1 ; i >= 0 ; i -- {
455
+ ul , err := layers [i ].Uncompressed ()
456
+ if err != nil {
457
+ return fmt .Errorf ("get uncompressed layer: %w" , err )
458
+ }
459
+
460
+ tr := tar .NewReader (ul )
461
+ for {
462
+ th , err := tr .Next ()
463
+ if err == io .EOF {
464
+ break
465
+ }
466
+
467
+ if err != nil {
468
+ return fmt .Errorf ("read tar header: %w" , err )
469
+ }
470
+
471
+ name := filepath .Clean (th .Name )
472
+ if th .Typeflag != tar .TypeReg {
473
+ tflog .Debug (ctx , "skip non-regular file" , map [string ]any {"name" : name , "layer_idx" : i + 1 })
474
+ continue
475
+ }
476
+
477
+ if name != needle {
478
+ tflog .Debug (ctx , "skip file" , map [string ]any {"name" : name , "layer_idx" : i + 1 })
479
+ continue
480
+ }
481
+
482
+ tflog .Debug (ctx , "found file" , map [string ]any {"name" : name , "layer_idx" : i + 1 })
483
+ if err := os .MkdirAll (filepath .Dir (destPath ), 0o755 ); err != nil {
484
+ return fmt .Errorf ("create parent directories: %w" , err )
485
+ }
486
+ destF , err := os .Create (destPath )
487
+ if err != nil {
488
+ return fmt .Errorf ("create dest file for writing: %w" , err )
489
+ }
490
+ defer destF .Close ()
491
+ _ , err = io .Copy (destF , tr )
492
+ if err != nil {
493
+ return fmt .Errorf ("copy dest file from image: %w" , err )
494
+ }
495
+ if err := destF .Close (); err != nil {
496
+ return fmt .Errorf ("close dest file: %w" , err )
497
+ }
498
+
499
+ if err := os .Chmod (destPath , 0o755 ); err != nil {
500
+ return fmt .Errorf ("chmod file: %w" , err )
501
+ }
502
+ return nil
503
+ }
504
+ }
505
+
506
+ return fmt .Errorf ("extract envbuilder binary from image %q: %w" , imgRef , os .ErrNotExist )
507
+ }
0 commit comments