diff options
27 files changed, 466 insertions, 31 deletions
@@ -44,6 +44,7 @@ MANDIR ?= ${PREFIX}/share/man SHAREDIR_CONTAINERS ?= ${PREFIX}/share/containers ETCDIR ?= ${PREFIX}/etc TMPFILESDIR ?= ${PREFIX}/lib/tmpfiles.d +MODULESLOADDIR ?= ${PREFIX}/lib/modules-load.d SYSTEMDDIR ?= ${PREFIX}/lib/systemd/system USERSYSTEMDDIR ?= ${PREFIX}/lib/systemd/user REMOTETAGS ?= remote exclude_graphdriver_btrfs btrfs_noversion exclude_graphdriver_devicemapper containers_image_openpgp @@ -779,6 +780,11 @@ install.bin: install ${SELINUXOPT} -m 755 -d ${DESTDIR}${TMPFILESDIR} install ${SELINUXOPT} -m 644 contrib/tmpfile/podman.conf ${DESTDIR}${TMPFILESDIR}/podman.conf +.PHONY: install.modules-load +install.modules-load: # This should only be used by distros which might use iptables-legacy, this is not needed on RHEL + install ${SELINUXOPT} -m 755 -d ${DESTDIR}${MODULESLOADDIR} + install ${SELINUXOPT} -m 644 contrib/modules-load.d/podman-iptables.conf ${DESTDIR}${MODULESLOADDIR}/podman-iptables.conf + .PHONY: install.man install.man: install ${SELINUXOPT} -d -m 755 $(DESTDIR)$(MANDIR)/man1 diff --git a/cmd/podman/containers/commit.go b/cmd/podman/containers/commit.go index f74fd4ab1..e0cadd5b0 100644 --- a/cmd/podman/containers/commit.go +++ b/cmd/podman/containers/commit.go @@ -77,6 +77,7 @@ func commitFlags(cmd *cobra.Command) { flags.BoolVarP(&commitOptions.Pause, "pause", "p", false, "Pause container during commit") flags.BoolVarP(&commitOptions.Quiet, "quiet", "q", false, "Suppress output") + flags.BoolVarP(&commitOptions.Squash, "squash", "s", false, "squash newly built layers into a single new layer") flags.BoolVar(&commitOptions.IncludeVolumes, "include-volumes", false, "Include container volumes as image volumes") } diff --git a/contrib/modules-load.d/podman-iptables.conf b/contrib/modules-load.d/podman-iptables.conf new file mode 100644 index 000000000..001ef8af8 --- /dev/null +++ b/contrib/modules-load.d/podman-iptables.conf @@ -0,0 +1,5 @@ +# On fedora 36 ip_tables is no longer auto loaded and rootless user have no permsissions to load it. +# When we have actual nftables support in the future we might want to revisit this. +# If you use iptables-nft this is not needed. +ip_tables +ip6_tables diff --git a/docs/source/markdown/podman-commit.1.md b/docs/source/markdown/podman-commit.1.md index 87b7e8aea..df3c38711 100644 --- a/docs/source/markdown/podman-commit.1.md +++ b/docs/source/markdown/podman-commit.1.md @@ -60,6 +60,11 @@ Set commit message for committed image.\ Pause the container when creating an image.\ The default is **false**. +#### **--squash**, **-s** + +Squash newly built layers into a single new layer.\ +The default is **false**. + #### **--quiet**, **-q** Suppresses output.\ @@ -8,7 +8,7 @@ require ( github.com/buger/goterm v1.0.4 github.com/checkpoint-restore/checkpointctl v0.0.0-20211204171957-54b4ebfdb681 github.com/checkpoint-restore/go-criu/v5 v5.3.0 - github.com/container-orchestrated-devices/container-device-interface v0.0.0-20220111162300-46367ec063fd + github.com/container-orchestrated-devices/container-device-interface v0.3.0 github.com/containernetworking/cni v1.0.1 github.com/containernetworking/plugins v1.0.1 github.com/containers/buildah v1.24.2 @@ -242,8 +242,8 @@ github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:z github.com/cockroachdb/datadriven v0.0.0-20200714090401-bf6692d28da5/go.mod h1:h6jFvWxBdQXxjopDMZyH2UVceIRfR84bdzbkoKrsWNo= github.com/cockroachdb/errors v1.2.4/go.mod h1:rQD95gz6FARkaKkQXUksEje/d9a6wBJoCr5oaCLELYA= github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f/go.mod h1:i/u985jwjWRlyHXQbwatDASoW0RMlZ/3i9yJHE2xLkI= -github.com/container-orchestrated-devices/container-device-interface v0.0.0-20220111162300-46367ec063fd h1:Pzh64A349jzW89R73gwkxWoPvpkd8rz2X+KLUaMmBRY= -github.com/container-orchestrated-devices/container-device-interface v0.0.0-20220111162300-46367ec063fd/go.mod h1:YqXyXS/oVW3ix0IHVXitKBq3RZoCF8ccm5RPmRBraUI= +github.com/container-orchestrated-devices/container-device-interface v0.3.0 h1:tM2zdVYZY8getsFaTc7Z+v+UqDXhk5alchOHVEADes0= +github.com/container-orchestrated-devices/container-device-interface v0.3.0/go.mod h1:LGs3yHVe1wZn2XsWl4AxywYQ3NRZ6osTEZozCHQCRSM= github.com/containerd/aufs v0.0.0-20200908144142-dab0cbea06f4/go.mod h1:nukgQABAEopAHvB6j7cnP5zJ+/3aVcE7hCYqvIwAHyE= github.com/containerd/aufs v0.0.0-20201003224125-76a6863f2989/go.mod h1:AkGGQs9NM2vtYHaUen+NljV0/baGCAPELGm2q9ZXpWU= github.com/containerd/aufs v0.0.0-20210316121734-20793ff83c97/go.mod h1:kL5kd6KM5TzQjR79jljyi4olc1Vrx6XBlcyj3gNv2PU= diff --git a/libpod/container_commit.go b/libpod/container_commit.go index 99d08ccf1..7018ee7d8 100644 --- a/libpod/container_commit.go +++ b/libpod/container_commit.go @@ -27,6 +27,7 @@ type ContainerCommitOptions struct { Author string Message string Changes []string + Squash bool } // Commit commits the changes between a container and its image, creating a new @@ -63,6 +64,7 @@ func (c *Container) Commit(ctx context.Context, destImage string, options Contai commitOptions := buildah.CommitOptions{ SignaturePolicyPath: options.SignaturePolicyPath, ReportWriter: options.ReportWriter, + Squash: options.Squash, SystemContext: c.runtime.imageContext, PreferredManifestType: options.PreferredManifestType, } diff --git a/pkg/api/handlers/compat/images.go b/pkg/api/handlers/compat/images.go index 3546f88a0..edefce010 100644 --- a/pkg/api/handlers/compat/images.go +++ b/pkg/api/handlers/compat/images.go @@ -102,6 +102,7 @@ func CommitContainer(w http.ResponseWriter, r *http.Request) { Comment string `schema:"comment"` Container string `schema:"container"` Pause bool `schema:"pause"` + Squash bool `schema:"squash"` Repo string `schema:"repo"` Tag string `schema:"tag"` // fromSrc string # fromSrc is currently unused @@ -138,6 +139,7 @@ func CommitContainer(w http.ResponseWriter, r *http.Request) { options.Message = query.Comment options.Author = query.Author options.Pause = query.Pause + options.Squash = query.Squash for _, change := range query.Changes { options.Changes = append(options.Changes, strings.Split(change, "\n")...) } diff --git a/pkg/api/handlers/libpod/images.go b/pkg/api/handlers/libpod/images.go index f078c13cc..eb9fb12a6 100644 --- a/pkg/api/handlers/libpod/images.go +++ b/pkg/api/handlers/libpod/images.go @@ -497,6 +497,7 @@ func CommitContainer(w http.ResponseWriter, r *http.Request) { Container string `schema:"container"` Format string `schema:"format"` Pause bool `schema:"pause"` + Squash bool `schema:"squash"` Repo string `schema:"repo"` Tag string `schema:"tag"` }{ @@ -543,6 +544,7 @@ func CommitContainer(w http.ResponseWriter, r *http.Request) { options.Message = query.Comment options.Author = query.Author options.Pause = query.Pause + options.Squash = query.Squash options.Changes = query.Changes ctr, err := runtime.LookupContainer(query.Container) if err != nil { diff --git a/pkg/api/server/register_images.go b/pkg/api/server/register_images.go index d7bc17093..017310f12 100644 --- a/pkg/api/server/register_images.go +++ b/pkg/api/server/register_images.go @@ -460,6 +460,10 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error { // name: changes // type: string // description: instructions to apply while committing in Dockerfile format + // - in: query + // name: squash + // type: boolean + // description: squash newly built layers into a single new layer // produces: // - application/json // responses: diff --git a/pkg/bindings/containers/types.go b/pkg/bindings/containers/types.go index 4915e3e23..66b90af9b 100644 --- a/pkg/bindings/containers/types.go +++ b/pkg/bindings/containers/types.go @@ -30,6 +30,7 @@ type CommitOptions struct { Comment *string Format *string Pause *bool + Squash *bool Repo *string Tag *string } diff --git a/pkg/bindings/containers/types_commit_options.go b/pkg/bindings/containers/types_commit_options.go index 7eb04198f..7b4745eb8 100644 --- a/pkg/bindings/containers/types_commit_options.go +++ b/pkg/bindings/containers/types_commit_options.go @@ -92,6 +92,21 @@ func (o *CommitOptions) GetPause() bool { return *o.Pause } +// WithSquash set field Squash to given value +func (o *CommitOptions) WithSquash(value bool) *CommitOptions { + o.Squash = &value + return o +} + +// GetSquash returns value of field Squash +func (o *CommitOptions) GetSquash() bool { + if o.Squash == nil { + var z bool + return z + } + return *o.Squash +} + // WithRepo set field Repo to given value func (o *CommitOptions) WithRepo(value string) *CommitOptions { o.Repo = &value diff --git a/pkg/domain/entities/containers.go b/pkg/domain/entities/containers.go index e9bce0eb7..79795a221 100644 --- a/pkg/domain/entities/containers.go +++ b/pkg/domain/entities/containers.go @@ -154,6 +154,7 @@ type CommitOptions struct { Message string Pause bool Quiet bool + Squash bool Writer io.Writer } diff --git a/pkg/domain/infra/abi/containers.go b/pkg/domain/infra/abi/containers.go index 92f5b1a80..e6feb7c82 100644 --- a/pkg/domain/infra/abi/containers.go +++ b/pkg/domain/infra/abi/containers.go @@ -529,6 +529,7 @@ func (ic *ContainerEngine) ContainerCommit(ctx context.Context, nameOrID string, Message: options.Message, Changes: options.Changes, Author: options.Author, + Squash: options.Squash, } newImage, err := ctr.Commit(ctx, options.ImageName, opts) if err != nil { diff --git a/pkg/domain/infra/tunnel/containers.go b/pkg/domain/infra/tunnel/containers.go index aa4baf846..fe986361b 100644 --- a/pkg/domain/infra/tunnel/containers.go +++ b/pkg/domain/infra/tunnel/containers.go @@ -302,7 +302,7 @@ func (ic *ContainerEngine) ContainerCommit(ctx context.Context, nameOrID string, return nil, errors.Errorf("invalid image name %q", opts.ImageName) } } - options := new(containers.CommitOptions).WithAuthor(opts.Author).WithChanges(opts.Changes).WithComment(opts.Message) + options := new(containers.CommitOptions).WithAuthor(opts.Author).WithChanges(opts.Changes).WithComment(opts.Message).WithSquash(opts.Squash) options.WithFormat(opts.Format).WithPause(opts.Pause).WithRepo(repo).WithTag(tag) response, err := containers.Commit(ic.ClientCtx, nameOrID, options) if err != nil { diff --git a/pkg/specgen/generate/kube/kube.go b/pkg/specgen/generate/kube/kube.go index a8f17e2b6..5e6671231 100644 --- a/pkg/specgen/generate/kube/kube.go +++ b/pkg/specgen/generate/kube/kube.go @@ -324,7 +324,7 @@ func ToSpecGen(ctx context.Context, opts *CtrSpecGenOptions) (*specgen.SpecGener continue } - dest, options, err := parseMountPath(volume.MountPath, volume.ReadOnly) + dest, options, err := parseMountPath(volume.MountPath, volume.ReadOnly, volume.MountPropagation) if err != nil { return nil, err } @@ -390,7 +390,7 @@ func ToSpecGen(ctx context.Context, opts *CtrSpecGenOptions) (*specgen.SpecGener return s, nil } -func parseMountPath(mountPath string, readOnly bool) (string, []string, error) { +func parseMountPath(mountPath string, readOnly bool, propagationMode *v1.MountPropagationMode) (string, []string, error) { options := []string{} splitVol := strings.Split(mountPath, ":") if len(splitVol) > 2 { @@ -410,6 +410,18 @@ func parseMountPath(mountPath string, readOnly bool) (string, []string, error) { if err != nil { return "", opts, errors.Wrapf(err, "parsing MountOptions") } + if propagationMode != nil { + switch *propagationMode { + case v1.MountPropagationNone: + opts = append(opts, "private") + case v1.MountPropagationHostToContainer: + opts = append(opts, "rslave") + case v1.MountPropagationBidirectional: + opts = append(opts, "rshared") + default: + return "", opts, errors.Errorf("unknown propagation mode %q", *propagationMode) + } + } return dest, opts, nil } diff --git a/pkg/specgen/generate/kube/kube_test.go b/pkg/specgen/generate/kube/kube_test.go new file mode 100644 index 000000000..62793ebb6 --- /dev/null +++ b/pkg/specgen/generate/kube/kube_test.go @@ -0,0 +1,42 @@ +package kube + +import ( + "testing" + + "github.com/stretchr/testify/assert" + v1 "k8s.io/api/core/v1" + //"github.com/stretchr/testify/require" +) + +func testPropagation(t *testing.T, propagation v1.MountPropagationMode, expected string) { + dest, options, err := parseMountPath("/to", false, &propagation) + assert.NoError(t, err) + assert.Equal(t, dest, "/to") + assert.Contains(t, options, expected) +} + +func TestParseMountPathPropagation(t *testing.T) { + testPropagation(t, v1.MountPropagationNone, "private") + testPropagation(t, v1.MountPropagationHostToContainer, "rslave") + testPropagation(t, v1.MountPropagationBidirectional, "rshared") + + prop := v1.MountPropagationMode("SpaceWave") + _, _, err := parseMountPath("/to", false, &prop) + assert.Error(t, err) + + _, options, err := parseMountPath("/to", false, nil) + assert.NoError(t, err) + assert.NotContains(t, options, "private") + assert.NotContains(t, options, "rslave") + assert.NotContains(t, options, "rshared") +} + +func TestParseMountPathRO(t *testing.T) { + _, options, err := parseMountPath("/to", true, nil) + assert.NoError(t, err) + assert.Contains(t, options, "ro") + + _, options, err = parseMountPath("/to", false, nil) + assert.NoError(t, err) + assert.NotContains(t, options, "ro") +} diff --git a/podman.spec.rpkg b/podman.spec.rpkg index d02b7ea99..f810d0307 100644 --- a/podman.spec.rpkg +++ b/podman.spec.rpkg @@ -206,6 +206,9 @@ PODMAN_VERSION=%{version} %{__make} DESTDIR=%{buildroot} PREFIX=%{_prefix} ETCDI install.docker \ install.docker-docs \ install.remote \ +%if 0%{?fedora} >= 36 + install.modules-load +%endif install -d -p %{buildroot}/%{_datadir}/%{name}/test/system cp -pav test/system %{buildroot}/%{_datadir}/%{name}/test/ @@ -242,6 +245,9 @@ done %{_userunitdir}/%{name}.socket %{_userunitdir}/%{name}-restart.service %{_usr}/lib/tmpfiles.d/%{name}.conf +%if 0%{?fedora} >= 36 + %{_usr}/lib/modules-load.d/%{name}-iptables.conf +%endif %files docker %{_bindir}/docker diff --git a/test/e2e/commit_test.go b/test/e2e/commit_test.go index 495e31520..6bcf17bfe 100644 --- a/test/e2e/commit_test.go +++ b/test/e2e/commit_test.go @@ -4,6 +4,7 @@ import ( "io/ioutil" "os" "path/filepath" + "strings" . "github.com/containers/podman/v4/test/utils" . "github.com/onsi/ginkgo" @@ -133,6 +134,23 @@ var _ = Describe("Podman commit", func() { Expect(foundBlue).To(Equal(true)) }) + It("podman commit container with --squash", func() { + test := podmanTest.Podman([]string{"run", "--name", "test1", "-d", ALPINE, "ls"}) + test.WaitWithDefaultTimeout() + Expect(test).Should(Exit(0)) + Expect(podmanTest.NumberOfContainers()).To(Equal(1)) + + session := podmanTest.Podman([]string{"commit", "--squash", "test1", "foobar.com/test1-image:latest"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + + session = podmanTest.Podman([]string{"inspect", "--format", "{{.RootFS.Layers}}", "foobar.com/test1-image:latest"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + // Check for one layers + Expect(strings.Fields(session.OutputToString())).To(HaveLen(1)) + }) + It("podman commit container with change flag and JSON entrypoint with =", func() { test := podmanTest.Podman([]string{"run", "--name", "test1", "-d", ALPINE, "ls"}) test.WaitWithDefaultTimeout() diff --git a/troubleshooting.md b/troubleshooting.md index dedcf6bb9..8bce8e50f 100644 --- a/troubleshooting.md +++ b/troubleshooting.md @@ -914,10 +914,25 @@ Error: error creating tmpdir: mkdir /run/user/1000: permission denied Podman expects a valid login session for the `rootless+cgroupv2` use-case. Podman execution is expected to fail if the login session is not present. In most cases, podman will figure out a solution on its own but if `XDG_RUNTIME_DIR` is pointing to a path that is not writable execution will most fail. Typical scenarious of such cases are seen when users are trying to use Podman with `su - <user> -c '<podman-command>`, or `sudo -l` and badly configured systemd session. -Resolution steps +Alternatives: + +* Execute Podman via __systemd-run__ that will first start a systemd login session: + + ``` + sudo systemd-run --machine=username@ --quiet --user --collect --pipe --wait podman run --rm docker.io/library/alpine echo hello + ``` +* Start an interactive shell in a systemd login session with the command `machinectl shell <username>@` + and then run Podman + + ``` + $ sudo -i + # machinectl shell username@ + Connected to the local host. Press ^] three times within 1s to exit session. + $ podman run --rm docker.io/library/alpine echo hello + ``` +* Start a new systemd login session by logging in with `ssh` i.e. `ssh <username>@localhost` and then run Podman. * Before invoking Podman command create a valid login session for your rootless user using `loginctl enable-linger <username>` -* If `loginctl` is unavailable you can also try logging in via `ssh` i.e `ssh <username>@localhost`. ### 31) 127.0.0.1:7777 port already bound diff --git a/vendor/github.com/container-orchestrated-devices/container-device-interface/pkg/cdi/annotations.go b/vendor/github.com/container-orchestrated-devices/container-device-interface/pkg/cdi/annotations.go new file mode 100644 index 000000000..1055c7df8 --- /dev/null +++ b/vendor/github.com/container-orchestrated-devices/container-device-interface/pkg/cdi/annotations.go @@ -0,0 +1,139 @@ +/* + Copyright © 2021-2022 The CDI 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 cdi + +import ( + "strings" + + "github.com/pkg/errors" +) + +const ( + // AnnotationPrefix is the prefix for CDI container annotation keys. + AnnotationPrefix = "cdi.k8s.io/" +) + +// UpdateAnnotations updates annotations with a plugin-specific CDI device +// injection request for the given devices. Upon any error a non-nil error +// is returned and annotations are left intact. By convention plugin should +// be in the format of "vendor.device-type". +func UpdateAnnotations(annotations map[string]string, plugin string, deviceID string, devices []string) (map[string]string, error) { + key, err := AnnotationKey(plugin, deviceID) + if err != nil { + return annotations, errors.Wrap(err, "CDI annotation failed") + } + if _, ok := annotations[key]; ok { + return annotations, errors.Errorf("CDI annotation failed, key %q used", key) + } + value, err := AnnotationValue(devices) + if err != nil { + return annotations, errors.Wrap(err, "CDI annotation failed") + } + + if annotations == nil { + annotations = make(map[string]string) + } + annotations[key] = value + + return annotations, nil +} + +// ParseAnnotations parses annotations for CDI device injection requests. +// The keys and devices from all such requests are collected into slices +// which are returned as the result. All devices are expected to be fully +// qualified CDI device names. If any device fails this check empty slices +// are returned along with a non-nil error. The annotations are expected +// to be formatted by, or in a compatible fashion to UpdateAnnotations(). +func ParseAnnotations(annotations map[string]string) ([]string, []string, error) { + var ( + keys []string + devices []string + ) + + for key, value := range annotations { + if !strings.HasPrefix(key, AnnotationPrefix) { + continue + } + for _, d := range strings.Split(value, ",") { + if !IsQualifiedName(d) { + return nil, nil, errors.Errorf("invalid CDI device name %q", d) + } + devices = append(devices, d) + } + keys = append(keys, key) + } + + return keys, devices, nil +} + +// AnnotationKey returns a unique annotation key for an device allocation +// by a K8s device plugin. pluginName should be in the format of +// "vendor.device-type". deviceID is the ID of the device the plugin is +// allocating. It is used to make sure that the generated key is unique +// even if multiple allocations by a single plugin needs to be annotated. +func AnnotationKey(pluginName, deviceID string) (string, error) { + const maxNameLen = 63 + + if pluginName == "" { + return "", errors.New("invalid plugin name, empty") + } + if deviceID == "" { + return "", errors.New("invalid deviceID, empty") + } + + name := pluginName + "_" + strings.ReplaceAll(deviceID, "/", "_") + + if len(name) > maxNameLen { + return "", errors.Errorf("invalid plugin+deviceID %q, too long", name) + } + + if c := rune(name[0]); !isAlphaNumeric(c) { + return "", errors.Errorf("invalid name %q, first '%c' should be alphanumeric", + name, c) + } + if len(name) > 2 { + for _, c := range name[1 : len(name)-1] { + switch { + case isAlphaNumeric(c): + case c == '_' || c == '-' || c == '.': + default: + return "", errors.Errorf("invalid name %q, invalid charcter '%c'", + name, c) + } + } + } + if c := rune(name[len(name)-1]); !isAlphaNumeric(c) { + return "", errors.Errorf("invalid name %q, last '%c' should be alphanumeric", + name, c) + } + + return AnnotationPrefix + name, nil +} + +// AnnotationValue returns an annotation value for the given devices. +func AnnotationValue(devices []string) (string, error) { + value, sep := "", "" + for _, d := range devices { + if _, _, _, err := ParseQualifiedName(d); err != nil { + return "", err + } + value += sep + d + sep = "," + } + + return value, nil +} diff --git a/vendor/github.com/container-orchestrated-devices/container-device-interface/pkg/cdi/cache.go b/vendor/github.com/container-orchestrated-devices/container-device-interface/pkg/cdi/cache.go index c4befc83e..5c3f803f4 100644 --- a/vendor/github.com/container-orchestrated-devices/container-device-interface/pkg/cdi/cache.go +++ b/vendor/github.com/container-orchestrated-devices/container-device-interface/pkg/cdi/cache.go @@ -153,10 +153,7 @@ func (c *Cache) Refresh() error { // returns any unresolvable devices and an error if injection fails for // any of the devices. func (c *Cache) InjectDevices(ociSpec *oci.Spec, devices ...string) ([]string, error) { - var ( - unresolved []string - specs = map[*Spec]struct{}{} - ) + var unresolved []string if ociSpec == nil { return devices, errors.Errorf("can't inject devices, nil OCI Spec") @@ -165,6 +162,9 @@ func (c *Cache) InjectDevices(ociSpec *oci.Spec, devices ...string) ([]string, e c.Lock() defer c.Unlock() + edits := &ContainerEdits{} + specs := map[*Spec]struct{}{} + for _, device := range devices { d := c.devices[device] if d == nil { @@ -173,9 +173,9 @@ func (c *Cache) InjectDevices(ociSpec *oci.Spec, devices ...string) ([]string, e } if _, ok := specs[d.GetSpec()]; !ok { specs[d.GetSpec()] = struct{}{} - d.GetSpec().ApplyEdits(ociSpec) + edits.Append(d.GetSpec().edits()) } - d.ApplyEdits(ociSpec) + edits.Append(d.edits()) } if unresolved != nil { @@ -183,6 +183,10 @@ func (c *Cache) InjectDevices(ociSpec *oci.Spec, devices ...string) ([]string, e strings.Join(devices, ", ")) } + if err := edits.Apply(ociSpec); err != nil { + return nil, errors.Wrap(err, "failed to inject devices") + } + return nil, nil } diff --git a/vendor/github.com/container-orchestrated-devices/container-device-interface/pkg/cdi/container-edits.go b/vendor/github.com/container-orchestrated-devices/container-device-interface/pkg/cdi/container-edits.go index 767cba57b..80d88b118 100644 --- a/vendor/github.com/container-orchestrated-devices/container-device-interface/pkg/cdi/container-edits.go +++ b/vendor/github.com/container-orchestrated-devices/container-device-interface/pkg/cdi/container-edits.go @@ -17,6 +17,9 @@ package cdi import ( + "os" + "path/filepath" + "sort" "strings" "github.com/pkg/errors" @@ -24,6 +27,8 @@ import ( "github.com/container-orchestrated-devices/container-device-interface/specs-go" oci "github.com/opencontainers/runtime-spec/specs-go" ocigen "github.com/opencontainers/runtime-tools/generate" + + runc "github.com/opencontainers/runc/libcontainer/devices" ) const ( @@ -78,12 +83,44 @@ func (e *ContainerEdits) Apply(spec *oci.Spec) error { if len(e.Env) > 0 { specgen.AddMultipleProcessEnv(e.Env) } + for _, d := range e.DeviceNodes { - specgen.AddDevice(d.ToOCI()) + dev := d.ToOCI() + if err := fillMissingInfo(&dev); err != nil { + return err + } + + if dev.UID == nil && spec.Process != nil { + if uid := spec.Process.User.UID; uid > 0 { + dev.UID = &uid + } + } + if dev.GID == nil && spec.Process != nil { + if gid := spec.Process.User.GID; gid > 0 { + dev.GID = &gid + } + } + + specgen.RemoveDevice(dev.Path) + specgen.AddDevice(dev) + + if dev.Type == "b" || dev.Type == "c" { + access := d.Permissions + if access == "" { + access = "rwm" + } + specgen.AddLinuxResourcesDevice(true, dev.Type, &dev.Major, &dev.Minor, access) + } } - for _, m := range e.Mounts { - specgen.AddMount(m.ToOCI()) + + if len(e.Mounts) > 0 { + for _, m := range e.Mounts { + specgen.RemoveMount(m.ContainerPath) + specgen.AddMount(m.ToOCI()) + } + sortMounts(&specgen) } + for _, h := range e.Hooks { switch h.HookName { case PrestartHook: @@ -138,6 +175,27 @@ func (e *ContainerEdits) Validate() error { return nil } +// Append other edits into this one. If called with a nil receiver, +// allocates and returns newly allocated edits. +func (e *ContainerEdits) Append(o *ContainerEdits) *ContainerEdits { + if o == nil || o.ContainerEdits == nil { + return e + } + if e == nil { + e = &ContainerEdits{} + } + if e.ContainerEdits == nil { + e.ContainerEdits = &specs.ContainerEdits{} + } + + e.Env = append(e.Env, o.Env...) + e.DeviceNodes = append(e.DeviceNodes, o.DeviceNodes...) + e.Hooks = append(e.Hooks, o.Hooks...) + e.Mounts = append(e.Mounts, o.Mounts...) + + return e +} + // isEmpty returns true if these edits are empty. This is valid in a // global Spec context but invalid in a Device context. func (e *ContainerEdits) isEmpty() bool { @@ -164,10 +222,18 @@ type DeviceNode struct { // Validate a CDI Spec DeviceNode. func (d *DeviceNode) Validate() error { + validTypes := map[string]struct{}{ + "": {}, + "b": {}, + "c": {}, + "u": {}, + "p": {}, + } + if d.Path == "" { return errors.New("invalid (empty) device path") } - if d.Type != "" && d.Type != "b" && d.Type != "c" { + if _, ok := validTypes[d.Type]; !ok { return errors.Errorf("device %q: invalid type %q", d.Path, d.Type) } for _, bit := range d.Permissions { @@ -220,3 +286,72 @@ func ensureOCIHooks(spec *oci.Spec) { spec.Hooks = &oci.Hooks{} } } + +// fillMissingInfo fills in missing mandatory attributes from the host device. +func fillMissingInfo(dev *oci.LinuxDevice) error { + if dev.Type != "" && (dev.Major != 0 || dev.Type == "p") { + return nil + } + hostDev, err := runc.DeviceFromPath(dev.Path, "rwm") + if err != nil { + return errors.Wrapf(err, "failed to stat CDI host device %q", dev.Path) + } + + if dev.Type == "" { + dev.Type = string(hostDev.Type) + } else { + if dev.Type != string(hostDev.Type) { + return errors.Errorf("CDI device %q, host type mismatch (%s, %s)", + dev.Path, dev.Type, string(hostDev.Type)) + } + } + if dev.Major == 0 && dev.Type != "p" { + dev.Major = hostDev.Major + dev.Minor = hostDev.Minor + } + + return nil +} + +// sortMounts sorts the mounts in the given OCI Spec. +func sortMounts(specgen *ocigen.Generator) { + mounts := specgen.Mounts() + specgen.ClearMounts() + sort.Sort(orderedMounts(mounts)) + specgen.Config.Mounts = mounts +} + +// orderedMounts defines how to sort an OCI Spec Mount slice. +// This is the almost the same implementation sa used by CRI-O and Docker, +// with a minor tweak for stable sorting order (easier to test): +// https://github.com/moby/moby/blob/17.05.x/daemon/volumes.go#L26 +type orderedMounts []oci.Mount + +// Len returns the number of mounts. Used in sorting. +func (m orderedMounts) Len() int { + return len(m) +} + +// Less returns true if the number of parts (a/b/c would be 3 parts) in the +// mount indexed by parameter 1 is less than that of the mount indexed by +// parameter 2. Used in sorting. +func (m orderedMounts) Less(i, j int) bool { + ip, jp := m.parts(i), m.parts(j) + if ip < jp { + return true + } + if jp < ip { + return false + } + return m[i].Destination < m[j].Destination +} + +// Swap swaps two items in an array of mounts. Used in sorting +func (m orderedMounts) Swap(i, j int) { + m[i], m[j] = m[j], m[i] +} + +// parts returns the number of parts in the destination of a mount. Used in sorting. +func (m orderedMounts) parts(i int) int { + return strings.Count(filepath.Clean(m[i].Destination), string(os.PathSeparator)) +} diff --git a/vendor/github.com/container-orchestrated-devices/container-device-interface/pkg/cdi/device.go b/vendor/github.com/container-orchestrated-devices/container-device-interface/pkg/cdi/device.go index 84b71429c..0bb1f531b 100644 --- a/vendor/github.com/container-orchestrated-devices/container-device-interface/pkg/cdi/device.go +++ b/vendor/github.com/container-orchestrated-devices/container-device-interface/pkg/cdi/device.go @@ -54,8 +54,12 @@ func (d *Device) GetQualifiedName() string { // ApplyEdits applies the device-speific container edits to an OCI Spec. func (d *Device) ApplyEdits(ociSpec *oci.Spec) error { - e := ContainerEdits{&d.ContainerEdits} - return e.Apply(ociSpec) + return d.edits().Apply(ociSpec) +} + +// edits returns the applicable container edits for this spec. +func (d *Device) edits() *ContainerEdits { + return &ContainerEdits{&d.ContainerEdits} } // Validate the device. @@ -63,7 +67,7 @@ func (d *Device) validate() error { if err := ValidateDeviceName(d.Name); err != nil { return err } - edits := ContainerEdits{&d.ContainerEdits} + edits := d.edits() if edits.isEmpty() { return errors.Errorf("invalid device, empty device edits") } diff --git a/vendor/github.com/container-orchestrated-devices/container-device-interface/pkg/cdi/spec.go b/vendor/github.com/container-orchestrated-devices/container-device-interface/pkg/cdi/spec.go index 20f188498..adebc101f 100644 --- a/vendor/github.com/container-orchestrated-devices/container-device-interface/pkg/cdi/spec.go +++ b/vendor/github.com/container-orchestrated-devices/container-device-interface/pkg/cdi/spec.go @@ -33,6 +33,7 @@ var ( validSpecVersions = map[string]struct{}{ "0.1.0": {}, "0.2.0": {}, + "0.3.0": {}, } ) @@ -120,14 +121,18 @@ func (s *Spec) GetPriority() int { // ApplyEdits applies the Spec's global-scope container edits to an OCI Spec. func (s *Spec) ApplyEdits(ociSpec *oci.Spec) error { - e := ContainerEdits{&s.ContainerEdits} - return e.Apply(ociSpec) + return s.edits().Apply(ociSpec) +} + +// edits returns the applicable global container edits for this spec. +func (s *Spec) edits() *ContainerEdits { + return &ContainerEdits{&s.ContainerEdits} } // Validate the Spec. func (s *Spec) validate() (map[string]*Device, error) { - if _, ok := validSpecVersions[s.Version]; !ok { - return nil, errors.Errorf("invalid version %q", s.Version) + if err := validateVersion(s.Version); err != nil { + return nil, err } if err := ValidateVendorName(s.vendor); err != nil { return nil, err @@ -135,8 +140,7 @@ func (s *Spec) validate() (map[string]*Device, error) { if err := ValidateClassName(s.class); err != nil { return nil, err } - edits := &ContainerEdits{&s.ContainerEdits} - if err := edits.Validate(); err != nil { + if err := s.edits().Validate(); err != nil { return nil, err } @@ -155,6 +159,15 @@ func (s *Spec) validate() (map[string]*Device, error) { return devices, nil } +// validateVersion checks whether the specified spec version is supported. +func validateVersion(version string) error { + if _, ok := validSpecVersions[version]; !ok { + return errors.Errorf("invalid version %q", version) + } + + return nil +} + // Parse raw CDI Spec file data. func parseSpec(data []byte) (*cdi.Spec, error) { raw := &cdi.Spec{} diff --git a/vendor/github.com/container-orchestrated-devices/container-device-interface/specs-go/config.go b/vendor/github.com/container-orchestrated-devices/container-device-interface/specs-go/config.go index 20914e5b6..090e30e43 100644 --- a/vendor/github.com/container-orchestrated-devices/container-device-interface/specs-go/config.go +++ b/vendor/github.com/container-orchestrated-devices/container-device-interface/specs-go/config.go @@ -2,11 +2,13 @@ package specs import "os" +// CurrentVersion is the current version of the Spec. +const CurrentVersion = "0.3.0" + // Spec is the base configuration for CDI type Spec struct { - Version string `json:"cdiVersion"` - Kind string `json:"kind"` - ContainerRuntime []string `json:"containerRuntime,omitempty"` + Version string `json:"cdiVersion"` + Kind string `json:"kind"` Devices []Device `json:"devices"` ContainerEdits ContainerEdits `json:"containerEdits,omitempty"` diff --git a/vendor/modules.txt b/vendor/modules.txt index 8d58ab5a5..32d0152e0 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -57,7 +57,7 @@ github.com/checkpoint-restore/go-criu/v5/rpc github.com/checkpoint-restore/go-criu/v5/stats # github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e github.com/chzyer/readline -# github.com/container-orchestrated-devices/container-device-interface v0.0.0-20220111162300-46367ec063fd +# github.com/container-orchestrated-devices/container-device-interface v0.3.0 ## explicit github.com/container-orchestrated-devices/container-device-interface/pkg/cdi github.com/container-orchestrated-devices/container-device-interface/specs-go |