2023-05-31 12:26:23 -05:00
|
|
|
//go:build sync
|
|
|
|
// +build sync
|
|
|
|
|
2021-06-08 15:11:18 -05:00
|
|
|
package sync
|
|
|
|
|
|
|
|
import (
|
2023-06-21 13:05:52 -05:00
|
|
|
"bytes"
|
2022-10-22 02:26:14 -05:00
|
|
|
"context"
|
2021-06-08 15:11:18 -05:00
|
|
|
"encoding/json"
|
2022-01-10 11:06:12 -05:00
|
|
|
"fmt"
|
2023-05-31 12:26:23 -05:00
|
|
|
"io"
|
2021-06-08 15:11:18 -05:00
|
|
|
"os"
|
|
|
|
"strings"
|
|
|
|
|
2023-03-07 12:58:42 -05:00
|
|
|
"github.com/containers/image/v5/copy"
|
2022-01-10 11:06:12 -05:00
|
|
|
"github.com/containers/image/v5/docker"
|
2022-12-16 12:33:46 -05:00
|
|
|
"github.com/containers/image/v5/manifest"
|
2023-06-21 13:05:52 -05:00
|
|
|
"github.com/containers/image/v5/pkg/blobinfocache/none"
|
2023-05-31 12:26:23 -05:00
|
|
|
"github.com/containers/image/v5/signature"
|
2021-06-08 15:11:18 -05:00
|
|
|
"github.com/containers/image/v5/types"
|
2023-05-31 12:26:23 -05:00
|
|
|
"github.com/docker/distribution/reference"
|
|
|
|
"github.com/opencontainers/go-digest"
|
2021-10-28 04:10:01 -05:00
|
|
|
ispec "github.com/opencontainers/image-spec/specs-go/v1"
|
2022-10-20 11:39:20 -05:00
|
|
|
|
2021-12-17 11:34:22 -05:00
|
|
|
zerr "zotregistry.io/zot/errors"
|
2021-12-29 10:14:56 -05:00
|
|
|
"zotregistry.io/zot/pkg/common"
|
2023-02-16 01:20:28 -05:00
|
|
|
syncconf "zotregistry.io/zot/pkg/extensions/config/sync"
|
2021-12-03 22:50:58 -05:00
|
|
|
"zotregistry.io/zot/pkg/log"
|
2023-05-26 13:08:19 -05:00
|
|
|
"zotregistry.io/zot/pkg/test/inject"
|
2021-06-08 15:11:18 -05:00
|
|
|
)
|
|
|
|
|
2023-05-31 12:26:23 -05:00
|
|
|
const (
|
|
|
|
SyncBlobUploadDir = ".sync"
|
|
|
|
)
|
2022-03-10 10:39:11 -05:00
|
|
|
|
2021-06-08 15:11:18 -05:00
|
|
|
// Get sync.FileCredentials from file.
|
2023-02-16 01:20:28 -05:00
|
|
|
func getFileCredentials(filepath string) (syncconf.CredentialsFile, error) {
|
2022-09-02 07:56:02 -05:00
|
|
|
credsFile, err := os.ReadFile(filepath)
|
2021-06-08 15:11:18 -05:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2023-02-16 01:20:28 -05:00
|
|
|
var creds syncconf.CredentialsFile
|
2021-06-08 15:11:18 -05:00
|
|
|
|
2021-12-13 14:23:31 -05:00
|
|
|
err = json.Unmarshal(credsFile, &creds)
|
2021-06-08 15:11:18 -05:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return creds, nil
|
|
|
|
}
|
2021-10-28 04:10:01 -05:00
|
|
|
|
2023-05-31 12:26:23 -05:00
|
|
|
func getUpstreamContext(certDir, username, password string, tlsVerify bool) *types.SystemContext {
|
|
|
|
upstreamCtx := &types.SystemContext{}
|
|
|
|
upstreamCtx.DockerCertPath = certDir
|
|
|
|
upstreamCtx.DockerDaemonCertPath = certDir
|
2021-10-28 04:10:01 -05:00
|
|
|
|
2023-05-31 12:26:23 -05:00
|
|
|
if tlsVerify {
|
|
|
|
upstreamCtx.DockerDaemonInsecureSkipTLSVerify = false
|
|
|
|
upstreamCtx.DockerInsecureSkipTLSVerify = types.NewOptionalBool(false)
|
|
|
|
} else {
|
|
|
|
upstreamCtx.DockerDaemonInsecureSkipTLSVerify = true
|
|
|
|
upstreamCtx.DockerInsecureSkipTLSVerify = types.NewOptionalBool(true)
|
2021-10-28 04:10:01 -05:00
|
|
|
}
|
|
|
|
|
2023-05-31 12:26:23 -05:00
|
|
|
if username != "" && password != "" {
|
|
|
|
upstreamCtx.DockerAuthConfig = &types.DockerAuthConfig{
|
|
|
|
Username: username,
|
|
|
|
Password: password,
|
2023-03-09 13:41:48 -05:00
|
|
|
}
|
2022-08-19 05:38:59 -05:00
|
|
|
}
|
|
|
|
|
2023-05-31 12:26:23 -05:00
|
|
|
return upstreamCtx
|
2022-08-19 05:38:59 -05:00
|
|
|
}
|
|
|
|
|
2021-12-29 10:14:56 -05:00
|
|
|
// sync needs transport to be stripped to not be wrongly interpreted as an image reference
|
|
|
|
// at a non-fully qualified registry (hostname as image and port as tag).
|
|
|
|
func StripRegistryTransport(url string) string {
|
|
|
|
return strings.Replace(strings.Replace(url, "http://", "", 1), "https://", "", 1)
|
|
|
|
}
|
2022-01-10 11:06:12 -05:00
|
|
|
|
2023-05-31 12:26:23 -05:00
|
|
|
// getRepoTags lists all tags in a repository.
|
|
|
|
// It returns a string slice of tags and any error encountered.
|
|
|
|
func getRepoTags(ctx context.Context, sysCtx *types.SystemContext, host, repo string) ([]string, error) {
|
|
|
|
repoRef, err := parseRepositoryReference(fmt.Sprintf("%s/%s", host, repo))
|
2022-01-10 11:06:12 -05:00
|
|
|
if err != nil {
|
2023-05-31 12:26:23 -05:00
|
|
|
return []string{}, err
|
2022-01-10 11:06:12 -05:00
|
|
|
}
|
|
|
|
|
2023-05-31 12:26:23 -05:00
|
|
|
dockerRef, err := docker.NewReference(reference.TagNameOnly(repoRef))
|
|
|
|
// hard to reach test case, injected error, see pkg/test/dev.go
|
|
|
|
if err = inject.Error(err); err != nil {
|
|
|
|
return nil, err // Should never happen for a reference with tag and no digest
|
2022-01-10 11:06:12 -05:00
|
|
|
}
|
|
|
|
|
2023-05-31 12:26:23 -05:00
|
|
|
tags, err := docker.GetRepositoryTags(ctx, sysCtx, dockerRef)
|
2022-01-10 11:06:12 -05:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2023-05-31 12:26:23 -05:00
|
|
|
return tags, nil
|
2022-01-10 11:06:12 -05:00
|
|
|
}
|
|
|
|
|
2023-05-31 12:26:23 -05:00
|
|
|
// parseRepositoryReference parses input into a reference.Named, and verifies that it names a repository, not an image.
|
|
|
|
func parseRepositoryReference(input string) (reference.Named, error) {
|
|
|
|
ref, err := reference.ParseNormalizedNamed(input)
|
2022-01-10 11:06:12 -05:00
|
|
|
if err != nil {
|
2022-04-05 10:18:31 -05:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2023-05-31 12:26:23 -05:00
|
|
|
if !reference.IsNameOnly(ref) {
|
|
|
|
return nil, zerr.ErrInvalidRepositoryName
|
2022-03-07 03:45:10 -05:00
|
|
|
}
|
|
|
|
|
2023-05-31 12:26:23 -05:00
|
|
|
return ref, nil
|
2022-03-07 03:45:10 -05:00
|
|
|
}
|
|
|
|
|
2022-10-27 11:39:59 -05:00
|
|
|
// parse a reference, return its digest and if it's valid.
|
2023-05-31 12:26:23 -05:00
|
|
|
func parseReference(reference string) (digest.Digest, bool) {
|
2022-10-27 11:39:59 -05:00
|
|
|
var ok bool
|
|
|
|
|
2023-05-31 12:26:23 -05:00
|
|
|
d, err := digest.Parse(reference)
|
2022-10-27 11:39:59 -05:00
|
|
|
if err == nil {
|
|
|
|
ok = true
|
|
|
|
}
|
|
|
|
|
|
|
|
return d, ok
|
|
|
|
}
|
|
|
|
|
2023-05-31 12:26:23 -05:00
|
|
|
func getCopyOptions(upstreamCtx, localCtx *types.SystemContext) copy.Options {
|
|
|
|
options := copy.Options{
|
|
|
|
DestinationCtx: localCtx,
|
|
|
|
SourceCtx: upstreamCtx,
|
|
|
|
ReportWriter: io.Discard,
|
|
|
|
ForceManifestMIMEType: ispec.MediaTypeImageManifest, // force only oci manifest MIME type
|
|
|
|
ImageListSelection: copy.CopyAllImages,
|
2022-03-07 03:45:10 -05:00
|
|
|
}
|
|
|
|
|
2023-05-31 12:26:23 -05:00
|
|
|
return options
|
2022-03-07 03:45:10 -05:00
|
|
|
}
|
|
|
|
|
2023-05-31 12:26:23 -05:00
|
|
|
func getPolicyContext(log log.Logger) (*signature.PolicyContext, error) {
|
|
|
|
policy := &signature.Policy{Default: []signature.PolicyRequirement{signature.NewPRInsecureAcceptAnything()}}
|
2022-04-14 14:07:44 -05:00
|
|
|
|
2023-05-31 12:26:23 -05:00
|
|
|
policyContext, err := signature.NewPolicyContext(policy)
|
|
|
|
if err := inject.Error(err); err != nil {
|
|
|
|
log.Error().Str("errorType", common.TypeOf(err)).
|
|
|
|
Err(err).Msg("couldn't create policy context")
|
2022-04-14 14:07:44 -05:00
|
|
|
|
2023-05-31 12:26:23 -05:00
|
|
|
return nil, err
|
2021-12-17 11:34:22 -05:00
|
|
|
}
|
|
|
|
|
2023-05-31 12:26:23 -05:00
|
|
|
return policyContext, nil
|
2021-12-17 11:34:22 -05:00
|
|
|
}
|
2022-12-09 14:38:00 -05:00
|
|
|
|
2023-05-31 12:26:23 -05:00
|
|
|
func getSupportedMediaType() []string {
|
|
|
|
return []string{
|
|
|
|
ispec.MediaTypeImageIndex,
|
|
|
|
ispec.MediaTypeImageManifest,
|
|
|
|
manifest.DockerV2ListMediaType,
|
|
|
|
manifest.DockerV2Schema2MediaType,
|
2022-12-09 14:38:00 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func isSupportedMediaType(mediaType string) bool {
|
2023-05-31 12:26:23 -05:00
|
|
|
mediaTypes := getSupportedMediaType()
|
|
|
|
for _, m := range mediaTypes {
|
|
|
|
if m == mediaType {
|
|
|
|
return true
|
2023-03-07 12:58:42 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-31 12:26:23 -05:00
|
|
|
return false
|
2023-03-07 12:58:42 -05:00
|
|
|
}
|
2023-06-21 13:05:52 -05:00
|
|
|
|
|
|
|
// given an imageSource and a docker manifest, convert it to OCI.
|
|
|
|
func convertDockerManifestToOCI(imageSource types.ImageSource, dockerManifestBuf []byte) ([]byte, error) {
|
|
|
|
var ociManifest ispec.Manifest
|
|
|
|
|
|
|
|
// unmarshal docker manifest into OCI manifest
|
|
|
|
err := json.Unmarshal(dockerManifestBuf, &ociManifest)
|
|
|
|
if err != nil {
|
|
|
|
return []byte{}, err
|
|
|
|
}
|
|
|
|
|
|
|
|
configContent, err := getImageConfigContent(imageSource, ociManifest.Config.Digest)
|
|
|
|
if err != nil {
|
|
|
|
return []byte{}, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// marshal config blob into OCI config, will remove keys specific to docker
|
|
|
|
var ociConfig ispec.Image
|
|
|
|
|
|
|
|
err = json.Unmarshal(configContent, &ociConfig)
|
|
|
|
if err != nil {
|
|
|
|
return []byte{}, err
|
|
|
|
}
|
|
|
|
|
|
|
|
ociConfigContent, err := json.Marshal(ociConfig)
|
|
|
|
if err != nil {
|
|
|
|
return []byte{}, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// convert layers
|
|
|
|
err = convertDockerLayersToOCI(ociManifest.Layers)
|
|
|
|
if err != nil {
|
|
|
|
return []byte{}, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// convert config and manifest mediatype
|
|
|
|
ociManifest.Config.Size = int64(len(ociConfigContent))
|
|
|
|
ociManifest.Config.Digest = digest.FromBytes(ociConfigContent)
|
|
|
|
ociManifest.Config.MediaType = ispec.MediaTypeImageConfig
|
|
|
|
ociManifest.MediaType = ispec.MediaTypeImageManifest
|
|
|
|
|
|
|
|
return json.Marshal(ociManifest)
|
|
|
|
}
|
|
|
|
|
|
|
|
// convert docker layers mediatypes to OCI mediatypes.
|
|
|
|
func convertDockerLayersToOCI(dockerLayers []ispec.Descriptor) error {
|
|
|
|
for idx, layer := range dockerLayers {
|
|
|
|
switch layer.MediaType {
|
|
|
|
case manifest.DockerV2Schema2ForeignLayerMediaType:
|
|
|
|
dockerLayers[idx].MediaType = ispec.MediaTypeImageLayerNonDistributable //nolint: staticcheck
|
|
|
|
case manifest.DockerV2Schema2ForeignLayerMediaTypeGzip:
|
|
|
|
dockerLayers[idx].MediaType = ispec.MediaTypeImageLayerNonDistributableGzip //nolint: staticcheck
|
|
|
|
case manifest.DockerV2SchemaLayerMediaTypeUncompressed:
|
|
|
|
dockerLayers[idx].MediaType = ispec.MediaTypeImageLayer
|
|
|
|
case manifest.DockerV2Schema2LayerMediaType:
|
|
|
|
dockerLayers[idx].MediaType = ispec.MediaTypeImageLayerGzip
|
|
|
|
default:
|
|
|
|
return zerr.ErrMediaTypeNotSupported
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// given an imageSource and a docker index manifest, convert it to OCI.
|
|
|
|
func convertDockerIndexToOCI(imageSource types.ImageSource, dockerManifestBuf []byte) ([]byte, error) {
|
|
|
|
// get docker index
|
|
|
|
originalIndex, err := manifest.ListFromBlob(dockerManifestBuf, manifest.DockerV2ListMediaType)
|
|
|
|
if err != nil {
|
|
|
|
return []byte{}, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// get manifests digests
|
|
|
|
manifestsDigests := originalIndex.Instances()
|
|
|
|
|
|
|
|
manifestsUpdates := make([]manifest.ListUpdate, 0, len(manifestsDigests))
|
|
|
|
|
|
|
|
// convert each manifests in index from docker to OCI
|
|
|
|
for _, manifestDigest := range manifestsDigests {
|
|
|
|
digestCopy := manifestDigest
|
|
|
|
|
|
|
|
indexManifestBuf, _, err := imageSource.GetManifest(context.Background(), &digestCopy)
|
|
|
|
if err != nil {
|
|
|
|
return []byte{}, err
|
|
|
|
}
|
|
|
|
|
|
|
|
convertedIndexManifest, err := convertDockerManifestToOCI(imageSource, indexManifestBuf)
|
|
|
|
if err != nil {
|
|
|
|
return []byte{}, err
|
|
|
|
}
|
|
|
|
|
|
|
|
manifestsUpdates = append(manifestsUpdates, manifest.ListUpdate{
|
|
|
|
Digest: digest.FromBytes(convertedIndexManifest),
|
|
|
|
Size: int64(len(convertedIndexManifest)),
|
|
|
|
MediaType: ispec.MediaTypeImageManifest,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
// update all manifests in index
|
|
|
|
if err := originalIndex.UpdateInstances(manifestsUpdates); err != nil {
|
|
|
|
return []byte{}, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// convert index to OCI
|
|
|
|
convertedList, err := originalIndex.ConvertToMIMEType(ispec.MediaTypeImageIndex)
|
|
|
|
if err != nil {
|
|
|
|
return []byte{}, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return convertedList.Serialize()
|
|
|
|
}
|
|
|
|
|
|
|
|
// given an image source and a config blob digest, get blob config content.
|
|
|
|
func getImageConfigContent(imageSource types.ImageSource, configDigest digest.Digest,
|
|
|
|
) ([]byte, error) {
|
|
|
|
configBlob, _, err := imageSource.GetBlob(context.Background(), types.BlobInfo{
|
|
|
|
Digest: configDigest,
|
|
|
|
}, none.NoCache)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
configBuf := new(bytes.Buffer)
|
|
|
|
|
|
|
|
_, err = configBuf.ReadFrom(configBlob)
|
|
|
|
|
|
|
|
return configBuf.Bytes(), err
|
|
|
|
}
|