mirror of https://github.com/portainer/portainer
152 lines
4.9 KiB
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
|
|
}
|