Skip to content

Commit fb0aac1

Browse files
committed
[index] Add OCI index detection for Nydus alternatives
Implement index detection feature that automatically discovers Nydus alternative manifests within OCI index manifests, similar to what SOCI snashotter is able to do. This enables transparent fallback to optimized Nydus images when available while keeping the original index manifest OCI compliant as non-nydus client can simply pull the regular OCI image. The feature is heavily based on the already existing ReferrerDetect feature which looks for nydus manifests using the referrer API. However, the referrer API is not supported by all registries and as it relies on changes happening outside the manifest being pulled it's a bit dangerous from a supply chain perspective. On the other hand, this new IndexDetect feature is supported everywhere and does not have the supply chain issue as everyting is packed in a single manifest. Building a manifest compatible with this feature can be easily done with the `--merge-platform` flag in `nydusify convert`. Key changes: - Add EnableIndexDetect config option to experimental features - Implement index package with manifest finding logic. Very similar to referrer package. The detection is based on the `platform.os.feature` (nydus.remoteimage.v1) or the `artifactType` (application/vnd.nydus.image.manifest.v1+json) field in the index manifest - Result is cached to avoid multiple API calls for the same digest - Integrate index detection into filesystem and snapshot layers. The index detection is done before the referrer detection: if both exist, we prefer to use the index detection for the supply chain issues mentionned above. - Add e2e test for index detection - Add documentation for the new feature
1 parent 98b5286 commit fb0aac1

File tree

19 files changed

+864
-19
lines changed

19 files changed

+864
-19
lines changed

.github/workflows/k8s-e2e-run.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,9 @@ jobs:
1919
uses: ./.github/workflows/k8s-e2e.yml
2020
with:
2121
auth-type: kubeconf
22+
23+
index_detect:
24+
uses: ./.github/workflows/k8s-e2e.yml
25+
with:
26+
auth-type: kubeconf
27+
index-detect: true

.github/workflows/k8s-e2e.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ on:
66
auth-type:
77
required: true
88
type: string
9+
index-detect:
10+
required: false
11+
type: boolean
912

1013
env:
1114
DOCKER_USER: testuser
@@ -28,7 +31,8 @@ jobs:
2831
cache-dependency-path: "go.sum"
2932
- name: Test
3033
run: |
31-
AUTH_TYPE='${{ inputs.auth-type }}'
34+
export AUTH_TYPE='${{ inputs.auth-type }}'
35+
export INDEX_DETECT='${{ inputs.index-detect }}'
3236
./tests/helpers/kind.sh
3337
- name: Dump logs
3438
if: failure()

