portainer/pkg/liboras/dummy_manifest.go

152 lines
4.9 KiB
Go

package liboras
import (
"bytes"
"context"
"fmt"
"time"
"github.com/opencontainers/go-digest"
"github.com/opencontainers/image-spec/specs-go"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/rs/zerolog/log"
"github.com/segmentio/encoding/json"
"oras.land/oras-go/v2/registry/remote"
)
// generateMinimalManifest creates a minimal OCI manifest with empty config and no layers
func generateMinimalManifest() (*ocispec.Manifest, []byte, error) {
// Create empty config blob
emptyConfig := []byte("{}")
configDescriptor := ocispec.Descriptor{
MediaType: "application/vnd.oci.image.config.v1+json",
Digest: digest.FromBytes(emptyConfig), // sha256 of empty JSON object "{}"
Size: int64(len(emptyConfig)),
}
// Create minimal manifest with no layers
manifest := &ocispec.Manifest{
Versioned: specs.Versioned{
SchemaVersion: 2,
},
MediaType: "application/vnd.oci.image.manifest.v1+json",
Config: configDescriptor,
Layers: []ocispec.Descriptor{}, // Empty layers array
}
// Marshal manifest to JSON
manifestBytes, err := json.Marshal(manifest)
if err != nil {
return nil, nil, fmt.Errorf("failed to marshal dummy manifest: %w", err)
}
return manifest, manifestBytes, nil
}
// CreateDummyManifest creates a minimal dummy manifest in the repository and returns its digest
func CreateDummyManifest(registryClient *remote.Registry, repository string) (string, error) {
ctx := context.Background()
// Get repository handle
repo, err := registryClient.Repository(ctx, repository)
if err != nil {
return "", fmt.Errorf("failed to get repository handle: %w", err)
}
// Generate minimal manifest
manifest, manifestBytes, err := generateMinimalManifest()
if err != nil {
return "", fmt.Errorf("failed to generate minimal manifest: %w", err)
}
// First, push the empty config blob
emptyConfig := []byte("{}")
configReader := bytes.NewReader(emptyConfig)
configDescriptor := ocispec.Descriptor{
MediaType: "application/vnd.oci.image.config.v1+json",
Digest: digest.FromBytes(emptyConfig),
Size: int64(len(emptyConfig)),
}
err = repo.Blobs().Push(ctx, configDescriptor, configReader)
if err != nil {
return "", fmt.Errorf("failed to push config blob: %w", err)
}
// Then push the manifest with a temporary tag
manifestReader := bytes.NewReader(manifestBytes)
manifestDescriptor := ocispec.Descriptor{
MediaType: manifest.MediaType,
Size: int64(len(manifestBytes)),
Digest: digest.FromBytes(manifestBytes),
}
// Use a unique temporary tag name for the dummy manifest
dummyTag := fmt.Sprintf("__portainer_dummy_%d", time.Now().UnixNano())
err = repo.Manifests().PushReference(ctx, manifestDescriptor, manifestReader, dummyTag)
if err != nil {
return "", fmt.Errorf("failed to push dummy manifest: %w", err)
}
// Return the manifest digest directly from what we calculated
return manifestDescriptor.Digest.String(), nil
}
// PointTagToDummy updates a tag to point to the dummy manifest
func PointTagToDummy(registryClient *remote.Registry, repository, tagName, dummyDigest string) error {
// Generate the same minimal manifest content
_, manifestBytes, err := generateMinimalManifest()
if err != nil {
return fmt.Errorf("failed to generate minimal manifest: %w", err)
}
return AddTagToManifest(registryClient, repository, tagName, dummyDigest, manifestBytes)
}
// SafeDeleteTags safely deletes multiple tags without affecting others pointing to the same manifest
func SafeDeleteTags(registryClient *remote.Registry, repository string, tagsToDelete []string) error {
if len(tagsToDelete) == 0 {
return nil
}
// Create a dummy manifest for all tags to delete
dummyDigest, err := CreateDummyManifest(registryClient, repository)
if err != nil {
return fmt.Errorf("failed to create dummy manifest: %w", err)
}
// Point all tags to the same dummy manifest
for _, tagToDelete := range tagsToDelete {
err = PointTagToDummy(registryClient, repository, tagToDelete, dummyDigest)
if err != nil {
// Cleanup: delete dummy manifest on failure
cleanupErr := DeleteManifestByDigest(registryClient, repository, dummyDigest)
if cleanupErr != nil {
log.Warn().
Err(cleanupErr).
Str("repository", repository).
Str("tag", tagToDelete).
Str("digest", dummyDigest).
Msg("Failed to cleanup dummy manifest after tag pointing error")
return fmt.Errorf("failed to point tag %s to dummy: %w (cleanup also failed: %w)", tagToDelete, err, cleanupErr)
}
return fmt.Errorf("failed to point tag %s to dummy: %w", tagToDelete, err)
}
}
// Delete the dummy manifest (removes ALL pointed tags safely)
err = DeleteManifestByDigest(registryClient, repository, dummyDigest)
if err != nil {
log.Error().
Err(err).
Str("repository", repository).
Str("digest", dummyDigest).
Int("tag_count", len(tagsToDelete)).
Msg("Failed to delete dummy manifest containing tags")
return fmt.Errorf("failed to delete dummy manifest: %w", err)
}
return nil
}