zotregistry.dev/zot@v1.4.4-0.20240314164342-eec277e14d20/pkg/extensions/sync/utils.go (about)

     1  //go:build sync
     2  // +build sync
     3  
     4  package sync
     5  
     6  import (
     7  	"bytes"
     8  	"context"
     9  	"encoding/json"
    10  	"fmt"
    11  	"io"
    12  	"os"
    13  	"strings"
    14  
    15  	"github.com/containers/image/v5/copy"
    16  	"github.com/containers/image/v5/docker"
    17  	"github.com/containers/image/v5/docker/reference"
    18  	"github.com/containers/image/v5/manifest"
    19  	"github.com/containers/image/v5/pkg/blobinfocache/none"
    20  	"github.com/containers/image/v5/signature"
    21  	"github.com/containers/image/v5/types"
    22  	"github.com/opencontainers/go-digest"
    23  	ispec "github.com/opencontainers/image-spec/specs-go/v1"
    24  
    25  	zerr "zotregistry.dev/zot/errors"
    26  	"zotregistry.dev/zot/pkg/common"
    27  	syncconf "zotregistry.dev/zot/pkg/extensions/config/sync"
    28  	"zotregistry.dev/zot/pkg/log"
    29  	"zotregistry.dev/zot/pkg/test/inject"
    30  )
    31  
    32  // Get sync.FileCredentials from file.
    33  func getFileCredentials(filepath string) (syncconf.CredentialsFile, error) {
    34  	credsFile, err := os.ReadFile(filepath)
    35  	if err != nil {
    36  		return nil, err
    37  	}
    38  
    39  	var creds syncconf.CredentialsFile
    40  
    41  	err = json.Unmarshal(credsFile, &creds)
    42  	if err != nil {
    43  		return nil, err
    44  	}
    45  
    46  	return creds, nil
    47  }
    48  
    49  func getUpstreamContext(certDir, username, password string, tlsVerify bool) *types.SystemContext {
    50  	upstreamCtx := &types.SystemContext{}
    51  	upstreamCtx.DockerCertPath = certDir
    52  	upstreamCtx.DockerDaemonCertPath = certDir
    53  
    54  	if tlsVerify {
    55  		upstreamCtx.DockerDaemonInsecureSkipTLSVerify = false
    56  		upstreamCtx.DockerInsecureSkipTLSVerify = types.NewOptionalBool(false)
    57  	} else {
    58  		upstreamCtx.DockerDaemonInsecureSkipTLSVerify = true
    59  		upstreamCtx.DockerInsecureSkipTLSVerify = types.NewOptionalBool(true)
    60  	}
    61  
    62  	if username != "" && password != "" {
    63  		upstreamCtx.DockerAuthConfig = &types.DockerAuthConfig{
    64  			Username: username,
    65  			Password: password,
    66  		}
    67  	}
    68  
    69  	return upstreamCtx
    70  }
    71  
    72  // sync needs transport to be stripped to not be wrongly interpreted as an image reference
    73  // at a non-fully qualified registry (hostname as image and port as tag).
    74  func StripRegistryTransport(url string) string {
    75  	return strings.Replace(strings.Replace(url, "http://", "", 1), "https://", "", 1)
    76  }
    77  
    78  // getRepoTags lists all tags in a repository.
    79  // It returns a string slice of tags and any error encountered.
    80  func getRepoTags(ctx context.Context, sysCtx *types.SystemContext, host, repo string) ([]string, error) {
    81  	repoRef, err := parseRepositoryReference(fmt.Sprintf("%s/%s", host, repo))
    82  	if err != nil {
    83  		return []string{}, err
    84  	}
    85  
    86  	dockerRef, err := docker.NewReference(reference.TagNameOnly(repoRef))
    87  	// hard to reach test case, injected error, see pkg/test/dev.go
    88  	if err = inject.Error(err); err != nil {
    89  		return nil, err // Should never happen for a reference with tag and no digest
    90  	}
    91  
    92  	tags, err := docker.GetRepositoryTags(ctx, sysCtx, dockerRef)
    93  	if err != nil {
    94  		return nil, err
    95  	}
    96  
    97  	return tags, nil
    98  }
    99  
   100  // parseRepositoryReference parses input into a reference.Named, and verifies that it names a repository, not an image.
   101  func parseRepositoryReference(input string) (reference.Named, error) {
   102  	ref, err := reference.ParseNormalizedNamed(input)
   103  	if err != nil {
   104  		return nil, err
   105  	}
   106  
   107  	if !reference.IsNameOnly(ref) {
   108  		return nil, zerr.ErrInvalidRepositoryName
   109  	}
   110  
   111  	return ref, nil
   112  }
   113  
   114  // parse a reference, return its digest and if it's valid.
   115  func parseReference(reference string) (digest.Digest, bool) {
   116  	var ok bool
   117  
   118  	d, err := digest.Parse(reference)
   119  	if err == nil {
   120  		ok = true
   121  	}
   122  
   123  	return d, ok
   124  }
   125  
   126  func getCopyOptions(upstreamCtx, localCtx *types.SystemContext) copy.Options {
   127  	options := copy.Options{
   128  		DestinationCtx:        localCtx,
   129  		SourceCtx:             upstreamCtx,
   130  		ReportWriter:          io.Discard,
   131  		ForceManifestMIMEType: ispec.MediaTypeImageManifest, // force only oci manifest MIME type
   132  		ImageListSelection:    copy.CopyAllImages,
   133  	}
   134  
   135  	return options
   136  }
   137  
   138  func getPolicyContext(log log.Logger) (*signature.PolicyContext, error) {
   139  	policy := &signature.Policy{Default: []signature.PolicyRequirement{signature.NewPRInsecureAcceptAnything()}}
   140  
   141  	policyContext, err := signature.NewPolicyContext(policy)
   142  	if err := inject.Error(err); err != nil {
   143  		log.Error().Str("errorType", common.TypeOf(err)).
   144  			Err(err).Msg("couldn't create policy context")
   145  
   146  		return nil, err
   147  	}
   148  
   149  	return policyContext, nil
   150  }
   151  
   152  func getSupportedMediaType() []string {
   153  	return []string{
   154  		ispec.MediaTypeImageIndex,
   155  		ispec.MediaTypeImageManifest,
   156  		manifest.DockerV2ListMediaType,
   157  		manifest.DockerV2Schema2MediaType,
   158  	}
   159  }
   160  
   161  func isSupportedMediaType(mediaType string) bool {
   162  	mediaTypes := getSupportedMediaType()
   163  	for _, m := range mediaTypes {
   164  		if m == mediaType {
   165  			return true
   166  		}
   167  	}
   168  
   169  	return false
   170  }
   171  
   172  // given an imageSource and a docker manifest, convert it to OCI.
   173  func convertDockerManifestToOCI(imageSource types.ImageSource, dockerManifestBuf []byte) ([]byte, error) {
   174  	var ociManifest ispec.Manifest
   175  
   176  	// unmarshal docker manifest into OCI manifest
   177  	err := json.Unmarshal(dockerManifestBuf, &ociManifest)
   178  	if err != nil {
   179  		return []byte{}, err
   180  	}
   181  
   182  	configContent, err := getImageConfigContent(imageSource, ociManifest.Config.Digest)
   183  	if err != nil {
   184  		return []byte{}, err
   185  	}
   186  
   187  	// marshal config blob into OCI config, will remove keys specific to docker
   188  	var ociConfig ispec.Image
   189  
   190  	err = json.Unmarshal(configContent, &ociConfig)
   191  	if err != nil {
   192  		return []byte{}, err
   193  	}
   194  
   195  	ociConfigContent, err := json.Marshal(ociConfig)
   196  	if err != nil {
   197  		return []byte{}, err
   198  	}
   199  
   200  	// convert layers
   201  	err = convertDockerLayersToOCI(ociManifest.Layers)
   202  	if err != nil {
   203  		return []byte{}, err
   204  	}
   205  
   206  	// convert config and manifest mediatype
   207  	ociManifest.Config.Size = int64(len(ociConfigContent))
   208  	ociManifest.Config.Digest = digest.FromBytes(ociConfigContent)
   209  	ociManifest.Config.MediaType = ispec.MediaTypeImageConfig
   210  	ociManifest.MediaType = ispec.MediaTypeImageManifest
   211  
   212  	return json.Marshal(ociManifest)
   213  }
   214  
   215  // convert docker layers mediatypes to OCI mediatypes.
   216  func convertDockerLayersToOCI(dockerLayers []ispec.Descriptor) error {
   217  	for idx, layer := range dockerLayers {
   218  		switch layer.MediaType {
   219  		case manifest.DockerV2Schema2ForeignLayerMediaType:
   220  			dockerLayers[idx].MediaType = ispec.MediaTypeImageLayerNonDistributable //nolint: staticcheck
   221  		case manifest.DockerV2Schema2ForeignLayerMediaTypeGzip:
   222  			dockerLayers[idx].MediaType = ispec.MediaTypeImageLayerNonDistributableGzip //nolint: staticcheck
   223  		case manifest.DockerV2SchemaLayerMediaTypeUncompressed:
   224  			dockerLayers[idx].MediaType = ispec.MediaTypeImageLayer
   225  		case manifest.DockerV2Schema2LayerMediaType:
   226  			dockerLayers[idx].MediaType = ispec.MediaTypeImageLayerGzip
   227  		default:
   228  			return zerr.ErrMediaTypeNotSupported
   229  		}
   230  	}
   231  
   232  	return nil
   233  }
   234  
   235  // given an imageSource and a docker index manifest, convert it to OCI.
   236  func convertDockerIndexToOCI(imageSource types.ImageSource, dockerManifestBuf []byte) ([]byte, error) {
   237  	// get docker index
   238  	originalIndex, err := manifest.ListFromBlob(dockerManifestBuf, manifest.DockerV2ListMediaType)
   239  	if err != nil {
   240  		return []byte{}, err
   241  	}
   242  
   243  	// get manifests digests
   244  	manifestsDigests := originalIndex.Instances()
   245  
   246  	manifestsUpdates := make([]manifest.ListUpdate, 0, len(manifestsDigests))
   247  
   248  	// convert each manifests in index from docker to OCI
   249  	for _, manifestDigest := range manifestsDigests {
   250  		digestCopy := manifestDigest
   251  
   252  		indexManifestBuf, _, err := imageSource.GetManifest(context.Background(), &digestCopy)
   253  		if err != nil {
   254  			return []byte{}, err
   255  		}
   256  
   257  		convertedIndexManifest, err := convertDockerManifestToOCI(imageSource, indexManifestBuf)
   258  		if err != nil {
   259  			return []byte{}, err
   260  		}
   261  
   262  		manifestsUpdates = append(manifestsUpdates, manifest.ListUpdate{
   263  			Digest:    digest.FromBytes(convertedIndexManifest),
   264  			Size:      int64(len(convertedIndexManifest)),
   265  			MediaType: ispec.MediaTypeImageManifest,
   266  		})
   267  	}
   268  
   269  	// update all manifests in index
   270  	if err := originalIndex.UpdateInstances(manifestsUpdates); err != nil {
   271  		return []byte{}, err
   272  	}
   273  
   274  	// convert index to OCI
   275  	convertedList, err := originalIndex.ConvertToMIMEType(ispec.MediaTypeImageIndex)
   276  	if err != nil {
   277  		return []byte{}, err
   278  	}
   279  
   280  	return convertedList.Serialize()
   281  }
   282  
   283  // given an image source and a config blob digest, get blob config content.
   284  func getImageConfigContent(imageSource types.ImageSource, configDigest digest.Digest,
   285  ) ([]byte, error) {
   286  	configBlob, _, err := imageSource.GetBlob(context.Background(), types.BlobInfo{
   287  		Digest: configDigest,
   288  	}, none.NoCache)
   289  	if err != nil {
   290  		return nil, err
   291  	}
   292  
   293  	configBuf := new(bytes.Buffer)
   294  
   295  	_, err = configBuf.ReadFrom(configBlob)
   296  
   297  	return configBuf.Bytes(), err
   298  }