From 951fcb49ac7a276063f2a110ac178e997efe8a66 Mon Sep 17 00:00:00 2001 From: Evan Lezar Date: Mon, 15 Jun 2026 11:36:48 +0200 Subject: [PATCH 1/4] Remove runtime-tools dependency Signed-off-by: Evan Lezar --- cmd/cdi/cmd/cdi-api.go | 4 +- cmd/cdi/cmd/cdi-api_test.go | 78 ++++++++++ cmd/cdi/go.mod | 5 +- cmd/cdi/go.sum | 15 +- cmd/validate/go.sum | 4 - go.mod | 3 - go.sum | 21 --- internal/ociedit/spec_editor.go | 255 ++++++++++++++++++++++++++++++++ pkg/cdi/container-edits.go | 57 ++----- pkg/cdi/container-edits_test.go | 62 ++++++++ schema/go.mod | 2 - schema/go.sum | 14 -- 12 files changed, 412 insertions(+), 108 deletions(-) create mode 100644 cmd/cdi/cmd/cdi-api_test.go create mode 100644 internal/ociedit/spec_editor.go diff --git a/cmd/cdi/cmd/cdi-api.go b/cmd/cdi/cmd/cdi-api.go index 199f6e12..543795ed 100644 --- a/cmd/cdi/cmd/cdi-api.go +++ b/cmd/cdi/cmd/cdi-api.go @@ -23,7 +23,7 @@ import ( "strings" oci "github.com/opencontainers/runtime-spec/specs-go" - gen "github.com/opencontainers/runtime-tools/generate" + "tags.cncf.io/container-device-interface/internal/ociedit" "tags.cncf.io/container-device-interface/pkg/cdi" "tags.cncf.io/container-device-interface/pkg/parser" ) @@ -227,7 +227,7 @@ func collectCDIDevicesFromOCISpec(spec *oci.Spec) []string { } devices := spec.Linux.Devices - g := gen.NewFromSpec(spec) + g := ociedit.NewSpecEditor(spec) g.ClearLinuxDevices() for _, d := range devices { diff --git a/cmd/cdi/cmd/cdi-api_test.go b/cmd/cdi/cmd/cdi-api_test.go new file mode 100644 index 00000000..660c8477 --- /dev/null +++ b/cmd/cdi/cmd/cdi-api_test.go @@ -0,0 +1,78 @@ +/* + Copyright 2026 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 cmd + +import ( + "testing" + + oci "github.com/opencontainers/runtime-spec/specs-go" + "github.com/stretchr/testify/require" +) + +func TestCollectCDIDevicesFromOCISpec(t *testing.T) { + for _, tc := range []struct { + name string + spec *oci.Spec + devices []string + linuxDevs []oci.LinuxDevice + emptyDevice bool + }{ + { + name: "collect CDI devices and keep non-CDI devices", + spec: &oci.Spec{ + Linux: &oci.Linux{ + Devices: []oci.LinuxDevice{ + {Path: "vendor.com/device=dev0"}, + {Path: "/dev/null", Type: "c", Major: 1, Minor: 3}, + {Path: "/dev/null", Type: "c", Major: 1, Minor: 5}, + {Path: "vendor.com/device=dev1"}, + }, + }, + }, + devices: []string{ + "vendor.com/device=dev0", + "vendor.com/device=dev1", + }, + linuxDevs: []oci.LinuxDevice{ + {Path: "/dev/null", Type: "c", Major: 1, Minor: 5}, + }, + }, + { + name: "all CDI devices leave an empty device slice", + spec: &oci.Spec{ + Linux: &oci.Linux{ + Devices: []oci.LinuxDevice{ + {Path: "vendor.com/device=dev0"}, + }, + }, + }, + devices: []string{"vendor.com/device=dev0"}, + linuxDevs: []oci.LinuxDevice{}, + emptyDevice: true, + }, + } { + t.Run(tc.name, func(t *testing.T) { + devices := collectCDIDevicesFromOCISpec(tc.spec) + + require.Equal(t, tc.devices, devices) + require.Equal(t, tc.linuxDevs, tc.spec.Linux.Devices) + if tc.emptyDevice { + require.NotNil(t, tc.spec.Linux.Devices) + } + }) + } +} diff --git a/cmd/cdi/go.mod b/cmd/cdi/go.mod index 4994a31c..cc5a317e 100644 --- a/cmd/cdi/go.mod +++ b/cmd/cdi/go.mod @@ -5,8 +5,8 @@ go 1.21 require ( github.com/fsnotify/fsnotify v1.5.1 github.com/opencontainers/runtime-spec v1.3.0 - github.com/opencontainers/runtime-tools v0.9.1-0.20251114084447-edf4cb3d2116 github.com/spf13/cobra v1.6.0 + github.com/stretchr/testify v1.7.0 gopkg.in/yaml.v3 v3.0.1 sigs.k8s.io/yaml v1.4.0 tags.cncf.io/container-device-interface v1.1.0 @@ -15,8 +15,9 @@ require ( ) require ( + github.com/davecgh/go-spew v1.1.1 // indirect github.com/inconshreveable/mousetrap v1.0.1 // indirect - github.com/moby/sys/capability v0.4.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect diff --git a/cmd/cdi/go.sum b/cmd/cdi/go.sum index 5d7a9d7e..9c2ce7cf 100644 --- a/cmd/cdi/go.sum +++ b/cmd/cdi/go.sum @@ -1,5 +1,3 @@ -github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= -github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -8,25 +6,13 @@ github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWp github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= -github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= -github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/moby/sys/capability v0.4.0 h1:4D4mI6KlNtWMCM1Z/K0i7RV1FkX+DBDHKVJpCndZoHk= -github.com/moby/sys/capability v0.4.0/go.mod h1:4g9IK291rVkms3LKCDOoYlnV8xKwoDTpIrNEE35Wq0I= github.com/opencontainers/runtime-spec v1.3.0 h1:YZupQUdctfhpZy3TM39nN9Ika5CBWT5diQ8ibYCRkxg= github.com/opencontainers/runtime-spec v1.3.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= -github.com/opencontainers/runtime-tools v0.9.1-0.20251114084447-edf4cb3d2116 h1:tAKu3NkKWZYpqBSOJKwTxT1wIGueiF7gcmcNgr5pNTY= -github.com/opencontainers/runtime-tools v0.9.1-0.20251114084447-edf4cb3d2116/go.mod h1:DKDEfzxvRkoQ6n9TGhxQgg2IM1lY4aM0eaQP4e3oElw= -github.com/opencontainers/selinux v1.10.0 h1:rAiKF8hTcgLI3w0DHm6i0ylVVcOrlgR1kK99DRLDhyU= -github.com/opencontainers/selinux v1.10.0/go.mod h1:2i0OySw99QjzBBQByd1Gr9gSjvuho1lHsJxIJ3gGbJI= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= -github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/spf13/cobra v1.6.0 h1:42a0n6jwCot1pUmomAp4T7DeMD+20LFv4Q54pxLf2LI= github.com/spf13/cobra v1.6.0/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= @@ -48,6 +34,7 @@ golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= diff --git a/cmd/validate/go.sum b/cmd/validate/go.sum index 39f8589d..99998750 100644 --- a/cmd/validate/go.sum +++ b/cmd/validate/go.sum @@ -5,12 +5,8 @@ github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWp github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/moby/sys/capability v0.4.0 h1:4D4mI6KlNtWMCM1Z/K0i7RV1FkX+DBDHKVJpCndZoHk= -github.com/moby/sys/capability v0.4.0/go.mod h1:4g9IK291rVkms3LKCDOoYlnV8xKwoDTpIrNEE35Wq0I= github.com/opencontainers/runtime-spec v1.3.0 h1:YZupQUdctfhpZy3TM39nN9Ika5CBWT5diQ8ibYCRkxg= github.com/opencontainers/runtime-spec v1.3.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= -github.com/opencontainers/runtime-tools v0.9.1-0.20251114084447-edf4cb3d2116 h1:tAKu3NkKWZYpqBSOJKwTxT1wIGueiF7gcmcNgr5pNTY= -github.com/opencontainers/runtime-tools v0.9.1-0.20251114084447-edf4cb3d2116/go.mod h1:DKDEfzxvRkoQ6n9TGhxQgg2IM1lY4aM0eaQP4e3oElw= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= diff --git a/go.mod b/go.mod index 9f1d07c6..8e7cc556 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,6 @@ go 1.21 require ( github.com/fsnotify/fsnotify v1.5.1 github.com/opencontainers/runtime-spec v1.3.0 - github.com/opencontainers/runtime-tools v0.9.1-0.20251114084447-edf4cb3d2116 github.com/stretchr/testify v1.7.0 golang.org/x/sys v0.19.0 gopkg.in/yaml.v3 v3.0.1 @@ -15,8 +14,6 @@ require ( require ( github.com/davecgh/go-spew v1.1.1 // indirect - github.com/moby/sys/capability v0.4.0 // indirect - github.com/opencontainers/selinux v1.10.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect golang.org/x/mod v0.19.0 // indirect ) diff --git a/go.sum b/go.sum index 9c10a46a..ce65d94d 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,3 @@ -github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= -github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -7,34 +5,15 @@ github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWp github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= -github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= -github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/moby/sys/capability v0.4.0 h1:4D4mI6KlNtWMCM1Z/K0i7RV1FkX+DBDHKVJpCndZoHk= -github.com/moby/sys/capability v0.4.0/go.mod h1:4g9IK291rVkms3LKCDOoYlnV8xKwoDTpIrNEE35Wq0I= github.com/opencontainers/runtime-spec v1.3.0 h1:YZupQUdctfhpZy3TM39nN9Ika5CBWT5diQ8ibYCRkxg= github.com/opencontainers/runtime-spec v1.3.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= -github.com/opencontainers/runtime-tools v0.9.1-0.20251114084447-edf4cb3d2116 h1:tAKu3NkKWZYpqBSOJKwTxT1wIGueiF7gcmcNgr5pNTY= -github.com/opencontainers/runtime-tools v0.9.1-0.20251114084447-edf4cb3d2116/go.mod h1:DKDEfzxvRkoQ6n9TGhxQgg2IM1lY4aM0eaQP4e3oElw= -github.com/opencontainers/selinux v1.10.0 h1:rAiKF8hTcgLI3w0DHm6i0ylVVcOrlgR1kK99DRLDhyU= -github.com/opencontainers/selinux v1.10.0/go.mod h1:2i0OySw99QjzBBQByd1Gr9gSjvuho1lHsJxIJ3gGbJI= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= -github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= -github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= -github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= -github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= -github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= -github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8= golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/sys v0.0.0-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/internal/ociedit/spec_editor.go b/internal/ociedit/spec_editor.go new file mode 100644 index 00000000..179d5cf7 --- /dev/null +++ b/internal/ociedit/spec_editor.go @@ -0,0 +1,255 @@ +/* + Copyright 2026 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 ociedit + +import ( + "slices" + "strings" + + oci "github.com/opencontainers/runtime-spec/specs-go" +) + +// SpecEditor is the internal boundary between CDI edits and OCI spec mutation. +// It deliberately models only the operations CDI needs, so a future external +// generator can be wired in without leaking that dependency elsewhere. +type SpecEditor interface { + AddMultipleProcessEnv([]string) + RemoveDevice(string) + AddDevice(oci.LinuxDevice) + AddLinuxResourcesDevice(bool, string, *int64, *int64, string) + SetLinuxNetDevice(string, *oci.LinuxNetDevice) + RemoveMount(string) + AddMount(oci.Mount) + Mounts() []oci.Mount + SetMounts([]oci.Mount) + AddPreStartHook(oci.Hook) + AddPostStartHook(oci.Hook) + AddPostStopHook(oci.Hook) + AddCreateRuntimeHook(oci.Hook) + AddCreateContainerHook(oci.Hook) + AddStartContainerHook(oci.Hook) + SetLinuxIntelRdt(*oci.LinuxIntelRdt) + AddProcessAdditionalGID(uint32) + ClearLinuxDevices() +} + +// NewSpecEditor returns CDI's native OCI spec editor. +func NewSpecEditor(spec *oci.Spec) SpecEditor { + envCache := map[string]int{} + if spec != nil && spec.Process != nil { + envCache = createEnvCacheMap(spec.Process.Env) + } + return &nativeSpecEditor{ + spec: spec, + envMap: envCache, + } +} + +type nativeSpecEditor struct { + spec *oci.Spec + envMap map[string]int +} + +func createEnvCacheMap(env []string) map[string]int { + envMap := make(map[string]int, len(env)) + for i, val := range env { + envMap[val] = i + } + return envMap +} + +func (e *nativeSpecEditor) initConfig() { + if e.spec == nil { + e.spec = &oci.Spec{} + } +} + +func (e *nativeSpecEditor) initProcess() { + e.initConfig() + if e.spec.Process == nil { + e.spec.Process = &oci.Process{} + } +} + +func (e *nativeSpecEditor) initHooks() { + e.initConfig() + if e.spec.Hooks == nil { + e.spec.Hooks = &oci.Hooks{} + } +} + +func (e *nativeSpecEditor) initLinux() { + e.initConfig() + if e.spec.Linux == nil { + e.spec.Linux = &oci.Linux{} + } +} + +func (e *nativeSpecEditor) initLinuxResources() { + e.initLinux() + if e.spec.Linux.Resources == nil { + e.spec.Linux.Resources = &oci.LinuxResources{} + } +} + +func (e *nativeSpecEditor) initLinuxNetDevices() { + e.initLinux() + if e.spec.Linux.NetDevices == nil { + e.spec.Linux.NetDevices = map[string]oci.LinuxNetDevice{} + } +} + +func (e *nativeSpecEditor) AddMultipleProcessEnv(envs []string) { + e.initProcess() + + for _, val := range envs { + split := strings.SplitN(val, "=", 2) + e.addEnv(val, split[0]) + } +} + +func (e *nativeSpecEditor) addEnv(env, key string) { + if idx, ok := e.envMap[key]; ok { + e.spec.Process.Env[idx] = env + return + } + + e.spec.Process.Env = append(e.spec.Process.Env, env) + e.envMap[key] = len(e.spec.Process.Env) - 1 +} + +func (e *nativeSpecEditor) RemoveDevice(path string) { + if e.spec == nil || e.spec.Linux == nil || e.spec.Linux.Devices == nil { + return + } + + for i, device := range e.spec.Linux.Devices { + if device.Path == path { + e.spec.Linux.Devices = append(e.spec.Linux.Devices[:i], e.spec.Linux.Devices[i+1:]...) + return + } + } +} + +func (e *nativeSpecEditor) AddDevice(device oci.LinuxDevice) { + e.initLinux() + + for i, dev := range e.spec.Linux.Devices { + if dev.Path == device.Path { + e.spec.Linux.Devices[i] = device + return + } + } + + e.spec.Linux.Devices = append(e.spec.Linux.Devices, device) +} + +func (e *nativeSpecEditor) AddLinuxResourcesDevice(allow bool, devType string, major, minor *int64, access string) { + e.initLinuxResources() + e.spec.Linux.Resources.Devices = append(e.spec.Linux.Resources.Devices, oci.LinuxDeviceCgroup{ + Allow: allow, + Type: devType, + Major: major, + Minor: minor, + Access: access, + }) +} + +func (e *nativeSpecEditor) SetLinuxNetDevice(hostIf string, netDev *oci.LinuxNetDevice) { + if netDev == nil { + return + } + + e.initLinuxNetDevices() + e.spec.Linux.NetDevices[hostIf] = *netDev +} + +func (e *nativeSpecEditor) RemoveMount(dest string) { + e.initConfig() + + for i, mount := range e.spec.Mounts { + if mount.Destination == dest { + e.spec.Mounts = append(e.spec.Mounts[:i], e.spec.Mounts[i+1:]...) + return + } + } +} + +func (e *nativeSpecEditor) AddMount(mnt oci.Mount) { + e.initConfig() + e.spec.Mounts = append(e.spec.Mounts, mnt) +} + +func (e *nativeSpecEditor) Mounts() []oci.Mount { + e.initConfig() + return e.spec.Mounts +} + +func (e *nativeSpecEditor) SetMounts(mounts []oci.Mount) { + e.initConfig() + e.spec.Mounts = mounts +} + +func (e *nativeSpecEditor) AddPreStartHook(hook oci.Hook) { + e.initHooks() + e.spec.Hooks.Prestart = append(e.spec.Hooks.Prestart, hook) //nolint:staticcheck // CDI still supports OCI prestart hooks. +} + +func (e *nativeSpecEditor) AddPostStartHook(hook oci.Hook) { + e.initHooks() + e.spec.Hooks.Poststart = append(e.spec.Hooks.Poststart, hook) +} + +func (e *nativeSpecEditor) AddPostStopHook(hook oci.Hook) { + e.initHooks() + e.spec.Hooks.Poststop = append(e.spec.Hooks.Poststop, hook) +} + +func (e *nativeSpecEditor) AddCreateRuntimeHook(hook oci.Hook) { + e.initHooks() + e.spec.Hooks.CreateRuntime = append(e.spec.Hooks.CreateRuntime, hook) +} + +func (e *nativeSpecEditor) AddCreateContainerHook(hook oci.Hook) { + e.initHooks() + e.spec.Hooks.CreateContainer = append(e.spec.Hooks.CreateContainer, hook) +} + +func (e *nativeSpecEditor) AddStartContainerHook(hook oci.Hook) { + e.initHooks() + e.spec.Hooks.StartContainer = append(e.spec.Hooks.StartContainer, hook) +} + +func (e *nativeSpecEditor) SetLinuxIntelRdt(rdt *oci.LinuxIntelRdt) { + e.initLinux() + e.spec.Linux.IntelRdt = rdt +} + +func (e *nativeSpecEditor) AddProcessAdditionalGID(gid uint32) { + e.initProcess() + if slices.Contains(e.spec.Process.User.AdditionalGids, gid) { + return + } + e.spec.Process.User.AdditionalGids = append(e.spec.Process.User.AdditionalGids, gid) +} + +func (e *nativeSpecEditor) ClearLinuxDevices() { + if e.spec == nil || e.spec.Linux == nil || e.spec.Linux.Devices == nil { + return + } + e.spec.Linux.Devices = []oci.LinuxDevice{} +} diff --git a/pkg/cdi/container-edits.go b/pkg/cdi/container-edits.go index 387219f5..e3ee10f0 100644 --- a/pkg/cdi/container-edits.go +++ b/pkg/cdi/container-edits.go @@ -25,7 +25,7 @@ import ( "strings" oci "github.com/opencontainers/runtime-spec/specs-go" - ocigen "github.com/opencontainers/runtime-tools/generate" + "tags.cncf.io/container-device-interface/internal/ociedit" cdi "tags.cncf.io/container-device-interface/specs-go" ) @@ -80,7 +80,7 @@ func (e *ContainerEdits) Apply(spec *oci.Spec) error { return nil } - specgen := ocigen.NewFromSpec(spec) + specgen := ociedit.NewSpecEditor(spec) if len(e.Env) > 0 { specgen.AddMultipleProcessEnv(e.Env) } @@ -120,10 +120,8 @@ func (e *ContainerEdits) Apply(spec *oci.Spec) error { } if len(e.NetDevices) > 0 { - // specgen is currently missing functionality to set Linux NetDevices, - // so we use a locally rolled function for now. for _, dev := range e.NetDevices { - specgenAddLinuxNetDevice(&specgen, dev.HostInterfaceName, (&LinuxNetDevice{dev}).toOCI()) + specgen.SetLinuxNetDevice(dev.HostInterfaceName, (&LinuxNetDevice{dev}).toOCI()) } } @@ -139,7 +137,7 @@ func (e *ContainerEdits) Apply(spec *oci.Spec) error { specgen.AddMount(mnt.toOCI(withIDMapForBindMount())) } } - sortMounts(&specgen) + sortMounts(specgen) } for _, h := range e.Hooks { @@ -151,56 +149,31 @@ func (e *ContainerEdits) Apply(spec *oci.Spec) error { specgen.AddPostStartHook(ociHook) case PoststopHook: specgen.AddPostStopHook(ociHook) - // TODO: Maybe runtime-tools/generate should be updated with these... case CreateRuntimeHook: - ensureOCIHooks(spec) - spec.Hooks.CreateRuntime = append(spec.Hooks.CreateRuntime, ociHook) + specgen.AddCreateRuntimeHook(ociHook) case CreateContainerHook: - ensureOCIHooks(spec) - spec.Hooks.CreateContainer = append(spec.Hooks.CreateContainer, ociHook) + specgen.AddCreateContainerHook(ociHook) case StartContainerHook: - ensureOCIHooks(spec) - spec.Hooks.StartContainer = append(spec.Hooks.StartContainer, ociHook) + specgen.AddStartContainerHook(ociHook) default: return fmt.Errorf("unknown hook name %q", h.HookName) } } if e.IntelRdt != nil { - // The specgen is missing functionality to set all parameters so we - // just piggy-back on it to initialize all structs and the copy over. - specgen.SetLinuxIntelRdtClosID(e.IntelRdt.ClosID) - spec.Linux.IntelRdt = (&IntelRdt{e.IntelRdt}).toOCI() + specgen.SetLinuxIntelRdt((&IntelRdt{e.IntelRdt}).toOCI()) } for _, additionalGID := range e.AdditionalGIDs { if additionalGID == 0 { continue } - specgen.AddProcessAdditionalGid(additionalGID) + specgen.AddProcessAdditionalGID(additionalGID) } return nil } -func specgenAddLinuxNetDevice(specgen *ocigen.Generator, hostIf string, netDev *oci.LinuxNetDevice) { - if specgen == nil || netDev == nil { - return - } - ensureLinuxNetDevices(specgen.Config) - specgen.Config.Linux.NetDevices[hostIf] = *netDev -} - -// Ensure OCI Spec Linux NetDevices map is not nil. -func ensureLinuxNetDevices(spec *oci.Spec) { - if spec.Linux == nil { - spec.Linux = &oci.Linux{} - } - if spec.Linux.NetDevices == nil { - spec.Linux.NetDevices = map[string]oci.LinuxNetDevice{} - } -} - // Validate container edits. func (e *ContainerEdits) Validate() error { if e == nil || e.ContainerEdits == nil { @@ -437,19 +410,11 @@ func (i *IntelRdt) Validate() error { return nil } -// Ensure OCI Spec hooks are not nil so we can add hooks. -func ensureOCIHooks(spec *oci.Spec) { - if spec.Hooks == nil { - spec.Hooks = &oci.Hooks{} - } -} - // sortMounts sorts the mounts in the given OCI Spec. -func sortMounts(specgen *ocigen.Generator) { +func sortMounts(specgen ociedit.SpecEditor) { mounts := specgen.Mounts() - specgen.ClearMounts() sort.Stable(orderedMounts(mounts)) - specgen.Config.Mounts = mounts + specgen.SetMounts(mounts) } // orderedMounts defines how to sort an OCI Spec Mount slice. diff --git a/pkg/cdi/container-edits_test.go b/pkg/cdi/container-edits_test.go index 2240f3d1..df3fb07b 100644 --- a/pkg/cdi/container-edits_test.go +++ b/pkg/cdi/container-edits_test.go @@ -397,6 +397,48 @@ func TestApplyContainerEdits(t *testing.T) { }, }, }, + { + name: "empty spec, duplicate env vars use last value", + spec: &oci.Spec{}, + edits: &cdi.ContainerEdits{ + Env: []string{ + "FOO=old", + "FOO=new", + }, + }, + result: &oci.Spec{ + Process: &oci.Process{ + Env: []string{ + "FOO=new", + }, + }, + }, + }, + { + name: "non-empty spec, env var with existing key is appended", + spec: &oci.Spec{ + Process: &oci.Process{ + Env: []string{ + "FOO=old", + "BAR=old", + }, + }, + }, + edits: &cdi.ContainerEdits{ + Env: []string{ + "FOO=new", + }, + }, + result: &oci.Spec{ + Process: &oci.Process{ + Env: []string{ + "FOO=old", + "BAR=old", + "FOO=new", + }, + }, + }, + }, { name: "empty spec, device", spec: &oci.Spec{}, @@ -788,6 +830,26 @@ func TestApplyContainerEdits(t *testing.T) { }, }, }, + { + name: "existing additional GIDs are not duplicated", + spec: &oci.Spec{ + Process: &oci.Process{ + User: oci.User{ + AdditionalGids: []uint32{4}, + }, + }, + }, + edits: &cdi.ContainerEdits{ + AdditionalGIDs: []uint32{4, 5}, + }, + result: &oci.Spec{ + Process: &oci.Process{ + User: oci.User{ + AdditionalGids: []uint32{4, 5}, + }, + }, + }, + }, { name: "duplicate GIDs are ignored", spec: &oci.Spec{}, diff --git a/schema/go.mod b/schema/go.mod index e5370e9d..68041f8b 100644 --- a/schema/go.mod +++ b/schema/go.mod @@ -13,9 +13,7 @@ require ( require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/fsnotify/fsnotify v1.5.1 // indirect - github.com/moby/sys/capability v0.4.0 // indirect github.com/opencontainers/runtime-spec v1.3.0 // indirect - github.com/opencontainers/runtime-tools v0.9.1-0.20251114084447-edf4cb3d2116 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect diff --git a/schema/go.sum b/schema/go.sum index d28c5582..8c58ee6f 100644 --- a/schema/go.sum +++ b/schema/go.sum @@ -1,5 +1,3 @@ -github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= -github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -7,22 +5,10 @@ github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWp github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= -github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= -github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/moby/sys/capability v0.4.0 h1:4D4mI6KlNtWMCM1Z/K0i7RV1FkX+DBDHKVJpCndZoHk= -github.com/moby/sys/capability v0.4.0/go.mod h1:4g9IK291rVkms3LKCDOoYlnV8xKwoDTpIrNEE35Wq0I= github.com/opencontainers/runtime-spec v1.3.0 h1:YZupQUdctfhpZy3TM39nN9Ika5CBWT5diQ8ibYCRkxg= github.com/opencontainers/runtime-spec v1.3.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= -github.com/opencontainers/runtime-tools v0.9.1-0.20251114084447-edf4cb3d2116 h1:tAKu3NkKWZYpqBSOJKwTxT1wIGueiF7gcmcNgr5pNTY= -github.com/opencontainers/runtime-tools v0.9.1-0.20251114084447-edf4cb3d2116/go.mod h1:DKDEfzxvRkoQ6n9TGhxQgg2IM1lY4aM0eaQP4e3oElw= -github.com/opencontainers/selinux v1.10.0 h1:rAiKF8hTcgLI3w0DHm6i0ylVVcOrlgR1kK99DRLDhyU= -github.com/opencontainers/selinux v1.10.0/go.mod h1:2i0OySw99QjzBBQByd1Gr9gSjvuho1lHsJxIJ3gGbJI= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= -github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= From 823d5487ac07bd5d3de9684efee5fa79908b5018 Mon Sep 17 00:00:00 2001 From: Evan Lezar Date: Mon, 15 Jun 2026 11:58:39 +0200 Subject: [PATCH 2/4] Rename OCI spec editor locals Signed-off-by: Evan Lezar --- cmd/cdi/cmd/cdi-api.go | 6 +++--- pkg/cdi/container-edits.go | 42 +++++++++++++++++++------------------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/cmd/cdi/cmd/cdi-api.go b/cmd/cdi/cmd/cdi-api.go index 543795ed..20790266 100644 --- a/cmd/cdi/cmd/cdi-api.go +++ b/cmd/cdi/cmd/cdi-api.go @@ -227,12 +227,12 @@ func collectCDIDevicesFromOCISpec(spec *oci.Spec) []string { } devices := spec.Linux.Devices - g := ociedit.NewSpecEditor(spec) - g.ClearLinuxDevices() + editor := ociedit.NewSpecEditor(spec) + editor.ClearLinuxDevices() for _, d := range devices { if !parser.IsQualifiedName(d.Path) { - g.AddDevice(d) + editor.AddDevice(d) continue } cdiDevs = append(cdiDevs, d.Path) diff --git a/pkg/cdi/container-edits.go b/pkg/cdi/container-edits.go index e3ee10f0..3f368aa8 100644 --- a/pkg/cdi/container-edits.go +++ b/pkg/cdi/container-edits.go @@ -80,9 +80,9 @@ func (e *ContainerEdits) Apply(spec *oci.Spec) error { return nil } - specgen := ociedit.NewSpecEditor(spec) + editor := ociedit.NewSpecEditor(spec) if len(e.Env) > 0 { - specgen.AddMultipleProcessEnv(e.Env) + editor.AddMultipleProcessEnv(e.Env) } for _, d := range e.DeviceNodes { @@ -104,8 +104,8 @@ func (e *ContainerEdits) Apply(spec *oci.Spec) error { } } - specgen.RemoveDevice(dev.Path) - specgen.AddDevice(dev) + editor.RemoveDevice(dev.Path) + editor.AddDevice(dev) if dev.Type == "b" || dev.Type == "c" { access := d.Permissions @@ -115,13 +115,13 @@ func (e *ContainerEdits) Apply(spec *oci.Spec) error { case NoPermissions: access = "" } - specgen.AddLinuxResourcesDevice(true, dev.Type, &dev.Major, &dev.Minor, access) + editor.AddLinuxResourcesDevice(true, dev.Type, &dev.Major, &dev.Minor, access) } } if len(e.NetDevices) > 0 { for _, dev := range e.NetDevices { - specgen.SetLinuxNetDevice(dev.HostInterfaceName, (&LinuxNetDevice{dev}).toOCI()) + editor.SetLinuxNetDevice(dev.HostInterfaceName, (&LinuxNetDevice{dev}).toOCI()) } } @@ -129,46 +129,46 @@ func (e *ContainerEdits) Apply(spec *oci.Spec) error { for _, m := range e.Mounts { mnt := &Mount{m} - specgen.RemoveMount(m.ContainerPath) + editor.RemoveMount(m.ContainerPath) if !specHasUserNamespace(spec) { - specgen.AddMount(mnt.toOCI()) + editor.AddMount(mnt.toOCI()) } else { - specgen.AddMount(mnt.toOCI(withIDMapForBindMount())) + editor.AddMount(mnt.toOCI(withIDMapForBindMount())) } } - sortMounts(specgen) + sortMounts(editor) } for _, h := range e.Hooks { ociHook := (&Hook{h}).toOCI() switch h.HookName { case PrestartHook: - specgen.AddPreStartHook(ociHook) + editor.AddPreStartHook(ociHook) case PoststartHook: - specgen.AddPostStartHook(ociHook) + editor.AddPostStartHook(ociHook) case PoststopHook: - specgen.AddPostStopHook(ociHook) + editor.AddPostStopHook(ociHook) case CreateRuntimeHook: - specgen.AddCreateRuntimeHook(ociHook) + editor.AddCreateRuntimeHook(ociHook) case CreateContainerHook: - specgen.AddCreateContainerHook(ociHook) + editor.AddCreateContainerHook(ociHook) case StartContainerHook: - specgen.AddStartContainerHook(ociHook) + editor.AddStartContainerHook(ociHook) default: return fmt.Errorf("unknown hook name %q", h.HookName) } } if e.IntelRdt != nil { - specgen.SetLinuxIntelRdt((&IntelRdt{e.IntelRdt}).toOCI()) + editor.SetLinuxIntelRdt((&IntelRdt{e.IntelRdt}).toOCI()) } for _, additionalGID := range e.AdditionalGIDs { if additionalGID == 0 { continue } - specgen.AddProcessAdditionalGID(additionalGID) + editor.AddProcessAdditionalGID(additionalGID) } return nil @@ -411,10 +411,10 @@ func (i *IntelRdt) Validate() error { } // sortMounts sorts the mounts in the given OCI Spec. -func sortMounts(specgen ociedit.SpecEditor) { - mounts := specgen.Mounts() +func sortMounts(editor ociedit.SpecEditor) { + mounts := editor.Mounts() sort.Stable(orderedMounts(mounts)) - specgen.SetMounts(mounts) + editor.SetMounts(mounts) } // orderedMounts defines how to sort an OCI Spec Mount slice. From 9c49c2b1a5930f9e11c7afed29fea75b31c90730 Mon Sep 17 00:00:00 2001 From: Evan Lezar Date: Mon, 15 Jun 2026 11:44:36 +0200 Subject: [PATCH 3/4] Fix OCI env cache keying Signed-off-by: Evan Lezar --- internal/ociedit/spec_editor.go | 1 + pkg/cdi/container-edits_test.go | 5 ++--- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/ociedit/spec_editor.go b/internal/ociedit/spec_editor.go index 179d5cf7..47fad61f 100644 --- a/internal/ociedit/spec_editor.go +++ b/internal/ociedit/spec_editor.go @@ -67,6 +67,7 @@ type nativeSpecEditor struct { func createEnvCacheMap(env []string) map[string]int { envMap := make(map[string]int, len(env)) for i, val := range env { + val, _, _ = strings.Cut(val, "=") envMap[val] = i } return envMap diff --git a/pkg/cdi/container-edits_test.go b/pkg/cdi/container-edits_test.go index df3fb07b..5c20dbee 100644 --- a/pkg/cdi/container-edits_test.go +++ b/pkg/cdi/container-edits_test.go @@ -415,7 +415,7 @@ func TestApplyContainerEdits(t *testing.T) { }, }, { - name: "non-empty spec, env var with existing key is appended", + name: "non-empty spec, env var with existing key is replaced", spec: &oci.Spec{ Process: &oci.Process{ Env: []string{ @@ -432,9 +432,8 @@ func TestApplyContainerEdits(t *testing.T) { result: &oci.Spec{ Process: &oci.Process{ Env: []string{ - "FOO=old", - "BAR=old", "FOO=new", + "BAR=old", }, }, }, From 488b34f4115b8ef0a035fbe0e830874430744041 Mon Sep 17 00:00:00 2001 From: Evan Lezar Date: Thu, 25 Jun 2026 11:21:36 +0200 Subject: [PATCH 4/4] Handle nil OCI specs in spec editor Signed-off-by: Evan Lezar --- cmd/cdi/cmd/cdi-api.go | 7 +++++-- internal/ociedit/spec_editor.go | 29 ++++++++++----------------- internal/ociedit/spec_editor_test.go | 30 ++++++++++++++++++++++++++++ pkg/cdi/container-edits.go | 5 ++++- 4 files changed, 49 insertions(+), 22 deletions(-) create mode 100644 internal/ociedit/spec_editor_test.go diff --git a/cmd/cdi/cmd/cdi-api.go b/cmd/cdi/cmd/cdi-api.go index 20790266..0fc5baeb 100644 --- a/cmd/cdi/cmd/cdi-api.go +++ b/cmd/cdi/cmd/cdi-api.go @@ -222,12 +222,15 @@ func collectCDIDevicesFromOCISpec(spec *oci.Spec) []string { cdiDevs []string ) - if spec.Linux == nil || len(spec.Linux.Devices) == 0 { + if spec == nil || spec.Linux == nil || len(spec.Linux.Devices) == 0 { return nil } devices := spec.Linux.Devices - editor := ociedit.NewSpecEditor(spec) + editor, err := ociedit.NewSpecEditor(spec) + if err != nil { + return nil + } editor.ClearLinuxDevices() for _, d := range devices { diff --git a/internal/ociedit/spec_editor.go b/internal/ociedit/spec_editor.go index 47fad61f..28e97e5b 100644 --- a/internal/ociedit/spec_editor.go +++ b/internal/ociedit/spec_editor.go @@ -17,6 +17,7 @@ package ociedit import ( + "errors" "slices" "strings" @@ -48,15 +49,19 @@ type SpecEditor interface { } // NewSpecEditor returns CDI's native OCI spec editor. -func NewSpecEditor(spec *oci.Spec) SpecEditor { +func NewSpecEditor(spec *oci.Spec) (SpecEditor, error) { + if spec == nil { + return nil, errors.New("can't edit nil OCI Spec") + } + envCache := map[string]int{} - if spec != nil && spec.Process != nil { + if spec.Process != nil { envCache = createEnvCacheMap(spec.Process.Env) } return &nativeSpecEditor{ spec: spec, envMap: envCache, - } + }, nil } type nativeSpecEditor struct { @@ -73,28 +78,19 @@ func createEnvCacheMap(env []string) map[string]int { return envMap } -func (e *nativeSpecEditor) initConfig() { - if e.spec == nil { - e.spec = &oci.Spec{} - } -} - func (e *nativeSpecEditor) initProcess() { - e.initConfig() if e.spec.Process == nil { e.spec.Process = &oci.Process{} } } func (e *nativeSpecEditor) initHooks() { - e.initConfig() if e.spec.Hooks == nil { e.spec.Hooks = &oci.Hooks{} } } func (e *nativeSpecEditor) initLinux() { - e.initConfig() if e.spec.Linux == nil { e.spec.Linux = &oci.Linux{} } @@ -118,8 +114,8 @@ func (e *nativeSpecEditor) AddMultipleProcessEnv(envs []string) { e.initProcess() for _, val := range envs { - split := strings.SplitN(val, "=", 2) - e.addEnv(val, split[0]) + key, _, _ := strings.Cut(val, "=") + e.addEnv(val, key) } } @@ -180,8 +176,6 @@ func (e *nativeSpecEditor) SetLinuxNetDevice(hostIf string, netDev *oci.LinuxNet } func (e *nativeSpecEditor) RemoveMount(dest string) { - e.initConfig() - for i, mount := range e.spec.Mounts { if mount.Destination == dest { e.spec.Mounts = append(e.spec.Mounts[:i], e.spec.Mounts[i+1:]...) @@ -191,17 +185,14 @@ func (e *nativeSpecEditor) RemoveMount(dest string) { } func (e *nativeSpecEditor) AddMount(mnt oci.Mount) { - e.initConfig() e.spec.Mounts = append(e.spec.Mounts, mnt) } func (e *nativeSpecEditor) Mounts() []oci.Mount { - e.initConfig() return e.spec.Mounts } func (e *nativeSpecEditor) SetMounts(mounts []oci.Mount) { - e.initConfig() e.spec.Mounts = mounts } diff --git a/internal/ociedit/spec_editor_test.go b/internal/ociedit/spec_editor_test.go new file mode 100644 index 00000000..a9134662 --- /dev/null +++ b/internal/ociedit/spec_editor_test.go @@ -0,0 +1,30 @@ +/* + Copyright 2026 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 ociedit + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestNewSpecEditorWithNilSpecReturnsError(t *testing.T) { + editor, err := NewSpecEditor(nil) + + require.Nil(t, editor) + require.EqualError(t, err, "can't edit nil OCI Spec") +} diff --git a/pkg/cdi/container-edits.go b/pkg/cdi/container-edits.go index 3f368aa8..1971e1f1 100644 --- a/pkg/cdi/container-edits.go +++ b/pkg/cdi/container-edits.go @@ -80,7 +80,10 @@ func (e *ContainerEdits) Apply(spec *oci.Spec) error { return nil } - editor := ociedit.NewSpecEditor(spec) + editor, err := ociedit.NewSpecEditor(spec) + if err != nil { + return fmt.Errorf("error creating spec editor: %w", err) + } if len(e.Env) > 0 { editor.AddMultipleProcessEnv(e.Env) }