github.com/diafour/helm@v3.0.0-beta.3+incompatible/internal/experimental/registry/cache.go (about) 1 /* 2 Copyright The Helm 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 registry // import "helm.sh/helm/internal/experimental/registry" 18 19 import ( 20 "bytes" 21 "encoding/json" 22 "fmt" 23 "io" 24 "io/ioutil" 25 "os" 26 "path/filepath" 27 "time" 28 29 "github.com/containerd/containerd/content" 30 "github.com/containerd/containerd/errdefs" 31 orascontent "github.com/deislabs/oras/pkg/content" 32 digest "github.com/opencontainers/go-digest" 33 specs "github.com/opencontainers/image-spec/specs-go" 34 ocispec "github.com/opencontainers/image-spec/specs-go/v1" 35 "github.com/pkg/errors" 36 37 "helm.sh/helm/pkg/chart" 38 "helm.sh/helm/pkg/chart/loader" 39 "helm.sh/helm/pkg/chartutil" 40 ) 41 42 const ( 43 // CacheRootDir is the root directory for a cache 44 CacheRootDir = "cache" 45 ) 46 47 type ( 48 // Cache handles local/in-memory storage of Helm charts, compliant with OCI Layout 49 Cache struct { 50 debug bool 51 out io.Writer 52 rootDir string 53 ociStore *orascontent.OCIStore 54 memoryStore *orascontent.Memorystore 55 } 56 57 // CacheRefSummary contains as much info as available describing a chart reference in cache 58 // Note: fields here are sorted by the order in which they are set in FetchReference method 59 CacheRefSummary struct { 60 Name string 61 Repo string 62 Tag string 63 Exists bool 64 Manifest *ocispec.Descriptor 65 Config *ocispec.Descriptor 66 ContentLayer *ocispec.Descriptor 67 Size int64 68 Digest digest.Digest 69 CreatedAt time.Time 70 Chart *chart.Chart 71 } 72 ) 73 74 // NewCache returns a new OCI Layout-compliant cache with config 75 func NewCache(opts ...CacheOption) (*Cache, error) { 76 cache := &Cache{ 77 out: ioutil.Discard, 78 } 79 for _, opt := range opts { 80 opt(cache) 81 } 82 // validate 83 if cache.rootDir == "" { 84 return nil, errors.New("must set cache root dir on initialization") 85 } 86 return cache, nil 87 } 88 89 // FetchReference retrieves a chart ref from cache 90 func (cache *Cache) FetchReference(ref *Reference) (*CacheRefSummary, error) { 91 if err := cache.init(); err != nil { 92 return nil, err 93 } 94 r := CacheRefSummary{ 95 Name: ref.FullName(), 96 Repo: ref.Repo, 97 Tag: ref.Tag, 98 } 99 for _, desc := range cache.ociStore.ListReferences() { 100 if desc.Annotations[ocispec.AnnotationRefName] == r.Name { 101 r.Exists = true 102 manifestBytes, err := cache.fetchBlob(&desc) 103 if err != nil { 104 return &r, err 105 } 106 var manifest ocispec.Manifest 107 err = json.Unmarshal(manifestBytes, &manifest) 108 if err != nil { 109 return &r, err 110 } 111 r.Manifest = &desc 112 r.Config = &manifest.Config 113 numLayers := len(manifest.Layers) 114 if numLayers != 1 { 115 return &r, errors.New( 116 fmt.Sprintf("manifest does not contain exactly 1 layer (total: %d)", numLayers)) 117 } 118 var contentLayer *ocispec.Descriptor 119 for _, layer := range manifest.Layers { 120 switch layer.MediaType { 121 case HelmChartContentLayerMediaType: 122 contentLayer = &layer 123 } 124 } 125 if contentLayer.Size == 0 { 126 return &r, errors.New( 127 fmt.Sprintf("manifest does not contain a layer with mediatype %s", HelmChartContentLayerMediaType)) 128 } 129 r.ContentLayer = contentLayer 130 info, err := cache.ociStore.Info(ctx(cache.out, cache.debug), contentLayer.Digest) 131 if err != nil { 132 return &r, err 133 } 134 r.Size = info.Size 135 r.Digest = info.Digest 136 r.CreatedAt = info.CreatedAt 137 contentBytes, err := cache.fetchBlob(contentLayer) 138 if err != nil { 139 return &r, err 140 } 141 ch, err := loader.LoadArchive(bytes.NewBuffer(contentBytes)) 142 if err != nil { 143 return &r, err 144 } 145 r.Chart = ch 146 } 147 } 148 return &r, nil 149 } 150 151 // StoreReference stores a chart ref in cache 152 func (cache *Cache) StoreReference(ref *Reference, ch *chart.Chart) (*CacheRefSummary, error) { 153 if err := cache.init(); err != nil { 154 return nil, err 155 } 156 r := CacheRefSummary{ 157 Name: ref.FullName(), 158 Repo: ref.Repo, 159 Tag: ref.Tag, 160 Chart: ch, 161 } 162 existing, _ := cache.FetchReference(ref) 163 r.Exists = existing.Exists 164 config, _, err := cache.saveChartConfig(ch) 165 if err != nil { 166 return &r, err 167 } 168 r.Config = config 169 contentLayer, _, err := cache.saveChartContentLayer(ch) 170 if err != nil { 171 return &r, err 172 } 173 r.ContentLayer = contentLayer 174 info, err := cache.ociStore.Info(ctx(cache.out, cache.debug), contentLayer.Digest) 175 if err != nil { 176 return &r, err 177 } 178 r.Size = info.Size 179 r.Digest = info.Digest 180 r.CreatedAt = info.CreatedAt 181 manifest, _, err := cache.saveChartManifest(config, contentLayer) 182 if err != nil { 183 return &r, err 184 } 185 r.Manifest = manifest 186 return &r, nil 187 } 188 189 // DeleteReference deletes a chart ref from cache 190 // TODO: garbage collection, only manifest removed 191 func (cache *Cache) DeleteReference(ref *Reference) (*CacheRefSummary, error) { 192 if err := cache.init(); err != nil { 193 return nil, err 194 } 195 r, err := cache.FetchReference(ref) 196 if err != nil || !r.Exists { 197 return r, err 198 } 199 cache.ociStore.DeleteReference(r.Name) 200 err = cache.ociStore.SaveIndex() 201 return r, err 202 } 203 204 // ListReferences lists all chart refs in a cache 205 func (cache *Cache) ListReferences() ([]*CacheRefSummary, error) { 206 if err := cache.init(); err != nil { 207 return nil, err 208 } 209 var rr []*CacheRefSummary 210 for _, desc := range cache.ociStore.ListReferences() { 211 name := desc.Annotations[ocispec.AnnotationRefName] 212 if name == "" { 213 if cache.debug { 214 fmt.Fprintf(cache.out, "warning: found manifest without name: %s", desc.Digest.Hex()) 215 } 216 continue 217 } 218 ref, err := ParseReference(name) 219 if err != nil { 220 return rr, err 221 } 222 r, err := cache.FetchReference(ref) 223 if err != nil { 224 return rr, err 225 } 226 rr = append(rr, r) 227 } 228 return rr, nil 229 } 230 231 // AddManifest provides a manifest to the cache index.json 232 func (cache *Cache) AddManifest(ref *Reference, manifest *ocispec.Descriptor) error { 233 if err := cache.init(); err != nil { 234 return err 235 } 236 cache.ociStore.AddReference(ref.FullName(), *manifest) 237 err := cache.ociStore.SaveIndex() 238 return err 239 } 240 241 // Provider provides a valid containerd Provider 242 func (cache *Cache) Provider() content.Provider { 243 return content.Provider(cache.ociStore) 244 } 245 246 // Ingester provides a valid containerd Ingester 247 func (cache *Cache) Ingester() content.Ingester { 248 return content.Ingester(cache.ociStore) 249 } 250 251 // ProvideIngester provides a valid oras ProvideIngester 252 func (cache *Cache) ProvideIngester() orascontent.ProvideIngester { 253 return orascontent.ProvideIngester(cache.ociStore) 254 } 255 256 // init creates files needed necessary for OCI layout store 257 func (cache *Cache) init() error { 258 if cache.ociStore == nil { 259 ociStore, err := orascontent.NewOCIStore(cache.rootDir) 260 if err != nil { 261 return err 262 } 263 cache.ociStore = ociStore 264 cache.memoryStore = orascontent.NewMemoryStore() 265 } 266 return nil 267 } 268 269 // saveChartConfig stores the Chart.yaml as json blob and returns a descriptor 270 func (cache *Cache) saveChartConfig(ch *chart.Chart) (*ocispec.Descriptor, bool, error) { 271 configBytes, err := json.Marshal(ch.Metadata) 272 if err != nil { 273 return nil, false, err 274 } 275 configExists, err := cache.storeBlob(configBytes) 276 if err != nil { 277 return nil, configExists, err 278 } 279 descriptor := cache.memoryStore.Add("", HelmChartConfigMediaType, configBytes) 280 return &descriptor, configExists, nil 281 } 282 283 // saveChartContentLayer stores the chart as tarball blob and returns a descriptor 284 func (cache *Cache) saveChartContentLayer(ch *chart.Chart) (*ocispec.Descriptor, bool, error) { 285 destDir := filepath.Join(cache.rootDir, ".build") 286 os.MkdirAll(destDir, 0755) 287 tmpFile, err := chartutil.Save(ch, destDir) 288 defer os.Remove(tmpFile) 289 if err != nil { 290 return nil, false, errors.Wrap(err, "failed to save") 291 } 292 contentBytes, err := ioutil.ReadFile(tmpFile) 293 if err != nil { 294 return nil, false, err 295 } 296 contentExists, err := cache.storeBlob(contentBytes) 297 if err != nil { 298 return nil, contentExists, err 299 } 300 descriptor := cache.memoryStore.Add("", HelmChartContentLayerMediaType, contentBytes) 301 return &descriptor, contentExists, nil 302 } 303 304 // saveChartManifest stores the chart manifest as json blob and returns a descriptor 305 func (cache *Cache) saveChartManifest(config *ocispec.Descriptor, contentLayer *ocispec.Descriptor) (*ocispec.Descriptor, bool, error) { 306 manifest := ocispec.Manifest{ 307 Versioned: specs.Versioned{SchemaVersion: 2}, 308 Config: *config, 309 Layers: []ocispec.Descriptor{*contentLayer}, 310 } 311 manifestBytes, err := json.Marshal(manifest) 312 if err != nil { 313 return nil, false, err 314 } 315 manifestExists, err := cache.storeBlob(manifestBytes) 316 if err != nil { 317 return nil, manifestExists, err 318 } 319 descriptor := ocispec.Descriptor{ 320 MediaType: ocispec.MediaTypeImageManifest, 321 Digest: digest.FromBytes(manifestBytes), 322 Size: int64(len(manifestBytes)), 323 } 324 return &descriptor, manifestExists, nil 325 } 326 327 // storeBlob stores a blob on filesystem 328 func (cache *Cache) storeBlob(blobBytes []byte) (bool, error) { 329 var exists bool 330 writer, err := cache.ociStore.Store.Writer(ctx(cache.out, cache.debug), 331 content.WithRef(digest.FromBytes(blobBytes).Hex())) 332 if err != nil { 333 return exists, err 334 } 335 _, err = writer.Write(blobBytes) 336 if err != nil { 337 return exists, err 338 } 339 err = writer.Commit(ctx(cache.out, cache.debug), 0, writer.Digest()) 340 if err != nil { 341 if !errdefs.IsAlreadyExists(err) { 342 return exists, err 343 } 344 exists = true 345 } 346 err = writer.Close() 347 return exists, err 348 } 349 350 // fetchBlob retrieves a blob from filesystem 351 func (cache *Cache) fetchBlob(desc *ocispec.Descriptor) ([]byte, error) { 352 reader, err := cache.ociStore.ReaderAt(ctx(cache.out, cache.debug), *desc) 353 if err != nil { 354 return nil, err 355 } 356 bytes := make([]byte, desc.Size) 357 _, err = reader.ReadAt(bytes, 0) 358 if err != nil { 359 return nil, err 360 } 361 return bytes, nil 362 }