//go:build sync // +build sync package sync import ( "context" "encoding/json" "errors" "fmt" godigest "github.com/opencontainers/go-digest" ispec "github.com/opencontainers/image-spec/specs-go/v1" "github.com/regclient/regclient" "github.com/regclient/regclient/config" "github.com/regclient/regclient/types/descriptor" "github.com/regclient/regclient/types/errs" "github.com/regclient/regclient/types/manifest" "github.com/regclient/regclient/types/mediatype" "github.com/regclient/regclient/types/ref" "github.com/regclient/regclient/types/repo" zerr "zotregistry.dev/zot/errors" "zotregistry.dev/zot/pkg/common" "zotregistry.dev/zot/pkg/log" ) type RemoteRegistry struct { client *regclient.RegClient hosts []config.Host primaryHost string log log.Logger } func NewRemoteRegistry(client *regclient.RegClient, hosts []config.Host, logger log.Logger) Remote { registry := &RemoteRegistry{} registry.log = logger registry.hosts = hosts registry.client = client // registry.primaryHost = hosts[0].Hostname return registry } func (registry *RemoteRegistry) GetHostName() string { return registry.primaryHost } func (registry *RemoteRegistry) GetRepositories(ctx context.Context) ([]string, error) { var err error var repoList *repo.RepoList for _, host := range registry.hosts { repoList, err = registry.client.RepoList(ctx, host.Hostname) if err != nil { registry.log.Error().Err(err).Str("remote", host.Name).Msg("failed to list repositories in remote registry") continue } return repoList.Repositories, nil } return []string{}, err } func (registry *RemoteRegistry) GetImageReference(repo, reference string) (ref.Ref, error) { digest, ok := parseReference(reference) var imageRefPath string if ok { imageRefPath = fmt.Sprintf("%s/%s@%s", registry.primaryHost, repo, digest.String()) } else { // is tag imageRefPath = fmt.Sprintf("%s/%s:%s", registry.primaryHost, repo, reference) } imageRef, err := ref.New(imageRefPath) if err != nil { return ref.Ref{}, err } if imageRef.Path != "" { return ref.Ref{}, zerr.ErrSyncParseRemoteRepo } // add check for imageref to be oci return imageRef, nil } func (registry *RemoteRegistry) headManifest(ctx context.Context, imageReference ref.Ref, ) (manifest.Manifest, error) { /// check what error it gives when not found man, err := registry.client.ManifestHead(ctx, imageReference) if err != nil { /* public registries may return 401 for image not found they will try to check private registries as a fallback => 401 */ if errors.Is(err, errs.ErrHTTPUnauthorized) { registry.log.Info().Str("errorType", common.TypeOf(err)). Str("repository", imageReference.Repository).Str("reference", imageReference.Reference). Err(err).Msg("failed to get manifest: unauthorized") return nil, zerr.ErrUnauthorizedAccess } else if errors.Is(err, errs.ErrNotFound) { registry.log.Info().Str("errorType", common.TypeOf(err)). Str("repository", imageReference.Repository).Str("reference", imageReference.Reference). Err(err).Msg("failed to find manifest") return nil, zerr.ErrManifestNotFound } return nil, err } return man, nil } func (registry *RemoteRegistry) getManifest(ctx context.Context, imageReference ref.Ref, ) (manifest.Manifest, error) { /// check what error it gives when not found man, err := registry.client.ManifestGet(ctx, imageReference) if err != nil { /* public registries may return 401 for image not found they will try to check private registries as a fallback => 401 */ if errors.Is(err, errs.ErrHTTPUnauthorized) { registry.log.Info().Str("errorType", common.TypeOf(err)). Str("repository", imageReference.Repository).Str("reference", imageReference.Reference). Err(err).Msg("failed to get manifest: unauthorized") return nil, zerr.ErrUnauthorizedAccess } else if errors.Is(err, errs.ErrNotFound) { registry.log.Info().Str("errorType", common.TypeOf(err)). Str("repository", imageReference.Repository).Str("reference", imageReference.Reference). Err(err).Msg("failed to find manifest") return nil, zerr.ErrManifestNotFound } return nil, err } return man, nil } func (registry *RemoteRegistry) GetDigest(ctx context.Context, repo, tag string, ) (godigest.Digest, error) { imageReference, err := registry.GetImageReference(repo, tag) if err != nil { return "", err } man, err := registry.headManifest(ctx, imageReference) if err != nil { return "", err } return man.GetDescriptor().Digest, err } // returns OCI remote digest, original remote digest (unconverted), if it was converted. func (registry *RemoteRegistry) GetOCIDigest(ctx context.Context, repo, tag string, ) (godigest.Digest, godigest.Digest, bool, error) { var isConverted bool var desc ispec.Descriptor imageReference, err := registry.GetImageReference(repo, tag) if err != nil { return "", "", false, err } man, err := registry.getManifest(ctx, imageReference) if err != nil { return "", "", false, err } switch man.GetDescriptor().MediaType { case mediatype.Docker2Manifest: desc, err = convertDockerManifestToOCI(ctx, man, man.GetDescriptor(), imageReference, registry.client) isConverted = true case mediatype.Docker2ManifestList: desc, err = convertDockerListToOCI(ctx, man, imageReference, registry.client) isConverted = true case mediatype.OCI1Manifest, mediatype.OCI1ManifestList: desc = toOCIDescriptor(man.GetDescriptor()) default: return "", "", false, zerr.ErrMediaTypeNotSupported } return desc.Digest, man.GetDescriptor().Digest, isConverted, err } func (registry *RemoteRegistry) GetTags(ctx context.Context, repo string) ([]string, error) { repoRefPath := fmt.Sprintf("%s/%s", registry.primaryHost, repo) repoReference, err := ref.New(repoRefPath) if err != nil { return []string{}, err } tl, err := registry.client.TagList(ctx, repoReference) if err != nil { return []string{}, err } return tl.GetTags() } func convertDockerListToOCI(ctx context.Context, man manifest.Manifest, imageReference ref.Ref, regclient *regclient.RegClient, ) ( ispec.Descriptor, error, ) { var index ispec.Index index.SchemaVersion = 2 index.Manifests = []ispec.Descriptor{} index.MediaType = ispec.MediaTypeImageIndex indexer, ok := man.(manifest.Indexer) if !ok { return ispec.Descriptor{}, zerr.ErrMediaTypeNotSupported } ociIndex, err := manifest.OCIIndexFromAny(man.GetOrig()) if err != nil { return ispec.Descriptor{}, zerr.ErrMediaTypeNotSupported } manifests, err := indexer.GetManifestList() if err != nil { return ispec.Descriptor{}, zerr.ErrMediaTypeNotSupported } for _, manDesc := range manifests { ref := imageReference ref.Digest = manDesc.Digest.String() manEntry, err := regclient.ManifestGet(ctx, ref) if err != nil { return ispec.Descriptor{}, err } regclient.Close(ctx, manEntry.GetRef()) var desc ispec.Descriptor switch manEntry.GetDescriptor().MediaType { case mediatype.Docker2Manifest: desc, err = convertDockerManifestToOCI(ctx, manEntry, manDesc, ref, regclient) if err != nil { return ispec.Descriptor{}, err } case mediatype.Docker2ManifestList: desc, err = convertDockerListToOCI(ctx, manEntry, ref, regclient) if err != nil { return ispec.Descriptor{}, err } default: return ispec.Descriptor{}, err } index.Manifests = append(index.Manifests, desc) } index.Annotations = ociIndex.Annotations indexBuf, err := json.Marshal(index) if err != nil { return ispec.Descriptor{}, err } indexDesc := toOCIDescriptor(man.GetDescriptor()) indexDesc.MediaType = ispec.MediaTypeImageIndex indexDesc.Digest = godigest.FromBytes(indexBuf) indexDesc.Size = int64(len(indexBuf)) return indexDesc, nil } func convertDockerManifestToOCI(ctx context.Context, man manifest.Manifest, desc descriptor.Descriptor, imageReference ref.Ref, regclient *regclient.RegClient, ) ( ispec.Descriptor, error, ) { imager, ok := man.(manifest.Imager) if !ok { return ispec.Descriptor{}, zerr.ErrMediaTypeNotSupported } var ociManifest ispec.Manifest manifestBuf, err := man.RawBody() if err != nil { return ispec.Descriptor{}, zerr.ErrMediaTypeNotSupported } if err := json.Unmarshal(manifestBuf, &ociManifest); err != nil { return ispec.Descriptor{}, err } configDesc, err := imager.GetConfig() if err != nil { return ispec.Descriptor{}, err } // get config blob config, err := regclient.BlobGetOCIConfig(ctx, imageReference, configDesc) if err != nil { return ispec.Descriptor{}, err } configBuf, err := config.RawBody() if err != nil { return ispec.Descriptor{}, err } // convert config and manifest mediatype ociManifest.Config.Size = int64(len(configBuf)) ociManifest.Config.Digest = godigest.FromBytes(configBuf) ociManifest.Config.MediaType = ispec.MediaTypeImageConfig ociManifest.MediaType = ispec.MediaTypeImageManifest layersDesc, err := imager.GetLayers() if err != nil { return ispec.Descriptor{}, err } ociManifest.Layers = []ispec.Descriptor{} for _, layerDesc := range layersDesc { ociManifest.Layers = append(ociManifest.Layers, toOCIDescriptor(layerDesc)) } ociManifestBuf, err := json.Marshal(ociManifest) if err != nil { return ispec.Descriptor{}, err } manifestDesc := toOCIDescriptor(desc) manifestDesc.MediaType = ispec.MediaTypeImageManifest manifestDesc.Digest = godigest.FromBytes(ociManifestBuf) manifestDesc.Size = int64(len(ociManifestBuf)) return manifestDesc, nil } func toOCIDescriptor(desc descriptor.Descriptor) ispec.Descriptor { ispecPlatform := &ispec.Platform{} platform := desc.Platform if platform != nil { ispecPlatform.Architecture = platform.Architecture ispecPlatform.OS = platform.OS ispecPlatform.OSFeatures = platform.OSFeatures ispecPlatform.OSVersion = platform.OSVersion ispecPlatform.Variant = platform.Variant } else { ispecPlatform = nil } var mediaType string switch desc.MediaType { case mediatype.Docker2Manifest: mediaType = ispec.MediaTypeImageManifest case mediatype.Docker2ManifestList: mediaType = ispec.MediaTypeImageIndex case mediatype.Docker2ImageConfig: mediaType = ispec.MediaTypeImageConfig case mediatype.Docker2ForeignLayer: mediaType = ispec.MediaTypeImageLayerNonDistributable //nolint: staticcheck case mediatype.Docker2LayerGzip: mediaType = ispec.MediaTypeImageLayerGzip default: mediaType = desc.MediaType } return ispec.Descriptor{ MediaType: mediaType, Digest: desc.Digest, Size: desc.Size, URLs: desc.URLs, Annotations: desc.Annotations, Platform: ispecPlatform, ArtifactType: desc.ArtifactType, } }