github.com/wmuizelaar/kpt@v0.0.0-20221018115725-bd564717b2ed/pkg/oci/storage.go (about)

     1  // Copyright 2022 Google LLC
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package oci
    16  
    17  import (
    18  	"context"
    19  	"errors"
    20  	"fmt"
    21  	"io"
    22  	"io/fs"
    23  	"net/http"
    24  	"os"
    25  	"path/filepath"
    26  
    27  	"github.com/google/go-containerregistry/pkg/gcrane"
    28  	"github.com/google/go-containerregistry/pkg/name"
    29  	v1 "github.com/google/go-containerregistry/pkg/v1"
    30  	"github.com/google/go-containerregistry/pkg/v1/cache"
    31  	"github.com/google/go-containerregistry/pkg/v1/google"
    32  	"github.com/google/go-containerregistry/pkg/v1/remote"
    33  	"k8s.io/klog/v2"
    34  )
    35  
    36  // Storage provides helper functions specifically for OCI storage.
    37  // It abstracts and simplifies the go-containerregistry library, but is agnostic to the contents of the images etc.
    38  type Storage struct {
    39  	imageCache cache.Cache
    40  	cacheDir   string
    41  
    42  	transport http.RoundTripper
    43  }
    44  
    45  // NewStorage creates a Storage for managing OCI images.
    46  func NewStorage(cacheDir string) (*Storage, error) {
    47  	if err := os.MkdirAll(cacheDir, 0755); err != nil {
    48  		return nil, fmt.Errorf("failed to create cache dir %q: %w", cacheDir, err)
    49  	}
    50  
    51  	c := cache.NewFilesystemCache(filepath.Join(cacheDir, "layers"))
    52  
    53  	return &Storage{
    54  		imageCache: c,
    55  		cacheDir:   cacheDir,
    56  		transport:  http.DefaultTransport,
    57  	}, nil
    58  }
    59  
    60  // ImageTagName holds an image we know by tag (but tags are mutable, so this often implies lookups)
    61  type ImageTagName struct {
    62  	Image string
    63  	Tag   string
    64  }
    65  
    66  func (i ImageTagName) String() string {
    67  	return fmt.Sprintf("%s:%s", i.Image, i.Tag)
    68  }
    69  
    70  func (i ImageTagName) ociReference() (name.Reference, error) {
    71  	imageRef, err := name.NewTag(i.Image + ":" + i.Tag)
    72  	if err != nil {
    73  		return nil, fmt.Errorf("cannot parse image name %q: %w", i, err)
    74  	}
    75  	return imageRef, nil
    76  }
    77  
    78  func ParseImageTagName(s string) (*ImageTagName, error) {
    79  	t, err := name.NewTag(s)
    80  	if err != nil {
    81  		return nil, fmt.Errorf("cannot parse %q as tag: %w", s, err)
    82  	}
    83  	return &ImageTagName{
    84  		Image: t.Repository.Name(),
    85  		Tag:   t.TagStr(),
    86  	}, nil
    87  }
    88  
    89  // ImageDigestName holds an image we know by digest (which is immutable and more cacheable)
    90  type ImageDigestName struct {
    91  	Image  string
    92  	Digest string
    93  }
    94  
    95  func (i ImageDigestName) String() string {
    96  	return fmt.Sprintf("%s:%s", i.Image, i.Digest)
    97  }
    98  
    99  func (i ImageDigestName) ociReference() (name.Reference, error) {
   100  	imageRef, err := name.NewDigest(i.Image + "@" + i.Digest)
   101  	if err != nil {
   102  		return nil, fmt.Errorf("cannot parse image name %q: %w", i, err)
   103  	}
   104  	return imageRef, nil
   105  }
   106  
   107  func (r *Storage) CreateOptions(ctx context.Context) []google.Option {
   108  	// TODO: Authentication must be set up correctly. Do we use:
   109  	// * Provided Service account?
   110  	// * Workload identity?
   111  	// * Caller credentials (is this possible with k8s apiserver)?
   112  	return []google.Option{
   113  		google.WithAuthFromKeychain(gcrane.Keychain),
   114  		google.WithContext(ctx),
   115  	}
   116  }
   117  
   118  type imageName interface {
   119  	ociReference() (name.Reference, error)
   120  }
   121  
   122  // ToRemoteImage builds a remote image reference for the given name, including caching and authentication.
   123  func (r *Storage) ToRemoteImage(ctx context.Context, imageName imageName) (v1.Image, error) {
   124  	// TODO: Authentication must be set up correctly. Do we use:
   125  	// * Provided Service account?
   126  	// * Workload identity?
   127  	// * Caller credentials (is this possible with k8s apiserver)?
   128  	options := []remote.Option{
   129  		remote.WithAuthFromKeychain(gcrane.Keychain),
   130  		remote.WithContext(ctx),
   131  		remote.WithTransport(r.transport),
   132  	}
   133  
   134  	imageRef, err := imageName.ociReference()
   135  	if err != nil {
   136  		return nil, err
   137  	}
   138  	// TODO: Can we use a digest to save a lookup?
   139  	// imageRef = imageRef.Context().Digest(digest)
   140  
   141  	ociImage, err := remote.Image(imageRef, options...)
   142  	if err != nil {
   143  		return nil, err
   144  	}
   145  
   146  	ociImage = cache.Image(ociImage, r.imageCache)
   147  
   148  	return ociImage, nil
   149  }
   150  
   151  func (r *Storage) GetCacheDir() string {
   152  	return r.cacheDir
   153  }
   154  
   155  // WithCacheFile runs with a filesystem-backed cache.
   156  // If cacheFilePath does not exist, it will be fetched with the function fetcher.
   157  // The file contents are then processed with the function reader.
   158  // TODO: We likely need some form of GC/LRU on the cache file paths.
   159  // We can probably use FS access time (or we might need to touch the files when we access them)!
   160  func WithCacheFile(cacheFilePath string, fetcher func() (io.ReadCloser, error)) (io.ReadCloser, error) {
   161  	dir := filepath.Dir(cacheFilePath)
   162  
   163  	f, err := os.Open(cacheFilePath)
   164  	if err != nil {
   165  		if !errors.Is(err, fs.ErrNotExist) {
   166  			return nil, fmt.Errorf("error opening cache file %q: %w", cacheFilePath, err)
   167  		}
   168  	} else {
   169  		// TODO: Delete file if corrupt?
   170  		return f, nil
   171  	}
   172  
   173  	r, err := fetcher()
   174  	if err != nil {
   175  		return nil, err
   176  	}
   177  	defer r.Close()
   178  
   179  	if err := os.MkdirAll(dir, 0755); err != nil {
   180  		return nil, fmt.Errorf("failed to create cache directory %q: %w", dir, err)
   181  	}
   182  
   183  	tempFile, err := os.CreateTemp(dir, "")
   184  	if err != nil {
   185  		return nil, fmt.Errorf("failed to create tempfile in directory %q: %w", dir, err)
   186  	}
   187  	defer func() {
   188  		if tempFile != nil {
   189  			if err := tempFile.Close(); err != nil {
   190  				klog.Warningf("error closing temp file: %v", err)
   191  			}
   192  
   193  			if err := os.Remove(tempFile.Name()); err != nil {
   194  				klog.Warningf("failed to write tempfile: %v", err)
   195  			}
   196  		}
   197  	}()
   198  	if _, err := io.Copy(tempFile, r); err != nil {
   199  		return nil, fmt.Errorf("error caching data: %w", err)
   200  	}
   201  
   202  	if err := tempFile.Close(); err != nil {
   203  		return nil, fmt.Errorf("error closing temp file: %w", err)
   204  	}
   205  
   206  	if err := os.Rename(tempFile.Name(), cacheFilePath); err != nil {
   207  		return nil, fmt.Errorf("error renaming temp file %q -> %q: %w", tempFile.Name(), cacheFilePath, err)
   208  	}
   209  
   210  	tempFile = nil
   211  
   212  	f, err = os.Open(cacheFilePath)
   213  	if err != nil {
   214  		return nil, fmt.Errorf("error opening cache file %q (after fetch): %w", cacheFilePath, err)
   215  	}
   216  	return f, nil
   217  }