mirror of
https://github.com/portainer/portainer.git
synced 2025-07-19 05:19:39 +02:00
127 lines
3.9 KiB
Go
127 lines
3.9 KiB
Go
|
package liboras
|
||
|
|
||
|
import (
|
||
|
"context"
|
||
|
"fmt"
|
||
|
"io"
|
||
|
"sort"
|
||
|
|
||
|
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||
|
portainer "github.com/portainer/portainer/api"
|
||
|
"github.com/portainer/portainer/api/concurrent"
|
||
|
"github.com/segmentio/encoding/json"
|
||
|
"golang.org/x/mod/semver"
|
||
|
"oras.land/oras-go/v2/registry"
|
||
|
"oras.land/oras-go/v2/registry/remote"
|
||
|
)
|
||
|
|
||
|
// ListRepositories retrieves all repositories from a registry using specialized repository listing clients
|
||
|
// Each registry type has different repository listing implementations that require specific API calls
|
||
|
func ListRepositories(ctx context.Context, registry *portainer.Registry, registryClient *remote.Registry) ([]string, error) {
|
||
|
factory := NewRepositoryListClientFactory()
|
||
|
listClient, err := factory.CreateListClientWithRegistry(registry, registryClient)
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("failed to create repository list client: %w", err)
|
||
|
}
|
||
|
|
||
|
return listClient.ListRepositories(ctx)
|
||
|
}
|
||
|
|
||
|
// FilterRepositoriesByMediaType filters repositories to only include those with the expected media type
|
||
|
func FilterRepositoriesByMediaType(ctx context.Context, repositoryNames []string, registryClient *remote.Registry, expectedMediaType string) ([]string, error) {
|
||
|
// Run concurrently as this can take 10s+ to complete in serial
|
||
|
var tasks []concurrent.Func
|
||
|
for _, repoName := range repositoryNames {
|
||
|
name := repoName
|
||
|
task := func(ctx context.Context) (any, error) {
|
||
|
repository, err := registryClient.Repository(ctx, name)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
if HasMediaType(ctx, repository, expectedMediaType) {
|
||
|
return name, nil
|
||
|
}
|
||
|
return nil, nil // not a repository with the expected media type
|
||
|
}
|
||
|
tasks = append(tasks, task)
|
||
|
}
|
||
|
|
||
|
// 10 is a reasonable max concurrency limit
|
||
|
results, err := concurrent.Run(ctx, 10, tasks...)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
// Collect repository names
|
||
|
var repositories []string
|
||
|
for _, result := range results {
|
||
|
if result.Result != nil {
|
||
|
if repoName, ok := result.Result.(string); ok {
|
||
|
repositories = append(repositories, repoName)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return repositories, nil
|
||
|
}
|
||
|
|
||
|
// HasMediaType checks if a repository has artifacts with the specified media type
|
||
|
func HasMediaType(ctx context.Context, repository registry.Repository, expectedMediaType string) bool {
|
||
|
// Check the first available tag
|
||
|
// Reasonable limitation - it won't work for repos where the latest tag is missing the expected media type but other tags have it
|
||
|
// This tradeoff is worth it for the performance benefits
|
||
|
var latestTag string
|
||
|
err := repository.Tags(ctx, "", func(tagList []string) error {
|
||
|
if len(tagList) > 0 {
|
||
|
// Order the taglist by latest semver, then get the latest tag
|
||
|
// e.g. ["1.0", "1.1"] -> ["1.1", "1.0"] -> "1.1"
|
||
|
sort.Slice(tagList, func(i, j int) bool {
|
||
|
return semver.Compare(tagList[i], tagList[j]) > 0
|
||
|
})
|
||
|
latestTag = tagList[0]
|
||
|
}
|
||
|
return nil
|
||
|
})
|
||
|
|
||
|
if err != nil {
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
if latestTag == "" {
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
descriptor, err := repository.Resolve(ctx, latestTag)
|
||
|
if err != nil {
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
return descriptorHasMediaType(ctx, repository, descriptor, expectedMediaType)
|
||
|
}
|
||
|
|
||
|
// descriptorHasMediaType checks if a descriptor or its manifest contains the expected media type
|
||
|
func descriptorHasMediaType(ctx context.Context, repository registry.Repository, descriptor ocispec.Descriptor, expectedMediaType string) bool {
|
||
|
// Check if the descriptor indicates the expected media type
|
||
|
if descriptor.MediaType == expectedMediaType {
|
||
|
return true
|
||
|
}
|
||
|
|
||
|
// Otherwise, look for the expected media type in the entire manifest content
|
||
|
manifestReader, err := repository.Manifests().Fetch(ctx, descriptor)
|
||
|
if err != nil {
|
||
|
return false
|
||
|
}
|
||
|
defer manifestReader.Close()
|
||
|
|
||
|
content, err := io.ReadAll(manifestReader)
|
||
|
if err != nil {
|
||
|
return false
|
||
|
}
|
||
|
var manifest ocispec.Manifest
|
||
|
if err := json.Unmarshal(content, &manifest); err != nil {
|
||
|
return false
|
||
|
}
|
||
|
return manifest.Config.MediaType == expectedMediaType
|
||
|
}
|