config/config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ const (
120120
type Experimental struct {
121121
EnableStargz bool `toml:"enable_stargz"`
122122
EnableReferrerDetect bool `toml:"enable_referrer_detect"`
123+
EnableIndexDetect bool `toml:"enable_index_detect"`
123124
TarfsConfig TarfsConfig `toml:"tarfs"`
124125
EnableBackendSource bool `toml:"enable_backend_source"`
125126
}

config/config_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ func TestLoadSnapshotterTOMLConfig(t *testing.T) {
3030
Experimental: Experimental{
3131
EnableStargz: false,
3232
EnableReferrerDetect: false,
33+
EnableIndexDetect: false,
3334
},
3435
CleanupOnClose: false,
3536
SystemControllerConfig: SystemControllerConfig{

docs/index_detection.md

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# Index Detection
2+
3+
## Overview
4+
5+
Index Detection is a feature that automatically discovers Nydus alternative manifests within OCI index manifests. This enables transparent fallback to optimized Nydus images when available while keeping the original index manifest OCI compliant, allowing non-Nydus clients to pull regular OCI images.
6+
7+
## Motivation
8+
9+
The Index Detection feature addresses several limitations of the existing Referrer API-based detection:
10+
11+
- **Registry Support**: Not all registries support the Referrer API
12+
- **Supply Chain Security**: Referrer API relies on external changes to the manifest, creating potential supply chain risks
13+
- **Universal Compatibility**: Index detection works with all OCI-compliant registries
14+
15+
Unlike referrer-based detection, Index Detection packages everything into a single manifest, eliminating supply chain concerns while maintaining universal registry support.
16+
17+
## How It Works
18+
19+
### Detection Process
20+
21+
1. **Index Manifest Parsing**: When a manifest digest is encountered, the system fetches and parses the original OCI index manifest
22+
2. **Platform Matching**: The system finds the original manifest descriptor within the index
23+
3. **Nydus Alternative Search**: It searches for platform-compatible manifests that contain:
24+
- `platform.os.features` containing `nydus.remoteimage.v1`, or
25+
- `artifactType` set to `application/vnd.nydus.image.manifest.v1+json`
26+
4. **Validation**: The found manifest is validated to ensure it's a valid Nydus manifest with metadata layers
27+
5. **Caching**: Results are cached to avoid repeated API calls for the same digest
28+
29+
Both `platform.os.features` and `artifactType` are looked at because index manifests built using `merge-platform` before acceleration-service: v0.2.19 or nydus: v2.3.5 will have `platform.os.features` configured while images built after will have `artifactType` configured.
30+
31+
### Detection Priority
32+
33+
When both Index Detection and Referrer Detection are available, Index Detection takes priority due to its superior supply chain security properties.
34+
35+
## Configuration
36+
37+
Index Detection is controlled by the `EnableIndexDetect` configuration option in the experimental features section:
38+
39+
```toml
40+
[experimental]
41+
enable_index_detect = true
42+
```
43+
44+
## Building Compatible Images
45+
46+
To create images compatible with Index Detection, use the `nydusify convert` command with the `--merge-platform` flag:
47+
48+
```bash
49+
nydusify convert --merge-platform <source-image> <target-image>
50+
```
51+
52+
This creates an OCI index manifest containing both the original image and the Nydus alternative:
53+
54+
```json
55+
{
56+
"schemaVersion": 2,
57+
"manifests": [
58+
{
59+
"mediaType": "application/vnd.oci.image.manifest.v1+json",
60+
"digest": "sha256:a63dfddecc661e4ad05896418b2f3774022269c3bf5b7e01baaa6e851a3a4a23",
61+
"size": 2320,
62+
"platform": {
63+
"architecture": "amd64",
64+
"os": "linux"
65+
}
66+
},
67+
{
68+
"mediaType": "application/vnd.oci.image.manifest.v1+json",
69+
"digest": "sha256:bb82dd8ee111bfe5fdf12145b6ed553da9c15de17f54b1f658d95ff26a65a01c",
70+
"size": 3229,
71+
"platform": {
72+
"architecture": "amd64",
73+
"os": "linux"
74+
},
75+
"artifactType": "application/vnd.nydus.image.manifest.v1+json"
76+
}
77+
]
78+
}
79+
```
80+
81+
## Comparison with Referrer Detection
82+
83+
| Aspect | Index Detection | Referrer Detection |
84+
|--------|----------------|-------------------|
85+
| Registry Support | Universal | Limited |
86+
| Supply Chain Security | Secure (single manifest) | Potential risks (external refs) |
87+
| OCI Compliance | Full compliance | Requires Referrer API |
88+
| Cache Behavior | Immutable (digest-based) | May require invalidation |
89+
| Detection Priority | Higher | Lower |

misc/snapshotter/config.toml

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ version = 1
33
root = "/var/lib/containerd/io.containerd.snapshotter.v1.nydus"
44
# The snapshotter's GRPC server socket, containerd will connect to plugin on this socket
55
address = "/run/containerd-nydus/containerd-nydus-grpc.sock"
6-
# The nydus daemon mode can be one of the following options: multiple, dedicated, shared, or none.
6+
# The nydus daemon mode can be one of the following options: multiple, dedicated, shared, or none.
77
# If `daemon_mode` option is not specified, the default value is multiple.
88
daemon_mode = "dedicated"
99
# Whether snapshotter should try to clean up resources when it is closed
@@ -27,7 +27,7 @@ pprof_address = ""
2727
nydusd_config = "/etc/nydus/nydusd-config.fusedev.json"
2828
nydusd_path = "/usr/local/bin/nydusd"
2929
nydusimage_path = "/usr/local/bin/nydus-image"
30-
# The fs driver can be one of the following options: fusedev, fscache, blockdev, proxy, or nodev.
30+
# The fs driver can be one of the following options: fusedev, fscache, blockdev, proxy, or nodev.
3131
# If `fs_driver` option is not specified, the default value is fusedev.
3232
fs_driver = "fusedev"
3333
# How to process when daemon dies: "none", "restart" or "failover"
@@ -112,6 +112,10 @@ enable_stargz = false
112112
# The option enables trying to fetch the Nydus image associated with the OCI image and run it.
113113
# Also see https://github.com/opencontainers/distribution-spec/blob/main/spec.md#listing-referrers
114114
enable_referrer_detect = false
115+
# Whether to enable index detection support
116+
# The option enables trying to fetch the Nydus image present in the same OCI index as the original OCI image and run it.
117+
# Also see https://github.com/containerd/nydus-snapshotter/blob/main/docs/index-detection.md
118+
enable_index_detect = false
115119
# Whether to enable authentication support
116120
# The option enables nydus snapshot to provide backend information to nydusd.
117121
enable_backend_source = false
@@ -134,4 +138,4 @@ max_concurrent_proc = 0
134138
# - "image_block": generate a raw block disk image with tarfs for an image
135139
# - "layer_block_with_verity": generate a raw block disk image with tarfs for a layer with dm-verity info
136140
# - "image_block_with_verity": generate a raw block disk image with tarfs for an image with dm-verity info
137-
export_mode = ""
141+
export_mode = ""

pkg/converter/constant.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@
77
package converter
88

99
const (
10-
ManifestOSFeatureNydus = "nydus.remoteimage.v1"
11-
MediaTypeNydusBlob = "application/vnd.oci.image.layer.nydus.blob.v1"
12-
BootstrapFileNameInLayer = "image/image.boot"
10+
ManifestOSFeatureNydus = "nydus.remoteimage.v1"
11+
ManifestArtifactTypeNydus = "application/vnd.nydus.image.manifest.v1+json"
12+
MediaTypeNydusBlob = "application/vnd.oci.image.layer.nydus.blob.v1"
13+
BootstrapFileNameInLayer = "image/image.boot"
1314

1415
ManifestNydusCache = "containerd.io/snapshot/nydus-cache"
1516

pkg/filesystem/config.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ package filesystem
99

1010
import (
1111
"github.com/containerd/nydus-snapshotter/pkg/cache"
12+
"github.com/containerd/nydus-snapshotter/pkg/index"
1213
"github.com/containerd/nydus-snapshotter/pkg/manager"
1314
"github.com/containerd/nydus-snapshotter/pkg/referrer"
1415
"github.com/containerd/nydus-snapshotter/pkg/signature"
@@ -60,6 +61,17 @@ func WithReferrerManager(rm *referrer.Manager) NewFSOpt {
6061
}
6162
}
6263

64+
func WithIndexManager(im *index.Manager) NewFSOpt {
65+
return func(fs *Filesystem) error {
66+
if im == nil {
67+
return errors.New("index manager cannot be nil")
68+
}
69+
70+
fs.indexMgr = im
71+
return nil
72+
}
73+
}
74+
6375
func WithTarfsManager(tm *tarfs.Manager) NewFSOpt {
6476
return func(fs *Filesystem) error {
6577
if tm == nil {

pkg/filesystem/fs.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,22 +15,22 @@ import (
1515
"os"
1616
"path"
1717

18+
"github.com/containerd/containerd/v2/core/snapshots"
19+
"github.com/containerd/containerd/v2/core/snapshots/storage"
1820
snpkg "github.com/containerd/containerd/v2/pkg/snapshotters"
21+
"github.com/containerd/log"
1922
"github.com/mohae/deepcopy"
2023
"github.com/opencontainers/go-digest"
2124
"github.com/pkg/errors"
2225
"golang.org/x/sync/errgroup"
2326

24-
"github.com/containerd/containerd/v2/core/snapshots"
25-
"github.com/containerd/containerd/v2/core/snapshots/storage"
26-
"github.com/containerd/log"
27-
2827
"github.com/containerd/nydus-snapshotter/config"
2928
"github.com/containerd/nydus-snapshotter/config/daemonconfig"
3029
"github.com/containerd/nydus-snapshotter/pkg/cache"
3130
"github.com/containerd/nydus-snapshotter/pkg/daemon"
3231
"github.com/containerd/nydus-snapshotter/pkg/daemon/types"
3332
"github.com/containerd/nydus-snapshotter/pkg/errdefs"
33+
"github.com/containerd/nydus-snapshotter/pkg/index"
3434
"github.com/containerd/nydus-snapshotter/pkg/label"
3535
"github.com/containerd/nydus-snapshotter/pkg/manager"
3636
racache "github.com/containerd/nydus-snapshotter/pkg/rafs"
@@ -46,6 +46,7 @@ type Filesystem struct {
4646
enabledManagers map[string]*manager.Manager
4747
cacheMgr *cache.Manager
4848
referrerMgr *referrer.Manager
49+
indexMgr *index.Manager
4950
stargzResolver *stargz.Resolver
5051
tarfsMgr *tarfs.Manager
5152
verifier *signature.Verifier

pkg/filesystem/index_adaptor.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
* Copyright (c) 2025. Nydus Developers. All rights reserved.
3+
*
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
package filesystem
8+
9+
import (
10+
"context"
11+
"fmt"
12+
13+
snpkg "github.com/containerd/containerd/v2/pkg/snapshotters"
14+
"github.com/containerd/log"
15+
"github.com/opencontainers/go-digest"
16+
"github.com/pkg/errors"
17+
)
18+
19+
func (fs *Filesystem) IndexDetectEnabled() bool {
20+
return fs.indexMgr != nil
21+
}
22+
23+
// CheckIndexAlternative attempts to find a nydus alternative manifest in the original OCI index manifest
24+
func (fs *Filesystem) CheckIndexAlternative(ctx context.Context, labels map[string]string) bool {
25+
if !fs.IndexDetectEnabled() {
26+
return false
27+
}
28+
29+
ref, ok := labels[snpkg.TargetRefLabel]
30+
if !ok {
31+
return false
32+
}
33+
34+
manifestDigest := digest.Digest(labels[snpkg.TargetManifestDigestLabel])
35+
if manifestDigest.Validate() != nil {
36+
return false
37+
}
38+
39+
log.G(ctx).WithField("ref", ref).WithField("digest", manifestDigest.String()).Debug("attempting index-based nydus detection")
40+
if _, err := fs.indexMgr.CheckIndexAlternative(ctx, ref, manifestDigest); err != nil {
41+
return false
42+
}
43+
44+
return true
45+
}
46+
47+
// TryFetchMetadataFromIndex attempts to fetch metadata from the nydus index alternative
48+
func (fs *Filesystem) TryFetchMetadataFromIndex(ctx context.Context, labels map[string]string, metadataPath string) error {
49+
ref, ok := labels[snpkg.TargetRefLabel]
50+
if !ok {
51+
return fmt.Errorf("empty label %s", snpkg.TargetRefLabel)
52+
}
53+
54+
manifestDigest := digest.Digest(labels[snpkg.TargetManifestDigestLabel])
55+
if err := manifestDigest.Validate(); err != nil {
56+
return fmt.Errorf("invalid label %s=%s", snpkg.TargetManifestDigestLabel, manifestDigest)
57+
}
58+
59+
if err := fs.indexMgr.TryFetchMetadata(ctx, ref, manifestDigest, metadataPath); err != nil {
60+
return errors.Wrap(err, "try fetch metadata")
61+
}
62+
63+
return nil
64+
}

0 commit comments

Comments
 (0)