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  }