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 }