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 }