github.com/appscode/helm@v3.0.0-alpha.1+incompatible/pkg/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/pkg/registry" 18 19 import ( 20 "bytes" 21 "encoding/json" 22 "fmt" 23 "io" 24 "io/ioutil" 25 "os" 26 "path/filepath" 27 "sort" 28 "strings" 29 "time" 30 31 orascontent "github.com/deislabs/oras/pkg/content" 32 units "github.com/docker/go-units" 33 checksum "github.com/opencontainers/go-digest" 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 var ( 43 tableHeaders = []string{"name", "version", "digest", "size", "created"} 44 ) 45 46 type ( 47 filesystemCache struct { 48 out io.Writer 49 rootDir string 50 store *orascontent.Memorystore 51 } 52 ) 53 54 func (cache *filesystemCache) LayersToChart(layers []ocispec.Descriptor) (*chart.Chart, error) { 55 metaLayer, contentLayer, err := extractLayers(layers) 56 if err != nil { 57 return nil, err 58 } 59 60 name, version, err := extractChartNameVersionFromLayer(contentLayer) 61 if err != nil { 62 return nil, err 63 } 64 65 // Obtain raw chart meta content (json) 66 _, metaJSONRaw, ok := cache.store.Get(metaLayer) 67 if !ok { 68 return nil, errors.New("error retrieving meta layer") 69 } 70 71 // Construct chart metadata object 72 metadata := chart.Metadata{} 73 err = json.Unmarshal(metaJSONRaw, &metadata) 74 if err != nil { 75 return nil, err 76 } 77 metadata.APIVersion = chart.APIVersionV1 78 metadata.Name = name 79 metadata.Version = version 80 81 // Obtain raw chart content 82 _, contentRaw, ok := cache.store.Get(contentLayer) 83 if !ok { 84 return nil, errors.New("error retrieving meta layer") 85 } 86 87 // Construct chart object and attach metadata 88 ch, err := loader.LoadArchive(bytes.NewBuffer(contentRaw)) 89 if err != nil { 90 return nil, err 91 } 92 ch.Metadata = &metadata 93 94 return ch, nil 95 } 96 97 func (cache *filesystemCache) ChartToLayers(ch *chart.Chart) ([]ocispec.Descriptor, error) { 98 99 // extract/separate the name and version from other metadata 100 if err := ch.Validate(); err != nil { 101 return nil, err 102 } 103 name := ch.Metadata.Name 104 version := ch.Metadata.Version 105 106 // Create meta layer, clear name and version from Chart.yaml and convert to json 107 ch.Metadata.Name = "" 108 ch.Metadata.Version = "" 109 metaJSONRaw, err := json.Marshal(ch.Metadata) 110 if err != nil { 111 return nil, err 112 } 113 metaLayer := cache.store.Add(HelmChartMetaFileName, HelmChartMetaMediaType, metaJSONRaw) 114 115 // Create content layer 116 // TODO: something better than this hack. Currently needed for chartutil.Save() 117 // If metadata does not contain Name or Version, an error is returned 118 // such as "no chart name specified (Chart.yaml)" 119 ch.Metadata = &chart.Metadata{ 120 APIVersion: chart.APIVersionV1, 121 Name: "-", 122 Version: "0.1.0", 123 } 124 destDir := mkdir(filepath.Join(cache.rootDir, "blobs", ".build")) 125 tmpFile, err := chartutil.Save(ch, destDir) 126 defer os.Remove(tmpFile) 127 if err != nil { 128 return nil, errors.Wrap(err, "failed to save") 129 } 130 contentRaw, err := ioutil.ReadFile(tmpFile) 131 if err != nil { 132 return nil, err 133 } 134 contentLayer := cache.store.Add(HelmChartContentFileName, HelmChartContentMediaType, contentRaw) 135 136 // Set annotations 137 contentLayer.Annotations[HelmChartNameAnnotation] = name 138 contentLayer.Annotations[HelmChartVersionAnnotation] = version 139 140 layers := []ocispec.Descriptor{metaLayer, contentLayer} 141 return layers, nil 142 } 143 144 func (cache *filesystemCache) LoadReference(ref *Reference) ([]ocispec.Descriptor, error) { 145 tagDir := filepath.Join(cache.rootDir, "refs", escape(ref.Repo), "tags", tagOrDefault(ref.Tag)) 146 147 // add meta layer 148 metaJSONRaw, err := getSymlinkDestContent(filepath.Join(tagDir, "meta")) 149 if err != nil { 150 return nil, err 151 } 152 metaLayer := cache.store.Add(HelmChartMetaFileName, HelmChartMetaMediaType, metaJSONRaw) 153 154 // add content layer 155 contentRaw, err := getSymlinkDestContent(filepath.Join(tagDir, "content")) 156 if err != nil { 157 return nil, err 158 } 159 contentLayer := cache.store.Add(HelmChartContentFileName, HelmChartContentMediaType, contentRaw) 160 161 // set annotations on content layer (chart name and version) 162 err = setLayerAnnotationsFromChartLink(contentLayer, filepath.Join(tagDir, "chart")) 163 if err != nil { 164 return nil, err 165 } 166 167 printChartSummary(cache.out, metaLayer, contentLayer) 168 layers := []ocispec.Descriptor{metaLayer, contentLayer} 169 return layers, nil 170 } 171 172 func (cache *filesystemCache) StoreReference(ref *Reference, layers []ocispec.Descriptor) (bool, error) { 173 tag := tagOrDefault(ref.Tag) 174 tagDir := mkdir(filepath.Join(cache.rootDir, "refs", escape(ref.Repo), "tags", tag)) 175 176 // Retrieve just the meta and content layers 177 metaLayer, contentLayer, err := extractLayers(layers) 178 if err != nil { 179 return false, err 180 } 181 182 // Extract chart name and version 183 name, version, err := extractChartNameVersionFromLayer(contentLayer) 184 if err != nil { 185 return false, err 186 } 187 188 // Create chart file 189 chartPath, err := createChartFile(filepath.Join(cache.rootDir, "charts"), name, version) 190 if err != nil { 191 return false, err 192 } 193 194 // Create chart symlink 195 err = createSymlink(chartPath, filepath.Join(tagDir, "chart")) 196 if err != nil { 197 return false, err 198 } 199 200 // Save meta blob 201 metaExists, metaPath := digestPath(filepath.Join(cache.rootDir, "blobs"), metaLayer.Digest) 202 if !metaExists { 203 fmt.Fprintf(cache.out, "%s: Saving meta (%s)\n", 204 shortDigest(metaLayer.Digest.Hex()), byteCountBinary(metaLayer.Size)) 205 _, metaJSONRaw, ok := cache.store.Get(metaLayer) 206 if !ok { 207 return false, errors.New("error retrieving meta layer") 208 } 209 err = writeFile(metaPath, metaJSONRaw) 210 if err != nil { 211 return false, err 212 } 213 } 214 215 // Create meta symlink 216 err = createSymlink(metaPath, filepath.Join(tagDir, "meta")) 217 if err != nil { 218 return false, err 219 } 220 221 // Save content blob 222 contentExists, contentPath := digestPath(filepath.Join(cache.rootDir, "blobs"), contentLayer.Digest) 223 if !contentExists { 224 fmt.Fprintf(cache.out, "%s: Saving content (%s)\n", 225 shortDigest(contentLayer.Digest.Hex()), byteCountBinary(contentLayer.Size)) 226 _, contentRaw, ok := cache.store.Get(contentLayer) 227 if !ok { 228 return false, errors.New("error retrieving content layer") 229 } 230 err = writeFile(contentPath, contentRaw) 231 if err != nil { 232 return false, err 233 } 234 } 235 236 // Create content symlink 237 err = createSymlink(contentPath, filepath.Join(tagDir, "content")) 238 if err != nil { 239 return false, err 240 } 241 242 printChartSummary(cache.out, metaLayer, contentLayer) 243 return metaExists && contentExists, nil 244 } 245 246 func (cache *filesystemCache) DeleteReference(ref *Reference) error { 247 tagDir := filepath.Join(cache.rootDir, "refs", escape(ref.Repo), "tags", tagOrDefault(ref.Tag)) 248 if _, err := os.Stat(tagDir); os.IsNotExist(err) { 249 return errors.New("ref not found") 250 } 251 return os.RemoveAll(tagDir) 252 } 253 254 func (cache *filesystemCache) TableRows() ([][]interface{}, error) { 255 return getRefsSorted(filepath.Join(cache.rootDir, "refs")) 256 } 257 258 // escape sanitizes a registry URL to remove characters such as ":" 259 // which are illegal on windows 260 func escape(s string) string { 261 return strings.ReplaceAll(s, ":", "_") 262 } 263 264 // escape reverses escape 265 func unescape(s string) string { 266 return strings.ReplaceAll(s, "_", ":") 267 } 268 269 // printChartSummary prints details about a chart layers 270 func printChartSummary(out io.Writer, metaLayer ocispec.Descriptor, contentLayer ocispec.Descriptor) { 271 fmt.Fprintf(out, "Name: %s\n", contentLayer.Annotations[HelmChartNameAnnotation]) 272 fmt.Fprintf(out, "Version: %s\n", contentLayer.Annotations[HelmChartVersionAnnotation]) 273 fmt.Fprintf(out, "Meta: %s\n", metaLayer.Digest) 274 fmt.Fprintf(out, "Content: %s\n", contentLayer.Digest) 275 } 276 277 // fileExists determines if a file exists 278 func fileExists(path string) bool { 279 if _, err := os.Stat(path); os.IsNotExist(err) { 280 return false 281 } 282 return true 283 } 284 285 // mkdir will create a directory (no error check) and return the path 286 func mkdir(dir string) string { 287 os.MkdirAll(dir, 0755) 288 return dir 289 } 290 291 // createSymlink creates a symbolic link, deleting existing one if exists 292 func createSymlink(src string, dest string) error { 293 os.Remove(dest) 294 err := os.Symlink(src, dest) 295 return err 296 } 297 298 // getSymlinkDestContent returns the file contents of a symlink's destination 299 func getSymlinkDestContent(linkPath string) ([]byte, error) { 300 src, err := os.Readlink(linkPath) 301 if err != nil { 302 return nil, err 303 } 304 return ioutil.ReadFile(src) 305 } 306 307 // setLayerAnnotationsFromChartLink will set chart name/version annotations on a layer 308 // based on the path of the chart link destination 309 func setLayerAnnotationsFromChartLink(layer ocispec.Descriptor, chartLinkPath string) error { 310 src, err := os.Readlink(chartLinkPath) 311 if err != nil { 312 return err 313 } 314 // example path: /some/path/charts/mychart/versions/1.2.0 315 chartName := filepath.Base(filepath.Dir(filepath.Dir(src))) 316 chartVersion := filepath.Base(src) 317 layer.Annotations[HelmChartNameAnnotation] = chartName 318 layer.Annotations[HelmChartVersionAnnotation] = chartVersion 319 return nil 320 } 321 322 // extractLayers obtains the meta and content layers from a list of layers 323 func extractLayers(layers []ocispec.Descriptor) (ocispec.Descriptor, ocispec.Descriptor, error) { 324 var metaLayer, contentLayer ocispec.Descriptor 325 326 if len(layers) != 2 { 327 return metaLayer, contentLayer, errors.New("manifest does not contain exactly 2 layers") 328 } 329 330 for _, layer := range layers { 331 switch layer.MediaType { 332 case HelmChartMetaMediaType: 333 metaLayer = layer 334 case HelmChartContentMediaType: 335 contentLayer = layer 336 } 337 } 338 339 if metaLayer.Size == 0 { 340 return metaLayer, contentLayer, errors.New("manifest does not contain a Helm chart meta layer") 341 } 342 343 if contentLayer.Size == 0 { 344 return metaLayer, contentLayer, errors.New("manifest does not contain a Helm chart content layer") 345 } 346 347 return metaLayer, contentLayer, nil 348 } 349 350 // extractChartNameVersionFromLayer retrieves the chart name and version from layer annotations 351 func extractChartNameVersionFromLayer(layer ocispec.Descriptor) (string, string, error) { 352 name, ok := layer.Annotations[HelmChartNameAnnotation] 353 if !ok { 354 return "", "", errors.New("could not find chart name in annotations") 355 } 356 version, ok := layer.Annotations[HelmChartVersionAnnotation] 357 if !ok { 358 return "", "", errors.New("could not find chart version in annotations") 359 } 360 return name, version, nil 361 } 362 363 // createChartFile creates a file under "<chartsdir>" dir which is linked to by ref 364 func createChartFile(chartsRootDir string, name string, version string) (string, error) { 365 chartPathDir := filepath.Join(chartsRootDir, name, "versions") 366 chartPath := filepath.Join(chartPathDir, version) 367 if _, err := os.Stat(chartPath); err != nil && os.IsNotExist(err) { 368 os.MkdirAll(chartPathDir, 0755) 369 err := ioutil.WriteFile(chartPath, []byte("-"), 0644) 370 if err != nil { 371 return "", err 372 } 373 } 374 return chartPath, nil 375 } 376 377 // digestPath returns the path to addressable content, and whether the file exists 378 func digestPath(rootDir string, digest checksum.Digest) (bool, string) { 379 path := filepath.Join(rootDir, "sha256", digest.Hex()) 380 exists := fileExists(path) 381 return exists, path 382 } 383 384 // writeFile creates a path, ensuring parent directory 385 func writeFile(path string, c []byte) error { 386 os.MkdirAll(filepath.Dir(path), 0755) 387 return ioutil.WriteFile(path, c, 0644) 388 } 389 390 // byteCountBinary produces a human-readable file size 391 func byteCountBinary(b int64) string { 392 const unit = 1024 393 if b < unit { 394 return fmt.Sprintf("%d B", b) 395 } 396 div, exp := int64(unit), 0 397 for n := b / unit; n >= unit; n /= unit { 398 div *= unit 399 exp++ 400 } 401 return fmt.Sprintf("%.1f %ciB", float64(b)/float64(div), "KMGTPE"[exp]) 402 } 403 404 // tagOrDefault returns the tag if present, if not the default tag 405 func tagOrDefault(tag string) string { 406 if tag != "" { 407 return tag 408 } 409 return HelmChartDefaultTag 410 } 411 412 // shortDigest returns first 7 characters of a sha256 digest 413 func shortDigest(digest string) string { 414 if len(digest) == 64 { 415 return digest[:7] 416 } 417 return digest 418 } 419 420 // getRefsSorted returns a map of all refs stored in a refsRootDir 421 func getRefsSorted(refsRootDir string) ([][]interface{}, error) { 422 refsMap := map[string]map[string]string{} 423 424 // Walk the storage dir, check for symlinks under "refs" dir pointing to valid files in "blobs/" and "charts/" 425 err := filepath.Walk(refsRootDir, func(path string, fileInfo os.FileInfo, fileError error) error { 426 427 // Check if this file is a symlink 428 linkPath, err := os.Readlink(path) 429 if err == nil { 430 destFileInfo, err := os.Stat(linkPath) 431 if err == nil { 432 tagDir := filepath.Dir(path) 433 434 // Determine the ref 435 repo := unescape(strings.TrimLeft( 436 strings.TrimPrefix(filepath.Dir(filepath.Dir(tagDir)), refsRootDir), "/\\")) 437 tag := filepath.Base(tagDir) 438 ref := fmt.Sprintf("%s:%s", repo, tag) 439 440 // Init hashmap entry if does not exist 441 if _, ok := refsMap[ref]; !ok { 442 refsMap[ref] = map[string]string{} 443 } 444 445 // Add data to entry based on file name (symlink name) 446 base := filepath.Base(path) 447 switch base { 448 case "chart": 449 refsMap[ref]["name"] = filepath.Base(filepath.Dir(filepath.Dir(linkPath))) 450 refsMap[ref]["version"] = destFileInfo.Name() 451 case "content": 452 453 // Make sure the filename looks like a sha256 digest (64 chars) 454 digest := destFileInfo.Name() 455 if len(digest) == 64 { 456 refsMap[ref]["digest"] = shortDigest(digest) 457 refsMap[ref]["size"] = byteCountBinary(destFileInfo.Size()) 458 refsMap[ref]["created"] = units.HumanDuration(time.Now().UTC().Sub(destFileInfo.ModTime())) 459 } 460 } 461 } 462 } 463 464 return nil 465 }) 466 467 // Filter out any refs that are incomplete (do not have all required fields) 468 for k, ref := range refsMap { 469 allKeysFound := true 470 for _, v := range tableHeaders { 471 if _, ok := ref[v]; !ok { 472 allKeysFound = false 473 break 474 } 475 } 476 if !allKeysFound { 477 delete(refsMap, k) 478 } 479 } 480 481 // Sort and convert to format expected by uitable 482 refs := make([][]interface{}, len(refsMap)) 483 keys := make([]string, 0, len(refsMap)) 484 for key := range refsMap { 485 keys = append(keys, key) 486 } 487 sort.Strings(keys) 488 for i, key := range keys { 489 refs[i] = make([]interface{}, len(tableHeaders)+1) 490 refs[i][0] = key 491 ref := refsMap[key] 492 for j, k := range tableHeaders { 493 refs[i][j+1] = ref[k] 494 } 495 } 496 497 return refs, err 498 }