Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ require (
github.com/cavaliergopher/cpio v1.0.1
github.com/containerd/containerd v1.7.30
github.com/containerd/containerd/api v1.10.0
github.com/containerd/platforms v0.2.1
github.com/containerd/ttrpc v1.2.7
github.com/creack/pty v1.1.24
github.com/elastic/go-seccomp-bpf v1.6.0
Expand All @@ -18,6 +19,7 @@ require (
github.com/nubificus/hedge_cli v0.0.3
github.com/onsi/ginkgo/v2 v2.28.1
github.com/onsi/gomega v1.39.1
github.com/opencontainers/image-spec v1.1.1
github.com/opencontainers/runc v1.3.4
github.com/opencontainers/runtime-spec v1.2.1
github.com/prometheus-community/pro-bing v0.8.0
Expand Down Expand Up @@ -45,7 +47,6 @@ require (
github.com/containerd/fifo v1.1.0 // indirect
github.com/containerd/go-runc v1.0.0 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/containerd/platforms v0.2.1 // indirect
github.com/containerd/typeurl/v2 v2.2.3 // indirect
github.com/coreos/go-systemd/v22 v22.7.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
Expand All @@ -67,7 +68,6 @@ require (
github.com/moby/sys/sequential v0.6.0 // indirect
github.com/moby/sys/user v0.4.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/stretchr/objx v0.5.3 // indirect
Expand Down
153 changes: 153 additions & 0 deletions pkg/containerd-shim/containerd/inject_missing_annotations.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
// Copyright (c) 2023-2026, Nubificus LTD
//
// 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.

// Temporary containerd helpers for urunc #565 (read urunc keys from image metadata).
package containerd

import (
"context"
"encoding/json"
"fmt"
"io"
"strings"

contentapi "github.com/containerd/containerd/api/services/content/v1"
imagesapi "github.com/containerd/containerd/api/services/images/v1"
"github.com/containerd/containerd/api/types"
"github.com/containerd/containerd/images"
"github.com/containerd/platforms"
imageSpec "github.com/opencontainers/image-spec/specs-go/v1"
)

// ImageAnnotations returns image config labels and manifest annotations with
// the provided prefix. Manifest annotations take precedence over config labels.
func (s *Session) ImageAnnotations(ctx context.Context, prefix string) (map[string]string, error) {
imageRef := s.container.GetImage()
if imageRef == "" {
return nil, fmt.Errorf("container %q has empty image ref", s.containerID)
}

imageResp, err := s.imagesClient().Get(withNamespace(ctx, s.namespace), &imagesapi.GetImageRequest{Name: imageRef})
if err != nil {
return nil, fmt.Errorf("get image %s: %w", imageRef, containerdErr(err))
}

return s.imageAnnotations(ctx, imageResp.Image.Target, prefix)
}

func (s *Session) imageAnnotations(ctx context.Context, target *types.Descriptor, prefix string) (map[string]string, error) {
ctx = withNamespace(ctx, s.namespace)
contentClient := s.contentClient()

manifestDesc, err := manifestDescriptor(ctx, contentClient, target)
if err != nil {
return nil, err
}

manifestRaw, err := readBlob(ctx, contentClient, manifestDesc.Digest, manifestDesc.Size)
if err != nil {
return nil, fmt.Errorf("read manifest blob: %w", err)
}
var manifest imageSpec.Manifest
if err := json.Unmarshal(manifestRaw, &manifest); err != nil {
return nil, fmt.Errorf("unmarshal manifest: %w", err)
}

configRaw, err := readBlob(ctx, contentClient, manifest.Config.Digest.String(), manifest.Config.Size)
if err != nil {
return nil, fmt.Errorf("read image config blob: %w", err)
}
var imageConfig imageSpec.Image
if err := json.Unmarshal(configRaw, &imageConfig); err != nil {
return nil, fmt.Errorf("unmarshal image config: %w", err)
}

annotations := make(map[string]string)
for key, value := range imageConfig.Config.Labels {
if strings.HasPrefix(key, prefix) {
annotations[key] = value
}
}
for key, value := range manifest.Annotations {
if strings.HasPrefix(key, prefix) {
annotations[key] = value
}
}

return annotations, nil
}

func manifestDescriptor(
ctx context.Context,
contentClient contentapi.ContentClient,
target *types.Descriptor,
) (*types.Descriptor, error) {
if images.IsManifestType(target.MediaType) {
return target, nil
}

if !images.IsIndexType(target.MediaType) {
return nil, fmt.Errorf("unsupported image target media type: %s", target.MediaType)
}

indexRaw, err := readBlob(ctx, contentClient, target.Digest, target.Size)
if err != nil {
return nil, fmt.Errorf("read image index blob: %w", err)
}

var index imageSpec.Index
if err := json.Unmarshal(indexRaw, &index); err != nil {
return nil, fmt.Errorf("unmarshal image index: %w", err)
}

matcher := platforms.DefaultStrict()
for _, manifest := range index.Manifests {
if manifest.Platform == nil {
continue
}
if matcher.Match(*manifest.Platform) {
return &types.Descriptor{
MediaType: manifest.MediaType,
Digest: manifest.Digest.String(),
Size: manifest.Size,
}, nil
}
}

return nil, fmt.Errorf("no matching manifest found in image index for platform %s", platforms.Format(platforms.DefaultSpec()))
}

func readBlob(ctx context.Context, contentClient contentapi.ContentClient, digest string, size int64) ([]byte, error) {
stream, err := contentClient.Read(ctx, &contentapi.ReadContentRequest{
Digest: digest,
Size: size,
})
if err != nil {
return nil, containerdErr(err)
}

var raw []byte
for {
resp, err := stream.Recv()
if err == io.EOF {
break
}
if err != nil {
return nil, containerdErr(err)
}
raw = append(raw, resp.Data...)
}

return raw, nil
}
127 changes: 127 additions & 0 deletions pkg/containerd-shim/guest_rootfs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// Copyright (c) 2023-2026, Nubificus LTD
//
// 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 containerdshim

import (
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"

taskAPI "github.com/containerd/containerd/api/runtime/task/v2"
containerdTypes "github.com/containerd/containerd/api/types"
specs "github.com/opencontainers/runtime-spec/specs-go"
"github.com/sirupsen/logrus"
"github.com/urunc-dev/urunc/pkg/unikontainers"
)

var errGuestRootfsChoiceSkipped = errors.New("guest rootfs choice skipped")

// chooseGuestRootfs selects guest rootfs parameters before inner task Create and
// persists them in the bundle OCI spec for runtime Exec to consume.
func chooseGuestRootfs(r *taskAPI.CreateTaskRequest) error {
spec, mode, err := loadSpec(r.Bundle)
if err != nil {
return err
}
log := logrus.WithFields(logrus.Fields{
"container_id": r.ID,
"bundle": filepath.Clean(r.Bundle),
})

config, err := unikontainers.GetUnikernelConfigFromSpecAnnotations(spec)
if err != nil {
return errGuestRootfsChoiceSkipped
}

annotations := config.Map()
uruncCfg, cfgErr := unikontainers.LoadUruncConfig(unikontainers.UruncConfigPath)
if cfgErr != nil {
log.WithError(cfgErr).Warn("urunc shim: failed to load urunc config; using defaults for guest rootfs choice")
}

rootfsParams, err := unikontainers.ChooseRootfs(filepath.Clean(r.Bundle), spec.Root.Path, annotations, uruncCfg, rootfsMountsFromCreateTask(r.Rootfs))
if err != nil {
return err
}

encoded, err := unikontainers.EncodeRootfsParams(rootfsParams)
if err != nil {
return err
}
if spec.Annotations == nil {
spec.Annotations = make(map[string]string)
}
spec.Annotations[unikontainers.RootfsParamsAnnotation()] = encoded
log.WithFields(logrus.Fields{
"rootfs_type": rootfsParams.Type,
"rootfs_path": rootfsParams.Path,
"mon_rootfs": rootfsParams.MonRootfs,
}).Info("urunc shim: wrote guest rootfs choice to bundle")

return saveSpec(r.Bundle, spec, mode)
}

func rootfsMountsFromCreateTask(rootfs []*containerdTypes.Mount) []unikontainers.RootfsMount {
mounts := make([]unikontainers.RootfsMount, 0, len(rootfs))
for _, m := range rootfs {
if m == nil {
continue
}
mounts = append(mounts, unikontainers.RootfsMount{
Type: m.Type,
Source: m.Source,
})
}
return mounts
}

// loadSpec reads the OCI runtime spec (config.json) from the task bundle at CreateTask time.
// Callers need the full spec on disk (root path, annotations read/write); the CreateTask RPC does
// not include the OCI document. injectMissingAnnotations runs before chooseGuestRootfs
// in taskService.Create.
func loadSpec(bundle string) (*specs.Spec, os.FileMode, error) {
configPath := filepath.Join(bundle, "config.json")
info, err := os.Stat(configPath)
if err != nil {
return nil, 0, fmt.Errorf("stat config.json: %w", err)
}

data, err := os.ReadFile(configPath)
if err != nil {
return nil, 0, fmt.Errorf("read config.json: %w", err)
}

var spec specs.Spec
if err := json.Unmarshal(data, &spec); err != nil {
return nil, 0, fmt.Errorf("unmarshal config.json: %w", err)
}
if spec.Root == nil {
return nil, 0, fmt.Errorf("invalid OCI spec: root section is required")
}

return &spec, info.Mode(), nil
}

func saveSpec(bundle string, spec *specs.Spec, mode os.FileMode) error {
data, err := json.MarshalIndent(spec, "", " ")
if err != nil {
return fmt.Errorf("marshal config.json: %w", err)
}

configPath := filepath.Join(bundle, "config.json")
return os.WriteFile(configPath, data, mode)
}
81 changes: 81 additions & 0 deletions pkg/containerd-shim/inject_missing_annotations.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// Copyright (c) 2023-2026, Nubificus LTD
//
// 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.

// Temporary shim-side copy of urunc #565 (inject missing image annotations into
// bundle config.json). Keep this file self-contained so it can be dropped or
// reconciled when #565 merges.
package containerdshim

import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"os"
"path/filepath"

taskAPI "github.com/containerd/containerd/api/runtime/task/v2"
specs "github.com/opencontainers/runtime-spec/specs-go"
shimcontainerd "github.com/urunc-dev/urunc/pkg/containerd-shim/containerd"
)

const uruncAnnotationPrefix = "com.urunc.unikernel."

func (s *taskService) injectMissingAnnotations(ctx context.Context, r *taskAPI.CreateTaskRequest, session *shimcontainerd.Session) error {
configPath := filepath.Join(r.Bundle, "config.json")
info, err := os.Stat(configPath)
if err != nil {
return fmt.Errorf("stat config.json: %w", err)
}

data, err := os.ReadFile(configPath)
if err != nil {
return fmt.Errorf("read config.json: %w", err)
}

var spec specs.Spec
if err := json.Unmarshal(data, &spec); err != nil {
return fmt.Errorf("unmarshal config.json: %w", err)
}
if spec.Annotations == nil {
spec.Annotations = make(map[string]string)
}

imageAnnots, err := session.ImageAnnotations(ctx, uruncAnnotationPrefix)
if err != nil {
return err
}

changed := false
for key, value := range imageAnnots {
if _, ok := spec.Annotations[key]; ok {
continue
}
spec.Annotations[key] = base64.StdEncoding.EncodeToString([]byte(value))
changed = true
}
if !changed {
return nil
}

out, err := json.MarshalIndent(&spec, "", " ")
if err != nil {
return fmt.Errorf("marshal config.json: %w", err)
}
if err := os.WriteFile(configPath, out, info.Mode()); err != nil {
return fmt.Errorf("write config.json: %w", err)
}

return nil
}
Loading