已合并
docs: update template store semantics #179
zhaoyifan28创建于 29 天前
docs: update template store semantics #179
已合并
共 18 个文件变更+1047-915
| @@ -24,9 +24,10 @@ its identity and storage key are its immutable Boot Index digest. | |||
| 24 | | `SourceSandboxID` | `string` | Identifies the Sandbox used to produce the Template. | Optional. | | 24 | | `SourceSandboxID` | `string` | Identifies the Sandbox used to produce the Template. | Optional. | |
| 25 | | `SourceRef` | `string` | Records the registry reference supplied to `pull`. | Optional. | | 25 | | `SourceRef` | `string` | Records the registry reference supplied to `pull`. | Optional. | |
| 26 | | `Labels` | `map[string]string` | Stores caller-defined metadata as key-value pairs. | Optional. | | 26 | | `Labels` | `map[string]string` | Stores caller-defined metadata as key-value pairs. | Optional. | |
| 27 | -| `CreatedAt` | `int64` | Records when the Template record was created. | Unix nanoseconds; assigned by `Create` when zero. | | 27 | +| `CreatedAt` | `int64` | Records when the Template record was created. | Unix nanoseconds; derived from the canonical containerd image record. | |
| 28 | 28 | ||
| 29 | -The corresponding containerd image record is also derived from the digest: | 29 | +Template metadata is stored as labels on a canonical containerd image record |
| 30 | +whose name is derived from the digest: | ||
| 30 | `localhost/conch/template:<algorithm>-<encoded-digest>`. | 31 | `localhost/conch/template:<algorithm>-<encoded-digest>`. |
| 31 | 32 | ||
| 32 | #### Origin | 33 | #### Origin |
| @@ -64,16 +65,18 @@ The corresponding containerd image record is also derived from the digest: | |||
| 64 | ### Create | 65 | ### Create |
| 65 | 66 | ||
| 66 | ```go | 67 | ```go |
| 67 | -Create(ctx context.Context, entry Entry) (Entry, error) | 68 | +Create(ctx context.Context, entry Entry, target ocispec.Descriptor) (Entry, error) |
| 68 | ``` | 69 | ``` |
| 69 | 70 | ||
| 70 | -Creates a new Template record after validating the `Entry` constraints above. | 71 | +Creates a new Template record after validating the `Entry` constraints above |
| 71 | -A zero `CreatedAt` is set to the current Unix time in nanoseconds. | 72 | +and confirming that `target` is a valid Boot Index whose digest and boot mode |
| 73 | +match the `Entry`. | ||
| 72 | 74 | ||
| 73 | -The record is inserted atomically using `BootIndexDigest` as the key. An | 75 | +The store records the Boot Index child references for containerd GC and inserts |
| 74 | -existing record is never overwritten; a duplicate digest returns | 76 | +the digest-derived canonical image record atomically. An existing record is |
| 75 | -`ErrAlreadyExists`. On success, `Create` returns the normalized record that was | 77 | +never overwritten; a duplicate digest returns `ErrAlreadyExists`. On success, |
| 76 | -stored. | 78 | +`Create` returns the normalized record with `CreatedAt` derived from the image |
| 79 | +record. | ||
| 77 | 80 | ||
| 78 | ### Get | 81 | ### Get |
| 79 | 82 | ||
| @@ -105,10 +108,11 @@ Every returned `Entry` is normalized using the same field rules as `Create`. | |||
| 105 | Delete(ctx context.Context, bootIndexDigest string) error | 108 | Delete(ctx context.Context, bootIndexDigest string) error |
| 106 | ``` | 109 | ``` |
| 107 | 110 | ||
| 108 | -Deletes the Template record identified by `bootIndexDigest`. The operation is | 111 | +Deletes the digest-derived canonical image record identified by |
| 109 | -idempotent: deleting an unknown digest succeeds without changing stored data. | 112 | +`bootIndexDigest`. The operation is idempotent: deleting an unknown digest |
| 113 | +succeeds without changing stored data. The delete is conditional on the record | ||
| 114 | +still targeting the requested digest. | ||
| 110 | 115 | ||
| 111 | `Delete` returns an error when the storage operation cannot be completed. | 116 | `Delete` returns an error when the storage operation cannot be completed. |
| 112 | -The runtime service additionally removes the digest-derived canonical | 117 | +Containerd GC decides when content that is no longer referenced by an image |
| 113 | -containerd image record; containerd GC then decides when unreferenced content | 118 | +record or another GC root is reclaimed. |
| 114 | -is reclaimed. | ||
| @@ -45,7 +45,7 @@ $ conch template rm sha256:1111... | |||
| 45 | Removed template: sha256:1111... | 45 | Removed template: sha256:1111... |
| 46 | ``` | 46 | ``` |
| 47 | 47 | ||
| 48 | -删除时会移除 Template metadata 和对应的 canonical image record;实际 content 由 containerd GC 在不再被其他记录引用后回收。 | 48 | +删除时会移除以 labels 承载 Template metadata 的 canonical image record;实际 content 由 containerd GC 在不再被其他记录引用后回收。 |
| 49 | 49 | ||
| 50 | ## 2. Template 分发 | 50 | ## 2. Template 分发 |
| 51 | 51 | ||
| @@ -20,8 +20,8 @@ import ( | |||
| 20 | "github.com/containerd/plugin/registry" | 20 | "github.com/containerd/plugin/registry" |
| 21 | 21 | ||
| 22 | containerdclient "github.com/openeuler/Conch/internal/adapters/containerd/client" | 22 | containerdclient "github.com/openeuler/Conch/internal/adapters/containerd/client" |
| 23 | + containerdtemplate "github.com/openeuler/Conch/internal/adapters/containerd/template" | ||
| 23 | "github.com/openeuler/Conch/internal/cleanupdiag" | 24 | "github.com/openeuler/Conch/internal/cleanupdiag" |
| 24 | - "github.com/openeuler/Conch/internal/daemon/state" | ||
| 25 | conchsandbox "github.com/openeuler/Conch/internal/sandbox" | 25 | conchsandbox "github.com/openeuler/Conch/internal/sandbox" |
| 26 | conchsnapshot "github.com/openeuler/Conch/internal/snapshot" | 26 | conchsnapshot "github.com/openeuler/Conch/internal/snapshot" |
| 27 | conchtemplate "github.com/openeuler/Conch/internal/template" | 27 | conchtemplate "github.com/openeuler/Conch/internal/template" |
| @@ -36,11 +36,10 @@ const ( | |||
| 36 | ) | 36 | ) |
| 37 | 37 | ||
| 38 | type Config struct { | 38 | type Config struct { |
| 39 | - RootDir string | 39 | + RootDir string |
| 40 | - StateDir string | 40 | + StateDir string |
| 41 | - Snapshot SnapshotConfig | 41 | + Snapshot SnapshotConfig |
| 42 | - TemplateStore state.Store | 42 | + Sandbox *conchsandbox.Config |
| 43 | - Sandbox *conchsandbox.Config | ||
| 44 | } | 43 | } |
| 45 | 44 | ||
| 46 | type SnapshotConfig struct { | 45 | type SnapshotConfig struct { |
| @@ -123,9 +122,6 @@ func Start(ctx context.Context, cfg Config) (*Host, error) { | |||
| 123 | if cfg.StateDir == "" { | 122 | if cfg.StateDir == "" { |
| 124 | return nil, errors.New("containerd state dir is required") | 123 | return nil, errors.New("containerd state dir is required") |
| 125 | } | 124 | } |
| 126 | - if cfg.Sandbox != nil && cfg.TemplateStore == nil { | ||
| 127 | - return nil, errors.New("template store is required when sandbox manager is enabled") | ||
| 128 | - } | ||
| 129 | if err := os.MkdirAll(cfg.RootDir, 0o700); err != nil { | 125 | if err := os.MkdirAll(cfg.RootDir, 0o700); err != nil { |
| 130 | return nil, fmt.Errorf("create containerd root dir: %w", err) | 126 | return nil, fmt.Errorf("create containerd root dir: %w", err) |
| 131 | } | 127 | } |
| @@ -185,9 +181,7 @@ func Start(ctx context.Context, cfg Config) (*Host, error) { | |||
| 185 | return fail("snapshot server", err) | 181 | return fail("snapshot server", err) |
| 186 | } | 182 | } |
| 187 | 183 | ||
| 188 | - if cfg.TemplateStore != nil { | 184 | + host.templateStore = containerdtemplate.NewStore(inst.client) |
| 189 | - host.templateStore = conchtemplate.NewStore(cfg.TemplateStore) | ||
| 190 | - } | ||
| 191 | 185 | ||
| 192 | if cfg.Sandbox != nil { | 186 | if cfg.Sandbox != nil { |
| 193 | host.sandboxManager, err = conchsandbox.New( | 187 | host.sandboxManager, err = conchsandbox.New( |
| @@ -0,0 +1,275 @@ | |||
| 1 | +package containerdtemplate | ||
| 2 | + | ||
| 3 | +import ( | ||
| 4 | + "context" | ||
| 5 | + "fmt" | ||
| 6 | + "strings" | ||
| 7 | + | ||
| 8 | + "github.com/containerd/containerd/v2/core/content" | ||
| 9 | + "github.com/containerd/containerd/v2/core/images" | ||
| 10 | + "github.com/containerd/containerd/v2/pkg/labels" | ||
| 11 | + "github.com/containerd/errdefs" | ||
| 12 | + "github.com/opencontainers/go-digest" | ||
| 13 | + ocispec "github.com/opencontainers/image-spec/specs-go/v1" | ||
| 14 | + | ||
| 15 | + containerdclient "github.com/openeuler/Conch/internal/adapters/containerd/client" | ||
| 16 | + conchimage "github.com/openeuler/Conch/internal/image" | ||
| 17 | + conchtemplate "github.com/openeuler/Conch/internal/template" | ||
| 18 | +) | ||
| 19 | + | ||
| 20 | +const ( | ||
| 21 | + schemaLabel = "io.conch.template.schema" | ||
| 22 | + originLabel = "io.conch.template.origin" | ||
| 23 | + parentLabel = "io.conch.template.parent" | ||
| 24 | + sourceSandboxLabel = "io.conch.template.source-sandbox" | ||
| 25 | + sourceRefLabel = "io.conch.template.source-ref" | ||
| 26 | + userLabelPrefix = "io.conch.template.user." | ||
| 27 | + schemaVersion = "1" | ||
| 28 | +) | ||
| 29 | + | ||
| 30 | +type Store struct { | ||
| 31 | + images images.Store | ||
| 32 | + content content.Store | ||
| 33 | +} | ||
| 34 | + | ||
| 35 | +func NewStore(client *containerdclient.Client) *Store { | ||
| 36 | + if client == nil || client.Client == nil { | ||
| 37 | + return &Store{} | ||
| 38 | + } | ||
| 39 | + return &Store{images: client.ImageService(), content: client.ContentStore()} | ||
| 40 | +} | ||
| 41 | + | ||
| 42 | +func (s *Store) Create(ctx context.Context, entry conchtemplate.Entry, target ocispec.Descriptor) (conchtemplate.Entry, error) { | ||
| 43 | + if err := s.configured(); err != nil { | ||
| 44 | + return conchtemplate.Entry{}, err | ||
| 45 | + } | ||
| 46 | + normalized, err := conchtemplate.NormalizeEntry(entry) | ||
| 47 | + if err != nil { | ||
| 48 | + return conchtemplate.Entry{}, err | ||
| 49 | + } | ||
| 50 | + if target.Digest.String() != normalized.BootIndexDigest { | ||
| 51 | + return conchtemplate.Entry{}, conchtemplate.ErrInvalidArtifact.Wrap(fmt.Errorf( | ||
| 52 | + "boot index target %s does not match Template digest %s", target.Digest, normalized.BootIndexDigest, | ||
| 53 | + )) | ||
| 54 | + } | ||
| 55 | + nsctx := containerdclient.NewNamespaceContext(ctx) | ||
| 56 | + info, err := conchimage.InspectBootIndexContent(nsctx, s.content, target) | ||
| 57 | + if err != nil { | ||
| 58 | + return conchtemplate.Entry{}, conchtemplate.ErrInvalidArtifact.Wrap(err) | ||
| 59 | + } | ||
| 60 | + kind, bootMode := conchimage.ImageKindBootIndexCold, conchtemplate.BootModeCold | ||
| 61 | + if info.Resume { | ||
| 62 | + kind, bootMode = conchimage.ImageKindBootIndexResume, conchtemplate.BootModeResume | ||
| 63 | + } | ||
| 64 | + if normalized.BootMode != bootMode { | ||
| 65 | + return conchtemplate.Entry{}, conchtemplate.ErrInvalidArtifact.Wrap(fmt.Errorf( | ||
| 66 | + "Template boot mode %q does not match Boot Index mode %q", normalized.BootMode, bootMode, | ||
| 67 | + )) | ||
| 68 | + } | ||
| 69 | + name, err := canonicalName(normalized.BootIndexDigest) | ||
| 70 | + if err != nil { | ||
| 71 | + return conchtemplate.Entry{}, err | ||
| 72 | + } | ||
| 73 | + recordLabels, err := encodeLabels(normalized, kind) | ||
| 74 | + if err != nil { | ||
| 75 | + return conchtemplate.Entry{}, err | ||
| 76 | + } | ||
| 77 | + labelChildren := images.SetChildrenLabels(s.content, images.ChildrenHandler(s.content)) | ||
| 78 | + if err := images.WalkNotEmpty(nsctx, labelChildren, target); err != nil { | ||
| 79 | + return conchtemplate.Entry{}, fmt.Errorf("label Template content: %w", err) | ||
| 80 | + } | ||
| 81 | + record, err := s.images.Create(nsctx, images.Image{ | ||
| 82 | + Name: name, Target: target, Labels: recordLabels, | ||
| 83 | + }) | ||
| 84 | + if err != nil { | ||
| 85 | + return conchtemplate.Entry{}, translateError("create Template image record", err) | ||
| 86 | + } | ||
| 87 | + normalized.CreatedAt = record.CreatedAt.UnixNano() | ||
| 88 | + return normalized, nil | ||
| 89 | +} | ||
| 90 | + | ||
| 91 | +func (s *Store) Get(ctx context.Context, rawDigest string) (conchtemplate.Entry, error) { | ||
| 92 | + if err := s.configured(); err != nil { | ||
| 93 | + return conchtemplate.Entry{}, err | ||
| 94 | + } | ||
| 95 | + name, err := canonicalName(rawDigest) | ||
| 96 | + if err != nil { | ||
| 97 | + return conchtemplate.Entry{}, err | ||
| 98 | + } | ||
| 99 | + nsctx := containerdclient.NewNamespaceContext(ctx) | ||
| 100 | + record, err := s.images.Get(nsctx, name) | ||
| 101 | + if err != nil { | ||
| 102 | + return conchtemplate.Entry{}, translateError("get Template image record", err) | ||
| 103 | + } | ||
| 104 | + return s.entryFromRecord(nsctx, record) | ||
| 105 | +} | ||
| 106 | + | ||
| 107 | +func (s *Store) List(ctx context.Context, filter conchtemplate.Filter) ([]conchtemplate.Entry, error) { | ||
| 108 | + if err := s.configured(); err != nil { | ||
| 109 | + return nil, err | ||
| 110 | + } | ||
| 111 | + if err := validateFilter(filter); err != nil { | ||
| 112 | + return nil, err | ||
| 113 | + } | ||
| 114 | + nsctx := containerdclient.NewNamespaceContext(ctx) | ||
| 115 | + records, err := s.images.List( | ||
| 116 | + nsctx, | ||
| 117 | + `labels."`+schemaLabel+`"==`+schemaVersion, | ||
| 118 | + ) | ||
| 119 | + if err != nil { | ||
| 120 | + return nil, translateError("list Template image records", err) | ||
| 121 | + } | ||
| 122 | + out := make([]conchtemplate.Entry, 0, len(records)) | ||
| 123 | + for _, record := range records { | ||
| 124 | + if record.Labels[schemaLabel] != schemaVersion { | ||
| 125 | + continue | ||
| 126 | + } | ||
| 127 | + entry, err := s.entryFromRecord(nsctx, record) | ||
| 128 | + if err != nil { | ||
| 129 | + return nil, err | ||
| 130 | + } | ||
| 131 | + if filter.Origin != "" && entry.Origin != filter.Origin { | ||
| 132 | + continue | ||
| 133 | + } | ||
| 134 | + if filter.BootMode != "" && entry.BootMode != filter.BootMode { | ||
| 135 | + continue | ||
| 136 | + } | ||
| 137 | + out = append(out, entry) | ||
| 138 | + } | ||
| 139 | + return out, nil | ||
| 140 | +} | ||
| 141 | + | ||
| 142 | +func (s *Store) Delete(ctx context.Context, rawDigest string) error { | ||
| 143 | + if err := s.configured(); err != nil { | ||
| 144 | + return err | ||
| 145 | + } | ||
| 146 | + expected, err := digest.Parse(strings.TrimSpace(rawDigest)) | ||
| 147 | + if err != nil { | ||
| 148 | + return conchtemplate.ErrInvalidArgument.Wrap(fmt.Errorf("invalid Template ID %q: %w", rawDigest, err)) | ||
| 149 | + } | ||
| 150 | + name, _ := conchimage.CanonicalTemplateRef(expected.String()) | ||
| 151 | + nsctx := containerdclient.NewNamespaceContext(ctx) | ||
| 152 | + record, err := s.images.Get(nsctx, name) | ||
| 153 | + if err != nil { | ||
| 154 | + if errdefs.IsNotFound(err) { | ||
| 155 | + return nil | ||
| 156 | + } | ||
| 157 | + return translateError("get Template image record", err) | ||
| 158 | + } | ||
| 159 | + if record.Labels[schemaLabel] != schemaVersion { | ||
| 160 | + return nil | ||
| 161 | + } | ||
| 162 | + if record.Target.Digest != expected { | ||
| 163 | + return conchtemplate.ErrFailedPrecondition.Wrap(fmt.Errorf( | ||
| 164 | + "canonical Template record %s targets %s, want %s", name, record.Target.Digest, expected, | ||
| 165 | + )) | ||
| 166 | + } | ||
| 167 | + if err := s.images.Delete(nsctx, name, images.DeleteTarget(&ocispec.Descriptor{Digest: expected})); err != nil && !errdefs.IsNotFound(err) { | ||
| 168 | + return translateError("delete Template image record", err) | ||
| 169 | + } | ||
| 170 | + return nil | ||
| 171 | +} | ||
| 172 | + | ||
| 173 | +func (s *Store) entryFromRecord(ctx context.Context, record images.Image) (conchtemplate.Entry, error) { | ||
| 174 | + if record.Labels[schemaLabel] != schemaVersion { | ||
| 175 | + return conchtemplate.Entry{}, conchtemplate.ErrNotFound.Wrap(fmt.Errorf("image record %s is not a Template", record.Name)) | ||
| 176 | + } | ||
| 177 | + wantName, err := canonicalName(record.Target.Digest.String()) | ||
| 178 | + if err != nil || record.Name != wantName { | ||
| 179 | + return conchtemplate.Entry{}, conchtemplate.ErrInvalidArtifact.Wrap(fmt.Errorf("invalid canonical Template record %s", record.Name)) | ||
| 180 | + } | ||
| 181 | + info, err := conchimage.InspectBootIndexContent(ctx, s.content, record.Target) | ||
| 182 | + if err != nil { | ||
| 183 | + return conchtemplate.Entry{}, conchtemplate.ErrInvalidArtifact.Wrap(err) | ||
| 184 | + } | ||
| 185 | + bootMode, wantKind := conchtemplate.BootModeCold, conchimage.ImageKindBootIndexCold | ||
| 186 | + if info.Resume { | ||
| 187 | + bootMode, wantKind = conchtemplate.BootModeResume, conchimage.ImageKindBootIndexResume | ||
| 188 | + } | ||
| 189 | + if record.Labels[conchimage.ImageKindLabel] != wantKind { | ||
| 190 | + return conchtemplate.Entry{}, conchtemplate.ErrInvalidArtifact.Wrap(fmt.Errorf("Template image kind does not match Boot Index")) | ||
| 191 | + } | ||
| 192 | + entry := conchtemplate.Entry{ | ||
| 193 | + Origin: conchtemplate.Origin(record.Labels[originLabel]), | ||
| 194 | + BootMode: bootMode, | ||
| 195 | + BootIndexDigest: record.Target.Digest.String(), | ||
| 196 | + ParentBootIndexDigest: record.Labels[parentLabel], | ||
| 197 | + SourceSandboxID: record.Labels[sourceSandboxLabel], | ||
| 198 | + SourceRef: record.Labels[sourceRefLabel], | ||
| 199 | + CreatedAt: record.CreatedAt.UnixNano(), | ||
| 200 | + } | ||
| 201 | + for key, value := range record.Labels { | ||
| 202 | + if strings.HasPrefix(key, userLabelPrefix) { | ||
| 203 | + if entry.Labels == nil { | ||
| 204 | + entry.Labels = make(map[string]string) | ||
| 205 | + } | ||
| 206 | + entry.Labels[strings.TrimPrefix(key, userLabelPrefix)] = value | ||
| 207 | + } | ||
| 208 | + } | ||
| 209 | + return conchtemplate.NormalizeEntry(entry) | ||
| 210 | +} | ||
| 211 | + | ||
| 212 | +func (s *Store) configured() error { | ||
| 213 | + if s == nil || s.images == nil || s.content == nil { | ||
| 214 | + return fmt.Errorf("template store is not configured") | ||
| 215 | + } | ||
| 216 | + return nil | ||
| 217 | +} | ||
| 218 | + | ||
| 219 | +func canonicalName(rawDigest string) (string, error) { | ||
| 220 | + parsed, err := digest.Parse(strings.TrimSpace(rawDigest)) | ||
| 221 | + if err != nil { | ||
| 222 | + return "", conchtemplate.ErrInvalidArgument.Wrap(fmt.Errorf("invalid Template ID %q: %w", rawDigest, err)) | ||
| 223 | + } | ||
| 224 | + return conchimage.CanonicalTemplateRef(parsed.String()) | ||
| 225 | +} | ||
| 226 | + | ||
| 227 | +func encodeLabels(entry conchtemplate.Entry, kind string) (map[string]string, error) { | ||
| 228 | + out := map[string]string{ | ||
| 229 | + conchimage.ImageKindLabel: kind, | ||
| 230 | + schemaLabel: schemaVersion, | ||
| 231 | + originLabel: string(entry.Origin), | ||
| 232 | + } | ||
| 233 | + for key, value := range map[string]string{ | ||
| 234 | + parentLabel: entry.ParentBootIndexDigest, sourceSandboxLabel: entry.SourceSandboxID, sourceRefLabel: entry.SourceRef, | ||
| 235 | + } { | ||
| 236 | + if value != "" { | ||
| 237 | + out[key] = value | ||
| 238 | + } | ||
| 239 | + } | ||
| 240 | + for key, value := range entry.Labels { | ||
| 241 | + if strings.TrimSpace(key) == "" { | ||
| 242 | + return nil, conchtemplate.ErrInvalidArgument.Wrap(fmt.Errorf("Template user label key is empty")) | ||
| 243 | + } | ||
| 244 | + out[userLabelPrefix+key] = value | ||
| 245 | + } | ||
| 246 | + for key, value := range out { | ||
| 247 | + if err := labels.Validate(key, value); err != nil { | ||
| 248 | + return nil, conchtemplate.ErrInvalidArgument.Wrap(err) | ||
| 249 | + } | ||
| 250 | + } | ||
| 251 | + return out, nil | ||
| 252 | +} | ||
| 253 | + | ||
| 254 | +func validateFilter(filter conchtemplate.Filter) error { | ||
| 255 | + if filter.Origin != "" && filter.Origin != conchtemplate.OriginImage && filter.Origin != conchtemplate.OriginCheckpoint { | ||
| 256 | + return conchtemplate.ErrInvalidArgument.Wrap(fmt.Errorf("unknown template origin %q", filter.Origin)) | ||
| 257 | + } | ||
| 258 | + if filter.BootMode != "" && filter.BootMode != conchtemplate.BootModeCold && filter.BootMode != conchtemplate.BootModeResume { | ||
| 259 | + return conchtemplate.ErrInvalidArgument.Wrap(fmt.Errorf("unknown template boot mode %q", filter.BootMode)) | ||
| 260 | + } | ||
| 261 | + return nil | ||
| 262 | +} | ||
| 263 | + | ||
| 264 | +func translateError(action string, err error) error { | ||
| 265 | + switch { | ||
| 266 | + case errdefs.IsNotFound(err): | ||
| 267 | + return conchtemplate.ErrNotFound.Wrap(err) | ||
| 268 | + case errdefs.IsAlreadyExists(err): | ||
| 269 | + return conchtemplate.ErrAlreadyExists.Wrap(err) | ||
| 270 | + case errdefs.IsInvalidArgument(err): | ||
| 271 | + return conchtemplate.ErrInvalidArgument.Wrap(err) | ||
| 272 | + default: | ||
| 273 | + return fmt.Errorf("%s: %w", action, err) | ||
| 274 | + } | ||
| 275 | +} | ||
| @@ -0,0 +1,356 @@ | |||
| 1 | +package containerdtemplate | ||
| 2 | + | ||
| 3 | +import ( | ||
| 4 | + "bytes" | ||
| 5 | + "context" | ||
| 6 | + "encoding/json" | ||
| 7 | + "errors" | ||
| 8 | + "reflect" | ||
| 9 | + "sort" | ||
| 10 | + "testing" | ||
| 11 | + "time" | ||
| 12 | + | ||
| 13 | + "github.com/containerd/containerd/v2/core/content" | ||
| 14 | + "github.com/containerd/containerd/v2/core/images" | ||
| 15 | + "github.com/containerd/containerd/v2/plugins/content/local" | ||
| 16 | + "github.com/containerd/errdefs" | ||
| 17 | + "github.com/opencontainers/go-digest" | ||
| 18 | + ispec "github.com/opencontainers/image-spec/specs-go" | ||
| 19 | + ocispec "github.com/opencontainers/image-spec/specs-go/v1" | ||
| 20 | + | ||
| 21 | + conchimage "github.com/openeuler/Conch/internal/image" | ||
| 22 | + "github.com/openeuler/Conch/internal/image/erofsconvert" | ||
| 23 | + conchtemplate "github.com/openeuler/Conch/internal/template" | ||
| 24 | +) | ||
| 25 | + | ||
| 26 | +var _ conchtemplate.Store = (*Store)(nil) | ||
| 27 | + | ||
| 28 | +func TestStoreCRUDUsesCanonicalImageRecord(t *testing.T) { | ||
| 29 | + ctx := context.Background() | ||
| 30 | + contentStore := newTestContentStore(t) | ||
| 31 | + target := buildTestBootIndex(t, ctx, contentStore, false) | ||
| 32 | + imageStore := newMemoryImageStore() | ||
| 33 | + store := &Store{images: imageStore, content: contentStore} | ||
| 34 | + | ||
| 35 | + entry, err := store.Create(ctx, conchtemplate.Entry{ | ||
| 36 | + Origin: conchtemplate.OriginImage, | ||
| 37 | + BootMode: conchtemplate.BootModeCold, | ||
| 38 | + BootIndexDigest: target.Digest.String(), | ||
| 39 | + SourceRef: "registry.example/conch/demo:v1", | ||
| 40 | + Labels: map[string]string{"owner": "team-a"}, | ||
| 41 | + }, target) | ||
| 42 | + if err != nil { | ||
| 43 | + t.Fatalf("Create() error = %v", err) | ||
| 44 | + } | ||
| 45 | + | ||
| 46 | + name, err := conchimage.CanonicalTemplateRef(target.Digest.String()) | ||
| 47 | + if err != nil { | ||
| 48 | + t.Fatal(err) | ||
| 49 | + } | ||
| 50 | + record, err := imageStore.Get(ctx, name) | ||
| 51 | + if err != nil { | ||
| 52 | + t.Fatalf("Get canonical image record: %v", err) | ||
| 53 | + } | ||
| 54 | + if !reflect.DeepEqual(record.Target, target) { | ||
| 55 | + t.Fatalf("record target = %#v, want %#v", record.Target, target) | ||
| 56 | + } | ||
| 57 | + if got := record.Labels[schemaLabel]; got != schemaVersion { | ||
| 58 | + t.Fatalf("schema label = %q, want %q", got, schemaVersion) | ||
| 59 | + } | ||
| 60 | + if got := record.Labels[conchimage.ImageKindLabel]; got != conchimage.ImageKindBootIndexCold { | ||
| 61 | + t.Fatalf("image kind = %q", got) | ||
| 62 | + } | ||
| 63 | + if got := record.Labels[userLabelPrefix+"owner"]; got != "team-a" { | ||
| 64 | + t.Fatalf("user owner label = %q", got) | ||
| 65 | + } | ||
| 66 | + if entry.CreatedAt != record.CreatedAt.UnixNano() { | ||
| 67 | + t.Fatalf("entry CreatedAt = %d, want %d", entry.CreatedAt, record.CreatedAt.UnixNano()) | ||
| 68 | + } | ||
| 69 | + | ||
| 70 | + contentInfo, err := contentStore.Info(ctx, target.Digest) | ||
| 71 | + if err != nil { | ||
| 72 | + t.Fatal(err) | ||
| 73 | + } | ||
| 74 | + if len(contentInfo.Labels) == 0 { | ||
| 75 | + t.Fatal("Boot Index content has no containerd GC child labels") | ||
| 76 | + } | ||
| 77 | + | ||
| 78 | + imageStore.records["registry.example/ordinary:v1"] = images.Image{ | ||
| 79 | + Name: "registry.example/ordinary:v1", | ||
| 80 | + Target: target, | ||
| 81 | + Labels: map[string]string{conchimage.ImageKindLabel: conchimage.ImageKindOCIImage}, | ||
| 82 | + } | ||
| 83 | + markerless, _ := conchimage.CanonicalTemplateRef(digest.FromString("markerless").String()) | ||
| 84 | + imageStore.records[markerless] = images.Image{Name: markerless, Target: target} | ||
| 85 | + | ||
| 86 | + got, err := store.Get(ctx, target.Digest.String()) | ||
| 87 | + if err != nil { | ||
| 88 | + t.Fatalf("Get() error = %v", err) | ||
| 89 | + } | ||
| 90 | + if got.SourceRef != entry.SourceRef || got.Labels["owner"] != "team-a" { | ||
| 91 | + t.Fatalf("Get() = %#v, want source and user labels", got) | ||
| 92 | + } | ||
| 93 | + items, err := store.List(ctx, conchtemplate.Filter{ | ||
| 94 | + Origin: conchtemplate.OriginImage, | ||
| 95 | + BootMode: conchtemplate.BootModeCold, | ||
| 96 | + }) | ||
| 97 | + if err != nil { | ||
| 98 | + t.Fatalf("List() error = %v", err) | ||
| 99 | + } | ||
| 100 | + if len(items) != 1 || items[0].BootIndexDigest != target.Digest.String() { | ||
| 101 | + t.Fatalf("List() = %#v, want only canonical Template", items) | ||
| 102 | + } | ||
| 103 | + | ||
| 104 | + if err := store.Delete(ctx, target.Digest.String()); err != nil { | ||
| 105 | + t.Fatalf("Delete() error = %v", err) | ||
| 106 | + } | ||
| 107 | + if err := store.Delete(ctx, target.Digest.String()); err != nil { | ||
| 108 | + t.Fatalf("idempotent Delete() error = %v", err) | ||
| 109 | + } | ||
| 110 | + if _, err := store.Get(ctx, target.Digest.String()); !errors.Is(err, conchtemplate.ErrNotFound) { | ||
| 111 | + t.Fatalf("Get() after Delete error = %v, want ErrNotFound", err) | ||
| 112 | + } | ||
| 113 | +} | ||
| 114 | + | ||
| 115 | +func TestStoreCreateDoesNotOverwriteExistingTemplate(t *testing.T) { | ||
| 116 | + ctx := context.Background() | ||
| 117 | + contentStore := newTestContentStore(t) | ||
| 118 | + target := buildTestBootIndex(t, ctx, contentStore, false) | ||
| 119 | + store := &Store{images: newMemoryImageStore(), content: contentStore} | ||
| 120 | + | ||
| 121 | + first := conchtemplate.Entry{ | ||
| 122 | + Origin: conchtemplate.OriginImage, | ||
| 123 | + BootMode: conchtemplate.BootModeCold, | ||
| 124 | + BootIndexDigest: target.Digest.String(), | ||
| 125 | + Labels: map[string]string{"owner": "first"}, | ||
| 126 | + } | ||
| 127 | + if _, err := store.Create(ctx, first, target); err != nil { | ||
| 128 | + t.Fatalf("first Create() error = %v", err) | ||
| 129 | + } | ||
| 130 | + first.Labels["owner"] = "second" | ||
| 131 | + if _, err := store.Create(ctx, first, target); !errors.Is(err, conchtemplate.ErrAlreadyExists) { | ||
| 132 | + t.Fatalf("second Create() error = %v, want ErrAlreadyExists", err) | ||
| 133 | + } | ||
| 134 | + got, err := store.Get(ctx, target.Digest.String()) | ||
| 135 | + if err != nil { | ||
| 136 | + t.Fatal(err) | ||
| 137 | + } | ||
| 138 | + if got.Labels["owner"] != "first" { | ||
| 139 | + t.Fatalf("existing Template was overwritten: %#v", got.Labels) | ||
| 140 | + } | ||
| 141 | +} | ||
| 142 | + | ||
| 143 | +func TestStoreCreateRejectsBootModeMismatch(t *testing.T) { | ||
| 144 | + ctx := context.Background() | ||
| 145 | + contentStore := newTestContentStore(t) | ||
| 146 | + target := buildTestBootIndex(t, ctx, contentStore, false) | ||
| 147 | + store := &Store{images: newMemoryImageStore(), content: contentStore} | ||
| 148 | + | ||
| 149 | + _, err := store.Create(ctx, conchtemplate.Entry{ | ||
| 150 | + Origin: conchtemplate.OriginCheckpoint, | ||
| 151 | + BootMode: conchtemplate.BootModeResume, | ||
| 152 | + BootIndexDigest: target.Digest.String(), | ||
| 153 | + }, target) | ||
| 154 | + if !errors.Is(err, conchtemplate.ErrInvalidArtifact) { | ||
| 155 | + t.Fatalf("Create() error = %v, want ErrInvalidArtifact", err) | ||
| 156 | + } | ||
| 157 | +} | ||
| 158 | + | ||
| 159 | +func TestStoreDeleteRejectsMovedCanonicalRecord(t *testing.T) { | ||
| 160 | + ctx := context.Background() | ||
| 161 | + contentStore := newTestContentStore(t) | ||
| 162 | + target := buildTestBootIndex(t, ctx, contentStore, false) | ||
| 163 | + imageStore := newMemoryImageStore() | ||
| 164 | + store := &Store{images: imageStore, content: contentStore} | ||
| 165 | + if _, err := store.Create(ctx, conchtemplate.Entry{ | ||
| 166 | + Origin: conchtemplate.OriginImage, | ||
| 167 | + BootMode: conchtemplate.BootModeCold, | ||
| 168 | + BootIndexDigest: target.Digest.String(), | ||
| 169 | + }, target); err != nil { | ||
| 170 | + t.Fatal(err) | ||
| 171 | + } | ||
| 172 | + name, _ := conchimage.CanonicalTemplateRef(target.Digest.String()) | ||
| 173 | + record := imageStore.records[name] | ||
| 174 | + record.Target.Digest = digest.FromString("moved-target") | ||
| 175 | + imageStore.records[name] = record | ||
| 176 | + | ||
| 177 | + if err := store.Delete(ctx, target.Digest.String()); !errors.Is(err, conchtemplate.ErrFailedPrecondition) { | ||
| 178 | + t.Fatalf("Delete() error = %v, want ErrFailedPrecondition", err) | ||
| 179 | + } | ||
| 180 | + if _, err := imageStore.Get(ctx, name); err != nil { | ||
| 181 | + t.Fatalf("moved canonical record was deleted: %v", err) | ||
| 182 | + } | ||
| 183 | +} | ||
| 184 | + | ||
| 185 | +type memoryImageStore struct { | ||
| 186 | + records map[string]images.Image | ||
| 187 | + filters []string | ||
| 188 | +} | ||
| 189 | + | ||
| 190 | +func newMemoryImageStore() *memoryImageStore { | ||
| 191 | + return &memoryImageStore{records: make(map[string]images.Image)} | ||
| 192 | +} | ||
| 193 | + | ||
| 194 | +func (s *memoryImageStore) Get(_ context.Context, name string) (images.Image, error) { | ||
| 195 | + record, ok := s.records[name] | ||
| 196 | + if !ok { | ||
| 197 | + return images.Image{}, errdefs.ErrNotFound | ||
| 198 | + } | ||
| 199 | + return record, nil | ||
| 200 | +} | ||
| 201 | + | ||
| 202 | +func (s *memoryImageStore) List(_ context.Context, filters ...string) ([]images.Image, error) { | ||
| 203 | + s.filters = append([]string(nil), filters...) | ||
| 204 | + out := make([]images.Image, 0, len(s.records)) | ||
| 205 | + for _, record := range s.records { | ||
| 206 | + out = append(out, record) | ||
| 207 | + } | ||
| 208 | + sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name }) | ||
| 209 | + return out, nil | ||
| 210 | +} | ||
| 211 | + | ||
| 212 | +func (s *memoryImageStore) Create(_ context.Context, record images.Image) (images.Image, error) { | ||
| 213 | + if _, ok := s.records[record.Name]; ok { | ||
| 214 | + return images.Image{}, errdefs.ErrAlreadyExists | ||
| 215 | + } | ||
| 216 | + record.CreatedAt = time.Unix(10, 0).UTC() | ||
| 217 | + record.UpdatedAt = record.CreatedAt | ||
| 218 | + s.records[record.Name] = record | ||
| 219 | + return record, nil | ||
| 220 | +} | ||
| 221 | + | ||
| 222 | +func (s *memoryImageStore) Update(context.Context, images.Image, ...string) (images.Image, error) { | ||
| 223 | + return images.Image{}, errors.New("unexpected image update") | ||
| 224 | +} | ||
| 225 | + | ||
| 226 | +func (s *memoryImageStore) Delete(ctx context.Context, name string, opts ...images.DeleteOpt) error { | ||
| 227 | + record, ok := s.records[name] | ||
| 228 | + if !ok { | ||
| 229 | + return errdefs.ErrNotFound | ||
| 230 | + } | ||
| 231 | + var options images.DeleteOptions | ||
| 232 | + for _, opt := range opts { | ||
| 233 | + if err := opt(ctx, &options); err != nil { | ||
| 234 | + return err | ||
| 235 | + } | ||
| 236 | + } | ||
| 237 | + if options.Target != nil && record.Target.Digest != options.Target.Digest { | ||
| 238 | + return errdefs.ErrNotFound | ||
| 239 | + } | ||
| 240 | + delete(s.records, name) | ||
| 241 | + return nil | ||
| 242 | +} | ||
| 243 | + | ||
| 244 | +type memoryLabelStore struct { | ||
| 245 | + labels map[digest.Digest]map[string]string | ||
| 246 | +} | ||
| 247 | + | ||
| 248 | +func (s *memoryLabelStore) Get(dgst digest.Digest) (map[string]string, error) { | ||
| 249 | + return copyLabels(s.labels[dgst]), nil | ||
| 250 | +} | ||
| 251 | + | ||
| 252 | +func (s *memoryLabelStore) Set(dgst digest.Digest, labels map[string]string) error { | ||
| 253 | + s.labels[dgst] = copyLabels(labels) | ||
| 254 | + return nil | ||
| 255 | +} | ||
| 256 | + | ||
| 257 | +func (s *memoryLabelStore) Update(dgst digest.Digest, update map[string]string) (map[string]string, error) { | ||
| 258 | + labels := copyLabels(s.labels[dgst]) | ||
| 259 | + if labels == nil { | ||
| 260 | + labels = make(map[string]string) | ||
| 261 | + } | ||
| 262 | + for key, value := range update { | ||
| 263 | + if value == "" { | ||
| 264 | + delete(labels, key) | ||
| 265 | + } else { | ||
| 266 | + labels[key] = value | ||
| 267 | + } | ||
| 268 | + } | ||
| 269 | + s.labels[dgst] = labels | ||
| 270 | + return copyLabels(labels), nil | ||
| 271 | +} | ||
| 272 | + | ||
| 273 | +func newTestContentStore(t *testing.T) content.Store { | ||
| 274 | + t.Helper() | ||
| 275 | + store, err := local.NewLabeledStore(t.TempDir(), &memoryLabelStore{ | ||
| 276 | + labels: make(map[digest.Digest]map[string]string), | ||
| 277 | + }) | ||
| 278 | + if err != nil { | ||
| 279 | + t.Fatal(err) | ||
| 280 | + } | ||
| 281 | + return store | ||
| 282 | +} | ||
| 283 | + | ||
| 284 | +func buildTestBootIndex(t *testing.T, ctx context.Context, store content.Store, resume bool) ocispec.Descriptor { | ||
| 285 | + t.Helper() | ||
| 286 | + rootfs := writeTestComponent(t, ctx, store, conchimage.KindRootfs) | ||
| 287 | + sandbox := writeTestComponent(t, ctx, store, conchimage.KindSandbox) | ||
| 288 | + opts := conchimage.BootIndexContentOptions{ | ||
| 289 | + RootfsDescriptor: rootfs, | ||
| 290 | + SandboxDescriptor: sandbox, | ||
| 291 | + } | ||
| 292 | + if resume { | ||
| 293 | + opts.MemDescriptor = writeTestComponent(t, ctx, store, conchimage.KindMemSnapshot) | ||
| 294 | + opts.VMMName = "cloud-hypervisor" | ||
| 295 | + opts.MemorySizeMB = 128 | ||
| 296 | + } | ||
| 297 | + target, err := conchimage.BuildBootIndexInContent(ctx, store, opts) | ||
| 298 | + if err != nil { | ||
| 299 | + t.Fatalf("BuildBootIndexInContent() error = %v", err) | ||
| 300 | + } | ||
| 301 | + return target | ||
| 302 | +} | ||
| 303 | + | ||
| 304 | +func writeTestComponent(t *testing.T, ctx context.Context, store content.Store, kind string) ocispec.Descriptor { | ||
| 305 | + t.Helper() | ||
| 306 | + layer := writeTestBlob(t, ctx, store, []byte("layer-"+kind), erofsconvert.NativeLayerMediaType) | ||
| 307 | + config := writeTestJSON(t, ctx, store, ocispec.Image{ | ||
| 308 | + Platform: ocispec.Platform{OS: "linux", Architecture: "amd64"}, | ||
| 309 | + RootFS: ocispec.RootFS{Type: "layers", DiffIDs: []digest.Digest{layer.Digest}}, | ||
| 310 | + }, ocispec.MediaTypeImageConfig) | ||
| 311 | + manifest := writeTestJSON(t, ctx, store, ocispec.Manifest{ | ||
| 312 | + Versioned: ispec.Versioned{SchemaVersion: 2}, | ||
| 313 | + MediaType: ocispec.MediaTypeImageManifest, | ||
| 314 | + Config: config, | ||
| 315 | + Layers: []ocispec.Descriptor{layer}, | ||
| 316 | + }, ocispec.MediaTypeImageManifest) | ||
| 317 | + manifest.Annotations = map[string]string{"io.conch.kind": kind} | ||
| 318 | + if kind == conchimage.KindMemSnapshot { | ||
| 319 | + manifest.Annotations[conchimage.AnnotationVMM] = "cloud-hypervisor" | ||
| 320 | + manifest.Annotations[conchimage.AnnotationMemorySizeMB] = "128" | ||
| 321 | + } | ||
| 322 | + return manifest | ||
| 323 | +} | ||
| 324 | + | ||
| 325 | +func writeTestJSON(t *testing.T, ctx context.Context, store content.Store, value any, mediaType string) ocispec.Descriptor { | ||
| 326 | + t.Helper() | ||
| 327 | + data, err := json.Marshal(value) | ||
| 328 | + if err != nil { | ||
| 329 | + t.Fatal(err) | ||
| 330 | + } | ||
| 331 | + return writeTestBlob(t, ctx, store, data, mediaType) | ||
| 332 | +} | ||
| 333 | + | ||
| 334 | +func writeTestBlob(t *testing.T, ctx context.Context, store content.Store, data []byte, mediaType string) ocispec.Descriptor { | ||
| 335 | + t.Helper() | ||
| 336 | + desc := ocispec.Descriptor{ | ||
| 337 | + MediaType: mediaType, | ||
| 338 | + Digest: digest.FromBytes(data), | ||
| 339 | + Size: int64(len(data)), | ||
| 340 | + } | ||
| 341 | + if err := content.WriteBlob(ctx, store, "test-"+desc.Digest.String(), bytes.NewReader(data), desc); err != nil { | ||
| 342 | + t.Fatal(err) | ||
| 343 | + } | ||
| 344 | + return desc | ||
| 345 | +} | ||
| 346 | + | ||
| 347 | +func copyLabels(in map[string]string) map[string]string { | ||
| 348 | + if in == nil { | ||
| 349 | + return nil | ||
| 350 | + } | ||
| 351 | + out := make(map[string]string, len(in)) | ||
| 352 | + for key, value := range in { | ||
| 353 | + out[key] = value | ||
| 354 | + } | ||
| 355 | + return out | ||
| 356 | +} | ||
| @@ -93,7 +93,6 @@ func New(sandboxOps SandboxOps, client *containerdclient.Client, store state.Sto | |||
| 93 | Sandbox: sandboxOps, | 93 | Sandbox: sandboxOps, |
| 94 | Containerd: client, | 94 | Containerd: client, |
| 95 | Store: store, | 95 | Store: store, |
| 96 | - Templates: conchtemplate.NewStore(store), | ||
| 97 | } | 96 | } |
| 98 | } | 97 | } |
| 99 | 98 | ||
| @@ -406,6 +405,9 @@ func (s *Service) CheckpointSandbox(ctx context.Context, opts SandboxCheckpointO | |||
| 406 | if s.Store == nil { | 405 | if s.Store == nil { |
| 407 | return SandboxCheckpointResult{}, fmt.Errorf("checkpoint publisher is not configured") | 406 | return SandboxCheckpointResult{}, fmt.Errorf("checkpoint publisher is not configured") |
| 408 | } | 407 | } |
| 408 | + if s.Templates == nil { | ||
| 409 | + return SandboxCheckpointResult{}, fmt.Errorf("template store is not configured") | ||
| 410 | + } | ||
| 409 | sandboxID := rec.SandboxID | 411 | sandboxID := rec.SandboxID |
| 410 | parentID := strings.TrimSpace(rec.CheckpointHeadTemplateID) | 412 | parentID := strings.TrimSpace(rec.CheckpointHeadTemplateID) |
| 411 | if parentID == "" { | 413 | if parentID == "" { |
| @@ -420,7 +422,12 @@ func (s *Service) CheckpointSandbox(ctx context.Context, opts SandboxCheckpointO | |||
| 420 | } | 422 | } |
| 421 | defer os.RemoveAll(captured.MemRootPath) | 423 | defer os.RemoveAll(captured.MemRootPath) |
| 422 | 424 | ||
| 423 | - published, err := conchimage.PublishCheckpointBootIndex(ctx, s.Containerd, conchimage.PublishCheckpointBootIndexOptions{ | 425 | + publishCtx, done, err := s.Containerd.WithLease(containerdclient.NewNamespaceContext(ctx)) |
| 426 | + if err != nil { | ||
| 427 | + return SandboxCheckpointResult{}, fmt.Errorf("create checkpoint content lease: %w", err) | ||
| 428 | + } | ||
| 429 | + defer done(publishCtx) | ||
| 430 | + published, err := conchimage.PublishCheckpointBootIndex(publishCtx, s.Containerd, conchimage.PublishCheckpointBootIndexOptions{ | ||
| 424 | SourceBootIndexDigest: parentID, | 431 | SourceBootIndexDigest: parentID, |
| 425 | MemRoot: captured.MemRootPath, | 432 | MemRoot: captured.MemRootPath, |
| 426 | VMMName: captured.VMMName, | 433 | VMMName: captured.VMMName, |
| @@ -429,13 +436,7 @@ func (s *Service) CheckpointSandbox(ctx context.Context, opts SandboxCheckpointO | |||
| 429 | if err != nil { | 436 | if err != nil { |
| 430 | return SandboxCheckpointResult{}, err | 437 | return SandboxCheckpointResult{}, err |
| 431 | } | 438 | } |
| 432 | - keepCanonicalRecord := false | 439 | + info, err := conchimage.InspectBootIndexContent(publishCtx, s.Containerd.ContentStore(), published.Target) |
| 433 | - defer func() { | ||
| 434 | - if !keepCanonicalRecord { | ||
| 435 | - s.cleanupCanonicalTemplateRecord(ctx, published.BootIndexDigest) | ||
| 436 | - } | ||
| 437 | - }() | ||
| 438 | - info, err := conchimage.InspectBootIndex(ctx, s.Containerd, published.BootIndexDigest) | ||
| 439 | if err != nil { | 440 | if err != nil { |
| 440 | return SandboxCheckpointResult{}, fmt.Errorf("validate published checkpoint boot index: %w", err) | 441 | return SandboxCheckpointResult{}, fmt.Errorf("validate published checkpoint boot index: %w", err) |
| 441 | } | 442 | } |
| @@ -463,20 +464,23 @@ func (s *Service) CheckpointSandbox(ctx context.Context, opts SandboxCheckpointO | |||
| 463 | captured.MemorySizeMB, | 464 | captured.MemorySizeMB, |
| 464 | ) | 465 | ) |
| 465 | } | 466 | } |
| 466 | - if err := s.Store.PublishCheckpoint(ctx, conchtemplate.Entry{ | 467 | + entry, err := s.Templates.Create(publishCtx, conchtemplate.Entry{ |
| 467 | Origin: conchtemplate.OriginCheckpoint, | 468 | Origin: conchtemplate.OriginCheckpoint, |
| 468 | BootMode: conchtemplate.BootModeResume, | 469 | BootMode: conchtemplate.BootModeResume, |
| 469 | BootIndexDigest: info.BootIndexDigest, | 470 | BootIndexDigest: info.BootIndexDigest, |
| 470 | ParentBootIndexDigest: parentID, | 471 | ParentBootIndexDigest: parentID, |
| 471 | SourceSandboxID: sandboxID, | 472 | SourceSandboxID: sandboxID, |
| 472 | Labels: copyMap(opts.Labels), | 473 | Labels: copyMap(opts.Labels), |
| 473 | - CreatedAt: time.Now().UnixNano(), | 474 | + }, published.Target) |
| 474 | - }); err != nil { | 475 | + if err != nil { |
| 476 | + return SandboxCheckpointResult{}, err | ||
| 477 | + } | ||
| 478 | + if err := s.Store.AdvanceCheckpointHead(ctx, sandboxID, parentID, entry.BootIndexDigest); err != nil { | ||
| 479 | + s.cleanupTemplateRecord(ctx, entry.BootIndexDigest) | ||
| 475 | return SandboxCheckpointResult{}, err | 480 | return SandboxCheckpointResult{}, err |
| 476 | } | 481 | } |
| 477 | - keepCanonicalRecord = true | ||
| 478 | return SandboxCheckpointResult{ | 482 | return SandboxCheckpointResult{ |
| 479 | - TemplateID: info.BootIndexDigest, | 483 | + TemplateID: entry.BootIndexDigest, |
| 480 | }, nil | 484 | }, nil |
| 481 | } | 485 | } |
| 482 | 486 | ||
| @@ -494,7 +498,12 @@ func (s *Service) PullTemplate(ctx context.Context, opts TemplatePullOptions) (T | |||
| 494 | if reference == "" { | 498 | if reference == "" { |
| 495 | return TemplatePullResult{}, conchtemplate.ErrInvalidArgument.Wrap(fmt.Errorf("template reference is required")) | 499 | return TemplatePullResult{}, conchtemplate.ErrInvalidArgument.Wrap(fmt.Errorf("template reference is required")) |
| 496 | } | 500 | } |
| 497 | - pulled, err := conchimage.PullBootIndex(ctx, s.Containerd, conchimage.RegistryPullOptions{ | 501 | + pullCtx, done, err := s.Containerd.WithLease(containerdclient.NewNamespaceContext(ctx)) |
| 502 | + if err != nil { | ||
| 503 | + return TemplatePullResult{}, fmt.Errorf("create pull content lease: %w", err) | ||
| 504 | + } | ||
| 505 | + defer done(pullCtx) | ||
| 506 | + pulled, err := conchimage.PullBootIndex(pullCtx, s.Containerd, conchimage.RegistryPullOptions{ | ||
| 498 | Reference: reference, | 507 | Reference: reference, |
| 499 | PlainHTTP: opts.PlainHTTP, | 508 | PlainHTTP: opts.PlainHTTP, |
| 500 | Username: opts.Username, | 509 | Username: opts.Username, |
| @@ -510,18 +519,22 @@ func (s *Service) PullTemplate(ctx context.Context, opts TemplatePullOptions) (T | |||
| 510 | origin = conchtemplate.OriginCheckpoint | 519 | origin = conchtemplate.OriginCheckpoint |
| 511 | bootMode = conchtemplate.BootModeResume | 520 | bootMode = conchtemplate.BootModeResume |
| 512 | } | 521 | } |
| 513 | - entry, err := s.Templates.Create(ctx, conchtemplate.Entry{ | 522 | + entry, createErr := s.Templates.Create(pullCtx, conchtemplate.Entry{ |
| 514 | Origin: origin, | 523 | Origin: origin, |
| 515 | BootMode: bootMode, | 524 | BootMode: bootMode, |
| 516 | BootIndexDigest: info.BootIndexDigest, | 525 | BootIndexDigest: info.BootIndexDigest, |
| 517 | SourceRef: reference, | 526 | SourceRef: reference, |
| 518 | Labels: opts.Labels, | 527 | Labels: opts.Labels, |
| 519 | - }) | 528 | + }, pulled.Target) |
| 520 | - if err != nil { | 529 | + cleanupErr := conchimage.RemoveFetchedImageRecord( |
| 521 | - if !errors.Is(err, conchtemplate.ErrAlreadyExists) { | 530 | + ctx, s.Containerd.ImageService(), pulled.SourceImageName, pulled.Target, |
| 522 | - s.cleanupCanonicalTemplateRecord(ctx, info.BootIndexDigest) | 531 | + ) |
| 523 | - } | 532 | + if createErr != nil { |
| 524 | - return TemplatePullResult{}, err | 533 | + return TemplatePullResult{}, errors.Join(createErr, cleanupErr) |
| 534 | + } | ||
| 535 | + if cleanupErr != nil { | ||
| 536 | + s.cleanupTemplateRecord(ctx, entry.BootIndexDigest) | ||
| 537 | + return TemplatePullResult{}, fmt.Errorf("remove fetched source image record %s: %w", pulled.SourceImageName, cleanupErr) | ||
| 525 | } | 538 | } |
| 526 | return TemplatePullResult{ | 539 | return TemplatePullResult{ |
| 527 | TemplateID: entry.BootIndexDigest, | 540 | TemplateID: entry.BootIndexDigest, |
| @@ -603,47 +616,15 @@ func (s *Service) CreateTemplate(ctx context.Context, opts TemplateCreateOptions | |||
| 603 | if err != nil { | 616 | if err != nil { |
| 604 | return TemplateCreateResult{}, err | 617 | return TemplateCreateResult{}, err |
| 605 | } | 618 | } |
| 606 | - keepCanonicalRecord := false | ||
| 607 | - defer func() { | ||
| 608 | - if !keepCanonicalRecord { | ||
| 609 | - s.cleanupCanonicalTemplateRecord(ctx, result.bootIndexDigest) | ||
| 610 | - } | ||
| 611 | - }() | ||
| 612 | - info, err := conchimage.InspectBootIndex(ctx, s.Containerd, result.bootIndexDigest) | ||
| 613 | - if err != nil { | ||
| 614 | - return TemplateCreateResult{}, fmt.Errorf("validate published boot index: %w", err) | ||
| 615 | - } | ||
| 616 | - if info.BootIndexDigest != result.bootIndexDigest { | ||
| 617 | - return TemplateCreateResult{}, fmt.Errorf( | ||
| 618 | - "validated boot index digest %s does not match published digest %s", | ||
| 619 | - info.BootIndexDigest, | ||
| 620 | - result.bootIndexDigest, | ||
| 621 | - ) | ||
| 622 | - } | ||
| 623 | - bootMode := conchtemplate.BootModeCold | ||
| 624 | - if info.Resume { | ||
| 625 | - bootMode = conchtemplate.BootModeResume | ||
| 626 | - } | ||
| 627 | - entry, err := s.Templates.Create(ctx, conchtemplate.Entry{ | ||
| 628 | - Origin: conchtemplate.OriginImage, | ||
| 629 | - BootMode: bootMode, | ||
| 630 | - BootIndexDigest: info.BootIndexDigest, | ||
| 631 | - SourceRef: source, | ||
| 632 | - Labels: opts.Labels, | ||
| 633 | - }) | ||
| 634 | - if err != nil { | ||
| 635 | - return TemplateCreateResult{}, err | ||
| 636 | - } | ||
| 637 | - keepCanonicalRecord = true | ||
| 638 | return TemplateCreateResult{ | 619 | return TemplateCreateResult{ |
| 639 | - TemplateID: entry.BootIndexDigest, | 620 | + TemplateID: result.entry.BootIndexDigest, |
| 640 | BuildRef: result.buildRef, | 621 | BuildRef: result.buildRef, |
| 641 | }, nil | 622 | }, nil |
| 642 | } | 623 | } |
| 643 | 624 | ||
| 644 | type templateBuildResult struct { | 625 | type templateBuildResult struct { |
| 645 | - bootIndexDigest string | 626 | + entry conchtemplate.Entry |
| 646 | - buildRef string | 627 | + buildRef string |
| 647 | } | 628 | } |
| 648 | 629 | ||
| 649 | func (s *Service) createTemplateFromSource(ctx context.Context, opts TemplateCreateOptions) (templateBuildResult, error) { | 630 | func (s *Service) createTemplateFromSource(ctx context.Context, opts TemplateCreateOptions) (templateBuildResult, error) { |
| @@ -669,6 +650,11 @@ func (s *Service) createTemplateFromSource(ctx context.Context, opts TemplateCre | |||
| 669 | return templateBuildResult{}, fmt.Errorf("resolve pulled rootfs source image %s: %w", opts.Source, err) | 650 | return templateBuildResult{}, fmt.Errorf("resolve pulled rootfs source image %s: %w", opts.Source, err) |
| 670 | } | 651 | } |
| 671 | } | 652 | } |
| 653 | + if conchimage.IsCanonicalTemplateRef(sourceImage.Name()) { | ||
| 654 | + return templateBuildResult{}, conchtemplate.ErrInvalidArgument.Wrap(fmt.Errorf( | ||
| 655 | + "canonical Template image %s cannot be used as a rootfs source", sourceImage.Name(), | ||
| 656 | + )) | ||
| 657 | + } | ||
| 672 | if err := conchimage.SetImageKindLabel(sourceCtx, s.Containerd.ImageService(), sourceImage.Name(), conchimage.ImageKindOCIImage); err != nil { | 658 | if err := conchimage.SetImageKindLabel(sourceCtx, s.Containerd.ImageService(), sourceImage.Name(), conchimage.ImageKindOCIImage); err != nil { |
| 673 | return templateBuildResult{}, fmt.Errorf("label rootfs source image: %w", err) | 659 | return templateBuildResult{}, fmt.Errorf("label rootfs source image: %w", err) |
| 674 | } | 660 | } |
| @@ -687,8 +673,24 @@ func (s *Service) createTemplateFromSource(ctx context.Context, opts TemplateCre | |||
| 687 | if err != nil { | 673 | if err != nil { |
| 688 | return templateBuildResult{}, conchimage.ErrConversionFailed.Wrap(fmt.Errorf("convert rootfs to EROFS: %w", err)) | 674 | return templateBuildResult{}, conchimage.ErrConversionFailed.Wrap(fmt.Errorf("convert rootfs to EROFS: %w", err)) |
| 689 | } | 675 | } |
| 676 | + defer func() { | ||
| 677 | + cleanupCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 10*time.Second) | ||
| 678 | + defer cancel() | ||
| 679 | + if err := conchimage.Remove(cleanupCtx, s.Containerd, runtimeapi.RemoveImageOptions{ | ||
| 680 | + ImageName: converted.ImageName, | ||
| 681 | + }); err != nil { | ||
| 682 | + ulog.GetLogger().Warn("failed to remove temporary converted rootfs image", | ||
| 683 | + ulog.F("image", converted.ImageName), | ||
| 684 | + ulog.F("error", err)) | ||
| 685 | + } | ||
| 686 | + }() | ||
| 690 | 687 | ||
| 691 | - published, err := conchimage.PublishBootIndex(ctx, s.Containerd, conchimage.PublishBootIndexOptions{ | 688 | + publishCtx, done, err := s.Containerd.WithLease(sourceCtx) |
| 689 | + if err != nil { | ||
| 690 | + return templateBuildResult{}, fmt.Errorf("create Template content lease: %w", err) | ||
| 691 | + } | ||
| 692 | + defer done(publishCtx) | ||
| 693 | + published, err := conchimage.PublishBootIndex(publishCtx, s.Containerd, conchimage.PublishBootIndexOptions{ | ||
| 692 | RootfsImageName: converted.ImageName, | 694 | RootfsImageName: converted.ImageName, |
| 693 | KernelPath: opts.KernelPath, | 695 | KernelPath: opts.KernelPath, |
| 694 | InitrdPath: opts.InitrdPath, | 696 | InitrdPath: opts.InitrdPath, |
| @@ -696,23 +698,20 @@ func (s *Service) createTemplateFromSource(ctx context.Context, opts TemplateCre | |||
| 696 | if err != nil { | 698 | if err != nil { |
| 697 | return templateBuildResult{}, fmt.Errorf("publish boot image: %w", err) | 699 | return templateBuildResult{}, fmt.Errorf("publish boot image: %w", err) |
| 698 | } | 700 | } |
| 699 | - | 701 | + entry, err := s.Templates.Create(publishCtx, conchtemplate.Entry{ |
| 700 | - // The converted image name is only a build-time handle. Once the Boot Index | 702 | + Origin: conchtemplate.OriginImage, |
| 701 | - // has been published, its digest-derived canonical image record is the GC | 703 | + BootMode: conchtemplate.BootModeCold, |
| 702 | - // root for the complete descriptor closure. | 704 | + BootIndexDigest: published.BootIndexDigest, |
| 703 | - cleanupCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 10*time.Second) | 705 | + SourceRef: opts.Source, |
| 704 | - defer cancel() | 706 | + Labels: opts.Labels, |
| 705 | - if err := conchimage.Remove(cleanupCtx, s.Containerd, runtimeapi.RemoveImageOptions{ | 707 | + }, published.Target) |
| 706 | - ImageName: converted.ImageName, | 708 | + if err != nil { |
| 707 | - }); err != nil { | 709 | + return templateBuildResult{}, err |
| 708 | - ulog.GetLogger().Warn("failed to remove temporary converted rootfs image", | ||
| 709 | - ulog.F("image", converted.ImageName), | ||
| 710 | - ulog.F("error", err)) | ||
| 711 | } | 710 | } |
| 712 | 711 | ||
| 713 | return templateBuildResult{ | 712 | return templateBuildResult{ |
| 714 | - bootIndexDigest: published.BootIndexDigest, | 713 | + entry: entry, |
| 715 | - buildRef: published.BuildRef, | 714 | + buildRef: published.BuildRef, |
| 716 | }, nil | 715 | }, nil |
| 717 | } | 716 | } |
| 718 | 717 | ||
| @@ -749,19 +748,7 @@ func (s *Service) RemoveTemplate(ctx context.Context, id string) error { | |||
| 749 | if s == nil || s.Templates == nil { | 748 | if s == nil || s.Templates == nil { |
| 750 | return fmt.Errorf("template store is not configured") | 749 | return fmt.Errorf("template store is not configured") |
| 751 | } | 750 | } |
| 752 | - if s.Containerd == nil { | 751 | + return s.Templates.Delete(ctx, id) |
| 753 | - return fmt.Errorf("containerd client is required") | ||
| 754 | - } | ||
| 755 | - if _, err := s.Templates.Get(ctx, id); err != nil { | ||
| 756 | - if !errors.Is(err, conchtemplate.ErrNotFound) { | ||
| 757 | - return err | ||
| 758 | - } | ||
| 759 | - } else if err := s.Templates.Delete(ctx, id); err != nil { | ||
| 760 | - return err | ||
| 761 | - } | ||
| 762 | - cleanupCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 10*time.Second) | ||
| 763 | - defer cancel() | ||
| 764 | - return conchimage.RemoveCanonicalBootIndexRecord(cleanupCtx, s.Containerd, id) | ||
| 765 | } | 752 | } |
| 766 | 753 | ||
| 767 | func publicTemplateRecord(entry conchtemplate.Entry) runtimeapi.TemplateRecord { | 754 | func publicTemplateRecord(entry conchtemplate.Entry) runtimeapi.TemplateRecord { |
| @@ -779,10 +766,10 @@ func publicTemplateRecord(entry conchtemplate.Entry) runtimeapi.TemplateRecord { | |||
| 779 | } | 766 | } |
| 780 | } | 767 | } |
| 781 | 768 | ||
| 782 | -func (s *Service) cleanupCanonicalTemplateRecord(ctx context.Context, bootIndexDigest string) { | 769 | +func (s *Service) cleanupTemplateRecord(ctx context.Context, bootIndexDigest string) { |
| 783 | cleanupCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 10*time.Second) | 770 | cleanupCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 10*time.Second) |
| 784 | defer cancel() | 771 | defer cancel() |
| 785 | - if err := conchimage.RemoveCanonicalBootIndexRecord(cleanupCtx, s.Containerd, bootIndexDigest); err != nil { | 772 | + if err := s.Templates.Delete(cleanupCtx, bootIndexDigest); err != nil { |
| 786 | ulog.GetLogger().Warn("failed to roll back canonical template image record", | 773 | ulog.GetLogger().Warn("failed to roll back canonical template image record", |
| 787 | ulog.F("boot_index_digest", bootIndexDigest), | 774 | ulog.F("boot_index_digest", bootIndexDigest), |
| 788 | ulog.F("error", err)) | 775 | ulog.F("error", err)) |
| @@ -15,6 +15,7 @@ import ( | |||
| 15 | 15 | ||
| 16 | containerderrdefs "github.com/containerd/errdefs" | 16 | containerderrdefs "github.com/containerd/errdefs" |
| 17 | "github.com/opencontainers/go-digest" | 17 | "github.com/opencontainers/go-digest" |
| 18 | + ocispec "github.com/opencontainers/image-spec/specs-go/v1" | ||
| 18 | containerdclient "github.com/openeuler/Conch/internal/adapters/containerd/client" | 19 | containerdclient "github.com/openeuler/Conch/internal/adapters/containerd/client" |
| 19 | containerdhost "github.com/openeuler/Conch/internal/adapters/containerd/host" | 20 | containerdhost "github.com/openeuler/Conch/internal/adapters/containerd/host" |
| 20 | agentprotocol "github.com/openeuler/Conch/internal/agent/protocol" | 21 | agentprotocol "github.com/openeuler/Conch/internal/agent/protocol" |
| @@ -49,6 +50,14 @@ type serializedDeleteOps struct { | |||
| 49 | calls atomic.Int32 | 50 | calls atomic.Int32 |
| 50 | } | 51 | } |
| 51 | 52 | ||
| 53 | +type failingCheckpointStore struct { | ||
| 54 | + state.Store | ||
| 55 | +} | ||
| 56 | + | ||
| 57 | +func (failingCheckpointStore) AdvanceCheckpointHead(context.Context, string, string, string) error { | ||
| 58 | + return errors.New("checkpoint head changed") | ||
| 59 | +} | ||
| 60 | + | ||
| 52 | func (f *serializedDeleteOps) Delete(sandbox.DeleteRequest) error { | 61 | func (f *serializedDeleteOps) Delete(sandbox.DeleteRequest) error { |
| 53 | if f.calls.Add(1) == 1 { | 62 | if f.calls.Add(1) == 1 { |
| 54 | close(f.firstEntered) | 63 | close(f.firstEntered) |
| @@ -156,7 +165,8 @@ func TestCheckpointSandboxPublishesCaptureAndAtomicallyAdvancesHead(t *testing.T | |||
| 156 | sandboxOps := &fakeSandboxOps{checkpointResults: []sandbox.CheckpointResult{captured}} | 165 | sandboxOps := &fakeSandboxOps{checkpointResults: []sandbox.CheckpointResult{captured}} |
| 157 | store := newTestStore(t) | 166 | store := newTestStore(t) |
| 158 | svc := New(sandboxOps, host.Client(), store) | 167 | svc := New(sandboxOps, host.Client(), store) |
| 159 | - seedTemplate(t, ctx, svc.Templates, t0Digest, conchtemplate.BootModeCold) | 168 | + svc.Templates = host.TemplateStore() |
| 169 | + seedTemplate(t, ctx, host, t0Digest, conchtemplate.BootModeCold) | ||
| 160 | 170 | ||
| 161 | before := state.SandboxRecord{ | 171 | before := state.SandboxRecord{ |
| 162 | SandboxID: "sandbox-a", | 172 | SandboxID: "sandbox-a", |
| @@ -190,7 +200,7 @@ func TestCheckpointSandboxPublishesCaptureAndAtomicallyAdvancesHead(t *testing.T | |||
| 190 | t.Fatalf("captured memory root still exists after publication: %v", err) | 200 | t.Fatalf("captured memory root still exists after publication: %v", err) |
| 191 | } | 201 | } |
| 192 | 202 | ||
| 193 | - t1, err := store.GetTemplate(ctx, result.TemplateID) | 203 | + t1, err := svc.Templates.Get(ctx, result.TemplateID) |
| 194 | if err != nil { | 204 | if err != nil { |
| 195 | t.Fatalf("GetTemplate(t1) error = %v", err) | 205 | t.Fatalf("GetTemplate(t1) error = %v", err) |
| 196 | } | 206 | } |
| @@ -215,7 +225,7 @@ func TestCheckpointSandboxPublishesCaptureAndAtomicallyAdvancesHead(t *testing.T | |||
| 215 | func TestCheckpointSandboxDoesNotPersistBeforeValidationSucceeds(t *testing.T) { | 225 | func TestCheckpointSandboxDoesNotPersistBeforeValidationSucceeds(t *testing.T) { |
| 216 | ctx := context.Background() | 226 | ctx := context.Background() |
| 217 | host := newRuntimeImageHost(t) | 227 | host := newRuntimeImageHost(t) |
| 218 | - sourceDigest := digest.FromString("checkpoint-source").String() | 228 | + sourceDigest := buildColdBootIndex(t, host, "checkpoint-source") |
| 219 | memRoot := t.TempDir() | 229 | memRoot := t.TempDir() |
| 220 | sandboxOps := &fakeSandboxOps{checkpointResults: []sandbox.CheckpointResult{{ | 230 | sandboxOps := &fakeSandboxOps{checkpointResults: []sandbox.CheckpointResult{{ |
| 221 | MemRootPath: memRoot, | 231 | MemRootPath: memRoot, |
| @@ -224,7 +234,13 @@ func TestCheckpointSandboxDoesNotPersistBeforeValidationSucceeds(t *testing.T) { | |||
| 224 | }}} | 234 | }}} |
| 225 | store := newTestStore(t) | 235 | store := newTestStore(t) |
| 226 | svc := New(sandboxOps, host.Client(), store) | 236 | svc := New(sandboxOps, host.Client(), store) |
| 227 | - seedTemplate(t, ctx, svc.Templates, sourceDigest, conchtemplate.BootModeCold) | 237 | + svc.Templates = host.TemplateStore() |
| 238 | + seedTemplate(t, ctx, host, sourceDigest, conchtemplate.BootModeCold) | ||
| 239 | + if err := host.Client().ContentStore().Delete( | ||
| 240 | + containerdclient.NewNamespaceContext(ctx), digest.Digest(sourceDigest), | ||
| 241 | + ); err != nil { | ||
| 242 | + t.Fatalf("delete source Boot Index content: %v", err) | ||
| 243 | + } | ||
| 228 | before := state.SandboxRecord{ | 244 | before := state.SandboxRecord{ |
| 229 | SandboxID: "sandbox-validation-failure", | 245 | SandboxID: "sandbox-validation-failure", |
| 230 | CheckpointHeadTemplateID: sourceDigest, | 246 | CheckpointHeadTemplateID: sourceDigest, |
| @@ -238,12 +254,18 @@ func TestCheckpointSandboxDoesNotPersistBeforeValidationSucceeds(t *testing.T) { | |||
| 238 | }); err == nil { | 254 | }); err == nil { |
| 239 | t.Fatal("CheckpointSandbox() error = nil, want validation failure") | 255 | t.Fatal("CheckpointSandbox() error = nil, want validation failure") |
| 240 | } | 256 | } |
| 241 | - templates, err := store.ListTemplates(ctx) | 257 | + records, err := host.Client().ImageService().List(containerdclient.NewNamespaceContext(ctx)) |
| 242 | if err != nil { | 258 | if err != nil { |
| 243 | - t.Fatalf("ListTemplates() error = %v", err) | 259 | + t.Fatalf("List image records: %v", err) |
| 244 | } | 260 | } |
| 245 | - if len(templates) != 1 || templates[0].BootIndexDigest != sourceDigest { | 261 | + canonicalRecords := 0 |
| 246 | - t.Fatalf("templates after failed validation = %#v, want only source template", templates) | 262 | + for _, record := range records { |
| 263 | + if conchimage.IsCanonicalTemplateRef(record.Name) { | ||
| 264 | + canonicalRecords++ | ||
| 265 | + } | ||
| 266 | + } | ||
| 267 | + if canonicalRecords != 1 { | ||
| 268 | + t.Fatalf("canonical records after failed validation = %d, want source only", canonicalRecords) | ||
| 247 | } | 269 | } |
| 248 | after, err := store.GetSandbox(ctx, before.SandboxID) | 270 | after, err := store.GetSandbox(ctx, before.SandboxID) |
| 249 | if err != nil { | 271 | if err != nil { |
| @@ -254,6 +276,43 @@ func TestCheckpointSandboxDoesNotPersistBeforeValidationSucceeds(t *testing.T) { | |||
| 254 | } | 276 | } |
| 255 | } | 277 | } |
| 256 | 278 | ||
| 279 | +func TestCheckpointSandboxRemovesTemplateWhenHeadAdvanceFails(t *testing.T) { | ||
| 280 | + ctx := context.Background() | ||
| 281 | + host := newRuntimeImageHost(t) | ||
| 282 | + sourceDigest := buildColdBootIndex(t, host, "checkpoint-cas-source") | ||
| 283 | + store := newTestStore(t) | ||
| 284 | + if err := store.UpsertSandbox(ctx, state.SandboxRecord{ | ||
| 285 | + SandboxID: "sandbox-cas", | ||
| 286 | + CheckpointHeadTemplateID: sourceDigest, | ||
| 287 | + }); err != nil { | ||
| 288 | + t.Fatal(err) | ||
| 289 | + } | ||
| 290 | + seedTemplate(t, ctx, host, sourceDigest, conchtemplate.BootModeCold) | ||
| 291 | + svc := New(&fakeSandboxOps{checkpointResults: []sandbox.CheckpointResult{{ | ||
| 292 | + MemRootPath: t.TempDir(), | ||
| 293 | + VMMName: "cloud-hypervisor", | ||
| 294 | + MemorySizeMB: 128, | ||
| 295 | + }}}, host.Client(), failingCheckpointStore{Store: store}) | ||
| 296 | + svc.Templates = host.TemplateStore() | ||
| 297 | + | ||
| 298 | + if _, err := svc.CheckpointSandbox(ctx, SandboxCheckpointOptions{SandboxID: "sandbox-cas"}); err == nil { | ||
| 299 | + t.Fatal("CheckpointSandbox() error = nil, want checkpoint head failure") | ||
| 300 | + } | ||
| 301 | + records, err := host.Client().ImageService().List(containerdclient.NewNamespaceContext(ctx)) | ||
| 302 | + if err != nil { | ||
| 303 | + t.Fatal(err) | ||
| 304 | + } | ||
| 305 | + canonical := make([]string, 0, len(records)) | ||
| 306 | + for _, record := range records { | ||
| 307 | + if conchimage.IsCanonicalTemplateRef(record.Name) { | ||
| 308 | + canonical = append(canonical, record.Target.Digest.String()) | ||
| 309 | + } | ||
| 310 | + } | ||
| 311 | + if len(canonical) != 1 || canonical[0] != sourceDigest { | ||
| 312 | + t.Fatalf("canonical Templates after CAS failure = %v, want source only", canonical) | ||
| 313 | + } | ||
| 314 | +} | ||
| 315 | + | ||
| 257 | func TestCheckpointSandboxBuildsConsecutiveTemplateLineage(t *testing.T) { | 316 | func TestCheckpointSandboxBuildsConsecutiveTemplateLineage(t *testing.T) { |
| 258 | ctx := context.Background() | 317 | ctx := context.Background() |
| 259 | host := newRuntimeImageHost(t) | 318 | host := newRuntimeImageHost(t) |
| @@ -266,7 +325,8 @@ func TestCheckpointSandboxBuildsConsecutiveTemplateLineage(t *testing.T) { | |||
| 266 | }} | 325 | }} |
| 267 | store := newTestStore(t) | 326 | store := newTestStore(t) |
| 268 | svc := New(sandboxOps, host.Client(), store) | 327 | svc := New(sandboxOps, host.Client(), store) |
| 269 | - seedTemplate(t, ctx, svc.Templates, t0Digest, conchtemplate.BootModeCold) | 328 | + svc.Templates = host.TemplateStore() |
| 329 | + seedTemplate(t, ctx, host, t0Digest, conchtemplate.BootModeCold) | ||
| 270 | if err := store.UpsertSandbox(ctx, state.SandboxRecord{ | 330 | if err := store.UpsertSandbox(ctx, state.SandboxRecord{ |
| 271 | SandboxID: "sandbox-lineage", | 331 | SandboxID: "sandbox-lineage", |
| 272 | CheckpointHeadTemplateID: t0Digest, | 332 | CheckpointHeadTemplateID: t0Digest, |
| @@ -286,11 +346,11 @@ func TestCheckpointSandboxBuildsConsecutiveTemplateLineage(t *testing.T) { | |||
| 286 | t.Fatalf("checkpoint digests = (%q, %q)", t1Result.TemplateID, t2Result.TemplateID) | 346 | t.Fatalf("checkpoint digests = (%q, %q)", t1Result.TemplateID, t2Result.TemplateID) |
| 287 | } | 347 | } |
| 288 | 348 | ||
| 289 | - t1, err := store.GetTemplate(ctx, t1Result.TemplateID) | 349 | + t1, err := svc.Templates.Get(ctx, t1Result.TemplateID) |
| 290 | if err != nil { | 350 | if err != nil { |
| 291 | t.Fatalf("GetTemplate(t1) error = %v", err) | 351 | t.Fatalf("GetTemplate(t1) error = %v", err) |
| 292 | } | 352 | } |
| 293 | - t2, err := store.GetTemplate(ctx, t2Result.TemplateID) | 353 | + t2, err := svc.Templates.Get(ctx, t2Result.TemplateID) |
| 294 | if err != nil { | 354 | if err != nil { |
| 295 | t.Fatalf("GetTemplate(t2) error = %v", err) | 355 | t.Fatalf("GetTemplate(t2) error = %v", err) |
| 296 | } | 356 | } |
| @@ -941,19 +1001,53 @@ func TestCreateTemplateRequiresContainerdClient(t *testing.T) { | |||
| 941 | } | 1001 | } |
| 942 | } | 1002 | } |
| 943 | 1003 | ||
| 1004 | +func TestCreateTemplateRejectsCanonicalTemplateSource(t *testing.T) { | ||
| 1005 | + ctx := context.Background() | ||
| 1006 | + host := newRuntimeImageHost(t) | ||
| 1007 | + bootIndexDigest := buildColdBootIndex(t, host, "canonical-rootfs-source") | ||
| 1008 | + seedTemplate(t, ctx, host, bootIndexDigest, conchtemplate.BootModeCold) | ||
| 1009 | + buildRef, err := conchimage.CanonicalTemplateRef(bootIndexDigest) | ||
| 1010 | + if err != nil { | ||
| 1011 | + t.Fatal(err) | ||
| 1012 | + } | ||
| 1013 | + | ||
| 1014 | + svc := New(nil, host.Client(), newTestStore(t)) | ||
| 1015 | + svc.Templates = host.TemplateStore() | ||
| 1016 | + _, err = svc.CreateTemplate(ctx, TemplateCreateOptions{ | ||
| 1017 | + Source: buildRef, | ||
| 1018 | + KernelPath: "unused-kernel", | ||
| 1019 | + InitrdPath: "unused-initrd", | ||
| 1020 | + }) | ||
| 1021 | + if !errors.Is(err, conchtemplate.ErrInvalidArgument) { | ||
| 1022 | + t.Fatalf("CreateTemplate() error = %v, want ErrInvalidArgument", err) | ||
| 1023 | + } | ||
| 1024 | + | ||
| 1025 | + record, err := host.Client().ImageService().Get(containerdclient.NewNamespaceContext(ctx), buildRef) | ||
| 1026 | + if err != nil { | ||
| 1027 | + t.Fatalf("get canonical Template image: %v", err) | ||
| 1028 | + } | ||
| 1029 | + if got := record.Labels[conchimage.ImageKindLabel]; got != conchimage.ImageKindBootIndexCold { | ||
| 1030 | + t.Fatalf("canonical Template image kind = %q, want %q", got, conchimage.ImageKindBootIndexCold) | ||
| 1031 | + } | ||
| 1032 | + if _, err := svc.Templates.Get(ctx, bootIndexDigest); err != nil { | ||
| 1033 | + t.Fatalf("Get() original Template after rejected create: %v", err) | ||
| 1034 | + } | ||
| 1035 | +} | ||
| 1036 | + | ||
| 944 | func TestUnpackTemplateResolvesBootIndexByDigest(t *testing.T) { | 1037 | func TestUnpackTemplateResolvesBootIndexByDigest(t *testing.T) { |
| 945 | ctx := context.Background() | 1038 | ctx := context.Background() |
| 946 | host := newRuntimeImageHost(t) | 1039 | host := newRuntimeImageHost(t) |
| 947 | bootIndexDigest := buildColdBootIndex(t, host, "explicit-unpack") | 1040 | bootIndexDigest := buildColdBootIndex(t, host, "explicit-unpack") |
| 948 | store := newTestStore(t) | 1041 | store := newTestStore(t) |
| 949 | svc := New(nil, host.Client(), store) | 1042 | svc := New(nil, host.Client(), store) |
| 1043 | + svc.Templates = host.TemplateStore() | ||
| 950 | 1044 | ||
| 951 | if _, err := svc.Templates.Create(ctx, conchtemplate.Entry{ | 1045 | if _, err := svc.Templates.Create(ctx, conchtemplate.Entry{ |
| 952 | Origin: conchtemplate.OriginImage, | 1046 | Origin: conchtemplate.OriginImage, |
| 953 | BootMode: conchtemplate.BootModeCold, | 1047 | BootMode: conchtemplate.BootModeCold, |
| 954 | BootIndexDigest: bootIndexDigest, | 1048 | BootIndexDigest: bootIndexDigest, |
| 955 | SourceRef: "not-the-boot-index:latest", | 1049 | SourceRef: "not-the-boot-index:latest", |
| 956 | - }); err != nil { | 1050 | + }, bootIndexTarget(t, host, bootIndexDigest)); err != nil { |
| 957 | t.Fatalf("create template: %v", err) | 1051 | t.Fatalf("create template: %v", err) |
| 958 | } | 1052 | } |
| 959 | 1053 | ||
| @@ -1030,20 +1124,35 @@ func buildColdBootIndex(t *testing.T, host *containerdhost.Host, name string) st | |||
| 1030 | func seedTemplate( | 1124 | func seedTemplate( |
| 1031 | t *testing.T, | 1125 | t *testing.T, |
| 1032 | ctx context.Context, | 1126 | ctx context.Context, |
| 1033 | - templates conchtemplate.Store, | 1127 | + host *containerdhost.Host, |
| 1034 | bootIndexDigest string, | 1128 | bootIndexDigest string, |
| 1035 | bootMode conchtemplate.BootMode, | 1129 | bootMode conchtemplate.BootMode, |
| 1036 | ) { | 1130 | ) { |
| 1037 | t.Helper() | 1131 | t.Helper() |
| 1038 | - if _, err := templates.Create(ctx, conchtemplate.Entry{ | 1132 | + if _, err := host.TemplateStore().Create(ctx, conchtemplate.Entry{ |
| 1039 | Origin: conchtemplate.OriginImage, | 1133 | Origin: conchtemplate.OriginImage, |
| 1040 | BootMode: bootMode, | 1134 | BootMode: bootMode, |
| 1041 | BootIndexDigest: bootIndexDigest, | 1135 | BootIndexDigest: bootIndexDigest, |
| 1042 | - }); err != nil { | 1136 | + }, bootIndexTarget(t, host, bootIndexDigest)); err != nil { |
| 1043 | t.Fatalf("CreateTemplate(%s) error = %v", bootIndexDigest, err) | 1137 | t.Fatalf("CreateTemplate(%s) error = %v", bootIndexDigest, err) |
| 1044 | } | 1138 | } |
| 1045 | } | 1139 | } |
| 1046 | 1140 | ||
| 1141 | +func bootIndexTarget(t *testing.T, host *containerdhost.Host, bootIndexDigest string) ocispec.Descriptor { | ||
| 1142 | + t.Helper() | ||
| 1143 | + info, err := host.Client().ContentStore().Info( | ||
| 1144 | + containerdclient.NewNamespaceContext(context.Background()), digest.Digest(bootIndexDigest), | ||
| 1145 | + ) | ||
| 1146 | + if err != nil { | ||
| 1147 | + t.Fatalf("resolve Boot Index %s: %v", bootIndexDigest, err) | ||
| 1148 | + } | ||
| 1149 | + return ocispec.Descriptor{ | ||
| 1150 | + MediaType: ocispec.MediaTypeImageIndex, | ||
| 1151 | + Digest: digest.Digest(bootIndexDigest), | ||
| 1152 | + Size: info.Size, | ||
| 1153 | + } | ||
| 1154 | +} | ||
| 1155 | + | ||
| 1047 | func newTestStore(t *testing.T) *state.BoltStore { | 1156 | func newTestStore(t *testing.T) *state.BoltStore { |
| 1048 | t.Helper() | 1157 | t.Helper() |
| 1049 | store, err := state.OpenBolt(t.TempDir() + "/state.db") | 1158 | store, err := state.OpenBolt(t.TempDir() + "/state.db") |
| @@ -127,9 +127,8 @@ func New(cfg *config.Config) (*Daemon, error) { | |||
| 127 | } | 127 | } |
| 128 | 128 | ||
| 129 | host, err := containerdhost.Start(ctx, containerdhost.Config{ | 129 | host, err := containerdhost.Start(ctx, containerdhost.Config{ |
| 130 | - RootDir: cfg.ContainerdRootDir(), | 130 | + RootDir: cfg.ContainerdRootDir(), |
| 131 | - StateDir: cfg.ContainerdStateDir(), | 131 | + StateDir: cfg.ContainerdStateDir(), |
| 132 | - TemplateStore: store, | ||
| 133 | Snapshot: containerdhost.SnapshotConfig{ | 132 | Snapshot: containerdhost.SnapshotConfig{ |
| 134 | WorkDir: cfg.Server.WorkDir, | 133 | WorkDir: cfg.Server.WorkDir, |
| 135 | }, | 134 | }, |
| @@ -9,9 +9,11 @@ import ( | |||
| 9 | "testing" | 9 | "testing" |
| 10 | 10 | ||
| 11 | digest "github.com/opencontainers/go-digest" | 11 | digest "github.com/opencontainers/go-digest" |
| 12 | + ocispec "github.com/opencontainers/image-spec/specs-go/v1" | ||
| 12 | "github.com/openeuler/Conch/internal/conchruntime" | 13 | "github.com/openeuler/Conch/internal/conchruntime" |
| 13 | "github.com/openeuler/Conch/internal/daemon/state" | 14 | "github.com/openeuler/Conch/internal/daemon/state" |
| 14 | "github.com/openeuler/Conch/internal/sandbox" | 15 | "github.com/openeuler/Conch/internal/sandbox" |
| 16 | + conchtemplate "github.com/openeuler/Conch/internal/template" | ||
| 15 | ) | 17 | ) |
| 16 | 18 | ||
| 17 | const ( | 19 | const ( |
| @@ -204,10 +206,9 @@ func TestHandleInspectMissingTemplateReturnsDomainError(t *testing.T) { | |||
| 204 | } | 206 | } |
| 205 | t.Cleanup(func() { _ = store.Close() }) | 207 | t.Cleanup(func() { _ = store.Close() }) |
| 206 | 208 | ||
| 207 | - server := &Daemon{ | 209 | + runtimeService := conchruntime.New(nil, nil, store) |
| 208 | - router: http.NewServeMux(), | 210 | + runtimeService.Templates = missingTemplateStore{} |
| 209 | - runtimeService: conchruntime.New(nil, nil, store), | 211 | + server := &Daemon{router: http.NewServeMux(), runtimeService: runtimeService} |
| 210 | - } | ||
| 211 | server.routes() | 212 | server.routes() |
| 212 | recorder := httptest.NewRecorder() | 213 | recorder := httptest.NewRecorder() |
| 213 | missingDigest := digest.FromString("missing-template").String() | 214 | missingDigest := digest.FromString("missing-template").String() |
| @@ -226,3 +227,19 @@ func TestHandleInspectMissingTemplateReturnsDomainError(t *testing.T) { | |||
| 226 | t.Fatalf("response = %#v, error = %v", response, err) | 227 | t.Fatalf("response = %#v, error = %v", response, err) |
| 227 | } | 228 | } |
| 228 | } | 229 | } |
| 230 | + | ||
| 231 | +type missingTemplateStore struct{} | ||
| 232 | + | ||
| 233 | +func (missingTemplateStore) Create(context.Context, conchtemplate.Entry, ocispec.Descriptor) (conchtemplate.Entry, error) { | ||
| 234 | + return conchtemplate.Entry{}, conchtemplate.ErrNotFound.New() | ||
| 235 | +} | ||
| 236 | + | ||
| 237 | +func (missingTemplateStore) Get(context.Context, string) (conchtemplate.Entry, error) { | ||
| 238 | + return conchtemplate.Entry{}, conchtemplate.ErrNotFound.New() | ||
| 239 | +} | ||
| 240 | + | ||
| 241 | +func (missingTemplateStore) List(context.Context, conchtemplate.Filter) ([]conchtemplate.Entry, error) { | ||
| 242 | + return nil, nil | ||
| 243 | +} | ||
| 244 | + | ||
| 245 | +func (missingTemplateStore) Delete(context.Context, string) error { return nil } | ||
| @@ -11,32 +11,18 @@ import ( | |||
| 11 | "time" | 11 | "time" |
| 12 | 12 | ||
| 13 | bolt "go.etcd.io/bbolt" | 13 | bolt "go.etcd.io/bbolt" |
| 14 | - | ||
| 15 | - conchtemplate "github.com/openeuler/Conch/internal/template" | ||
| 16 | ) | 14 | ) |
| 17 | 15 | ||
| 18 | var ErrNotFound = errors.New("state record not found") | 16 | var ErrNotFound = errors.New("state record not found") |
| 19 | 17 | ||
| 20 | var buckets = [][]byte{ | 18 | var buckets = [][]byte{ |
| 21 | []byte("sandboxes"), | 19 | []byte("sandboxes"), |
| 22 | - []byte("templates"), | ||
| 23 | } | 20 | } |
| 24 | 21 | ||
| 25 | type BoltStore struct { | 22 | type BoltStore struct { |
| 26 | db *bolt.DB | 23 | db *bolt.DB |
| 27 | } | 24 | } |
| 28 | 25 | ||
| 29 | -type templateRecord struct { | ||
| 30 | - Origin string `json:"origin"` | ||
| 31 | - BootMode string `json:"boot_mode"` | ||
| 32 | - BootIndexDigest string `json:"boot_index_digest"` | ||
| 33 | - ParentBootIndexDigest string `json:"parent_boot_index_digest,omitempty"` | ||
| 34 | - SourceSandboxID string `json:"source_sandbox_id,omitempty"` | ||
| 35 | - SourceRef string `json:"source_ref,omitempty"` | ||
| 36 | - Labels map[string]string `json:"labels,omitempty"` | ||
| 37 | - CreatedAt int64 `json:"created_at"` | ||
| 38 | -} | ||
| 39 | - | ||
| 40 | func OpenBolt(path string) (*BoltStore, error) { | 26 | func OpenBolt(path string) (*BoltStore, error) { |
| 41 | if path == "" { | 27 | if path == "" { |
| 42 | return nil, fmt.Errorf("state db path is required") | 28 | return nil, fmt.Errorf("state db path is required") |
| @@ -159,140 +145,31 @@ func (s *BoltStore) DeleteSandbox(ctx context.Context, id string) error { | |||
| 159 | return s.delete(ctx, []byte("sandboxes"), id) | 145 | return s.delete(ctx, []byte("sandboxes"), id) |
| 160 | } | 146 | } |
| 161 | 147 | ||
| 162 | -func (s *BoltStore) CreateTemplate(_ context.Context, entry conchtemplate.Entry) error { | 148 | +func (s *BoltStore) AdvanceCheckpointHead(_ context.Context, sandboxID, expectedDigest, nextDigest string) error { |
| 163 | - rec := templateRecordFromEntry(entry) | 149 | + sandboxID = strings.TrimSpace(sandboxID) |
| 164 | - data, err := json.Marshal(rec) | 150 | + expectedDigest = strings.TrimSpace(expectedDigest) |
| 165 | - if err != nil { | 151 | + nextDigest = strings.TrimSpace(nextDigest) |
| 166 | - return fmt.Errorf("marshal template entry: %w", err) | 152 | + if sandboxID == "" || expectedDigest == "" || nextDigest == "" { |
| 153 | + return fmt.Errorf("sandbox id and checkpoint head digests are required") | ||
| 167 | } | 154 | } |
| 168 | return s.db.Update(func(tx *bolt.Tx) error { | 155 | return s.db.Update(func(tx *bolt.Tx) error { |
| 169 | - templates := tx.Bucket([]byte("templates")) | ||
| 170 | - if templates.Get([]byte(entry.BootIndexDigest)) != nil { | ||
| 171 | - return fmt.Errorf("%w: %s", conchtemplate.ErrAlreadyExists, entry.BootIndexDigest) | ||
| 172 | - } | ||
| 173 | - return templates.Put([]byte(entry.BootIndexDigest), data) | ||
| 174 | - }) | ||
| 175 | -} | ||
| 176 | - | ||
| 177 | -func (s *BoltStore) GetTemplate(ctx context.Context, bootIndexDigest string) (conchtemplate.Entry, error) { | ||
| 178 | - var rec templateRecord | ||
| 179 | - if err := s.get(ctx, []byte("templates"), bootIndexDigest, &rec); err != nil { | ||
| 180 | - if errors.Is(err, ErrNotFound) { | ||
| 181 | - return conchtemplate.Entry{}, conchtemplate.ErrNotFound.Wrap(err) | ||
| 182 | - } | ||
| 183 | - return conchtemplate.Entry{}, err | ||
| 184 | - } | ||
| 185 | - return templateEntryFromRecord(rec), nil | ||
| 186 | -} | ||
| 187 | - | ||
| 188 | -func (s *BoltStore) ListTemplates(ctx context.Context) ([]conchtemplate.Entry, error) { | ||
| 189 | - var out []conchtemplate.Entry | ||
| 190 | - err := s.list(ctx, []byte("templates"), func(data []byte) error { | ||
| 191 | - var rec templateRecord | ||
| 192 | - if err := json.Unmarshal(data, &rec); err != nil { | ||
| 193 | - return err | ||
| 194 | - } | ||
| 195 | - out = append(out, templateEntryFromRecord(rec)) | ||
| 196 | - return nil | ||
| 197 | - }) | ||
| 198 | - return out, err | ||
| 199 | -} | ||
| 200 | - | ||
| 201 | -func (s *BoltStore) DeleteTemplate(ctx context.Context, bootIndexDigest string) error { | ||
| 202 | - return s.delete(ctx, []byte("templates"), bootIndexDigest) | ||
| 203 | -} | ||
| 204 | - | ||
| 205 | -// PublishCheckpoint atomically creates a complete checkpoint Template Entry | ||
| 206 | -// and advances the Sandbox checkpoint head. Content publication and validation | ||
| 207 | -// happen before this transaction, so a failed transaction can only leave safe | ||
| 208 | -// orphaned content. | ||
| 209 | -func (s *BoltStore) PublishCheckpoint(_ context.Context, checkpoint conchtemplate.Entry) error { | ||
| 210 | - entry := checkpoint | ||
| 211 | - bootIndexDigest := entry.BootIndexDigest | ||
| 212 | - sandboxID := strings.TrimSpace(entry.SourceSandboxID) | ||
| 213 | - if sandboxID == "" { | ||
| 214 | - return fmt.Errorf("sandbox id is required") | ||
| 215 | - } | ||
| 216 | - expectedHeadDigest := strings.TrimSpace(entry.ParentBootIndexDigest) | ||
| 217 | - if expectedHeadDigest == "" { | ||
| 218 | - return fmt.Errorf("expected checkpoint head boot index digest is required") | ||
| 219 | - } | ||
| 220 | - if entry.Origin != conchtemplate.OriginCheckpoint { | ||
| 221 | - return fmt.Errorf( | ||
| 222 | - "checkpoint template origin is %q, want %q", | ||
| 223 | - entry.Origin, | ||
| 224 | - conchtemplate.OriginCheckpoint, | ||
| 225 | - ) | ||
| 226 | - } | ||
| 227 | - if entry.BootMode != conchtemplate.BootModeResume { | ||
| 228 | - return fmt.Errorf( | ||
| 229 | - "checkpoint template boot mode is %q, want %q", | ||
| 230 | - entry.BootMode, | ||
| 231 | - conchtemplate.BootModeResume, | ||
| 232 | - ) | ||
| 233 | - } | ||
| 234 | - templateData, err := json.Marshal(templateRecordFromEntry(entry)) | ||
| 235 | - if err != nil { | ||
| 236 | - return fmt.Errorf("marshal template entry: %w", err) | ||
| 237 | - } | ||
| 238 | - | ||
| 239 | - return s.db.Update(func(tx *bolt.Tx) error { | ||
| 240 | - templates := tx.Bucket([]byte("templates")) | ||
| 241 | sandboxes := tx.Bucket([]byte("sandboxes")) | 156 | sandboxes := tx.Bucket([]byte("sandboxes")) |
| 242 | - if templates.Get([]byte(bootIndexDigest)) != nil { | 157 | + data := sandboxes.Get([]byte(sandboxID)) |
| 243 | - return fmt.Errorf("%w: %s", conchtemplate.ErrAlreadyExists, bootIndexDigest) | 158 | + if data == nil { |
| 244 | - } | ||
| 245 | - | ||
| 246 | - var sandboxRecord SandboxRecord | ||
| 247 | - if data := sandboxes.Get([]byte(sandboxID)); data == nil { | ||
| 248 | return fmt.Errorf("%w: %s", ErrNotFound, sandboxID) | 159 | return fmt.Errorf("%w: %s", ErrNotFound, sandboxID) |
| 249 | - } else if err := json.Unmarshal(data, &sandboxRecord); err != nil { | 160 | + } |
| 161 | + var record SandboxRecord | ||
| 162 | + if err := json.Unmarshal(data, &record); err != nil { | ||
| 250 | return fmt.Errorf("unmarshal sandbox record %s: %w", sandboxID, err) | 163 | return fmt.Errorf("unmarshal sandbox record %s: %w", sandboxID, err) |
| 251 | } | 164 | } |
| 252 | - currentHeadDigest := strings.TrimSpace(sandboxRecord.CheckpointHeadTemplateID) | 165 | + if record.CheckpointHeadTemplateID != expectedDigest { |
| 253 | - if currentHeadDigest == "" { | 166 | + return fmt.Errorf("sandbox %s checkpoint head changed from %s to %s", sandboxID, expectedDigest, record.CheckpointHeadTemplateID) |
| 254 | - return fmt.Errorf("sandbox %s has no checkpoint head Template ID", sandboxID) | ||
| 255 | } | 167 | } |
| 256 | - if currentHeadDigest != expectedHeadDigest { | 168 | + record.CheckpointHeadTemplateID = nextDigest |
| 257 | - return fmt.Errorf("sandbox %s checkpoint head boot index digest changed from %s to %s", sandboxID, expectedHeadDigest, currentHeadDigest) | 169 | + data, err := json.Marshal(record) |
| 258 | - } | ||
| 259 | - if sourceID := entry.SourceSandboxID; sourceID != strings.TrimSpace(sandboxRecord.SandboxID) { | ||
| 260 | - return fmt.Errorf("template %s source sandbox %s does not match %s", bootIndexDigest, sourceID, sandboxRecord.SandboxID) | ||
| 261 | - } | ||
| 262 | - sandboxRecord.CheckpointHeadTemplateID = entry.BootIndexDigest | ||
| 263 | - sandboxData, err := json.Marshal(sandboxRecord) | ||
| 264 | if err != nil { | 170 | if err != nil { |
| 265 | return fmt.Errorf("marshal sandbox record: %w", err) | 171 | return fmt.Errorf("marshal sandbox record: %w", err) |
| 266 | } | 172 | } |
| 267 | - if err := templates.Put([]byte(bootIndexDigest), templateData); err != nil { | 173 | + return sandboxes.Put([]byte(sandboxID), data) |
| 268 | - return err | ||
| 269 | - } | ||
| 270 | - return sandboxes.Put([]byte(sandboxID), sandboxData) | ||
| 271 | }) | 174 | }) |
| 272 | } | 175 | } |
| 273 | - | ||
| 274 | -func templateRecordFromEntry(entry conchtemplate.Entry) templateRecord { | ||
| 275 | - return templateRecord{ | ||
| 276 | - Origin: string(entry.Origin), | ||
| 277 | - BootMode: string(entry.BootMode), | ||
| 278 | - BootIndexDigest: entry.BootIndexDigest, | ||
| 279 | - ParentBootIndexDigest: entry.ParentBootIndexDigest, | ||
| 280 | - SourceSandboxID: entry.SourceSandboxID, | ||
| 281 | - SourceRef: entry.SourceRef, | ||
| 282 | - Labels: entry.Labels, | ||
| 283 | - CreatedAt: entry.CreatedAt, | ||
| 284 | - } | ||
| 285 | -} | ||
| 286 | - | ||
| 287 | -func templateEntryFromRecord(rec templateRecord) conchtemplate.Entry { | ||
| 288 | - return conchtemplate.Entry{ | ||
| 289 | - Origin: conchtemplate.Origin(rec.Origin), | ||
| 290 | - BootMode: conchtemplate.BootMode(rec.BootMode), | ||
| 291 | - BootIndexDigest: rec.BootIndexDigest, | ||
| 292 | - ParentBootIndexDigest: rec.ParentBootIndexDigest, | ||
| 293 | - SourceSandboxID: rec.SourceSandboxID, | ||
| 294 | - SourceRef: rec.SourceRef, | ||
| 295 | - Labels: rec.Labels, | ||
| 296 | - CreatedAt: rec.CreatedAt, | ||
| 297 | - } | ||
| 298 | -} | ||
| @@ -2,23 +2,13 @@ package state | |||
| 2 | 2 | ||
| 3 | import ( | 3 | import ( |
| 4 | "context" | 4 | "context" |
| 5 | - "encoding/json" | ||
| 6 | - "errors" | ||
| 7 | "testing" | 5 | "testing" |
| 8 | 6 | ||
| 9 | - "github.com/opencontainers/go-digest" | ||
| 10 | bolt "go.etcd.io/bbolt" | 7 | bolt "go.etcd.io/bbolt" |
| 11 | - | ||
| 12 | - conchtemplate "github.com/openeuler/Conch/internal/template" | ||
| 13 | ) | 8 | ) |
| 14 | 9 | ||
| 15 | func TestBoltStoreSandboxCRUD(t *testing.T) { | 10 | func TestBoltStoreSandboxCRUD(t *testing.T) { |
| 16 | - store, err := OpenBolt(t.TempDir() + "/state.db") | 11 | + store := newBoltStore(t) |
| 17 | - if err != nil { | ||
| 18 | - t.Fatalf("OpenBolt() error = %v", err) | ||
| 19 | - } | ||
| 20 | - defer store.Close() | ||
| 21 | - | ||
| 22 | ctx := context.Background() | 12 | ctx := context.Background() |
| 23 | sandbox := SandboxRecord{ | 13 | sandbox := SandboxRecord{ |
| 24 | SandboxID: "sandbox-1", | 14 | SandboxID: "sandbox-1", |
| @@ -27,92 +17,69 @@ func TestBoltStoreSandboxCRUD(t *testing.T) { | |||
| 27 | if err := store.UpsertSandbox(ctx, sandbox); err != nil { | 17 | if err := store.UpsertSandbox(ctx, sandbox); err != nil { |
| 28 | t.Fatalf("UpsertSandbox() error = %v", err) | 18 | t.Fatalf("UpsertSandbox() error = %v", err) |
| 29 | } | 19 | } |
| 30 | - duplicate := sandbox | 20 | + sandbox.CheckpointHeadTemplateID = "template-replacement" |
| 31 | - duplicate.CheckpointHeadTemplateID = "template-replacement" | 21 | + if err := store.UpsertSandbox(ctx, sandbox); err != nil { |
| 32 | - if err := store.UpsertSandbox(ctx, duplicate); err != nil { | ||
| 33 | t.Fatalf("UpsertSandbox(duplicate) error = %v", err) | 22 | t.Fatalf("UpsertSandbox(duplicate) error = %v", err) |
| 34 | } | 23 | } |
| 35 | - gotSandbox, err := store.GetSandbox(ctx, sandbox.SandboxID) | 24 | + got, err := store.GetSandbox(ctx, sandbox.SandboxID) |
| 36 | - if err != nil { | 25 | + if err != nil || got != sandbox { |
| 37 | - t.Fatalf("GetSandbox() error = %v", err) | 26 | + t.Fatalf("GetSandbox() = %#v, %v; want %#v", got, err, sandbox) |
| 38 | } | 27 | } |
| 39 | - if gotSandbox != duplicate { | ||
| 40 | - t.Fatalf("GetSandbox() = %#v, want %#v", gotSandbox, duplicate) | ||
| 41 | - } | ||
| 42 | - | ||
| 43 | if err := store.DeleteSandbox(ctx, sandbox.SandboxID); err != nil { | 28 | if err := store.DeleteSandbox(ctx, sandbox.SandboxID); err != nil { |
| 44 | t.Fatalf("DeleteSandbox() error = %v", err) | 29 | t.Fatalf("DeleteSandbox() error = %v", err) |
| 45 | } | 30 | } |
| 46 | if _, err := store.GetSandbox(ctx, sandbox.SandboxID); err == nil { | 31 | if _, err := store.GetSandbox(ctx, sandbox.SandboxID); err == nil { |
| 47 | - t.Fatalf("GetSandbox() after delete got nil error") | 32 | + t.Fatal("GetSandbox() after delete error = nil") |
| 48 | } | 33 | } |
| 49 | } | 34 | } |
| 50 | 35 | ||
| 51 | func TestBoltStoreRejectsIncompleteSandboxRecord(t *testing.T) { | 36 | func TestBoltStoreRejectsIncompleteSandboxRecord(t *testing.T) { |
| 52 | - store, err := OpenBolt(t.TempDir() + "/state.db") | 37 | + store := newBoltStore(t) |
| 53 | - if err != nil { | ||
| 54 | - t.Fatalf("OpenBolt() error = %v", err) | ||
| 55 | - } | ||
| 56 | - defer store.Close() | ||
| 57 | - | ||
| 58 | valid := SandboxRecord{ | 38 | valid := SandboxRecord{ |
| 59 | SandboxID: "sandbox-1", | 39 | SandboxID: "sandbox-1", |
| 60 | CheckpointHeadTemplateID: "template-1", | 40 | CheckpointHeadTemplateID: "template-1", |
| 61 | } | 41 | } |
| 62 | - tests := []struct { | 42 | + for _, tc := range []struct { |
| 63 | name string | 43 | name string |
| 64 | mutate func(*SandboxRecord) | 44 | mutate func(*SandboxRecord) |
| 65 | }{ | 45 | }{ |
| 66 | {name: "sandbox id", mutate: func(rec *SandboxRecord) { rec.SandboxID = "" }}, | 46 | {name: "sandbox id", mutate: func(rec *SandboxRecord) { rec.SandboxID = "" }}, |
| 67 | - {name: "checkpoint head digest", mutate: func(rec *SandboxRecord) { rec.CheckpointHeadTemplateID = "" }}, | 47 | + {name: "checkpoint head", mutate: func(rec *SandboxRecord) { rec.CheckpointHeadTemplateID = "" }}, |
| 68 | - } | 48 | + } { |
| 69 | - for _, tc := range tests { | ||
| 70 | t.Run(tc.name, func(t *testing.T) { | 49 | t.Run(tc.name, func(t *testing.T) { |
| 71 | - rec := valid | 50 | + record := valid |
| 72 | - tc.mutate(&rec) | 51 | + tc.mutate(&record) |
| 73 | - if err := store.UpsertSandbox(context.Background(), rec); err == nil { | 52 | + if err := store.UpsertSandbox(context.Background(), record); err == nil { |
| 74 | - t.Fatal("UpsertSandbox() error = nil, want incomplete record rejection") | 53 | + t.Fatal("UpsertSandbox() error = nil") |
| 75 | } | 54 | } |
| 76 | }) | 55 | }) |
| 77 | } | 56 | } |
| 78 | } | 57 | } |
| 79 | 58 | ||
| 80 | func TestBoltStoreAcceptsCreatingSandboxRecord(t *testing.T) { | 59 | func TestBoltStoreAcceptsCreatingSandboxRecord(t *testing.T) { |
| 81 | - store, err := OpenBolt(t.TempDir() + "/state.db") | 60 | + store := newBoltStore(t) |
| 82 | - if err != nil { | 61 | + record := SandboxRecord{ |
| 83 | - t.Fatalf("OpenBolt() error = %v", err) | ||
| 84 | - } | ||
| 85 | - defer store.Close() | ||
| 86 | - | ||
| 87 | - rec := SandboxRecord{ | ||
| 88 | SandboxID: "sandbox-creating", | 62 | SandboxID: "sandbox-creating", |
| 89 | State: SandboxCreating, | 63 | State: SandboxCreating, |
| 90 | SourceTemplateID: "template-1", | 64 | SourceTemplateID: "template-1", |
| 91 | } | 65 | } |
| 92 | - if err := store.UpsertSandbox(context.Background(), rec); err != nil { | 66 | + if err := store.UpsertSandbox(context.Background(), record); err != nil { |
| 93 | t.Fatalf("UpsertSandbox() error = %v", err) | 67 | t.Fatalf("UpsertSandbox() error = %v", err) |
| 94 | } | 68 | } |
| 95 | - got, err := store.GetSandbox(context.Background(), rec.SandboxID) | 69 | + got, err := store.GetSandbox(context.Background(), record.SandboxID) |
| 96 | - if err != nil { | 70 | + if err != nil || got != record { |
| 97 | - t.Fatalf("GetSandbox() error = %v", err) | 71 | + t.Fatalf("GetSandbox() = %#v, %v; want %#v", got, err, record) |
| 98 | - } | ||
| 99 | - if got != rec { | ||
| 100 | - t.Fatalf("GetSandbox() = %#v, want %#v", got, rec) | ||
| 101 | } | 72 | } |
| 102 | } | 73 | } |
| 103 | 74 | ||
| 104 | -func TestBoltStoreInitializesCurrentBuckets(t *testing.T) { | 75 | +func TestBoltStoreInitializesOnlySandboxBucket(t *testing.T) { |
| 105 | - store, err := OpenBolt(t.TempDir() + "/state.db") | 76 | + store := newBoltStore(t) |
| 106 | - if err != nil { | ||
| 107 | - t.Fatalf("OpenBolt() error = %v", err) | ||
| 108 | - } | ||
| 109 | - defer store.Close() | ||
| 110 | - | ||
| 111 | if err := store.db.View(func(tx *bolt.Tx) error { | 77 | if err := store.db.View(func(tx *bolt.Tx) error { |
| 112 | - for _, bucket := range buckets { | 78 | + if tx.Bucket([]byte("sandboxes")) == nil { |
| 113 | - if tx.Bucket(bucket) == nil { | 79 | + t.Fatal("sandboxes bucket is missing") |
| 114 | - t.Fatalf("bucket %q is missing", bucket) | 80 | + } |
| 115 | - } | 81 | + if tx.Bucket([]byte("templates")) != nil { |
| 82 | + t.Fatal("templates bucket must not be owned by state.db") | ||
| 116 | } | 83 | } |
| 117 | return nil | 84 | return nil |
| 118 | }); err != nil { | 85 | }); err != nil { |
| @@ -120,179 +87,52 @@ func TestBoltStoreInitializesCurrentBuckets(t *testing.T) { | |||
| 120 | } | 87 | } |
| 121 | } | 88 | } |
| 122 | 89 | ||
| 123 | -func TestBoltStoreTemplateCRUD(t *testing.T) { | 90 | +func TestBoltStoreAdvanceCheckpointHead(t *testing.T) { |
| 124 | - store, err := OpenBolt(t.TempDir() + "/state.db") | 91 | + store := newBoltStore(t) |
| 125 | - if err != nil { | ||
| 126 | - t.Fatalf("OpenBolt() error = %v", err) | ||
| 127 | - } | ||
| 128 | - defer store.Close() | ||
| 129 | - | ||
| 130 | ctx := context.Background() | 92 | ctx := context.Background() |
| 131 | - rec := conchtemplate.Entry{ | ||
| 132 | - Origin: conchtemplate.OriginImage, | ||
| 133 | - BootMode: conchtemplate.BootModeCold, | ||
| 134 | - BootIndexDigest: digest.FromString("template-1").String(), | ||
| 135 | - Labels: map[string]string{"purpose": "test"}, | ||
| 136 | - CreatedAt: 1, | ||
| 137 | - } | ||
| 138 | - if err := store.CreateTemplate(ctx, rec); err != nil { | ||
| 139 | - t.Fatalf("CreateTemplate() error = %v", err) | ||
| 140 | - } | ||
| 141 | - got, err := store.GetTemplate(ctx, rec.BootIndexDigest) | ||
| 142 | - if err != nil { | ||
| 143 | - t.Fatalf("GetTemplate() error = %v", err) | ||
| 144 | - } | ||
| 145 | - if got.Origin != rec.Origin || got.Labels["purpose"] != "test" { | ||
| 146 | - t.Fatalf("GetTemplate() = %#v, want %#v", got, rec) | ||
| 147 | - } | ||
| 148 | - | ||
| 149 | - duplicate := rec | ||
| 150 | - duplicate.Origin = conchtemplate.OriginCheckpoint | ||
| 151 | - duplicate.BootMode = conchtemplate.BootModeResume | ||
| 152 | - if err := store.CreateTemplate(ctx, duplicate); !errors.Is(err, conchtemplate.ErrAlreadyExists) { | ||
| 153 | - t.Fatalf("CreateTemplate(duplicate) error = %v, want ErrAlreadyExists", err) | ||
| 154 | - } | ||
| 155 | - got, err = store.GetTemplate(ctx, rec.BootIndexDigest) | ||
| 156 | - if err != nil { | ||
| 157 | - t.Fatalf("GetTemplate() after duplicate error = %v", err) | ||
| 158 | - } | ||
| 159 | - if got.BootIndexDigest != rec.BootIndexDigest { | ||
| 160 | - t.Fatalf("duplicate CreateTemplate overwrote digest: got %q, want %q", got.BootIndexDigest, rec.BootIndexDigest) | ||
| 161 | - } | ||
| 162 | - items, err := store.ListTemplates(ctx) | ||
| 163 | - if err != nil { | ||
| 164 | - t.Fatalf("ListTemplates() error = %v", err) | ||
| 165 | - } | ||
| 166 | - if len(items) != 1 || items[0].BootIndexDigest != rec.BootIndexDigest { | ||
| 167 | - t.Fatalf("ListTemplates() = %#v", items) | ||
| 168 | - } | ||
| 169 | - if err := store.DeleteTemplate(ctx, rec.BootIndexDigest); err != nil { | ||
| 170 | - t.Fatalf("DeleteTemplate() error = %v", err) | ||
| 171 | - } | ||
| 172 | - if err := store.DeleteTemplate(ctx, rec.BootIndexDigest); err != nil { | ||
| 173 | - t.Fatalf("DeleteTemplate() second call error = %v", err) | ||
| 174 | - } | ||
| 175 | - if _, err := store.GetTemplate(ctx, rec.BootIndexDigest); err == nil { | ||
| 176 | - t.Fatalf("GetTemplate() after delete got nil error") | ||
| 177 | - } | ||
| 178 | -} | ||
| 179 | - | ||
| 180 | -func TestBoltStoreLeavesTemplateValidationToDomain(t *testing.T) { | ||
| 181 | - store, err := OpenBolt(t.TempDir() + "/state.db") | ||
| 182 | - if err != nil { | ||
| 183 | - t.Fatalf("OpenBolt() error = %v", err) | ||
| 184 | - } | ||
| 185 | - defer store.Close() | ||
| 186 | - | ||
| 187 | - record := templateRecord{ | ||
| 188 | - Origin: string(conchtemplate.OriginImage), | ||
| 189 | - BootMode: string(conchtemplate.BootModeCold), | ||
| 190 | - BootIndexDigest: "not-a-digest", | ||
| 191 | - } | ||
| 192 | - data, err := json.Marshal(record) | ||
| 193 | - if err != nil { | ||
| 194 | - t.Fatal(err) | ||
| 195 | - } | ||
| 196 | - if err := store.db.Update(func(tx *bolt.Tx) error { | ||
| 197 | - return tx.Bucket([]byte("templates")).Put([]byte(record.BootIndexDigest), data) | ||
| 198 | - }); err != nil { | ||
| 199 | - t.Fatal(err) | ||
| 200 | - } | ||
| 201 | - | ||
| 202 | - ctx := context.Background() | ||
| 203 | - got, err := store.GetTemplate(ctx, record.BootIndexDigest) | ||
| 204 | - if err != nil { | ||
| 205 | - t.Fatalf("GetTemplate() error = %v", err) | ||
| 206 | - } | ||
| 207 | - if got.BootIndexDigest != record.BootIndexDigest { | ||
| 208 | - t.Fatalf("GetTemplate() = %#v", got) | ||
| 209 | - } | ||
| 210 | - items, err := store.ListTemplates(ctx) | ||
| 211 | - if err != nil { | ||
| 212 | - t.Fatalf("ListTemplates() error = %v", err) | ||
| 213 | - } | ||
| 214 | - if len(items) != 1 || items[0].BootIndexDigest != record.BootIndexDigest { | ||
| 215 | - t.Fatalf("ListTemplates() = %#v", items) | ||
| 216 | - } | ||
| 217 | - if _, err := conchtemplate.NewStore(store).List(ctx, conchtemplate.Filter{}); err == nil { | ||
| 218 | - t.Fatal("domain List() error = nil, want invalid digest rejection") | ||
| 219 | - } | ||
| 220 | -} | ||
| 221 | - | ||
| 222 | -func TestBoltStorePublishCheckpointAdvancesHeadAtomically(t *testing.T) { | ||
| 223 | - store, err := OpenBolt(t.TempDir() + "/state.db") | ||
| 224 | - if err != nil { | ||
| 225 | - t.Fatalf("OpenBolt() error = %v", err) | ||
| 226 | - } | ||
| 227 | - defer store.Close() | ||
| 228 | - | ||
| 229 | - ctx := context.Background() | ||
| 230 | - checkpointDigest := digest.FromString("checkpoint").String() | ||
| 231 | - sourceDigest := digest.FromString("source").String() | ||
| 232 | - if err := store.UpsertSandbox(ctx, SandboxRecord{ | ||
| 233 | - SandboxID: "sb-1", | ||
| 234 | - CheckpointHeadTemplateID: sourceDigest, | ||
| 235 | - }); err != nil { | ||
| 236 | - t.Fatal(err) | ||
| 237 | - } | ||
| 238 | - | ||
| 239 | - if err := store.PublishCheckpoint(ctx, conchtemplate.Entry{ | ||
| 240 | - Origin: conchtemplate.OriginCheckpoint, | ||
| 241 | - BootMode: conchtemplate.BootModeResume, | ||
| 242 | - BootIndexDigest: checkpointDigest, | ||
| 243 | - ParentBootIndexDigest: sourceDigest, | ||
| 244 | - SourceSandboxID: "sb-1", | ||
| 245 | - }); err != nil { | ||
| 246 | - t.Fatalf("PublishCheckpoint() error = %v", err) | ||
| 247 | - } | ||
| 248 | - templateRecord, err := store.GetTemplate(ctx, checkpointDigest) | ||
| 249 | - if err != nil { | ||
| 250 | - t.Fatal(err) | ||
| 251 | - } | ||
| 252 | - if templateRecord.BootIndexDigest != checkpointDigest || | ||
| 253 | - templateRecord.Origin != conchtemplate.OriginCheckpoint || | ||
| 254 | - templateRecord.BootMode != conchtemplate.BootModeResume { | ||
| 255 | - t.Fatalf("published template = %#v", templateRecord) | ||
| 256 | - } | ||
| 257 | - sandboxRecord, err := store.GetSandbox(ctx, "sb-1") | ||
| 258 | - if err != nil { | ||
| 259 | - t.Fatal(err) | ||
| 260 | - } | ||
| 261 | - if sandboxRecord.CheckpointHeadTemplateID != checkpointDigest { | ||
| 262 | - t.Fatalf("checkpoint head = %#v", sandboxRecord) | ||
| 263 | - } | ||
| 264 | -} | ||
| 265 | - | ||
| 266 | -func TestBoltStorePublishCheckpointCASFailureLeavesBothRecordsUnchanged(t *testing.T) { | ||
| 267 | - store, err := OpenBolt(t.TempDir() + "/state.db") | ||
| 268 | - if err != nil { | ||
| 269 | - t.Fatalf("OpenBolt() error = %v", err) | ||
| 270 | - } | ||
| 271 | - defer store.Close() | ||
| 272 | - ctx := context.Background() | ||
| 273 | - newHeadDigest := digest.FromString("new-head").String() | ||
| 274 | - oldHeadDigest := digest.FromString("old-head").String() | ||
| 275 | - checkpointDigest := digest.FromString("checkpoint").String() | ||
| 276 | if err := store.UpsertSandbox(ctx, SandboxRecord{ | 93 | if err := store.UpsertSandbox(ctx, SandboxRecord{ |
| 277 | SandboxID: "sandbox-1", | 94 | SandboxID: "sandbox-1", |
| 278 | - CheckpointHeadTemplateID: newHeadDigest, | 95 | + CheckpointHeadTemplateID: "source", |
| 279 | }); err != nil { | 96 | }); err != nil { |
| 280 | t.Fatal(err) | 97 | t.Fatal(err) |
| 281 | } | 98 | } |
| 282 | - if err := store.PublishCheckpoint(ctx, conchtemplate.Entry{ | 99 | + if err := store.AdvanceCheckpointHead(ctx, "sandbox-1", "source", "checkpoint"); err != nil { |
| 283 | - Origin: conchtemplate.OriginCheckpoint, | 100 | + t.Fatalf("AdvanceCheckpointHead() error = %v", err) |
| 284 | - BootMode: conchtemplate.BootModeResume, | ||
| 285 | - BootIndexDigest: checkpointDigest, | ||
| 286 | - ParentBootIndexDigest: oldHeadDigest, | ||
| 287 | - SourceSandboxID: "sandbox-1", | ||
| 288 | - }); err == nil { | ||
| 289 | - t.Fatal("PublishCheckpoint() error = nil, want CAS failure") | ||
| 290 | } | 101 | } |
| 291 | - if _, err := store.GetTemplate(ctx, checkpointDigest); !errors.Is(err, ErrNotFound) { | 102 | + record, err := store.GetSandbox(ctx, "sandbox-1") |
| 292 | - t.Fatalf("GetTemplate() error = %v, want ErrNotFound", err) | 103 | + if err != nil || record.CheckpointHeadTemplateID != "checkpoint" { |
| 293 | - } | 104 | + t.Fatalf("checkpoint head = %q, %v", record.CheckpointHeadTemplateID, err) |
| 294 | - sandboxRecord, _ := store.GetSandbox(ctx, "sandbox-1") | ||
| 295 | - if sandboxRecord.CheckpointHeadTemplateID != newHeadDigest { | ||
| 296 | - t.Fatalf("sandbox changed after failed transaction: %#v", sandboxRecord) | ||
| 297 | } | 105 | } |
| 298 | } | 106 | } |
| 107 | + | ||
| 108 | +func TestBoltStoreAdvanceCheckpointHeadCASFailureLeavesRecordUnchanged(t *testing.T) { | ||
| 109 | + store := newBoltStore(t) | ||
| 110 | + ctx := context.Background() | ||
| 111 | + if err := store.UpsertSandbox(ctx, SandboxRecord{ | ||
| 112 | + SandboxID: "sandbox-1", | ||
| 113 | + CheckpointHeadTemplateID: "current", | ||
| 114 | + }); err != nil { | ||
| 115 | + t.Fatal(err) | ||
| 116 | + } | ||
| 117 | + if err := store.AdvanceCheckpointHead(ctx, "sandbox-1", "stale", "checkpoint"); err == nil { | ||
| 118 | + t.Fatal("AdvanceCheckpointHead() error = nil, want CAS failure") | ||
| 119 | + } | ||
| 120 | + record, err := store.GetSandbox(ctx, "sandbox-1") | ||
| 121 | + if err != nil || record.CheckpointHeadTemplateID != "current" { | ||
| 122 | + t.Fatalf("checkpoint head after CAS failure = %q, %v", record.CheckpointHeadTemplateID, err) | ||
| 123 | + } | ||
| 124 | +} | ||
| 125 | + | ||
| 126 | +func newBoltStore(t *testing.T) *BoltStore { | ||
| 127 | + t.Helper() | ||
| 128 | + store, err := OpenBolt(t.TempDir() + "/state.db") | ||
| 129 | + if err != nil { | ||
| 130 | + t.Fatalf("OpenBolt() error = %v", err) | ||
| 131 | + } | ||
| 132 | + t.Cleanup(func() { | ||
| 133 | + if err := store.Close(); err != nil { | ||
| 134 | + t.Errorf("Close() error = %v", err) | ||
| 135 | + } | ||
| 136 | + }) | ||
| 137 | + return store | ||
| 138 | +} | ||
| @@ -2,8 +2,6 @@ package state | |||
| 2 | 2 | ||
| 3 | import ( | 3 | import ( |
| 4 | "context" | 4 | "context" |
| 5 | - | ||
| 6 | - "github.com/openeuler/Conch/internal/template" | ||
| 7 | ) | 5 | ) |
| 8 | 6 | ||
| 9 | type Store interface { | 7 | type Store interface { |
| @@ -14,9 +12,5 @@ type Store interface { | |||
| 14 | ListSandboxes(context.Context) ([]SandboxRecord, error) | 12 | ListSandboxes(context.Context) ([]SandboxRecord, error) |
| 15 | DeleteSandbox(context.Context, string) error | 13 | DeleteSandbox(context.Context, string) error |
| 16 | 14 | ||
| 17 | - CreateTemplate(context.Context, template.Entry) error | 15 | + AdvanceCheckpointHead(context.Context, string, string, string) error |
| 18 | - GetTemplate(context.Context, string) (template.Entry, error) | ||
| 19 | - ListTemplates(context.Context) ([]template.Entry, error) | ||
| 20 | - DeleteTemplate(context.Context, string) error | ||
| 21 | - PublishCheckpoint(context.Context, template.Entry) error | ||
| 22 | } | 16 | } |
| @@ -6,11 +6,8 @@ import ( | |||
| 6 | "strings" | 6 | "strings" |
| 7 | 7 | ||
| 8 | containerd "github.com/containerd/containerd/v2/client" | 8 | containerd "github.com/containerd/containerd/v2/client" |
| 9 | - "github.com/containerd/containerd/v2/core/images" | ||
| 10 | "github.com/containerd/containerd/v2/core/remotes/docker" | 9 | "github.com/containerd/containerd/v2/core/remotes/docker" |
| 11 | - "github.com/containerd/errdefs" | ||
| 12 | "github.com/opencontainers/go-digest" | 10 | "github.com/opencontainers/go-digest" |
| 13 | - ocispec "github.com/opencontainers/image-spec/specs-go/v1" | ||
| 14 | 11 | ||
| 15 | containerdclient "github.com/openeuler/Conch/internal/adapters/containerd/client" | 12 | containerdclient "github.com/openeuler/Conch/internal/adapters/containerd/client" |
| 16 | ) | 13 | ) |
| @@ -45,73 +42,6 @@ func IsCanonicalTemplateRef(ref string) bool { | |||
| 45 | return err == nil && canonical == ref | 42 | return err == nil && canonical == ref |
| 46 | } | 43 | } |
| 47 | 44 | ||
| 48 | -// EnsureCanonicalBootIndexRecord creates or validates the canonical local | ||
| 49 | -// image record for target. Content is not copied; the record is another GC | ||
| 50 | -// root for the same immutable descriptor closure. | ||
| 51 | -func EnsureCanonicalBootIndexRecord( | ||
| 52 | - ctx context.Context, | ||
| 53 | - client *containerdclient.Client, | ||
| 54 | - target ocispec.Descriptor, | ||
| 55 | - kind string, | ||
| 56 | -) (string, error) { | ||
| 57 | - if client == nil || client.Client == nil { | ||
| 58 | - return "", fmt.Errorf("containerd client is required") | ||
| 59 | - } | ||
| 60 | - if target.Digest == "" { | ||
| 61 | - return "", fmt.Errorf("%w: boot index target digest is required", ErrInvalidArgument) | ||
| 62 | - } | ||
| 63 | - if kind != ImageKindBootIndexCold && kind != ImageKindBootIndexResume { | ||
| 64 | - return "", fmt.Errorf("%w: invalid boot index image kind %q", ErrInvalidArgument, kind) | ||
| 65 | - } | ||
| 66 | - name, err := CanonicalTemplateRef(target.Digest.String()) | ||
| 67 | - if err != nil { | ||
| 68 | - return "", err | ||
| 69 | - } | ||
| 70 | - namespaceCtx := containerdclient.NewNamespaceContext(ctx) | ||
| 71 | - existing, err := client.ImageService().Get(namespaceCtx, name) | ||
| 72 | - if err == nil && existing.Target.Digest != target.Digest { | ||
| 73 | - return "", fmt.Errorf("canonical template image %s targets %s, want %s", name, existing.Target.Digest, target.Digest) | ||
| 74 | - } | ||
| 75 | - if err != nil && !errdefs.IsNotFound(err) { | ||
| 76 | - return "", fmt.Errorf("lookup canonical template image %s: %w", name, err) | ||
| 77 | - } | ||
| 78 | - if err := publishBootIndexRecord(namespaceCtx, client, name, target, kind); err != nil { | ||
| 79 | - return "", err | ||
| 80 | - } | ||
| 81 | - return name, nil | ||
| 82 | -} | ||
| 83 | - | ||
| 84 | -// RemoveCanonicalBootIndexRecord removes only the image metadata root. The | ||
| 85 | -// embedded containerd GC decides when the descriptor closure can be reclaimed. | ||
| 86 | -func RemoveCanonicalBootIndexRecord(ctx context.Context, client *containerdclient.Client, rawDigest string) error { | ||
| 87 | - if client == nil || client.Client == nil { | ||
| 88 | - return fmt.Errorf("containerd client is required") | ||
| 89 | - } | ||
| 90 | - parsed, err := digest.Parse(strings.TrimSpace(rawDigest)) | ||
| 91 | - if err != nil { | ||
| 92 | - return fmt.Errorf("%w: invalid boot index digest %q: %v", ErrInvalidArgument, rawDigest, err) | ||
| 93 | - } | ||
| 94 | - name, err := CanonicalTemplateRef(parsed.String()) | ||
| 95 | - if err != nil { | ||
| 96 | - return err | ||
| 97 | - } | ||
| 98 | - namespaceCtx := containerdclient.NewNamespaceContext(ctx) | ||
| 99 | - record, err := client.ImageService().Get(namespaceCtx, name) | ||
| 100 | - if err != nil { | ||
| 101 | - if errdefs.IsNotFound(err) { | ||
| 102 | - return nil | ||
| 103 | - } | ||
| 104 | - return fmt.Errorf("lookup canonical template image %s: %w", name, err) | ||
| 105 | - } | ||
| 106 | - if record.Target.Digest != parsed { | ||
| 107 | - return fmt.Errorf("canonical template image %s targets %s, want %s", name, record.Target.Digest, parsed) | ||
| 108 | - } | ||
| 109 | - if err := client.ImageService().Delete(namespaceCtx, name, images.DeleteTarget(&record.Target)); err != nil && !errdefs.IsNotFound(err) { | ||
| 110 | - return fmt.Errorf("remove canonical template image %s: %w", name, err) | ||
| 111 | - } | ||
| 112 | - return nil | ||
| 113 | -} | ||
| 114 | - | ||
| 115 | func PublishBootIndex(ctx context.Context, client *containerdclient.Client, req PublishBootIndexOptions) (PublishBootIndexResult, error) { | 45 | func PublishBootIndex(ctx context.Context, client *containerdclient.Client, req PublishBootIndexOptions) (PublishBootIndexResult, error) { |
| 116 | if client == nil || client.Client == nil { | 46 | if client == nil || client.Client == nil { |
| 117 | return PublishBootIndexResult{}, fmt.Errorf("containerd client is required") | 47 | return PublishBootIndexResult{}, fmt.Errorf("containerd client is required") |
| @@ -125,18 +55,11 @@ func PublishBootIndex(ctx context.Context, client *containerdclient.Client, req | |||
| 125 | if req.InitrdPath == "" { | 55 | if req.InitrdPath == "" { |
| 126 | return PublishBootIndexResult{}, fmt.Errorf("%w: initrd_path is required", ErrInvalidArgument) | 56 | return PublishBootIndexResult{}, fmt.Errorf("%w: initrd_path is required", ErrInvalidArgument) |
| 127 | } | 57 | } |
| 128 | - namespaceCtx := containerdclient.NewNamespaceContext(ctx) | 58 | + rootfsImage, err := client.ImageService().Get(ctx, req.RootfsImageName) |
| 129 | - publishCtx, done, err := client.WithLease(namespaceCtx) | ||
| 130 | - if err != nil { | ||
| 131 | - return PublishBootIndexResult{}, fmt.Errorf("create content lease: %w", err) | ||
| 132 | - } | ||
| 133 | - defer done(publishCtx) | ||
| 134 | - | ||
| 135 | - rootfsImage, err := client.ImageService().Get(publishCtx, req.RootfsImageName) | ||
| 136 | if err != nil { | 59 | if err != nil { |
| 137 | return PublishBootIndexResult{}, fmt.Errorf("lookup rootfs image %s: %w", req.RootfsImageName, err) | 60 | return PublishBootIndexResult{}, fmt.Errorf("lookup rootfs image %s: %w", req.RootfsImageName, err) |
| 138 | } | 61 | } |
| 139 | - indexDesc, err := BuildBootIndexInContent(publishCtx, client.ContentStore(), BootIndexContentOptions{ | 62 | + indexDesc, err := BuildBootIndexInContent(ctx, client.ContentStore(), BootIndexContentOptions{ |
| 140 | RootfsDescriptor: rootfsImage.Target, | 63 | RootfsDescriptor: rootfsImage.Target, |
| 141 | KernelPath: req.KernelPath, | 64 | KernelPath: req.KernelPath, |
| 142 | InitrdPath: req.InitrdPath, | 65 | InitrdPath: req.InitrdPath, |
| @@ -145,7 +68,7 @@ func PublishBootIndex(ctx context.Context, client *containerdclient.Client, req | |||
| 145 | return PublishBootIndexResult{}, fmt.Errorf("build boot index content: %w", err) | 68 | return PublishBootIndexResult{}, fmt.Errorf("build boot index content: %w", err) |
| 146 | } | 69 | } |
| 147 | 70 | ||
| 148 | - buildRef, err := EnsureCanonicalBootIndexRecord(publishCtx, client, indexDesc, ImageKindBootIndexCold) | 71 | + buildRef, err := CanonicalTemplateRef(indexDesc.Digest.String()) |
| 149 | if err != nil { | 72 | if err != nil { |
| 150 | return PublishBootIndexResult{}, err | 73 | return PublishBootIndexResult{}, err |
| 151 | } | 74 | } |
| @@ -153,6 +76,7 @@ func PublishBootIndex(ctx context.Context, client *containerdclient.Client, req | |||
| 153 | return PublishBootIndexResult{ | 76 | return PublishBootIndexResult{ |
| 154 | BootIndexDigest: indexDesc.Digest.String(), | 77 | BootIndexDigest: indexDesc.Digest.String(), |
| 155 | BuildRef: buildRef, | 78 | BuildRef: buildRef, |
| 79 | + Target: indexDesc, | ||
| 156 | }, nil | 80 | }, nil |
| 157 | } | 81 | } |
| 158 | 82 | ||
| @@ -220,14 +144,7 @@ func PublishCheckpointBootIndex( | |||
| 220 | return PublishCheckpointBootIndexResult{}, fmt.Errorf("%w: memory_size_mb must be positive", ErrInvalidArgument) | 144 | return PublishCheckpointBootIndexResult{}, fmt.Errorf("%w: memory_size_mb must be positive", ErrInvalidArgument) |
| 221 | } | 145 | } |
| 222 | 146 | ||
| 223 | - namespaceCtx := containerdclient.NewNamespaceContext(ctx) | 147 | + _, sourceInfo, err := inspectBootIndexByDigest(ctx, client.ContentStore(), req.SourceBootIndexDigest) |
| 224 | - publishCtx, done, err := client.WithLease(namespaceCtx) | ||
| 225 | - if err != nil { | ||
| 226 | - return PublishCheckpointBootIndexResult{}, fmt.Errorf("create content lease: %w", err) | ||
| 227 | - } | ||
| 228 | - defer done(publishCtx) | ||
| 229 | - | ||
| 230 | - _, sourceInfo, err := inspectBootIndexByDigest(publishCtx, client.ContentStore(), req.SourceBootIndexDigest) | ||
| 231 | if err != nil { | 148 | if err != nil { |
| 232 | return PublishCheckpointBootIndexResult{}, fmt.Errorf("inspect source boot index: %w", err) | 149 | return PublishCheckpointBootIndexResult{}, fmt.Errorf("inspect source boot index: %w", err) |
| 233 | } | 150 | } |
| @@ -235,11 +152,11 @@ func PublishCheckpointBootIndex( | |||
| 235 | return PublishCheckpointBootIndexResult{}, fmt.Errorf("source boot index VMM %q does not match capture VMM %q", sourceInfo.VMMName, req.VMMName) | 152 | return PublishCheckpointBootIndexResult{}, fmt.Errorf("source boot index VMM %q does not match capture VMM %q", sourceInfo.VMMName, req.VMMName) |
| 236 | } | 153 | } |
| 237 | 154 | ||
| 238 | - memDesc, err := BuildNativeComponentInContent(publishCtx, client.ContentStore(), []string{req.MemRoot}, KindMemSnapshot) | 155 | + memDesc, err := BuildNativeComponentInContent(ctx, client.ContentStore(), []string{req.MemRoot}, KindMemSnapshot) |
| 239 | if err != nil { | 156 | if err != nil { |
| 240 | return PublishCheckpointBootIndexResult{}, fmt.Errorf("publish captured mem component: %w", err) | 157 | return PublishCheckpointBootIndexResult{}, fmt.Errorf("publish captured mem component: %w", err) |
| 241 | } | 158 | } |
| 242 | - indexDesc, err := BuildBootIndexInContent(publishCtx, client.ContentStore(), BootIndexContentOptions{ | 159 | + indexDesc, err := BuildBootIndexInContent(ctx, client.ContentStore(), BootIndexContentOptions{ |
| 243 | RootfsDescriptor: sourceInfo.RootfsDescriptor, | 160 | RootfsDescriptor: sourceInfo.RootfsDescriptor, |
| 244 | MemDescriptor: memDesc, | 161 | MemDescriptor: memDesc, |
| 245 | SandboxDescriptor: sourceInfo.SandboxDescriptor, | 162 | SandboxDescriptor: sourceInfo.SandboxDescriptor, |
| @@ -249,38 +166,13 @@ func PublishCheckpointBootIndex( | |||
| 249 | if err != nil { | 166 | if err != nil { |
| 250 | return PublishCheckpointBootIndexResult{}, fmt.Errorf("build checkpoint boot index: %w", err) | 167 | return PublishCheckpointBootIndexResult{}, fmt.Errorf("build checkpoint boot index: %w", err) |
| 251 | } | 168 | } |
| 252 | - buildRef, err := EnsureCanonicalBootIndexRecord(publishCtx, client, indexDesc, ImageKindBootIndexResume) | 169 | + buildRef, err := CanonicalTemplateRef(indexDesc.Digest.String()) |
| 253 | if err != nil { | 170 | if err != nil { |
| 254 | return PublishCheckpointBootIndexResult{}, err | 171 | return PublishCheckpointBootIndexResult{}, err |
| 255 | } | 172 | } |
| 256 | return PublishCheckpointBootIndexResult{ | 173 | return PublishCheckpointBootIndexResult{ |
| 257 | BootIndexDigest: indexDesc.Digest.String(), | 174 | BootIndexDigest: indexDesc.Digest.String(), |
| 258 | BuildRef: buildRef, | 175 | BuildRef: buildRef, |
| 176 | + Target: indexDesc, | ||
| 259 | }, nil | 177 | }, nil |
| 260 | } | 178 | } |
| 261 | - | ||
| 262 | -func publishBootIndexRecord(ctx context.Context, client *containerdclient.Client, tag string, indexDesc ocispec.Descriptor, kind string) error { | ||
| 263 | - labelHandler := images.SetChildrenLabels(client.ContentStore(), images.ChildrenHandler(client.ContentStore())) | ||
| 264 | - if err := images.WalkNotEmpty(ctx, labelHandler, indexDesc); err != nil { | ||
| 265 | - return fmt.Errorf("label boot index content: %w", err) | ||
| 266 | - } | ||
| 267 | - imageRecord := images.Image{ | ||
| 268 | - Name: tag, | ||
| 269 | - Target: indexDesc, | ||
| 270 | - Labels: map[string]string{ | ||
| 271 | - ImageKindLabel: kind, | ||
| 272 | - }, | ||
| 273 | - } | ||
| 274 | - if _, err := client.ImageService().Update(ctx, imageRecord, "target", "labels."+ImageKindLabel); err != nil { | ||
| 275 | - if !errdefs.IsNotFound(err) { | ||
| 276 | - return fmt.Errorf("update boot image record %s: %w", tag, err) | ||
| 277 | - } | ||
| 278 | - if _, err := client.ImageService().Create(ctx, imageRecord); err != nil { | ||
| 279 | - if errdefs.IsAlreadyExists(err) { | ||
| 280 | - return ErrAlreadyExists.Wrap(err) | ||
| 281 | - } | ||
| 282 | - return fmt.Errorf("create boot image record %s: %w", tag, err) | ||
| 283 | - } | ||
| 284 | - } | ||
| 285 | - return nil | ||
| 286 | -} | ||
| @@ -2,9 +2,11 @@ package image | |||
| 2 | 2 | ||
| 3 | import ( | 3 | import ( |
| 4 | "context" | 4 | "context" |
| 5 | + "errors" | ||
| 5 | "fmt" | 6 | "fmt" |
| 6 | "sort" | 7 | "sort" |
| 7 | "strings" | 8 | "strings" |
| 9 | + "time" | ||
| 8 | 10 | ||
| 9 | containerd "github.com/containerd/containerd/v2/client" | 11 | containerd "github.com/containerd/containerd/v2/client" |
| 10 | "github.com/containerd/containerd/v2/core/images" | 12 | "github.com/containerd/containerd/v2/core/images" |
| @@ -57,23 +59,44 @@ func PullBootIndex(ctx context.Context, client *containerdclient.Client, req Reg | |||
| 57 | } | 59 | } |
| 58 | 60 | ||
| 59 | pullCtx := containerdclient.NewNamespaceContext(ctx) | 61 | pullCtx := containerdclient.NewNamespaceContext(ctx) |
| 60 | - fetched, kind, err := pullRegistryContent(pullCtx, client, req, true) | 62 | + fetched, _, err := pullRegistryContent(pullCtx, client, req, true) |
| 61 | if err != nil { | 63 | if err != nil { |
| 62 | return PullBootIndexResult{}, translateRegistryError(err) | 64 | return PullBootIndexResult{}, translateRegistryError(err) |
| 63 | } | 65 | } |
| 64 | info, err := InspectBootIndexContent(pullCtx, client.ContentStore(), fetched.Target) | 66 | info, err := InspectBootIndexContent(pullCtx, client.ContentStore(), fetched.Target) |
| 65 | if err != nil { | 67 | if err != nil { |
| 66 | - return PullBootIndexResult{}, fmt.Errorf("validate pulled Boot Index %s: %w", fetched.Name, err) | 68 | + return PullBootIndexResult{}, errors.Join( |
| 69 | + fmt.Errorf("validate pulled Boot Index %s: %w", fetched.Name, err), | ||
| 70 | + RemoveFetchedImageRecord(ctx, client.ImageService(), fetched.Name, fetched.Target), | ||
| 71 | + ) | ||
| 67 | } | 72 | } |
| 68 | - buildRef, err := EnsureCanonicalBootIndexRecord(pullCtx, client, fetched.Target, kind) | 73 | + buildRef, err := CanonicalTemplateRef(fetched.Target.Digest.String()) |
| 69 | if err != nil { | 74 | if err != nil { |
| 70 | - return PullBootIndexResult{}, err | 75 | + return PullBootIndexResult{}, errors.Join( |
| 76 | + err, | ||
| 77 | + RemoveFetchedImageRecord(ctx, client.ImageService(), fetched.Name, fetched.Target), | ||
| 78 | + ) | ||
| 71 | } | 79 | } |
| 72 | - if err := client.ImageService().Delete(pullCtx, fetched.Name, images.DeleteTarget(&fetched.Target)); err != nil && !errdefs.IsNotFound(err) { | 80 | + return PullBootIndexResult{ |
| 73 | - _ = RemoveCanonicalBootIndexRecord(context.WithoutCancel(ctx), client, info.BootIndexDigest) | 81 | + Info: info, |
| 74 | - return PullBootIndexResult{}, fmt.Errorf("remove fetched source image record %s: %w", fetched.Name, err) | 82 | + BuildRef: buildRef, |
| 83 | + SourceImageName: fetched.Name, | ||
| 84 | + Target: fetched.Target, | ||
| 85 | + }, nil | ||
| 86 | +} | ||
| 87 | + | ||
| 88 | +// RemoveFetchedImageRecord releases the temporary image root without inheriting request cancellation. | ||
| 89 | +func RemoveFetchedImageRecord(ctx context.Context, store images.Store, name string, target ocispec.Descriptor) error { | ||
| 90 | + cleanupCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 10*time.Second) | ||
| 91 | + defer cancel() | ||
| 92 | + if err := store.Delete( | ||
| 93 | + containerdclient.NewNamespaceContext(cleanupCtx), | ||
| 94 | + name, | ||
| 95 | + images.DeleteTarget(&target), | ||
| 96 | + ); err != nil && !errdefs.IsNotFound(err) { | ||
| 97 | + return fmt.Errorf("remove fetched image record %s: %w", name, err) | ||
| 75 | } | 98 | } |
| 76 | - return PullBootIndexResult{Info: info, BuildRef: buildRef}, nil | 99 | + return nil |
| 77 | } | 100 | } |
| 78 | 101 | ||
| 79 | func pullRegistryContent( | 102 | func pullRegistryContent( |
| @@ -1,6 +1,37 @@ | |||
| 1 | package image | 1 | package image |
| 2 | 2 | ||
| 3 | -import "testing" | 3 | +import ( |
| 4 | + "context" | ||
| 5 | + "testing" | ||
| 6 | + | ||
| 7 | + "github.com/containerd/containerd/v2/core/images" | ||
| 8 | + ocispec "github.com/opencontainers/image-spec/specs-go/v1" | ||
| 9 | +) | ||
| 10 | + | ||
| 11 | +func TestRemoveFetchedImageRecordDetachesCleanupFromRequestCancellation(t *testing.T) { | ||
| 12 | + ctx, cancel := context.WithCancel(context.Background()) | ||
| 13 | + cancel() | ||
| 14 | + store := &cleanupImageStore{} | ||
| 15 | + | ||
| 16 | + if err := RemoveFetchedImageRecord( | ||
| 17 | + ctx, store, "registry.example.invalid/conch/template:latest", ocispec.Descriptor{}, | ||
| 18 | + ); err != nil { | ||
| 19 | + t.Fatalf("RemoveFetchedImageRecord() error = %v", err) | ||
| 20 | + } | ||
| 21 | + if !store.hasDeadline { | ||
| 22 | + t.Fatal("Delete() context has no cleanup deadline") | ||
| 23 | + } | ||
| 24 | +} | ||
| 25 | + | ||
| 26 | +type cleanupImageStore struct { | ||
| 27 | + images.Store | ||
| 28 | + hasDeadline bool | ||
| 29 | +} | ||
| 30 | + | ||
| 31 | +func (s *cleanupImageStore) Delete(ctx context.Context, _ string, _ ...images.DeleteOpt) error { | ||
| 32 | + _, s.hasDeadline = ctx.Deadline() | ||
| 33 | + return ctx.Err() | ||
| 34 | +} | ||
| 4 | 35 | ||
| 5 | func TestImageRepoDigests(t *testing.T) { | 36 | func TestImageRepoDigests(t *testing.T) { |
| 6 | tests := []struct { | 37 | tests := []struct { |
| @@ -19,13 +19,16 @@ type PublishBootIndexOptions struct { | |||
| 19 | } | 19 | } |
| 20 | 20 | ||
| 21 | type PublishBootIndexResult struct { | 21 | type PublishBootIndexResult struct { |
| 22 | - BootIndexDigest string `json:"boot_index_digest"` | 22 | + BootIndexDigest string `json:"boot_index_digest"` |
| 23 | - BuildRef string `json:"build_ref"` | 23 | + BuildRef string `json:"build_ref"` |
| 24 | + Target ocispec.Descriptor `json:"-"` | ||
| 24 | } | 25 | } |
| 25 | 26 | ||
| 26 | type PullBootIndexResult struct { | 27 | type PullBootIndexResult struct { |
| 27 | - Info BootIndexInfo | 28 | + Info BootIndexInfo |
| 28 | - BuildRef string | 29 | + BuildRef string |
| 30 | + SourceImageName string | ||
| 31 | + Target ocispec.Descriptor `json:"-"` | ||
| 29 | } | 32 | } |
| 30 | 33 | ||
| 31 | // BootIndexInfo is the validated, content-addressed view of a Conch Boot | 34 | // BootIndexInfo is the validated, content-addressed view of a Conch Boot |
| @@ -55,8 +58,9 @@ type PublishCheckpointBootIndexOptions struct { | |||
| 55 | // PublishCheckpointBootIndexResult deliberately contains no snapshot keys: | 58 | // PublishCheckpointBootIndexResult deliberately contains no snapshot keys: |
| 56 | // publishing checkpoint content must not create checkpoint snapshots. | 59 | // publishing checkpoint content must not create checkpoint snapshots. |
| 57 | type PublishCheckpointBootIndexResult struct { | 60 | type PublishCheckpointBootIndexResult struct { |
| 58 | - BootIndexDigest string `json:"boot_index_digest"` | 61 | + BootIndexDigest string `json:"boot_index_digest"` |
| 59 | - BuildRef string `json:"build_ref"` | 62 | + BuildRef string `json:"build_ref"` |
| 63 | + Target ocispec.Descriptor `json:"-"` | ||
| 60 | } | 64 | } |
| 61 | 65 | ||
| 62 | // PushBootIndexOptions publishes the descriptor closure rooted at an | 66 | // PushBootIndexOptions publishes the descriptor closure rooted at an |
| @@ -2,9 +2,8 @@ package template | |||
| 2 | 2 | ||
| 3 | import ( | 3 | import ( |
| 4 | "context" | 4 | "context" |
| 5 | - "fmt" | 5 | + |
| 6 | - "strings" | 6 | + ocispec "github.com/opencontainers/image-spec/specs-go/v1" |
| 7 | - "time" | ||
| 8 | ) | 7 | ) |
| 9 | 8 | ||
| 10 | type Filter struct { | 9 | type Filter struct { |
| @@ -13,108 +12,12 @@ type Filter struct { | |||
| 13 | } | 12 | } |
| 14 | 13 | ||
| 15 | type Store interface { | 14 | type Store interface { |
| 16 | - Create(context.Context, Entry) (Entry, error) | 15 | + Create(context.Context, Entry, ocispec.Descriptor) (Entry, error) |
| 17 | Get(context.Context, string) (Entry, error) | 16 | Get(context.Context, string) (Entry, error) |
| 18 | List(context.Context, Filter) ([]Entry, error) | 17 | List(context.Context, Filter) ([]Entry, error) |
| 19 | Delete(context.Context, string) error | 18 | Delete(context.Context, string) error |
| 20 | } | 19 | } |
| 21 | 20 | ||
| 22 | -// StateStore is the persistence capability required by the Template domain. | ||
| 23 | -// Implementations must provide insert-only CreateTemplate semantics. | ||
| 24 | -type StateStore interface { | ||
| 25 | - CreateTemplate(context.Context, Entry) error | ||
| 26 | - GetTemplate(context.Context, string) (Entry, error) | ||
| 27 | - ListTemplates(context.Context) ([]Entry, error) | ||
| 28 | - DeleteTemplate(context.Context, string) error | ||
| 29 | -} | ||
| 30 | - | ||
| 31 | -type PersistentStore struct { | ||
| 32 | - store StateStore | ||
| 33 | - now func() time.Time | ||
| 34 | -} | ||
| 35 | - | ||
| 36 | -func NewStore(store StateStore) *PersistentStore { | ||
| 37 | - return &PersistentStore{ | ||
| 38 | - store: store, | ||
| 39 | - now: time.Now, | ||
| 40 | - } | ||
| 41 | -} | ||
| 42 | - | ||
| 43 | -func (s *PersistentStore) Create(ctx context.Context, entry Entry) (Entry, error) { | ||
| 44 | - if s == nil || s.store == nil { | ||
| 45 | - return Entry{}, fmt.Errorf("template store is not configured") | ||
| 46 | - } | ||
| 47 | - if entry.CreatedAt == 0 { | ||
| 48 | - entry.CreatedAt = s.now().UnixNano() | ||
| 49 | - } | ||
| 50 | - normalized, err := NormalizeEntry(entry) | ||
| 51 | - if err != nil { | ||
| 52 | - return Entry{}, err | ||
| 53 | - } | ||
| 54 | - if err := s.store.CreateTemplate(ctx, normalized); err != nil { | ||
| 55 | - return Entry{}, err | ||
| 56 | - } | ||
| 57 | - return normalized, nil | ||
| 58 | -} | ||
| 59 | - | ||
| 60 | -func (s *PersistentStore) Get(ctx context.Context, rawDigest string) (Entry, error) { | ||
| 61 | - if s == nil || s.store == nil { | ||
| 62 | - return Entry{}, fmt.Errorf("template store is not configured") | ||
| 63 | - } | ||
| 64 | - bootIndexDigest := strings.TrimSpace(rawDigest) | ||
| 65 | - entry, err := s.store.GetTemplate(ctx, bootIndexDigest) | ||
| 66 | - if err != nil { | ||
| 67 | - return Entry{}, err | ||
| 68 | - } | ||
| 69 | - return NormalizeEntry(entry) | ||
| 70 | -} | ||
| 71 | - | ||
| 72 | -func (s *PersistentStore) List(ctx context.Context, filter Filter) ([]Entry, error) { | ||
| 73 | - if s == nil || s.store == nil { | ||
| 74 | - return nil, fmt.Errorf("template store is not configured") | ||
| 75 | - } | ||
| 76 | - if filter.Origin != "" { | ||
| 77 | - switch filter.Origin { | ||
| 78 | - case OriginImage, OriginCheckpoint: | ||
| 79 | - default: | ||
| 80 | - return nil, ErrInvalidArgument.Wrap(fmt.Errorf("unknown template origin %q", filter.Origin)) | ||
| 81 | - } | ||
| 82 | - } | ||
| 83 | - if filter.BootMode != "" { | ||
| 84 | - switch filter.BootMode { | ||
| 85 | - case BootModeCold, BootModeResume: | ||
| 86 | - default: | ||
| 87 | - return nil, ErrInvalidArgument.Wrap(fmt.Errorf("unknown template boot mode %q", filter.BootMode)) | ||
| 88 | - } | ||
| 89 | - } | ||
| 90 | - items, err := s.store.ListTemplates(ctx) | ||
| 91 | - if err != nil { | ||
| 92 | - return nil, err | ||
| 93 | - } | ||
| 94 | - out := make([]Entry, 0, len(items)) | ||
| 95 | - for _, raw := range items { | ||
| 96 | - item, err := NormalizeEntry(raw) | ||
| 97 | - if err != nil { | ||
| 98 | - return nil, err | ||
| 99 | - } | ||
| 100 | - if filter.Origin != "" && item.Origin != filter.Origin { | ||
| 101 | - continue | ||
| 102 | - } | ||
| 103 | - if filter.BootMode != "" && item.BootMode != filter.BootMode { | ||
| 104 | - continue | ||
| 105 | - } | ||
| 106 | - out = append(out, item) | ||
| 107 | - } | ||
| 108 | - return out, nil | ||
| 109 | -} | ||
| 110 | - | ||
| 111 | -func (s *PersistentStore) Delete(ctx context.Context, rawDigest string) error { | ||
| 112 | - if s == nil || s.store == nil { | ||
| 113 | - return fmt.Errorf("template store is not configured") | ||
| 114 | - } | ||
| 115 | - return s.store.DeleteTemplate(ctx, strings.TrimSpace(rawDigest)) | ||
| 116 | -} | ||
| 117 | - | ||
| 118 | func copyMap(in map[string]string) map[string]string { | 21 | func copyMap(in map[string]string) map[string]string { |
| 119 | if len(in) == 0 { | 22 | if len(in) == 0 { |
| 120 | return nil | 23 | return nil |
| @@ -1,173 +0,0 @@ | |||
| 1 | -package template | ||
| 2 | - | ||
| 3 | -import ( | ||
| 4 | - "context" | ||
| 5 | - "errors" | ||
| 6 | - "strings" | ||
| 7 | - "testing" | ||
| 8 | - "time" | ||
| 9 | - | ||
| 10 | - "github.com/opencontainers/go-digest" | ||
| 11 | -) | ||
| 12 | - | ||
| 13 | -func TestStoreCRUDAndList(t *testing.T) { | ||
| 14 | - ctx := context.Background() | ||
| 15 | - raw := newMemoryStateStore() | ||
| 16 | - store := NewStore(raw) | ||
| 17 | - store.now = func() time.Time { return time.Unix(10, 0) } | ||
| 18 | - | ||
| 19 | - bootIndexDigest := digest.FromString("cold boot index").String() | ||
| 20 | - entry, err := store.Create(ctx, Entry{ | ||
| 21 | - Origin: OriginImage, | ||
| 22 | - BootMode: BootModeCold, | ||
| 23 | - BootIndexDigest: bootIndexDigest, | ||
| 24 | - SourceRef: "image-ref", | ||
| 25 | - Labels: map[string]string{"purpose": "test"}, | ||
| 26 | - }) | ||
| 27 | - if err != nil { | ||
| 28 | - t.Fatalf("Create() error = %v", err) | ||
| 29 | - } | ||
| 30 | - if entry.CreatedAt != time.Unix(10, 0).UnixNano() { | ||
| 31 | - t.Fatalf("CreatedAt = %d", entry.CreatedAt) | ||
| 32 | - } | ||
| 33 | - | ||
| 34 | - got, err := store.Get(ctx, " "+entry.BootIndexDigest+" ") | ||
| 35 | - if err != nil { | ||
| 36 | - t.Fatalf("Get() error = %v", err) | ||
| 37 | - } | ||
| 38 | - if got.BootIndexDigest != bootIndexDigest || got.BootMode != BootModeCold { | ||
| 39 | - t.Fatalf("entry = %#v", got) | ||
| 40 | - } | ||
| 41 | - | ||
| 42 | - items, err := store.List(ctx, Filter{ | ||
| 43 | - Origin: OriginImage, | ||
| 44 | - BootMode: BootModeCold, | ||
| 45 | - }) | ||
| 46 | - if err != nil { | ||
| 47 | - t.Fatalf("List() error = %v", err) | ||
| 48 | - } | ||
| 49 | - if len(items) != 1 || items[0].BootIndexDigest != entry.BootIndexDigest { | ||
| 50 | - t.Fatalf("List() = %#v", items) | ||
| 51 | - } | ||
| 52 | - | ||
| 53 | - if err := store.Delete(ctx, " "+entry.BootIndexDigest+" "); err != nil { | ||
| 54 | - t.Fatalf("Delete() error = %v", err) | ||
| 55 | - } | ||
| 56 | - if _, err := store.Get(ctx, entry.BootIndexDigest); err == nil { | ||
| 57 | - t.Fatal("Get() after Delete() error = nil") | ||
| 58 | - } | ||
| 59 | -} | ||
| 60 | - | ||
| 61 | -func TestStoreCreateValidatesCompleteEntry(t *testing.T) { | ||
| 62 | - validDigest := digest.FromString("template").String() | ||
| 63 | - for _, tt := range []struct { | ||
| 64 | - name string | ||
| 65 | - entry Entry | ||
| 66 | - want string | ||
| 67 | - }{ | ||
| 68 | - { | ||
| 69 | - name: "missing digest", | ||
| 70 | - entry: Entry{ | ||
| 71 | - Origin: OriginImage, BootMode: BootModeCold, | ||
| 72 | - }, | ||
| 73 | - want: "invalid boot index digest", | ||
| 74 | - }, | ||
| 75 | - { | ||
| 76 | - name: "invalid origin", | ||
| 77 | - entry: Entry{ | ||
| 78 | - Origin: "archive", BootMode: BootModeCold, BootIndexDigest: validDigest, | ||
| 79 | - }, | ||
| 80 | - want: "unknown template origin", | ||
| 81 | - }, | ||
| 82 | - { | ||
| 83 | - name: "invalid boot mode", | ||
| 84 | - entry: Entry{ | ||
| 85 | - Origin: OriginImage, BootMode: "warm", BootIndexDigest: validDigest, | ||
| 86 | - }, | ||
| 87 | - want: "unknown template boot mode", | ||
| 88 | - }, | ||
| 89 | - { | ||
| 90 | - name: "invalid digest", | ||
| 91 | - entry: Entry{ | ||
| 92 | - Origin: OriginImage, BootMode: BootModeCold, BootIndexDigest: "sha256:invalid", | ||
| 93 | - }, | ||
| 94 | - want: "invalid boot index digest", | ||
| 95 | - }, | ||
| 96 | - } { | ||
| 97 | - t.Run(tt.name, func(t *testing.T) { | ||
| 98 | - store := NewStore(newMemoryStateStore()) | ||
| 99 | - _, err := store.Create(context.Background(), tt.entry) | ||
| 100 | - if err == nil || !strings.Contains(err.Error(), tt.want) { | ||
| 101 | - t.Fatalf("Create() error = %v, want %q", err, tt.want) | ||
| 102 | - } | ||
| 103 | - }) | ||
| 104 | - } | ||
| 105 | -} | ||
| 106 | - | ||
| 107 | -func TestStoreDuplicateDigestDoesNotOverwrite(t *testing.T) { | ||
| 108 | - ctx := context.Background() | ||
| 109 | - raw := newMemoryStateStore() | ||
| 110 | - store := NewStore(raw) | ||
| 111 | - firstDigest := digest.FromString("first").String() | ||
| 112 | - first, err := store.Create(ctx, Entry{ | ||
| 113 | - Origin: OriginImage, | ||
| 114 | - BootMode: BootModeCold, | ||
| 115 | - BootIndexDigest: firstDigest, | ||
| 116 | - }) | ||
| 117 | - if err != nil { | ||
| 118 | - t.Fatalf("first Create() error = %v", err) | ||
| 119 | - } | ||
| 120 | - | ||
| 121 | - _, err = store.Create(ctx, Entry{ | ||
| 122 | - Origin: OriginCheckpoint, | ||
| 123 | - BootMode: BootModeResume, | ||
| 124 | - BootIndexDigest: first.BootIndexDigest, | ||
| 125 | - }) | ||
| 126 | - if !errors.Is(err, ErrAlreadyExists) { | ||
| 127 | - t.Fatalf("second Create() error = %v, want ErrAlreadyExists", err) | ||
| 128 | - } | ||
| 129 | - got, err := store.Get(ctx, first.BootIndexDigest) | ||
| 130 | - if err != nil { | ||
| 131 | - t.Fatalf("Get() error = %v", err) | ||
| 132 | - } | ||
| 133 | - if got.BootIndexDigest != firstDigest || got.Origin != OriginImage || got.BootMode != BootModeCold { | ||
| 134 | - t.Fatalf("first entry was overwritten: %#v", got) | ||
| 135 | - } | ||
| 136 | -} | ||
| 137 | - | ||
| 138 | -type memoryStateStore struct { | ||
| 139 | - entries map[string]Entry | ||
| 140 | -} | ||
| 141 | - | ||
| 142 | -func newMemoryStateStore() *memoryStateStore { | ||
| 143 | - return &memoryStateStore{entries: make(map[string]Entry)} | ||
| 144 | -} | ||
| 145 | - | ||
| 146 | -func (s *memoryStateStore) CreateTemplate(_ context.Context, entry Entry) error { | ||
| 147 | - if _, exists := s.entries[entry.BootIndexDigest]; exists { | ||
| 148 | - return ErrAlreadyExists | ||
| 149 | - } | ||
| 150 | - s.entries[entry.BootIndexDigest] = entry | ||
| 151 | - return nil | ||
| 152 | -} | ||
| 153 | - | ||
| 154 | -func (s *memoryStateStore) GetTemplate(_ context.Context, bootIndexDigest string) (Entry, error) { | ||
| 155 | - entry, exists := s.entries[bootIndexDigest] | ||
| 156 | - if !exists { | ||
| 157 | - return Entry{}, errors.New("not found") | ||
| 158 | - } | ||
| 159 | - return entry, nil | ||
| 160 | -} | ||
| 161 | - | ||
| 162 | -func (s *memoryStateStore) ListTemplates(context.Context) ([]Entry, error) { | ||
| 163 | - out := make([]Entry, 0, len(s.entries)) | ||
| 164 | - for _, entry := range s.entries { | ||
| 165 | - out = append(out, entry) | ||
| 166 | - } | ||
| 167 | - return out, nil | ||
| 168 | -} | ||
| 169 | - | ||
| 170 | -func (s *memoryStateStore) DeleteTemplate(_ context.Context, bootIndexDigest string) error { | ||
| 171 | - delete(s.entries, bootIndexDigest) | ||
| 172 | - return nil | ||
| 173 | -} | ||