diff --git a/test/functional/lcow_container_bench_test.go b/test/functional/lcow_container_bench_test.go index d2b02838c9..26327fceea 100644 --- a/test/functional/lcow_container_bench_test.go +++ b/test/functional/lcow_container_bench_test.go @@ -4,7 +4,6 @@ package functional import ( - "context" "testing" ctrdoci "github.com/containerd/containerd/oci" @@ -15,7 +14,6 @@ import ( "github.com/Microsoft/hcsshim/osversion" "github.com/Microsoft/hcsshim/test/internal/cmd" - "github.com/Microsoft/hcsshim/test/internal/constants" "github.com/Microsoft/hcsshim/test/internal/container" "github.com/Microsoft/hcsshim/test/internal/layers" "github.com/Microsoft/hcsshim/test/internal/oci" @@ -27,9 +25,8 @@ func BenchmarkLCOW_Container(b *testing.B) { requireFeatures(b, featureLCOW, featureContainer) require.Build(b, osversion.RS5) - ctx, _, client := newContainerdClient(context.Background(), b) - ls := layers.FromImage(ctx, b, client, constants.ImageLinuxAlpineLatest, - constants.PlatformLinux, constants.SnapshotterLinux) + ctx := namespacedContext() + ls := linuxImageLayers(ctx, b) // Create a new uvm per benchmark in case any left over state lingers diff --git a/test/functional/lcow_container_test.go b/test/functional/lcow_container_test.go index 452705ce9c..d8b3372098 100644 --- a/test/functional/lcow_container_test.go +++ b/test/functional/lcow_container_test.go @@ -4,7 +4,6 @@ package functional import ( - "context" "strings" "testing" @@ -13,7 +12,6 @@ import ( "github.com/Microsoft/hcsshim/osversion" "github.com/Microsoft/hcsshim/test/internal/cmd" - "github.com/Microsoft/hcsshim/test/internal/constants" "github.com/Microsoft/hcsshim/test/internal/container" "github.com/Microsoft/hcsshim/test/internal/layers" "github.com/Microsoft/hcsshim/test/internal/oci" @@ -25,10 +23,8 @@ func TestLCOW_ContainerLifecycle(t *testing.T) { requireFeatures(t, featureLCOW, featureContainer) require.Build(t, osversion.RS5) - ctx, _, client := newContainerdClient(context.Background(), t) - ls := layers.FromImage(ctx, t, client, constants.ImageLinuxAlpineLatest, - constants.PlatformLinux, constants.SnapshotterLinux) - + ctx := namespacedContext() + ls := linuxImageLayers(ctx, t) opts := defaultLCOWOptions(t) vm := uvm.CreateAndStartLCOWFromOpts(ctx, t, opts) @@ -81,10 +77,8 @@ func TestLCOW_ContainerIO(t *testing.T) { requireFeatures(t, featureLCOW, featureContainer) require.Build(t, osversion.RS5) - ctx, _, client := newContainerdClient(context.Background(), t) - ls := layers.FromImage(ctx, t, client, constants.ImageLinuxAlpineLatest, - constants.PlatformLinux, constants.SnapshotterLinux) - + ctx := namespacedContext() + ls := linuxImageLayers(ctx, t) opts := defaultLCOWOptions(t) cache := layers.CacheFile(ctx, t, "") vm := uvm.CreateAndStartLCOWFromOpts(ctx, t, opts) @@ -125,10 +119,8 @@ func TestLCOW_ContainerExec(t *testing.T) { requireFeatures(t, featureLCOW, featureContainer) require.Build(t, osversion.RS5) - ctx, _, client := newContainerdClient(context.Background(), t) - ls := layers.FromImage(ctx, t, client, constants.ImageLinuxAlpineLatest, - constants.PlatformLinux, constants.SnapshotterLinux) - + ctx := namespacedContext() + ls := linuxImageLayers(ctx, t) opts := defaultLCOWOptions(t) vm := uvm.CreateAndStartLCOWFromOpts(ctx, t, opts) diff --git a/test/functional/main_test.go b/test/functional/main_test.go index 59d87891ec..40aaa9b4fd 100644 --- a/test/functional/main_test.go +++ b/test/functional/main_test.go @@ -13,12 +13,11 @@ import ( "log" "os" "os/exec" - "regexp" "strconv" "testing" "time" - "github.com/containerd/containerd" + "github.com/containerd/containerd/namespaces" "github.com/sirupsen/logrus" "github.com/Microsoft/hcsshim/internal/cow" @@ -27,14 +26,28 @@ import ( "github.com/Microsoft/hcsshim/internal/uvm" "github.com/Microsoft/hcsshim/internal/winapi" - testctrd "github.com/Microsoft/hcsshim/test/internal/containerd" + "github.com/Microsoft/hcsshim/test/internal/constants" testflag "github.com/Microsoft/hcsshim/test/internal/flag" + "github.com/Microsoft/hcsshim/test/internal/layers" "github.com/Microsoft/hcsshim/test/internal/require" + "github.com/Microsoft/hcsshim/test/internal/util" ) // owner field for uVMs. const hcsOwner = "hcsshim-functional-tests" +var ( + alpineImagePaths = layers.LazyImageLayers{ + Image: constants.ImageLinuxAlpineLatest, + Platform: constants.PlatformLinux, + } + //TODO: pick appropriate image based on OS build + nanoserverImagePaths = layers.LazyImageLayers{ + Image: constants.ImageWindowsNanoserverLTSC2022, + Platform: constants.PlatformWindows, + } +) + const ( featureLCOW = "LCOW" featureWCOW = "WCOW" @@ -61,16 +74,25 @@ var allFeatures = []string{ featureVPMEM, } -// todo: use a new containerd namespace and then nuke everything in it - var ( - debug bool - pauseDurationOnCreateContainerFailure time.Duration - - flagFeatures = testflag.NewFeatureFlag(allFeatures) - flagContainerdAddress = flag.String("ctr-address", "tcp://127.0.0.1:2376", "`address` for containerd's GRPC server") - flagContainerdNamespace = flag.String("ctr-namespace", "k8s.io", "containerd `namespace`") - flagLinuxBootFilesPath = flag.String("linux-bootfiles", + flagPauseAfterCreateContainerFailure time.Duration + + flagFeatures = testflag.NewFeatureFlag(allFeatures) + flagDebug = flag.Bool("debug", + os.Getenv("HCSSHIM_FUNCTIONAL_TESTS_DEBUG") != "", + "set logging level to debug [%HCSSHIM_FUNCTIONAL_TESTS_DEBUG%]") + flagContainerdNamespace = flag.String("ctr-namespace", hcsOwner, + "containerd `namespace` to use when creating OCI specs") + flagLCOWLayerPaths = testflag.NewStringSlice("lcow-layer-paths", + "comma separated list of image layer `paths` to use as LCOW container rootfs. "+ + "If empty, \""+alpineImagePaths.Image+"\" will be pulled and unpacked.") + //nolint:unused // will be used when WCOW tests are updated + flagWCOWLayerPaths = testflag.NewStringSlice("wcow-layer-paths", + "comma separated list of image layer `paths` to use as WCO as WCOW uVM and container rootfs. "+ + "If empty, \""+nanoserverImagePaths.Image+"\" will be pulled and unpacked.") + flagLayerTempDir = flag.String("layer-temp-dir", "", + "`directory` to unpack image layers to, if not provided. Leave empty to use os.TempDir.") + flagLinuxBootFilesPath = flag.String("linux-bootfiles", `C:\\ContainerPlat\\LinuxBootFiles`, "`path` to LCOW UVM boot files (rootfs.vhd, initrd.img, kernel, and vmlinux)") ) @@ -80,20 +102,15 @@ func init() { log.Fatal("tests must be run in an elevated context") } - if _, ok := os.LookupEnv("HCSSHIM_FUNCTIONAL_TESTS_DEBUG"); ok { - debug = true - } - flag.BoolVar(&debug, "debug", debug, "set logging level to debug [%HCSSHIM_FUNCTIONAL_TESTS_DEBUG%]") - // This allows for debugging a utility VM. if s := os.Getenv("HCSSHIM_FUNCTIONAL_TESTS_PAUSE_ON_CREATECONTAINER_FAIL_IN_MINUTES"); s != "" { if t, err := strconv.Atoi(s); err == nil { - pauseDurationOnCreateContainerFailure = time.Duration(t) * time.Minute + flagPauseAfterCreateContainerFailure = time.Duration(t) * time.Minute } } - flag.DurationVar(&pauseDurationOnCreateContainerFailure, + flag.DurationVar(&flagPauseAfterCreateContainerFailure, "container-creation-failure-pause", - pauseDurationOnCreateContainerFailure, + flagPauseAfterCreateContainerFailure, "the number of minutes to wait after a container creation failure to try again "+ "[%HCSSHIM_FUNCTIONAL_TESTS_PAUSE_ON_CREATECONTAINER_FAIL_IN_MINUTES%]") } @@ -102,19 +119,25 @@ func TestMain(m *testing.M) { flag.Parse() lvl := logrus.WarnLevel - if vf := flag.Lookup("test.v"); debug || (vf != nil && vf.Value.String() == strconv.FormatBool(true)) { + if vf := flag.Lookup("test.v"); *flagDebug || (vf != nil && vf.Value.String() == strconv.FormatBool(true)) { lvl = logrus.DebugLevel } logrus.SetLevel(lvl) logrus.SetFormatter(&logrus.TextFormatter{FullTimestamp: true}) logrus.Infof("using features %q", flagFeatures.S.Strings()) + alpineImagePaths.TempPath = *flagLayerTempDir + nanoserverImagePaths.TempPath = *flagLayerTempDir + // delete downloaded layers + defer alpineImagePaths.Close(context.Background()) + defer nanoserverImagePaths.Close(context.Background()) + e := m.Run() // close any uVMs that escaped cmdStr := ` foreach ($vm in Get-ComputeProcess -Owner '` + hcsOwner + `') { Write-Output "uVM $($vm.Id) was left running" ; Stop-ComputeProcess -Force -Id $vm.Id } ` - cmd := exec.Command("powershell", "-NoLogo", " -NonInteractive", "-Command", cmdStr) + cmd := exec.Command("powershell.exe", "-NoLogo", " -NonInteractive", "-Command", cmdStr) o, err := cmd.CombinedOutput() if err != nil { logrus.Warningf("could not call %q to clean up remaining uVMs: %v", cmdStr, err) @@ -126,13 +149,13 @@ func TestMain(m *testing.M) { } func CreateContainerTestWrapper(ctx context.Context, options *hcsoci.CreateOptions) (cow.Container, *resources.Resources, error) { - if pauseDurationOnCreateContainerFailure != 0 { + if flagPauseAfterCreateContainerFailure != 0 { options.DoNotReleaseResourcesOnFailure = true } s, r, err := hcsoci.CreateContainer(ctx, options) if err != nil { - logrus.Warnf("Test is pausing for %s for debugging CreateContainer failure", pauseDurationOnCreateContainerFailure) - time.Sleep(pauseDurationOnCreateContainerFailure) + logrus.Warnf("Test is pausing for %s for debugging CreateContainer failure", flagPauseAfterCreateContainerFailure) + time.Sleep(flagPauseAfterCreateContainerFailure) _ = resources.ReleaseResources(ctx, r, options.HostingSystem, true) } @@ -144,21 +167,9 @@ func requireFeatures(tb testing.TB, features ...string) { require.Features(tb, flagFeatures.S, features...) } -func getContainerdOptions() testctrd.ContainerdClientOptions { - return testctrd.ContainerdClientOptions{ - Address: *flagContainerdAddress, - Namespace: *flagContainerdNamespace, - } -} - -func newContainerdClient(ctx context.Context, tb testing.TB) (context.Context, context.CancelFunc, *containerd.Client) { - tb.Helper() - return getContainerdOptions().NewClient(ctx, tb) -} - func defaultLCOWOptions(tb testing.TB) *uvm.OptionsLCOW { tb.Helper() - opts := uvm.NewDefaultOptionsLCOW(cleanName(tb.Name()), "") + opts := uvm.NewDefaultOptionsLCOW(util.CleanName(tb.Name()), hcsOwner) opts.BootFilesPath = *flagLinuxBootFilesPath return opts @@ -167,13 +178,33 @@ func defaultLCOWOptions(tb testing.TB) *uvm.OptionsLCOW { //nolint:deadcode,unused // will be used when WCOW tests are updated func defaultWCOWOptions(tb testing.TB) *uvm.OptionsWCOW { tb.Helper() - opts := uvm.NewDefaultOptionsWCOW(cleanName(tb.Name()), "") + return uvm.NewDefaultOptionsWCOW(util.CleanName(tb.Name()), hcsOwner) +} - return opts +// linuxImageLayers returns image layer paths appropriate for use as a container rootfs. +// If layer paths were provided on the command line, they are returned. +// Otherwise, it pulls an appropriate image. +func linuxImageLayers(ctx context.Context, tb testing.TB) []string { + if ss := flagLCOWLayerPaths.S.Strings(); len(ss) > 0 { + return ss + } + return alpineImagePaths.ImageLayers(ctx, tb) } -var _nameRegex = regexp.MustCompile(`[\\\/\s]`) +// windowsImageLayers returns image layer paths appropriate for use as a uVM or container rootfs. +// If layer paths were provided on the command line, they are returned. +// Otherwise, it pulls an appropriate image. +// +//nolint:deadcode,unused // will be used when WCOW tests are updated +func windowsImageLayers(ctx context.Context, tb testing.TB) []string { + if ss := flagWCOWLayerPaths.S.Strings(); len(ss) > 0 { + return ss + } + return nanoserverImagePaths.ImageLayers(ctx, tb) +} -func cleanName(n string) string { - return _nameRegex.ReplaceAllString(n, "") +// namespacedContext returns a [context.Context] with the provided namespace added via +// [github.com/containerd/containerd/namespaces.WithNamespace]. +func namespacedContext() context.Context { + return namespaces.WithNamespace(context.Background(), *flagContainerdNamespace) } diff --git a/test/functional/uvm_update_test.go b/test/functional/uvm_update_test.go index a6f16b6ce9..51a3c1ae12 100644 --- a/test/functional/uvm_update_test.go +++ b/test/functional/uvm_update_test.go @@ -7,10 +7,12 @@ import ( "context" "testing" + "github.com/opencontainers/runtime-spec/specs-go" + "github.com/Microsoft/hcsshim/internal/protocol/guestrequest" - "github.com/Microsoft/hcsshim/internal/uvm" "github.com/Microsoft/hcsshim/pkg/ctrdtaskapi" - "github.com/opencontainers/runtime-spec/specs-go" + + "github.com/Microsoft/hcsshim/test/internal/uvm" ) func Test_LCOW_Update_Resources(t *testing.T) { @@ -47,19 +49,8 @@ func Test_LCOW_Update_Resources(t *testing.T) { } { t.Run(config.name, func(t *testing.T) { ctx := context.Background() - opts := uvm.NewDefaultOptionsLCOW(t.Name(), t.Name()) - vm, err := uvm.CreateLCOW(ctx, opts) - if err != nil { - t.Fatalf("failed to create LCOW UVM: %s", err) - } - if err := vm.Start(ctx); err != nil { - t.Fatalf("failed to start LCOW UVM: %s", err) - } - t.Cleanup(func() { - if err := vm.Close(); err != nil { - t.Log(err) - } - }) + vm := uvm.CreateLCOW(ctx, t, defaultLCOWOptions(t)) + uvm.Start(ctx, t, vm) if err := vm.Update(ctx, config.resource, nil); err != nil { if config.valid { t.Fatalf("failed to update LCOW UVM constraints: %s", err) diff --git a/test/go.mod b/test/go.mod index 2eefc037fb..3973e204b4 100644 --- a/test/go.mod +++ b/test/go.mod @@ -11,6 +11,7 @@ require ( github.com/containerd/ttrpc v1.1.0 github.com/containerd/typeurl v1.0.2 github.com/gogo/protobuf v1.3.2 + github.com/google/go-containerregistry v0.11.0 github.com/kevpar/cri v1.11.1-0.20220302210600-4c5c347230b2 github.com/opencontainers/go-digest v1.0.0 github.com/opencontainers/image-spec v1.0.3-0.20220114050600-8b9d41f48198 @@ -34,6 +35,7 @@ require ( github.com/containerd/console v1.0.3 // indirect github.com/containerd/continuity v0.2.2 // indirect github.com/containerd/fifo v1.0.0 // indirect + github.com/containerd/stargz-snapshotter/estargz v0.12.0 // indirect github.com/coreos/go-systemd/v22 v22.3.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/docker/cli v20.10.17+incompatible // indirect @@ -48,7 +50,6 @@ require ( github.com/gogo/googleapis v1.4.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.2 // indirect - github.com/google/go-containerregistry v0.11.0 // indirect github.com/google/uuid v1.3.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect @@ -68,6 +69,7 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 // indirect github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 // indirect + github.com/vbatts/tar-split v0.11.2 // indirect github.com/vektah/gqlparser/v2 v2.4.5 // indirect github.com/vishvananda/netlink v1.1.1-0.20210330154013-f5de75959ad5 // indirect github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f // indirect diff --git a/test/internal/containerd/containerd.go b/test/internal/containerd/containerd.go index 8b835c7033..3f717bee9a 100644 --- a/test/internal/containerd/containerd.go +++ b/test/internal/containerd/containerd.go @@ -12,7 +12,6 @@ import ( "github.com/containerd/containerd/errdefs" kubeutil "github.com/containerd/containerd/integration/remote/util" "github.com/containerd/containerd/mount" - "github.com/containerd/containerd/namespaces" "github.com/containerd/containerd/platforms" "github.com/containerd/containerd/remotes" "github.com/containerd/containerd/remotes/docker" @@ -43,21 +42,19 @@ type ContainerdClientOptions struct { Namespace string } -// NewClient returns a containerd client, a context with the namespace set, and the -// context's cancel function. The context should be used for containerd operations, and -// cancel function will terminate those operations early. +// NewClient returns a containerd client connected using the specified address and namespace. func (cco ContainerdClientOptions) NewClient( ctx context.Context, tb testing.TB, opts ...containerd.ClientOpt, -) (context.Context, context.CancelFunc, *containerd.Client) { +) *containerd.Client { tb.Helper() - // regular `New` does not work on windows, need to use `WithConn` - cctx, ccancel := context.WithTimeout(ctx, timeout.ConnectTimeout) - defer ccancel() + ctx, cancel := context.WithTimeout(ctx, timeout.ConnectTimeout) + defer cancel() - conn, err := createGRPCConn(cctx, cco.Address) + // regular `New` does not work on windows, need to use `WithConn` + conn, err := createGRPCConn(ctx, cco.Address) if err != nil { tb.Fatalf("failed to dial runtime client: %v", err) } @@ -74,11 +71,7 @@ func (cco ContainerdClientOptions) NewClient( c.Close() }) - ctx = namespaces.WithNamespace(ctx, cco.Namespace) - ctx, cancel := context.WithCancel(ctx) - tb.Cleanup(cancel) - - return ctx, cancel, c + return c } func GetPlatformComparer(tb testing.TB, platform string) platforms.MatchComparer { diff --git a/test/internal/flag/flag.go b/test/internal/flag/flag.go index a4cb2c6b5f..fcd25e4890 100644 --- a/test/internal/flag/flag.go +++ b/test/internal/flag/flag.go @@ -11,18 +11,15 @@ import ( const FeatureFlagName = "feature" func NewFeatureFlag(all []string) *StringSlice { - ff := NewStringSlice() - flag.Var(ff, FeatureFlagName, + return NewStringSlice(FeatureFlagName, "the sets of functionality to test; can be set multiple times, or separated with commas. "+ "Supported features: "+strings.Join(all, ", "), ) - - return ff } // StringSlice is a type to be used with the standard library's flag.Var // function as a custom flag value, similar to "github.com/urfave/cli".StringSlice. -// It takes either a comma-separated list of strings, or repeat invocations. +// It takes either a comma-separated list of strings, or repeated invocations. type StringSlice struct { S StringSet } @@ -30,10 +27,12 @@ type StringSlice struct { var _ flag.Value = &StringSlice{} // NewStringSetFlag returns a new StringSetFlag with an empty set. -func NewStringSlice() *StringSlice { - return &StringSlice{ +func NewStringSlice(name, usage string) *StringSlice { + ss := &StringSlice{ S: make(StringSet), } + flag.Var(ss, name, usage) + return ss } // Strings returns a string slice of the flags provided to the flag diff --git a/test/internal/layers/layerfolders.go b/test/internal/layers/layerfolders.go index be0c506c57..481c296740 100644 --- a/test/internal/layers/layerfolders.go +++ b/test/internal/layers/layerfolders.go @@ -67,6 +67,7 @@ func LayerFolders(tb testing.TB, imageName string) []string { return imageLayers[imageName] } +// Deprecated: This relies on docker. Use [FromChainID] or [FromMount] instead. func getLayers(tb testing.TB, imageName string) []string { tb.Helper() cmd := exec.Command("docker", "inspect", imageName, "-f", `"{{.GraphDriver.Data.dir}}"`) @@ -80,6 +81,7 @@ func getLayers(tb testing.TB, imageName string) []string { return append([]string{imagePath}, layers...) } +// Deprecated: This relies on docker. Use [FromChainID] or [FromMount] instead. func getLayerChain(tb testing.TB, layerFolder string) []string { tb.Helper() jPath := filepath.Join(layerFolder, "layerchain.json") diff --git a/test/internal/layers/lazy.go b/test/internal/layers/lazy.go new file mode 100644 index 0000000000..eb65ba2e5a --- /dev/null +++ b/test/internal/layers/lazy.go @@ -0,0 +1,163 @@ +//go:build windows + +package layers + +import ( + "context" + "fmt" + "io" + "os" + "path/filepath" + "runtime" + "strconv" + "sync" + "testing" + + "github.com/Microsoft/go-winio" + "github.com/google/go-containerregistry/pkg/crane" + v1 "github.com/google/go-containerregistry/pkg/v1" + "golang.org/x/sync/errgroup" + + "github.com/Microsoft/hcsshim/ext4/tar2ext4" + "github.com/Microsoft/hcsshim/internal/log" + "github.com/Microsoft/hcsshim/internal/security" + "github.com/Microsoft/hcsshim/internal/wclayer" + "github.com/Microsoft/hcsshim/pkg/ociwclayer" + + "github.com/Microsoft/hcsshim/test/internal/constants" + "github.com/Microsoft/hcsshim/test/internal/util" +) + +// helper utilities for dealing with images + +type LazyImageLayers struct { + Image string + Platform string + TempPath string // TempPath is the path to create a temporary directory in. Default in [os.TempDir] + once sync.Once + layers []string +} + +// ImageLayers returns the image layer paths, from lowest to highest, for a particular image. +func (x *LazyImageLayers) ImageLayers(ctx context.Context, tb testing.TB) []string { + tb.Helper() + tb.Logf("pulling and unpacking %s image %q", x.Platform, x.Image) + // don't use tb.Error/Log inside Once.Do stack, since we cannot call tb.Helper before executing f() + // within Once.Do and that will therefore show the wrong stack/location + var err error + x.once.Do(func() { + var dir string + // tb.TempDir is deleted at the end of a test, but we want the image for future test runs + dir, err = os.MkdirTemp(x.TempPath, util.CleanName(x.Image)) + if err != nil { + err = fmt.Errorf("failed to create temp directory: %w", err) + return + } + + switch x.Platform { + case constants.PlatformLinux: + err = x.linuxImage(ctx, dir) + case constants.PlatformWindows: + err = x.windowsImage(ctx, dir) + default: + err = fmt.Errorf("unsupported platform %q", x.Platform) + } + }) + if err != nil { + x.Close(ctx) + tb.Fatal(err) + } + return x.layers +} + +// Close removes the downloaded image layers. +func (x *LazyImageLayers) Close(ctx context.Context) { + for _, dir := range x.layers { + d, err := filepath.Abs(dir) + if err != nil { + log.G(ctx).WithError(err).Errorf("count not get absolute path to %q", dir) + continue + } + if _, err := os.Stat(d); err != nil { + log.G(ctx).WithError(err).Errorf("path %q is not valid", d) + continue + } + if err := wclayer.DestroyLayer(ctx, d); err != nil { + log.G(ctx).WithError(err).Errorf("could not destroy layer %q", d) + } + } +} + +func (x *LazyImageLayers) linuxImage(ctx context.Context, dir string) error { + img, err := crane.Pull(x.Image, crane.WithPlatform(&v1.Platform{OS: x.Platform, Architecture: runtime.GOARCH})) + if err != nil { + return fmt.Errorf("failed to pull %q: %w", x.Image, err) + } + + f, err := os.Create(filepath.Join(dir, "layer.vhd")) + if err != nil { + return fmt.Errorf("failed to create layer vhd: %w", err) + } + defer f.Close() + // update x.layers so x.close() does the right thing if this function fails + x.layers = []string{dir} + + r, w := io.Pipe() + eg := errgroup.Group{} + eg.Go(func() error { + defer w.Close() + if err := crane.Export(img, w); err != nil { + return fmt.Errorf("export image %q: %w", x.Image, err) + } + return nil + }) + + eg.Go(func() error { + defer r.Close() + if err := tar2ext4.Convert(r, f, tar2ext4.AppendVhdFooter, tar2ext4.ConvertWhiteout); err != nil { + return fmt.Errorf("convert image %q to vhd %q: %w", x.Image, f.Name(), err) + } + if err := f.Sync(); err != nil { + return fmt.Errorf("sync vhd %q to disk: %w", f.Name(), err) + } + f.Close() + if err = security.GrantVmGroupAccess(f.Name()); err != nil { + return fmt.Errorf("grant vm group access to %s: %w", f.Name(), err) + } + return nil + }) + + return eg.Wait() +} + +func (x *LazyImageLayers) windowsImage(ctx context.Context, dir string) error { + img, err := crane.Pull(x.Image, crane.WithPlatform(&v1.Platform{OS: x.Platform, Architecture: runtime.GOARCH})) + if err != nil { + return fmt.Errorf("failed to pull %q: %w", x.Image, err) + } + + layers, err := img.Layers() + if err != nil { + return fmt.Errorf("failed to get image %q layers: %w", x.Image, err) + } + if err := winio.EnableProcessPrivileges([]string{winio.SeBackupPrivilege, winio.SeRestorePrivilege}); err != nil { + return fmt.Errorf("could not set process privileges: %w", err) + } + + for i, l := range layers { + d := filepath.Join(dir, strconv.FormatInt(int64(i), 10)) + if err := os.Mkdir(d, 0755); err != nil { + return err + } + rc, err := l.Uncompressed() + if err != nil { + return fmt.Errorf("failed to load uncompressed layer: %w", err) + } + if _, err := ociwclayer.ImportLayerFromTar(ctx, rc, d, x.layers); err != nil { + return fmt.Errorf("failed to import wc layer %d: %w", i, err) + } + + x.layers = append(x.layers, d) + } + return nil +} diff --git a/test/internal/oci/oci.go b/test/internal/oci/oci.go index 5369bda298..25d1a74983 100644 --- a/test/internal/oci/oci.go +++ b/test/internal/oci/oci.go @@ -43,28 +43,32 @@ func DefaultLinuxSpecOpts(nns string, extra ...ctrdoci.SpecOpts) []ctrdoci.SpecO } // DefaultLinuxSpec returns a default OCI spec for a Linux container. -// See CreateSpecWithPlatform for more details. +// +// See [CreateSpecWithPlatform] for more details. func DefaultLinuxSpec(ctx context.Context, tb testing.TB, nns string) *specs.Spec { tb.Helper() return CreateLinuxSpec(ctx, tb, tb.Name(), DefaultLinuxSpecOpts(nns)...) } // CreateLinuxSpec returns the OCI spec for a Linux container. -// See CreateSpecWithPlatform for more details. +// +// See [CreateSpecWithPlatform] for more details. func CreateLinuxSpec(ctx context.Context, tb testing.TB, id string, opts ...ctrdoci.SpecOpts) *specs.Spec { tb.Helper() return CreateSpecWithPlatform(ctx, tb, constants.PlatformLinux, id, opts...) } // CreateWindowsSpec returns the OCI spec for a Windows container. -// See CreateSpecWithPlatform for more details. +// +// See [CreateSpecWithPlatform] for more details. func CreateWindowsSpec(ctx context.Context, tb testing.TB, id string, opts ...ctrdoci.SpecOpts) *specs.Spec { tb.Helper() return CreateSpecWithPlatform(ctx, tb, constants.PlatformWindows, id, opts...) } // CreateSpecWithPlatform returns the OCI spec for the specified platform. -// The context must contain a containerd namespace (via "github.com/containerd/containerd/namespaces".WithNamespace) +// The context must contain a containerd namespace added by +// [github.com/containerd/containerd/namespaces.WithNamespace] func CreateSpecWithPlatform(ctx context.Context, tb testing.TB, plat, id string, opts ...ctrdoci.SpecOpts) *specs.Spec { tb.Helper() container := &containers.Container{ID: id} diff --git a/test/internal/util/util.go b/test/internal/util/util.go new file mode 100644 index 0000000000..5f3d15e386 --- /dev/null +++ b/test/internal/util/util.go @@ -0,0 +1,20 @@ +package util + +import ( + "strings" + "unicode" +) + +// CleanName returns a string appropriate for uVM, container, or file names. +// +// Based on [testing.TB.TempDir]. +func CleanName(n string) string { + mapper := func(r rune) rune { + const allowed = "!#$%&()+,-.=@^_{}~ " + if unicode.IsLetter(r) || unicode.IsNumber(r) || strings.ContainsRune(allowed, r) { + return r + } + return -1 + } + return strings.TrimSpace(strings.Map(mapper, n)) +}