k8s.io/registry.k8s.io@v0.3.1/cmd/geranos/s3uploader.go (about) 1 /* 2 Copyright 2022 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package main 18 19 import ( 20 "bytes" 21 "encoding/base64" 22 "encoding/hex" 23 "errors" 24 "io" 25 "strings" 26 27 "github.com/google/go-containerregistry/pkg/crane" 28 "github.com/google/go-containerregistry/pkg/name" 29 v1 "github.com/google/go-containerregistry/pkg/v1" 30 31 "github.com/aws/aws-sdk-go/aws" 32 "github.com/aws/aws-sdk-go/aws/awserr" 33 "github.com/aws/aws-sdk-go/aws/credentials" 34 "github.com/aws/aws-sdk-go/aws/session" 35 "github.com/aws/aws-sdk-go/service/s3" 36 "github.com/aws/aws-sdk-go/service/s3/s3manager" 37 38 "k8s.io/klog/v2" 39 ) 40 41 // see cmd/archeio, this matches the layout of GCR's GCS bucket 42 // containers/images/sha256:$layer_digest 43 const blobKeyPrefix = "containers/images/" 44 45 // this is where geranos *internally* records manifests 46 // these are not for user consumption 47 const manifestKeyPrefix = "geranos/uploaded-images/" 48 49 type s3Uploader struct { 50 svc *s3.S3 51 uploader *s3manager.Uploader 52 reuploadLayers bool 53 dryRun bool 54 } 55 56 func newS3Uploader(dryRun bool) (*s3Uploader, error) { 57 cfg := []*aws.Config{} 58 // force anonymous configs for dry run uploaders 59 if dryRun { 60 cfg = append(cfg, &aws.Config{ 61 Credentials: credentials.AnonymousCredentials, 62 }) 63 } 64 sess, err := session.NewSession(cfg...) 65 if err != nil { 66 return nil, err 67 } 68 r := &s3Uploader{ 69 dryRun: dryRun, 70 svc: s3.New(sess), 71 } 72 r.uploader = s3manager.NewUploaderWithClient(r.svc) 73 return r, nil 74 } 75 76 func (s *s3Uploader) UploadImage(bucket string, ref name.Reference, layers []v1.Layer, opts ...crane.Option) error { 77 for _, layer := range layers { 78 if err := s.copyLayerToS3(bucket, layer); err != nil { 79 return err 80 } 81 } 82 m, err := manifestBlobFromRef(ref, opts...) 83 if err != nil { 84 return err 85 } 86 return s.copyManifestToS3(bucket, m) 87 } 88 89 func (s *s3Uploader) ImageAlreadyUploaded(bucket string, imageDigest string) (bool, error) { 90 return s.blobExists(bucket, keyForImageRecord(imageDigest)) 91 } 92 93 // imageBlob requires the subset of v1.Layer methods 94 // required for uploading a blob 95 type imageBlob interface { 96 Digest() (v1.Hash, error) 97 Compressed() (io.ReadCloser, error) 98 } 99 100 type manifestBlob struct { 101 raw []byte 102 digest v1.Hash 103 } 104 105 func manifestBlobFromRef(ref name.Reference, opts ...crane.Option) (*manifestBlob, error) { 106 p := strings.Split(ref.Name(), "@") 107 if len(p) != 2 { 108 return nil, errors.New("invalid reference") 109 } 110 digest, err := v1.NewHash(p[1]) 111 if err != nil { 112 return nil, err 113 } 114 manifest, err := crane.Manifest(ref.Name(), opts...) 115 if err != nil { 116 return nil, err 117 } 118 return &manifestBlob{ 119 raw: manifest, 120 digest: digest, 121 }, nil 122 } 123 124 func (m *manifestBlob) Digest() (v1.Hash, error) { 125 return m.digest, nil 126 } 127 128 func (m *manifestBlob) Compressed() (io.ReadCloser, error) { 129 return io.NopCloser(bytes.NewReader(m.raw)), nil 130 } 131 132 func (s *s3Uploader) copyManifestToS3(bucket string, layer imageBlob) error { 133 digest, err := layer.Digest() 134 if err != nil { 135 return err 136 } 137 key := keyForImageRecord(digest.String()) 138 return s.copyToS3(bucket, key, layer) 139 } 140 141 func (s *s3Uploader) copyLayerToS3(bucket string, layer imageBlob) error { 142 digest, err := layer.Digest() 143 if err != nil { 144 return err 145 } 146 key := keyForLayer(digest.String()) 147 return s.copyToS3(bucket, key, layer) 148 } 149 150 func (s *s3Uploader) copyToS3(bucket, key string, layer imageBlob) error { 151 digest, err := layer.Digest() 152 if err != nil { 153 return err 154 } 155 if !s.reuploadLayers { 156 exists, err := s.blobExists(bucket, key) 157 if err != nil { 158 klog.Errorf("failed to check if blob exists: %v", err) 159 } else if exists { 160 klog.V(4).Infof("Layer already exists: %s", key) 161 return nil 162 } 163 } 164 r, err := layer.Compressed() 165 if err != nil { 166 return err 167 } 168 defer r.Close() 169 uploadInput := &s3manager.UploadInput{ 170 Bucket: aws.String(bucket), 171 Key: aws.String(key), 172 Body: r, 173 } 174 // TODO: what if it isn't sha256? 175 if digest.Algorithm == "SHA256" { 176 b, err := hex.DecodeString(digest.Hex) 177 if err != nil { 178 return err 179 } 180 uploadInput.ChecksumSHA256 = aws.String(base64.StdEncoding.EncodeToString(b)) 181 } 182 // skip actually uploading if this is a dry-run, otherwise finally upload 183 klog.Infof("Uploading: %s", key) 184 if s.dryRun { 185 return nil 186 } 187 _, err = s.uploader.Upload(uploadInput) 188 return err 189 } 190 191 func keyForLayer(digest string) string { 192 return blobKeyPrefix + digest 193 } 194 195 func keyForImageRecord(imageDigest string) string { 196 return manifestKeyPrefix + imageDigest 197 } 198 199 func (s *s3Uploader) blobExists(bucket, key string) (bool, error) { 200 _, err := s.svc.HeadObject(&s3.HeadObjectInput{ 201 Bucket: aws.String(bucket), 202 Key: aws.String(key), 203 }) 204 if err != nil { 205 // yes, we really have to typecast to compare against an undocument string 206 // to check if the object doesn't exist vs an error making the call 207 if aerr, ok := err.(awserr.Error); ok && aerr.Code() == "NotFound" { 208 return false, nil 209 } 210 return false, err 211 } 212 213 return true, nil 214 }