From 6997dec3d0885073da087ae387d81535ae0e2992 Mon Sep 17 00:00:00 2001 From: James Sturtevant Date: Wed, 28 Apr 2021 20:48:18 -0700 Subject: [PATCH] Run windows containers Signed-off-by: James Sturtevant --- .cirrus.yml | 13 +++ README.md | 60 +++++----- cmd/nerdctl/client.go | 11 +- cmd/nerdctl/run.go | 169 +++++++-------------------- cmd/nerdctl/run_cgroup_linux_test.go | 21 ++++ cmd/nerdctl/run_freebsd.go | 4 + cmd/nerdctl/run_linux.go | 111 ++++++++++++++++++ cmd/nerdctl/run_mount.go | 33 ++++-- cmd/nerdctl/run_test.go | 32 ++--- cmd/nerdctl/run_user_windows_test.go | 45 +++++++ cmd/nerdctl/run_windows.go | 31 ++++- hack/configure-windows-ci.ps1 | 59 ++++++++++ pkg/defaults/defaults_windows.go | 2 +- pkg/testutil/testutil.go | 10 ++ 14 files changed, 404 insertions(+), 197 deletions(-) create mode 100644 cmd/nerdctl/run_user_windows_test.go create mode 100644 hack/configure-windows-ci.ps1 diff --git a/.cirrus.yml b/.cirrus.yml index be9b574c2f2..34cdda3363a 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -12,3 +12,16 @@ task: - go test -v ./pkg/... - cd cmd/nerdctl - sudo go run . run $NERDCTL_RUN_ARGS | grep running + +docker_builder: + name: windows + platform: windows + os_version: 2019 + env: + CGO_ENABLED: 0 + build_script: + - mkdir "C:\Windows\system32\config\systemprofile\AppData\Local\Temp\" + - powershell hack/configure-windows-ci.ps1 + - refreshenv + - go install .\cmd\nerdctl\ + - go test -v -run "^(TestRunWorkdir|TestRunWithDoubleDash|TestRunExitCode|TestRunCIDFile|TestRunEnvFile|TestRunEnv|TestExec|ImageInspect|TestRunUserName)$" ./cmd/... diff --git a/README.md b/README.md index 713d99b00de..16b2c87f071 100644 --- a/README.md +++ b/README.md @@ -96,7 +96,7 @@ See [`./docs/freebsd.md`](docs/freebsd.md). ### Windows - Linux containers: Known to work on WSL2 -- Windows containers: WIP, see [PR #197](https://github.com/containerd/nerdctl/pull/197) +- Windows containers: experimental support for Windows (see below for features that are currently known to work) ### Docker @@ -194,6 +194,8 @@ Please certify your [Developer Certificate of Origin (DCO)](https://developercer :nerd_face: = nerdctl specific +:window: = Windows enabled + Unlisted `docker` CLI flags are unimplemented yet in `nerdctl` CLI. It does not necessarily mean that the corresponding features are missing in containerd. @@ -202,10 +204,10 @@ It does not necessarily mean that the corresponding features are missing in cont - [Run & Exec](#run--exec) - - [:whale: nerdctl run](#whale-nerdctl-run) - - [:whale: nerdctl exec](#whale-nerdctl-exec) + - [:whale: :window: nerdctl run](#whale-nerdctl-run) + - [:whale: :window: nerdctl exec](#whale-nerdctl-exec) - [Container management](#container-management) - - [:whale: nerdctl ps](#whale-nerdctl-ps) + - [:whale: :window: nerdctl ps](#whale-nerdctl-ps) - [:whale: nerdctl inspect](#whale-nerdctl-inspect) - [:whale: nerdctl logs](#whale-nerdctl-logs) - [:whale: nerdctl port](#whale-nerdctl-port) @@ -221,8 +223,8 @@ It does not necessarily mean that the corresponding features are missing in cont - [:whale: nerdctl build](#whale-nerdctl-build) - [:whale: nerdctl commit](#whale-nerdctl-commit) - [Image management](#image-management) - - [:whale: nerdctl images](#whale-nerdctl-images) - - [:whale: nerdctl pull](#whale-nerdctl-pull) + - [:whale: :window: nerdctl images](#whale-nerdctl-images) + - [:whale: :window: nerdctl pull](#whale-nerdctl-pull) - [:whale: nerdctl push](#whale-nerdctl-push) - [:whale: nerdctl load](#whale-nerdctl-load) - [:whale: nerdctl save](#whale-nerdctl-save) @@ -246,7 +248,7 @@ It does not necessarily mean that the corresponding features are missing in cont - [:whale: nerdctl volume inspect](#whale-nerdctl-volume-inspect) - [:whale: nerdctl volume rm](#whale-nerdctl-volume-rm) - [Namespace management](#namespace-management) - - [:nerd_face: nerdctl namespace ls](#nerd_face-nerdctl-namespace-ls) + - [:nerd_face: :window: nerdctl namespace ls](#nerd_face-nerdctl-namespace-ls) - [System](#system) - [:whale: nerdctl events](#whale-nerdctl-events) - [:whale: nerdctl info](#whale-nerdctl-info) @@ -280,10 +282,10 @@ Run a command in a new container. Usage: `nerdctl run [OPTIONS] IMAGE [COMMAND] [ARG...]` Basic flags: -- :whale: `-i, --interactive`: Keep STDIN open even if not attached" -- :whale: `-t, --tty`: Allocate a pseudo-TTY +- :whale: :window: `-i, --interactive`: Keep STDIN open even if not attached" +- :whale: :window: `-t, --tty`: Allocate a pseudo-TTY - :warning: WIP: currently `-t` requires `-i`, and conflicts with `-d` -- :whale: `-d, --detach`: Run container in background and print container ID +- :whale: :window: `-d, --detach`: Run container in background and print container ID - :whale: `--restart=(no|always)`: Restart policy to apply when a container exits - Default: "no" - :warning: No support for `on-failure` and `unless-stopped` @@ -317,7 +319,7 @@ Cgroup flags: - :whale: `--device`: Add a host device to the container User flags: -- :whale: `-u, --user`: Username or UID (format: [:]) +- :whale: :window: `-u, --user`: Username or UID (format: [:]) Security flags: - :whale: `--security-opt seccomp=`: specify custom seccomp profile @@ -332,7 +334,7 @@ Runtime flags: - :whale: `--sysctl`: Sysctl options, e.g \"net.ipv4.ip_forward=1\" Volume flags: -- :whale: `-v, --volume`: Bind mount a volume +- :whale: :window: `-v, --volume`: Bind mount a volume - :whale: `--tmpfs`: Mount a tmpfs directory Rootfs flags: @@ -341,16 +343,16 @@ Rootfs flags: Corresponds to Podman CLI. Env flags: -- :whale: `--entrypoint`: Overwrite the default ENTRYPOINT of the image -- :whale: `-w, --workdir`: Working directory inside the container -- :whale: `-e, --env`: Set environment variables -- :whale: `--env-file`: Set environment variables from file +- :whale: :window: `--entrypoint`: Overwrite the default ENTRYPOINT of the image +- :whale: :window: `-w, --workdir`: Working directory inside the container +- :whale: :window: `-e, --env`: Set environment variables +- :whale: :window: `--env-file`: Set environment variables from file Metadata flags: -- :whale: `--name`: Assign a name to the container -- :whale: `-l, --label`: Set meta data on a container -- :whale: `--label-file`: Read in a line delimited file of labels -- :whale: `--cidfile`: Write the container ID to the file +- :whale: :window: `--name`: Assign a name to the container +- :whale: :window: `-l, --label`: Set meta data on a container +- :whale: :window: `--label-file`: Read in a line delimited file of labels +- :whale: :window: `--cidfile`: Write the container ID to the file - :nerd_face: `--pidfile`: file path to write the task's pid. The CLI syntax conforms to Podman convention. Shared memory flags: @@ -480,7 +482,7 @@ Options: -### :whale: nerdctl exec +### :whale: :window: nerdctl exec Run a command in a running container. Usage: `nerdctl exec [OPTIONS] CONTAINER COMMAND [ARG...]` @@ -511,7 +513,7 @@ Flags: Unimplemented `docker ps` flags: `--filter`, `--last`, `--size` -### :whale: nerdctl inspect +### :whale: :window: nerdctl inspect Display detailed information on one or more containers. Usage: `nerdctl inspect [OPTIONS] NAME|ID [NAME|ID...]` @@ -895,7 +897,7 @@ Usage: `nerdctl volume rm [OPTIONS] VOLUME [VOLUME...]` ## Namespace management -### :nerd_face: nerdctl namespace ls +### :nerd_face: :window: nerdctl namespace ls List containerd namespaces such as "default", "moby", or "k8s.io". Usage: `nerdctl namespace ls [OPTIONS]` @@ -1039,13 +1041,13 @@ Unimplemented `docker-compose ps` (V1) flags: `--quiet`, `--services`, `--filter Unimplemented `docker compose ps` (V2) flags: `--format`, `--status` ## Global flags -- :nerd_face: `-a`, `--address`: containerd address, optionally with "unix://" prefix +- :nerd_face: :window: `-a`, `--address`: containerd address, optionally with "unix://" prefix - :whale: `-H`, `--host`: Docker-compatible alias for `-a`, `--address` -- :nerd_face: `-n`, `--namespace`: containerd namespace -- :nerd_face: `--snapshotter`: containerd snapshotter -- :nerd_face: `--cni-path`: CNI binary path (default: `/opt/cni/bin`) [`$CNI_PATH`] -- :nerd_face: `--cni-netconfpath`: CNI netconf path (default: `/etc/cni/net.d`) [`$NETCONFPATH`] -- :nerd_face: `--data-root`: nerdctl data root, e.g. "/var/lib/nerdctl" +- :nerd_face: :window: `-n`, `--namespace`: containerd namespace +- :nerd_face: :window: `--snapshotter`: containerd snapshotter +- :nerd_face: :window: `--cni-path`: CNI binary path (default: `/opt/cni/bin`) [`$CNI_PATH`] +- :nerd_face: :window: `--cni-netconfpath`: CNI netconf path (default: `/etc/cni/net.d`) [`$NETCONFPATH`] +- :nerd_face: :window: `--data-root`: nerdctl data root, e.g. "/var/lib/nerdctl" - :nerd_face: `--cgroup-manager=(cgroupfs|systemd|none)`: cgroup manager - Default: "systemd" on cgroup v2 (rootful & rootless), "cgroupfs" on v1 rootful, "none" on v1 rootless - :nerd_face: `--insecure-registry`: skips verifying HTTPS certs, and allows falling back to plain HTTP diff --git a/cmd/nerdctl/client.go b/cmd/nerdctl/client.go index b77902c61bd..d239c7258f0 100644 --- a/cmd/nerdctl/client.go +++ b/cmd/nerdctl/client.go @@ -63,6 +63,7 @@ func newClient(cmd *cobra.Command, opts ...containerd.ClientOpt) (*containerd.Cl // getDataStore returns a string like "/var/lib/nerdctl/1935db59". // "1935db9" is from `$(echo -n "/run/containerd/containerd.sock" | sha256sum | cut -c1-8)`` +// on Windows it will return "%PROGRAMFILES%/nerdctl/1935db59" func getDataStore(cmd *cobra.Command) (string, error) { dataRoot, err := cmd.Flags().GetString("data-root") if err != nil { @@ -91,12 +92,12 @@ func getAddrHash(addr string) (string, error) { if runtime.GOOS != "windows" { addr = strings.TrimPrefix(addr, "unix://") - } - var err error - addr, err = filepath.EvalSymlinks(addr) - if err != nil { - return "", err + var err error + addr, err = filepath.EvalSymlinks(addr) + if err != nil { + return "", err + } } d := digest.SHA256.FromString(addr) diff --git a/cmd/nerdctl/run.go b/cmd/nerdctl/run.go index 588ae50ae1c..0ca6d752a6c 100644 --- a/cmd/nerdctl/run.go +++ b/cmd/nerdctl/run.go @@ -57,7 +57,6 @@ import ( "github.com/containerd/nerdctl/pkg/strutil" "github.com/containerd/nerdctl/pkg/taskutil" "github.com/docker/cli/opts" - "github.com/docker/go-units" "github.com/opencontainers/runtime-spec/specs-go" "github.com/sirupsen/logrus" @@ -294,15 +293,11 @@ func runAction(cmd *cobra.Command, args []string) error { opts = append(opts, oci.WithDefaultSpec(), - oci.WithDefaultUnixDevices, - WithoutRunMount(), // unmount default tmpfs on "/run": https://github.com/containerd/nerdctl/issues/157 ) - if runtime.GOOS == "linux" { - opts = append(opts, - oci.WithMounts([]specs.Mount{ - {Type: "cgroup", Source: "cgroup", Destination: "/sys/fs/cgroup", Options: []string{"ro", "nosuid", "noexec", "nodev"}}, - })) + opts, err = setPlatformOptions(opts, cmd, id) + if err != nil { + return err } rootfsOpts, rootfsCOpts, ensuredImage, err := generateRootfsOpts(ctx, client, platform, cmd, args, id) @@ -452,42 +447,45 @@ func runAction(cmd *cobra.Command, args []string) error { if err != nil { return err } - conf, err := resolvconf.Get() - if err != nil { - return err - } - slirp4Dns := []string{} - if rootlessutil.IsRootlessChild() { - slirp4Dns, err = dnsutil.GetSlirp4netnsDns() + if runtime.GOOS == "linux" { + conf, err := resolvconf.Get() if err != nil { return err } - } - conf, err = resolvconf.FilterResolvDNS(conf.Content, true) - if err != nil { - return err - } - searchDomains := resolvconf.GetSearchDomains(conf.Content) - dnsOptions := resolvconf.GetOptions(conf.Content) - nameServers := strutil.DedupeStrSlice(dnsValue) - if len(nameServers) == 0 { - nameServers = resolvconf.GetNameservers(conf.Content, resolvconf.IPv4) - } - if _, err := resolvconf.Build(resolvConfPath, append(slirp4Dns, nameServers...), searchDomains, dnsOptions); err != nil { - return err - } - // the content of /etc/hosts is created in OCI Hook - etcHostsPath, err := hostsstore.AllocHostsFile(dataStore, ns, id) - if err != nil { - return err - } - opts = append(opts, withCustomResolvConf(resolvConfPath), withCustomHosts(etcHostsPath)) - for _, p := range portSlice { - pm, err := portutil.ParseFlagP(p) + slirp4Dns := []string{} + if rootlessutil.IsRootlessChild() { + slirp4Dns, err = dnsutil.GetSlirp4netnsDns() + if err != nil { + return err + } + } + conf, err = resolvconf.FilterResolvDNS(conf.Content, true) if err != nil { return err } - ports = append(ports, pm...) + searchDomains := resolvconf.GetSearchDomains(conf.Content) + dnsOptions := resolvconf.GetOptions(conf.Content) + nameServers := strutil.DedupeStrSlice(dnsValue) + if len(nameServers) == 0 { + nameServers = resolvconf.GetNameservers(conf.Content, resolvconf.IPv4) + } + if _, err := resolvconf.Build(resolvConfPath, append(slirp4Dns, nameServers...), searchDomains, dnsOptions); err != nil { + return err + } + + // the content of /etc/hosts is created in OCI Hook + etcHostsPath, err := hostsstore.AllocHostsFile(dataStore, ns, id) + if err != nil { + return err + } + opts = append(opts, withCustomResolvConf(resolvConfPath), withCustomHosts(etcHostsPath)) + for _, p := range portSlice { + pm, err := portutil.ParseFlagP(p) + if err != nil { + return err + } + ports = append(ports, pm...) + } } default: return fmt.Errorf("unexpected network type %v", netType) @@ -517,87 +515,12 @@ func runAction(cmd *cobra.Command, args []string) error { } opts = append(opts, hookOpt) - if cgOpts, err := generateCgroupOpts(cmd, id); err != nil { - return err - } else { - opts = append(opts, cgOpts...) - } - if uOpts, err := generateUserOpts(cmd); err != nil { return err } else { opts = append(opts, uOpts...) } - securityOpt, err := cmd.Flags().GetStringArray("security-opt") - if err != nil { - return err - } - securityOptsMaps := strutil.ConvertKVStringsToMap(strutil.DedupeStrSlice(securityOpt)) - if secOpts, err := generateSecurityOpts(securityOptsMaps); err != nil { - return err - } else { - opts = append(opts, secOpts...) - } - - capAdd, err := cmd.Flags().GetStringSlice("cap-add") - if err != nil { - return err - } - capDrop, err := cmd.Flags().GetStringSlice("cap-drop") - if err != nil { - return err - } - if capOpts, err := generateCapOpts( - strutil.DedupeStrSlice(capAdd), - strutil.DedupeStrSlice(capDrop)); err != nil { - return err - } else { - opts = append(opts, capOpts...) - } - - privileged, err := cmd.Flags().GetBool("privileged") - if err != nil { - return err - } - if privileged { - opts = append(opts, privilegedOpts...) - } - - shmSize, err := cmd.Flags().GetString("shm-size") - if err != nil { - return err - } - if len(shmSize) > 0 { - shmBytes, err := units.RAMInBytes(shmSize) - if err != nil { - return err - } - opts = append(opts, oci.WithDevShmSize(shmBytes/1024)) - } - - pidNs, err := cmd.Flags().GetString("pid") - if err != nil { - return err - } - pidNs = strings.ToLower(pidNs) - if pidNs != "" { - if pidNs != "host" { - return fmt.Errorf("Invalid pid namespace. Set --pid=host to enable host pid namespace.") - } else { - opts = append(opts, oci.WithHostNamespace(specs.PIDNamespace)) - if rootlessutil.IsRootless() { - opts = append(opts, withBindMountHostProcfs) - } - } - } - - ulimitOpts, err := generateUlimitsOpts(cmd) - if err != nil { - return err - } - opts = append(opts, ulimitOpts...) - rtCOpts, err := generateRuntimeCOpts(cmd) if err != nil { return err @@ -646,22 +569,6 @@ func runAction(cmd *cobra.Command, args []string) error { opts = append(opts, propagateContainerdLabelsToOCIAnnotations()) - sysctl, err := cmd.Flags().GetStringArray("sysctl") - if err != nil { - return err - } - opts = append(opts, WithSysctls(strutil.ConvertKVStringsToMap(sysctl))) - - gpus, err := cmd.Flags().GetStringArray("gpus") - if err != nil { - return err - } - gpuOpt, err := parseGPUOpts(gpus) - if err != nil { - return err - } - opts = append(opts, gpuOpt...) - var s specs.Spec spec := containerd.WithSpec(&s, opts...) cOpts = append(cOpts, spec) @@ -951,6 +858,10 @@ func generateLogURI(dataStore string) (*url.URL, error) { args := map[string]string{ logging.MagicArgv1: dataStore, } + if runtime.GOOS == "windows" { + return nil, nil + } + return cio.LogURIGenerator("binary", selfExe, args) } diff --git a/cmd/nerdctl/run_cgroup_linux_test.go b/cmd/nerdctl/run_cgroup_linux_test.go index 7c5906c3de1..cdd59856150 100644 --- a/cmd/nerdctl/run_cgroup_linux_test.go +++ b/cmd/nerdctl/run_cgroup_linux_test.go @@ -142,3 +142,24 @@ func TestParseDevice(t *testing.T) { } } + +func TestRunCgroupConf(t *testing.T) { + if cgroups.Mode() != cgroups.Unified { + t.Skip("test requires cgroup v2") + } + base := testutil.NewBase(t) + + base.Cmd("run", "--rm", "--cgroup-conf", "memory.high=33554432", testutil.AlpineImage, + "sh", "-ec", "cd /sys/fs/cgroup && cat memory.high").AssertOutContains("33554432") +} + +func TestRunBlkioWeightCgroupV2(t *testing.T) { + if cgroups.Mode() != cgroups.Unified { + t.Skip("test requires cgroup v2") + } + base := testutil.NewBase(t) + + // when bfq io scheduler is used, the io.weight knob is exposed as io.bfq.weight + base.Cmd("run", "--rm", "--blkio-weight", "300", testutil.AlpineImage, + "sh", "-ec", "cd /sys/fs/cgroup && cat io.bfq.weight").AssertOutContains("300") +} diff --git a/cmd/nerdctl/run_freebsd.go b/cmd/nerdctl/run_freebsd.go index 797eba55cbb..d4717bd4887 100644 --- a/cmd/nerdctl/run_freebsd.go +++ b/cmd/nerdctl/run_freebsd.go @@ -37,3 +37,7 @@ func capShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]s func runShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return nil, cobra.ShellCompDirectiveNoFileComp } + +func setPlatformOptions(opts []oci.SpecOpts, cmd *cobra.Command, id string) ([]oci.SpecOpts, error) { + return opts, nil +} diff --git a/cmd/nerdctl/run_linux.go b/cmd/nerdctl/run_linux.go index b71c63a4e89..42cc8f70f9e 100644 --- a/cmd/nerdctl/run_linux.go +++ b/cmd/nerdctl/run_linux.go @@ -18,8 +18,14 @@ package main import ( "context" + "fmt" "strings" + "github.com/containerd/nerdctl/pkg/rootlessutil" + "github.com/containerd/nerdctl/pkg/strutil" + "github.com/docker/go-units" + "github.com/opencontainers/runtime-spec/specs-go" + "github.com/containerd/containerd/containers" "github.com/containerd/containerd/oci" "github.com/containerd/containerd/pkg/cap" @@ -47,3 +53,108 @@ func runShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]s return nil, cobra.ShellCompDirectiveNoFileComp } } + +func setPlatformOptions(opts []oci.SpecOpts, cmd *cobra.Command, id string) ([]oci.SpecOpts, error) { + opts = append(opts, + oci.WithDefaultUnixDevices, + WithoutRunMount(), // unmount default tmpfs on "/run": https://github.com/containerd/nerdctl/issues/157) + ) + + opts = append(opts, + oci.WithMounts([]specs.Mount{ + {Type: "cgroup", Source: "cgroup", Destination: "/sys/fs/cgroup", Options: []string{"ro", "nosuid", "noexec", "nodev"}}, + })) + + if cgOpts, err := generateCgroupOpts(cmd, id); err != nil { + return nil, err + } else { + opts = append(opts, cgOpts...) + } + + securityOpt, err := cmd.Flags().GetStringArray("security-opt") + if err != nil { + return nil, err + } + securityOptsMaps := strutil.ConvertKVStringsToMap(strutil.DedupeStrSlice(securityOpt)) + if secOpts, err := generateSecurityOpts(securityOptsMaps); err != nil { + return nil, err + } else { + opts = append(opts, secOpts...) + } + + capAdd, err := cmd.Flags().GetStringSlice("cap-add") + if err != nil { + return nil, err + } + capDrop, err := cmd.Flags().GetStringSlice("cap-drop") + if err != nil { + return nil, err + } + if capOpts, err := generateCapOpts( + strutil.DedupeStrSlice(capAdd), + strutil.DedupeStrSlice(capDrop)); err != nil { + return nil, err + } else { + opts = append(opts, capOpts...) + } + + privileged, err := cmd.Flags().GetBool("privileged") + if err != nil { + return nil, err + } + if privileged { + opts = append(opts, privilegedOpts...) + } + + shmSize, err := cmd.Flags().GetString("shm-size") + if err != nil { + return nil, err + } + if len(shmSize) > 0 { + shmBytes, err := units.RAMInBytes(shmSize) + if err != nil { + return nil, err + } + opts = append(opts, oci.WithDevShmSize(shmBytes/1024)) + } + + pidNs, err := cmd.Flags().GetString("pid") + if err != nil { + return nil, err + } + pidNs = strings.ToLower(pidNs) + if pidNs != "" { + if pidNs != "host" { + return nil, fmt.Errorf("Invalid pid namespace. Set --pid=host to enable host pid namespace.") + } else { + opts = append(opts, oci.WithHostNamespace(specs.PIDNamespace)) + if rootlessutil.IsRootless() { + opts = append(opts, withBindMountHostProcfs) + } + } + } + + ulimitOpts, err := generateUlimitsOpts(cmd) + if err != nil { + return nil, err + } + opts = append(opts, ulimitOpts...) + + sysctl, err := cmd.Flags().GetStringArray("sysctl") + if err != nil { + return nil, err + } + opts = append(opts, WithSysctls(strutil.ConvertKVStringsToMap(sysctl))) + + gpus, err := cmd.Flags().GetStringArray("gpus") + if err != nil { + return nil, err + } + gpuOpt, err := parseGPUOpts(gpus) + if err != nil { + return nil, err + } + opts = append(opts, gpuOpt...) + + return opts, nil +} diff --git a/cmd/nerdctl/run_mount.go b/cmd/nerdctl/run_mount.go index f3dcab40fba..aa7bd824346 100644 --- a/cmd/nerdctl/run_mount.go +++ b/cmd/nerdctl/run_mount.go @@ -21,6 +21,7 @@ import ( "fmt" "os" "path/filepath" + "runtime" "github.com/containerd/containerd" "github.com/containerd/containerd/errdefs" @@ -122,16 +123,34 @@ func generateMountOpts(cmd *cobra.Command, ctx context.Context, client *containe return nil, nil, err } - // We should do defer first, if not we will not do Unmount when only a part of Mounts are failed. - defer func() { - err = mount.UnmountAll(tempDir, 0) - }() + // windows has additional steps for mounting see + // https://github.com/containerd/containerd/commit/791e175c79930a34cfbb2048fbcaa8493fd2c86b + unmounter := func(mountPath string) { + if uerr := mount.Unmount(mountPath, 0); uerr != nil { + logrus.Debugf("Failed to unmount snapshot %q", tempDir) + if err == nil { + err = uerr + } + } + } - if err := mount.All(mounts, tempDir); err != nil { - if err := s.Remove(ctx, tempDir); err != nil && !errdefs.IsNotFound(err) { + if runtime.GOOS == "windows" { + for _, m := range mounts { + // appending the layerID to the root. + mountPath := filepath.Join(tempDir, filepath.Base(m.Source)) + if err := m.Mount(mountPath); err != nil { + return nil, nil, err + } + defer unmounter(m.Source) + } + } else { + if err := mount.All(mounts, tempDir); err != nil { + if err := s.Remove(ctx, tempDir); err != nil && !errdefs.IsNotFound(err) { + return nil, nil, err + } return nil, nil, err } - return nil, nil, err + defer unmounter(tempDir) } } diff --git a/cmd/nerdctl/run_test.go b/cmd/nerdctl/run_test.go index 1be597ef387..a87a43f29fc 100644 --- a/cmd/nerdctl/run_test.go +++ b/cmd/nerdctl/run_test.go @@ -23,13 +23,12 @@ import ( "fmt" "os" "path/filepath" + "runtime" "strconv" "strings" "testing" - "github.com/containerd/cgroups" "github.com/containerd/nerdctl/pkg/testutil" - "gotest.tools/v3/assert" ) @@ -86,7 +85,11 @@ CMD ["echo", "bar"] func TestRunWorkdir(t *testing.T) { base := testutil.NewBase(t) - cmd := base.Cmd("run", "--rm", "--workdir=/foo", testutil.AlpineImage, "pwd") + dir := "/foo" + if runtime.GOOS == "windows" { + dir = "c:" + dir + } + cmd := base.Cmd("run", "--rm", "--workdir="+dir, testutil.AlpineImage, "pwd") cmd.AssertOutContains("/foo") } @@ -199,27 +202,6 @@ func TestRunPidHost(t *testing.T) { base.Cmd("run", "--rm", "--pid=host", testutil.AlpineImage, "ps", "auxw").AssertOutContains(strconv.Itoa(pid)) } -func TestRunCgroupConf(t *testing.T) { - if cgroups.Mode() != cgroups.Unified { - t.Skip("test requires cgroup v2") - } - base := testutil.NewBase(t) - - base.Cmd("run", "--rm", "--cgroup-conf", "memory.high=33554432", testutil.AlpineImage, - "sh", "-ec", "cd /sys/fs/cgroup && cat memory.high").AssertOutContains("33554432") -} - -func TestRunBlkioWeightCgroupV2(t *testing.T) { - if cgroups.Mode() != cgroups.Unified { - t.Skip("test requires cgroup v2") - } - base := testutil.NewBase(t) - - // when bfq io scheduler is used, the io.weight knob is exposed as io.bfq.weight - base.Cmd("run", "--rm", "--blkio-weight", "300", testutil.AlpineImage, - "sh", "-ec", "cd /sys/fs/cgroup && cat io.bfq.weight").AssertOutContains("300") -} - func TestRunAddHost(t *testing.T) { base := testutil.NewBase(t) base.Cmd("run", "--rm", "--add-host", "testing.example.com:10.0.0.1", testutil.AlpineImage, "sh", "-c", "cat /etc/hosts").AssertOutWithFunc(func(stdout string) error { @@ -264,7 +246,7 @@ func TestRunEnv(t *testing.T) { if !strings.Contains(stdout, "\nBAR=bar1 bar2\n") { return errors.New("got bad BAR") } - if !strings.Contains(stdout, "\nBAZ=\n") { + if !strings.Contains(stdout, "\nBAZ=\n") && runtime.GOOS != "windows" { return errors.New("got bad BAZ") } if strings.Contains(stdout, "QUX") { diff --git a/cmd/nerdctl/run_user_windows_test.go b/cmd/nerdctl/run_user_windows_test.go new file mode 100644 index 00000000000..27cfd0697af --- /dev/null +++ b/cmd/nerdctl/run_user_windows_test.go @@ -0,0 +1,45 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package main + +import ( + "testing" + + "github.com/containerd/nerdctl/pkg/testutil" +) + +func TestRunUserName(t *testing.T) { + base := testutil.NewBase(t) + testCases := map[string]string{ + "": "ContainerAdministrator", + "ContainerAdministrator": "ContainerAdministrator", + "ContainerUser": "ContainerUser", + } + for userStr, expected := range testCases { + userStr := userStr + expected := expected + t.Run(userStr, func(t *testing.T) { + t.Parallel() + cmd := []string{"run", "--rm"} + if userStr != "" { + cmd = append(cmd, "--user", userStr) + } + cmd = append(cmd, testutil.WindowsNano, "whoami") + base.Cmd(cmd...).AssertOutContains(expected) + }) + } +} diff --git a/cmd/nerdctl/run_windows.go b/cmd/nerdctl/run_windows.go index a71f4b6a265..8aa98570008 100644 --- a/cmd/nerdctl/run_windows.go +++ b/cmd/nerdctl/run_windows.go @@ -18,9 +18,10 @@ package main import ( "context" - + "fmt" "github.com/containerd/containerd/containers" "github.com/containerd/containerd/oci" + "github.com/docker/go-units" "github.com/spf13/cobra" ) @@ -37,3 +38,31 @@ func capShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]s func runShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return nil, cobra.ShellCompDirectiveNoFileComp } + +func setPlatformOptions(opts []oci.SpecOpts, cmd *cobra.Command, id string) ([]oci.SpecOpts, error) { + cpus, err := cmd.Flags().GetFloat64("cpus") + if err != nil { + return nil, err + } + if cpus > 0.0 { + opts = append(opts, oci.WithWindowsCPUCount(uint64(cpus))) + } + + memStr, err := cmd.Flags().GetString("memory") + if err != nil { + return nil, err + } + if memStr != "" { + mem64, err := units.RAMInBytes(memStr) + if err != nil { + return nil, fmt.Errorf("failed to parse memory bytes %q: %w", memStr, err) + } + opts = append(opts, oci.WithMemoryLimit(uint64(mem64))) + } + + opts = append(opts, + oci.WithWindowNetworksAllowUnqualifiedDNSQuery(), + oci.WithWindowsIgnoreFlushesDuringBoot()) + + return opts, nil +} diff --git a/hack/configure-windows-ci.ps1 b/hack/configure-windows-ci.ps1 new file mode 100644 index 00000000000..8ba782ffd36 --- /dev/null +++ b/hack/configure-windows-ci.ps1 @@ -0,0 +1,59 @@ +$ErrorActionPreference = "Stop" + +#install golang +choco install --limitoutput --no-progress -y golang + +#install containerd +$Version="1.5.7" +curl.exe -L https://github.com/containerd/containerd/releases/download/v$Version/containerd-$Version-windows-amd64.tar.gz -o containerd-windows-amd64.tar.gz +tar xvf containerd-windows-amd64.tar.gz +mkdir -force "$Env:ProgramFiles\containerd" +mv ./bin/* "$Env:ProgramFiles\containerd" + +& $Env:ProgramFiles\containerd\containerd.exe config default | Out-File "$Env:ProgramFiles\containerd\config.toml" -Encoding ascii +& $Env:ProgramFiles\containerd\containerd.exe --register-service +Start-Service containerd + +#configure cni +mkdir -force "$Env:ProgramFiles\containerd\cni\bin" +mkdir -force "$Env:ProgramFiles\containerd\cni\conf" +curl.exe -LO https://github.com/microsoft/windows-container-networking/releases/download/v0.2.0/windows-container-networking-cni-amd64-v0.2.0.zip +Expand-Archive windows-container-networking-cni-amd64-v0.2.0.zip -DestinationPath "$Env:ProgramFiles\containerd\cni\bin" -Force + +curl.exe -LO https://raw.githubusercontent.com/microsoft/SDN/master/Kubernetes/windows/hns.psm1 +ipmo ./hns.psm1 + +# cirrus already has nat net work configured for docker. We can re-use that for testing +$sn=(get-hnsnetwork | ? Name -Like "nat" | select -ExpandProperty subnets) +$subnet=$sn.AddressPrefix +$gateway=$sn.GatewayAddress +@" +{ + "cniVersion": "0.2.0", + "name": "nat", + "type": "nat", + "master": "Ethernet", + "ipam": { + "subnet": "$subnet", + "routes": [ + { + "gateway": "$gateway" + } + ] + }, + "capabilities": { + "portMappings": true, + "dns": true + } +} +"@ | Set-Content "$Env:ProgramFiles\containerd\cni\conf\0-containerd-nat.conf" -Force + +echo "configuration complete! Printing configuration..." +echo "Service:" +get-service containerd +echo "cni configuraiton" +cat "$Env:ProgramFiles\containerd\cni\conf\0-containerd-nat.conf" +ls "$Env:ProgramFiles\containerd\cni\bin" +echo "containerd install" +ls "$Env:ProgramFiles\containerd\" +& "$Env:ProgramFiles\containerd\containerd.exe" --version diff --git a/pkg/defaults/defaults_windows.go b/pkg/defaults/defaults_windows.go index f13be4eaaa4..04a50625e6f 100644 --- a/pkg/defaults/defaults_windows.go +++ b/pkg/defaults/defaults_windows.go @@ -23,7 +23,7 @@ import ( ) const AppArmorProfileName = "" -const Runtime = "" +const Runtime = "io.containerd.runhcs.v1" func DataRoot() string { return filepath.Join(os.Getenv("ProgramData"), "nerdctl") diff --git a/pkg/testutil/testutil.go b/pkg/testutil/testutil.go index 05c985805ad..76b9973bf85 100644 --- a/pkg/testutil/testutil.go +++ b/pkg/testutil/testutil.go @@ -23,6 +23,7 @@ import ( "io" "os" "os/exec" + "runtime" "strings" "testing" "time" @@ -399,6 +400,14 @@ func NewBase(t *testing.T) *Base { } func mirrorOf(s string) string { + if runtime.GOOS == "windows" { + // More work needs to be done to support windows containers in test framework + // for the tests that are run now this image (used in k8s upstream testing) meets the needs + // use gcr.io/k8s-staging-e2e-test-images/busybox:1.29-2-windows-amd64-ltsc2022 locally on windows 11 + // https://github.com/microsoft/Windows-Containers/issues/179 + return "gcr.io/k8s-staging-e2e-test-images/busybox:1.29-2" + } + // plain mirror, NOT stargz-converted images return fmt.Sprintf("ghcr.io/stargz-containers/%s-org", s) } @@ -412,6 +421,7 @@ var ( WordpressIndexHTMLSnippet = "WordPress › Installation" MariaDBImage = mirrorOf("mariadb:10.5") DockerAuthImage = mirrorOf("cesanta/docker_auth:1.7") + WindowsNano = "gcr.io/k8s-staging-e2e-test-images/busybox:1.29-2-windows-amd64-ltsc2022" ) const (