ref(internal/experimental/registry): pkg refactor (#6205)

No more magic separating the metadata from chart tarball - charts are
pushed to registry as a single tarball layer with Chart.yaml in tact.

No more fragile custom symlink chart storage, now following
the OCI Image Layout Specification for chart filesystem cache.

Also:
- Update to ORAS 0.6.0
- Simplify registry client setup with NewClientWithDefaults()
- Remove needless annotations and constants

Fixes #6068
Fixes #6141

Signed-off-by: Josh Dolitsky <jdolitsky@gmail.com>
pull/6207/head
Josh Dolitsky 6 years ago committed by GitHub
parent 6ab25d2242
commit 6095070817
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

486
Gopkg.lock generated

File diff suppressed because it is too large Load Diff

@ -44,7 +44,7 @@
[[constraint]] [[constraint]]
name = "github.com/deislabs/oras" name = "github.com/deislabs/oras"
version = "0.5.0" version = "0.6.0"
[[constraint]] [[constraint]]
name = "github.com/sirupsen/logrus" name = "github.com/sirupsen/logrus"

@ -17,17 +17,13 @@ limitations under the License.
package main // import "helm.sh/helm/cmd/helm" package main // import "helm.sh/helm/cmd/helm"
import ( import (
"context"
"io" "io"
"path/filepath"
auth "github.com/deislabs/oras/pkg/auth/docker"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"helm.sh/helm/cmd/helm/require" "helm.sh/helm/cmd/helm/require"
"helm.sh/helm/internal/experimental/registry" "helm.sh/helm/internal/experimental/registry"
"helm.sh/helm/pkg/action" "helm.sh/helm/pkg/action"
"helm.sh/helm/pkg/helmpath"
) )
const ( const (
@ -132,30 +128,7 @@ func newRootCmd(actionConfig *action.Configuration, out io.Writer, args []string
// set defaults from environment // set defaults from environment
settings.Init(flags) settings.Init(flags)
// Add the registry client based on settings // Add subcommands
// TODO: Move this elsewhere (first, settings.Init() must move)
// TODO: handle errors, dont panic
credentialsFile := filepath.Join(helmpath.Registry(), registry.CredentialsFileBasename)
client, err := auth.NewClient(credentialsFile)
if err != nil {
panic(err)
}
resolver, err := client.Resolver(context.Background())
if err != nil {
panic(err)
}
actionConfig.RegistryClient = registry.NewClient(&registry.ClientOptions{
Debug: settings.Debug,
Out: out,
Authorizer: registry.Authorizer{
Client: client,
},
Resolver: registry.Resolver{
Resolver: resolver,
},
CacheRootDir: helmpath.Registry(),
})
cmd.AddCommand( cmd.AddCommand(
// chart commands // chart commands
newCreateCmd(out), newCreateCmd(out),
@ -168,10 +141,6 @@ func newRootCmd(actionConfig *action.Configuration, out io.Writer, args []string
newSearchCmd(out), newSearchCmd(out),
newVerifyCmd(out), newVerifyCmd(out),
// registry/chart cache commands
newRegistryCmd(actionConfig, out),
newChartCmd(actionConfig, out),
// release commands // release commands
newGetCmd(actionConfig, out), newGetCmd(actionConfig, out),
newHistoryCmd(actionConfig, out), newHistoryCmd(actionConfig, out),
@ -193,6 +162,21 @@ func newRootCmd(actionConfig *action.Configuration, out io.Writer, args []string
newDocsCmd(out), newDocsCmd(out),
) )
// Add *experimental* subcommands
registryClient, err := registry.NewClient(
registry.ClientOptDebug(settings.Debug),
registry.ClientOptWriter(out),
)
if err != nil {
// TODO: dont panic here, refactor newRootCmd to return error
panic(err)
}
actionConfig.RegistryClient = registryClient
cmd.AddCommand(
newRegistryCmd(actionConfig, out),
newChartCmd(actionConfig, out),
)
// Find and add plugins // Find and add plugins
loadPlugins(cmd, out) loadPlugins(cmd, out)

@ -24,13 +24,13 @@ import (
"io/ioutil" "io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"sort"
"strings"
"time" "time"
"github.com/containerd/containerd/content"
"github.com/containerd/containerd/errdefs"
orascontent "github.com/deislabs/oras/pkg/content" orascontent "github.com/deislabs/oras/pkg/content"
units "github.com/docker/go-units" digest "github.com/opencontainers/go-digest"
checksum "github.com/opencontainers/go-digest" specs "github.com/opencontainers/image-spec/specs-go"
ocispec "github.com/opencontainers/image-spec/specs-go/v1" ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors" "github.com/pkg/errors"
@ -39,451 +39,324 @@ import (
"helm.sh/helm/pkg/chartutil" "helm.sh/helm/pkg/chartutil"
) )
var ( const (
tableHeaders = []string{"name", "version", "digest", "size", "created"} // CacheRootDir is the root directory for a cache
CacheRootDir = "cache"
) )
type ( type (
filesystemCache struct { // Cache handles local/in-memory storage of Helm charts, compliant with OCI Layout
Cache struct {
debug bool
out io.Writer out io.Writer
rootDir string rootDir string
store *orascontent.Memorystore ociStore *orascontent.OCIStore
memoryStore *orascontent.Memorystore
}
// CacheRefSummary contains as much info as available describing a chart reference in cache
// Note: fields here are sorted by the order in which they are set in FetchReference method
CacheRefSummary struct {
Name string
Repo string
Tag string
Exists bool
Manifest *ocispec.Descriptor
Config *ocispec.Descriptor
ContentLayer *ocispec.Descriptor
Size int64
Digest digest.Digest
CreatedAt time.Time
Chart *chart.Chart
} }
) )
func (cache *filesystemCache) LayersToChart(layers []ocispec.Descriptor) (*chart.Chart, error) { // NewCache returns a new OCI Layout-compliant cache with config
metaLayer, contentLayer, err := extractLayers(layers) func NewCache(opts ...CacheOption) (*Cache, error) {
if err != nil { cache := &Cache{
return nil, err out: ioutil.Discard,
} }
for _, opt := range opts {
name, version, err := extractChartNameVersionFromLayer(contentLayer) opt(cache)
if err != nil {
return nil, err
} }
// validate
// Obtain raw chart meta content (json) if cache.rootDir == "" {
_, metaJSONRaw, ok := cache.store.Get(metaLayer) return nil, errors.New("must set cache root dir on initialization")
if !ok {
return nil, errors.New("error retrieving meta layer")
} }
return cache, nil
}
// Construct chart metadata object // FetchReference retrieves a chart ref from cache
metadata := chart.Metadata{} func (cache *Cache) FetchReference(ref *Reference) (*CacheRefSummary, error) {
err = json.Unmarshal(metaJSONRaw, &metadata) if err := cache.init(); err != nil {
if err != nil {
return nil, err return nil, err
} }
metadata.APIVersion = chart.APIVersionV1 r := CacheRefSummary{
metadata.Name = name Name: ref.FullName(),
metadata.Version = version Repo: ref.Repo,
Tag: ref.Tag,
// Obtain raw chart content
_, contentRaw, ok := cache.store.Get(contentLayer)
if !ok {
return nil, errors.New("error retrieving meta layer")
} }
for _, desc := range cache.ociStore.ListReferences() {
// Construct chart object and attach metadata if desc.Annotations[ocispec.AnnotationRefName] == r.Name {
ch, err := loader.LoadArchive(bytes.NewBuffer(contentRaw)) r.Exists = true
manifestBytes, err := cache.fetchBlob(&desc)
if err != nil { if err != nil {
return nil, err return &r, err
} }
ch.Metadata = &metadata var manifest ocispec.Manifest
err = json.Unmarshal(manifestBytes, &manifest)
return ch, nil if err != nil {
} return &r, err
func (cache *filesystemCache) ChartToLayers(ch *chart.Chart) ([]ocispec.Descriptor, error) {
// extract/separate the name and version from other metadata
if err := ch.Validate(); err != nil {
return nil, err
} }
name := ch.Metadata.Name r.Manifest = &desc
version := ch.Metadata.Version r.Config = &manifest.Config
numLayers := len(manifest.Layers)
// Create meta layer, clear name and version from Chart.yaml and convert to json if numLayers != 1 {
ch.Metadata.Name = "" return &r, errors.New(
ch.Metadata.Version = "" fmt.Sprintf("manifest does not contain exactly 1 layer (total: %d)", numLayers))
metaJSONRaw, err := json.Marshal(ch.Metadata) }
var contentLayer *ocispec.Descriptor
for _, layer := range manifest.Layers {
switch layer.MediaType {
case HelmChartContentLayerMediaType:
contentLayer = &layer
}
}
if contentLayer.Size == 0 {
return &r, errors.New(
fmt.Sprintf("manifest does not contain a layer with mediatype %s", HelmChartContentLayerMediaType))
}
r.ContentLayer = contentLayer
info, err := cache.ociStore.Info(ctx(cache.out, cache.debug), contentLayer.Digest)
if err != nil { if err != nil {
return nil, err return &r, err
} }
metaLayer := cache.store.Add(HelmChartMetaFileName, HelmChartMetaLayerMediaType, metaJSONRaw) r.Size = info.Size
r.Digest = info.Digest
// Create content layer r.CreatedAt = info.CreatedAt
// TODO: something better than this hack. Currently needed for chartutil.Save() contentBytes, err := cache.fetchBlob(contentLayer)
// If metadata does not contain Name or Version, an error is returned
// such as "no chart name specified (Chart.yaml)"
ch.Metadata = &chart.Metadata{
APIVersion: chart.APIVersionV1,
Name: "-",
Version: "0.1.0",
}
destDir := mkdir(filepath.Join(cache.rootDir, "blobs", ".build"))
tmpFile, err := chartutil.Save(ch, destDir)
defer os.Remove(tmpFile)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "failed to save") return &r, err
} }
contentRaw, err := ioutil.ReadFile(tmpFile) ch, err := loader.LoadArchive(bytes.NewBuffer(contentBytes))
if err != nil { if err != nil {
return nil, err return &r, err
} }
contentLayer := cache.store.Add(HelmChartContentFileName, HelmChartContentLayerMediaType, contentRaw) r.Chart = ch
}
// Set annotations }
contentLayer.Annotations[HelmChartNameAnnotation] = name return &r, nil
contentLayer.Annotations[HelmChartVersionAnnotation] = version
layers := []ocispec.Descriptor{metaLayer, contentLayer}
return layers, nil
} }
func (cache *filesystemCache) LoadReference(ref *Reference) ([]ocispec.Descriptor, error) { // StoreReference stores a chart ref in cache
tagDir := filepath.Join(cache.rootDir, "refs", escape(ref.Repo), "tags", ref.Tag) func (cache *Cache) StoreReference(ref *Reference, ch *chart.Chart) (*CacheRefSummary, error) {
if err := cache.init(); err != nil {
// add meta layer
metaJSONRaw, err := getSymlinkDestContent(filepath.Join(tagDir, "meta"))
if err != nil {
return nil, err return nil, err
} }
metaLayer := cache.store.Add(HelmChartMetaFileName, HelmChartMetaLayerMediaType, metaJSONRaw) r := CacheRefSummary{
Name: ref.FullName(),
// add content layer Repo: ref.Repo,
contentRaw, err := getSymlinkDestContent(filepath.Join(tagDir, "content")) Tag: ref.Tag,
if err != nil { Chart: ch,
return nil, err
} }
contentLayer := cache.store.Add(HelmChartContentFileName, HelmChartContentLayerMediaType, contentRaw) existing, _ := cache.FetchReference(ref)
r.Exists = existing.Exists
// set annotations on content layer (chart name and version) config, _, err := cache.saveChartConfig(ch)
err = setLayerAnnotationsFromChartLink(contentLayer, filepath.Join(tagDir, "chart"))
if err != nil { if err != nil {
return nil, err return &r, err
} }
r.Config = config
printChartSummary(cache.out, metaLayer, contentLayer) contentLayer, _, err := cache.saveChartContentLayer(ch)
layers := []ocispec.Descriptor{metaLayer, contentLayer}
return layers, nil
}
func (cache *filesystemCache) StoreReference(ref *Reference, layers []ocispec.Descriptor) (bool, error) {
tagDir := mkdir(filepath.Join(cache.rootDir, "refs", escape(ref.Repo), "tags", ref.Tag))
// Retrieve just the meta and content layers
metaLayer, contentLayer, err := extractLayers(layers)
if err != nil { if err != nil {
return false, err return &r, err
} }
r.ContentLayer = contentLayer
// Extract chart name and version info, err := cache.ociStore.Info(ctx(cache.out, cache.debug), contentLayer.Digest)
name, version, err := extractChartNameVersionFromLayer(contentLayer)
if err != nil { if err != nil {
return false, err return &r, err
} }
r.Size = info.Size
// Create chart file r.Digest = info.Digest
chartPath, err := createChartFile(filepath.Join(cache.rootDir, "charts"), name, version) r.CreatedAt = info.CreatedAt
manifest, _, err := cache.saveChartManifest(config, contentLayer)
if err != nil { if err != nil {
return false, err return &r, err
} }
r.Manifest = manifest
return &r, nil
}
// Create chart symlink // DeleteReference deletes a chart ref from cache
err = createSymlink(chartPath, filepath.Join(tagDir, "chart")) // TODO: garbage collection, only manifest removed
if err != nil { func (cache *Cache) DeleteReference(ref *Reference) (*CacheRefSummary, error) {
return false, err if err := cache.init(); err != nil {
return nil, err
} }
r, err := cache.FetchReference(ref)
if err != nil || !r.Exists {
return r, err
}
cache.ociStore.DeleteReference(r.Name)
err = cache.ociStore.SaveIndex()
return r, err
}
// Save meta blob // ListReferences lists all chart refs in a cache
metaExists, metaPath := digestPath(filepath.Join(cache.rootDir, "blobs"), metaLayer.Digest) func (cache *Cache) ListReferences() ([]*CacheRefSummary, error) {
if !metaExists { if err := cache.init(); err != nil {
fmt.Fprintf(cache.out, "%s: Saving meta (%s)\n", return nil, err
shortDigest(metaLayer.Digest.Hex()), byteCountBinary(metaLayer.Size))
_, metaJSONRaw, ok := cache.store.Get(metaLayer)
if !ok {
return false, errors.New("error retrieving meta layer")
}
err = writeFile(metaPath, metaJSONRaw)
if err != nil {
return false, err
} }
var rr []*CacheRefSummary
for _, desc := range cache.ociStore.ListReferences() {
name := desc.Annotations[ocispec.AnnotationRefName]
if name == "" {
if cache.debug {
fmt.Fprintf(cache.out, "warning: found manifest without name: %s", desc.Digest.Hex())
} }
continue
// Create meta symlink
err = createSymlink(metaPath, filepath.Join(tagDir, "meta"))
if err != nil {
return false, err
} }
ref, err := ParseReference(name)
// Save content blob
contentExists, contentPath := digestPath(filepath.Join(cache.rootDir, "blobs"), contentLayer.Digest)
if !contentExists {
fmt.Fprintf(cache.out, "%s: Saving content (%s)\n",
shortDigest(contentLayer.Digest.Hex()), byteCountBinary(contentLayer.Size))
_, contentRaw, ok := cache.store.Get(contentLayer)
if !ok {
return false, errors.New("error retrieving content layer")
}
err = writeFile(contentPath, contentRaw)
if err != nil { if err != nil {
return false, err return rr, err
}
} }
r, err := cache.FetchReference(ref)
// Create content symlink
err = createSymlink(contentPath, filepath.Join(tagDir, "content"))
if err != nil { if err != nil {
return false, err return rr, err
} }
rr = append(rr, r)
printChartSummary(cache.out, metaLayer, contentLayer)
return metaExists && contentExists, nil
}
func (cache *filesystemCache) DeleteReference(ref *Reference) error {
tagDir := filepath.Join(cache.rootDir, "refs", escape(ref.Repo), "tags", ref.Tag)
if _, err := os.Stat(tagDir); os.IsNotExist(err) {
return errors.New("ref not found")
} }
return os.RemoveAll(tagDir) return rr, nil
}
func (cache *filesystemCache) TableRows() ([][]interface{}, error) {
return getRefsSorted(filepath.Join(cache.rootDir, "refs"))
} }
// escape sanitizes a registry URL to remove characters such as ":" // AddManifest provides a manifest to the cache index.json
// which are illegal on windows func (cache *Cache) AddManifest(ref *Reference, manifest *ocispec.Descriptor) error {
func escape(s string) string { if err := cache.init(); err != nil {
return strings.ReplaceAll(s, ":", "_") return err
}
// escape reverses escape
func unescape(s string) string {
return strings.ReplaceAll(s, "_", ":")
}
// printChartSummary prints details about a chart layers
func printChartSummary(out io.Writer, metaLayer ocispec.Descriptor, contentLayer ocispec.Descriptor) {
fmt.Fprintf(out, "Name: %s\n", contentLayer.Annotations[HelmChartNameAnnotation])
fmt.Fprintf(out, "Version: %s\n", contentLayer.Annotations[HelmChartVersionAnnotation])
fmt.Fprintf(out, "Meta: %s\n", metaLayer.Digest)
fmt.Fprintf(out, "Content: %s\n", contentLayer.Digest)
}
// fileExists determines if a file exists
func fileExists(path string) bool {
if _, err := os.Stat(path); os.IsNotExist(err) {
return false
} }
return true cache.ociStore.AddReference(ref.FullName(), *manifest)
err := cache.ociStore.SaveIndex()
return err
} }
// mkdir will create a directory (no error check) and return the path // Provider provides a valid containerd Provider
func mkdir(dir string) string { func (cache *Cache) Provider() content.Provider {
os.MkdirAll(dir, 0755) return content.Provider(cache.ociStore)
return dir
} }
// createSymlink creates a symbolic link, deleting existing one if exists // Ingester provides a valid containerd Ingester
func createSymlink(src string, dest string) error { func (cache *Cache) Ingester() content.Ingester {
os.Remove(dest) return content.Ingester(cache.ociStore)
err := os.Symlink(src, dest)
return err
} }
// getSymlinkDestContent returns the file contents of a symlink's destination // ProvideIngester provides a valid oras ProvideIngester
func getSymlinkDestContent(linkPath string) ([]byte, error) { func (cache *Cache) ProvideIngester() orascontent.ProvideIngester {
src, err := os.Readlink(linkPath) return orascontent.ProvideIngester(cache.ociStore)
if err != nil {
return nil, err
}
return ioutil.ReadFile(src)
} }
// setLayerAnnotationsFromChartLink will set chart name/version annotations on a layer // init creates files needed necessary for OCI layout store
// based on the path of the chart link destination func (cache *Cache) init() error {
func setLayerAnnotationsFromChartLink(layer ocispec.Descriptor, chartLinkPath string) error { if cache.ociStore == nil {
src, err := os.Readlink(chartLinkPath) ociStore, err := orascontent.NewOCIStore(cache.rootDir)
if err != nil { if err != nil {
return err return err
} }
// example path: /some/path/charts/mychart/versions/1.2.0 cache.ociStore = ociStore
chartName := filepath.Base(filepath.Dir(filepath.Dir(src))) cache.memoryStore = orascontent.NewMemoryStore()
chartVersion := filepath.Base(src)
layer.Annotations[HelmChartNameAnnotation] = chartName
layer.Annotations[HelmChartVersionAnnotation] = chartVersion
return nil
}
// extractLayers obtains the meta and content layers from a list of layers
func extractLayers(layers []ocispec.Descriptor) (ocispec.Descriptor, ocispec.Descriptor, error) {
var metaLayer, contentLayer ocispec.Descriptor
if len(layers) != 2 {
return metaLayer, contentLayer, errors.New("manifest does not contain exactly 2 layers")
}
for _, layer := range layers {
switch layer.MediaType {
case HelmChartMetaLayerMediaType:
metaLayer = layer
case HelmChartContentLayerMediaType:
contentLayer = layer
}
}
if metaLayer.Size == 0 {
return metaLayer, contentLayer, errors.New("manifest does not contain a Helm chart meta layer")
}
if contentLayer.Size == 0 {
return metaLayer, contentLayer, errors.New("manifest does not contain a Helm chart content layer")
} }
return nil
return metaLayer, contentLayer, nil
} }
// extractChartNameVersionFromLayer retrieves the chart name and version from layer annotations // saveChartConfig stores the Chart.yaml as json blob and returns a descriptor
func extractChartNameVersionFromLayer(layer ocispec.Descriptor) (string, string, error) { func (cache *Cache) saveChartConfig(ch *chart.Chart) (*ocispec.Descriptor, bool, error) {
name, ok := layer.Annotations[HelmChartNameAnnotation] configBytes, err := json.Marshal(ch.Metadata)
if !ok { if err != nil {
return "", "", errors.New("could not find chart name in annotations") return nil, false, err
} }
version, ok := layer.Annotations[HelmChartVersionAnnotation] configExists, err := cache.storeBlob(configBytes)
if !ok { if err != nil {
return "", "", errors.New("could not find chart version in annotations") return nil, configExists, err
} }
return name, version, nil descriptor := cache.memoryStore.Add("", HelmChartConfigMediaType, configBytes)
return &descriptor, configExists, nil
} }
// createChartFile creates a file under "<chartsdir>" dir which is linked to by ref // saveChartContentLayer stores the chart as tarball blob and returns a descriptor
func createChartFile(chartsRootDir string, name string, version string) (string, error) { func (cache *Cache) saveChartContentLayer(ch *chart.Chart) (*ocispec.Descriptor, bool, error) {
chartPathDir := filepath.Join(chartsRootDir, name, "versions") destDir := filepath.Join(cache.rootDir, ".build")
chartPath := filepath.Join(chartPathDir, version) os.MkdirAll(destDir, 0755)
if _, err := os.Stat(chartPath); err != nil && os.IsNotExist(err) { tmpFile, err := chartutil.Save(ch, destDir)
os.MkdirAll(chartPathDir, 0755) defer os.Remove(tmpFile)
err := ioutil.WriteFile(chartPath, []byte("-"), 0644)
if err != nil { if err != nil {
return "", err return nil, false, errors.Wrap(err, "failed to save")
} }
contentBytes, err := ioutil.ReadFile(tmpFile)
if err != nil {
return nil, false, err
} }
return chartPath, nil contentExists, err := cache.storeBlob(contentBytes)
} if err != nil {
return nil, contentExists, err
// digestPath returns the path to addressable content, and whether the file exists
func digestPath(rootDir string, digest checksum.Digest) (bool, string) {
path := filepath.Join(rootDir, "sha256", digest.Hex())
exists := fileExists(path)
return exists, path
}
// writeFile creates a path, ensuring parent directory
func writeFile(path string, c []byte) error {
os.MkdirAll(filepath.Dir(path), 0755)
return ioutil.WriteFile(path, c, 0644)
}
// byteCountBinary produces a human-readable file size
func byteCountBinary(b int64) string {
const unit = 1024
if b < unit {
return fmt.Sprintf("%d B", b)
}
div, exp := int64(unit), 0
for n := b / unit; n >= unit; n /= unit {
div *= unit
exp++
}
return fmt.Sprintf("%.1f %ciB", float64(b)/float64(div), "KMGTPE"[exp])
}
// shortDigest returns first 7 characters of a sha256 digest
func shortDigest(digest string) string {
if len(digest) == 64 {
return digest[:7]
} }
return digest descriptor := cache.memoryStore.Add("", HelmChartContentLayerMediaType, contentBytes)
return &descriptor, contentExists, nil
} }
// getRefsSorted returns a map of all refs stored in a refsRootDir // saveChartManifest stores the chart manifest as json blob and returns a descriptor
func getRefsSorted(refsRootDir string) ([][]interface{}, error) { func (cache *Cache) saveChartManifest(config *ocispec.Descriptor, contentLayer *ocispec.Descriptor) (*ocispec.Descriptor, bool, error) {
refsMap := map[string]map[string]string{} manifest := ocispec.Manifest{
Versioned: specs.Versioned{SchemaVersion: 2},
// Walk the storage dir, check for symlinks under "refs" dir pointing to valid files in "blobs/" and "charts/" Config: *config,
err := filepath.Walk(refsRootDir, func(path string, fileInfo os.FileInfo, fileError error) error { Layers: []ocispec.Descriptor{*contentLayer},
// Check if this file is a symlink
linkPath, err := os.Readlink(path)
if err == nil {
destFileInfo, err := os.Stat(linkPath)
if err == nil {
tagDir := filepath.Dir(path)
// Determine the ref
repo := unescape(strings.TrimLeft(
strings.TrimPrefix(filepath.Dir(filepath.Dir(tagDir)), refsRootDir), "/\\"))
tag := filepath.Base(tagDir)
ref := fmt.Sprintf("%s:%s", repo, tag)
// Init hashmap entry if does not exist
if _, ok := refsMap[ref]; !ok {
refsMap[ref] = map[string]string{}
}
// Add data to entry based on file name (symlink name)
base := filepath.Base(path)
switch base {
case "chart":
refsMap[ref]["name"] = filepath.Base(filepath.Dir(filepath.Dir(linkPath)))
refsMap[ref]["version"] = destFileInfo.Name()
case "content":
// Make sure the filename looks like a sha256 digest (64 chars)
digest := destFileInfo.Name()
if len(digest) == 64 {
refsMap[ref]["digest"] = shortDigest(digest)
refsMap[ref]["size"] = byteCountBinary(destFileInfo.Size())
refsMap[ref]["created"] = units.HumanDuration(time.Now().UTC().Sub(destFileInfo.ModTime()))
} }
manifestBytes, err := json.Marshal(manifest)
if err != nil {
return nil, false, err
} }
manifestExists, err := cache.storeBlob(manifestBytes)
if err != nil {
return nil, manifestExists, err
} }
descriptor := ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageManifest,
Digest: digest.FromBytes(manifestBytes),
Size: int64(len(manifestBytes)),
} }
return &descriptor, manifestExists, nil
}
return nil // storeBlob stores a blob on filesystem
}) func (cache *Cache) storeBlob(blobBytes []byte) (bool, error) {
var exists bool
// Filter out any refs that are incomplete (do not have all required fields) writer, err := cache.ociStore.Store.Writer(ctx(cache.out, cache.debug),
for k, ref := range refsMap { content.WithRef(digest.FromBytes(blobBytes).Hex()))
allKeysFound := true if err != nil {
for _, v := range tableHeaders { return exists, err
if _, ok := ref[v]; !ok {
allKeysFound = false
break
} }
_, err = writer.Write(blobBytes)
if err != nil {
return exists, err
} }
if !allKeysFound { err = writer.Commit(ctx(cache.out, cache.debug), 0, writer.Digest())
delete(refsMap, k) if err != nil {
if !errdefs.IsAlreadyExists(err) {
return exists, err
} }
exists = true
} }
err = writer.Close()
return exists, err
}
// Sort and convert to format expected by uitable // fetchBlob retrieves a blob from filesystem
refs := make([][]interface{}, len(refsMap)) func (cache *Cache) fetchBlob(desc *ocispec.Descriptor) ([]byte, error) {
keys := make([]string, 0, len(refsMap)) reader, err := cache.ociStore.ReaderAt(ctx(cache.out, cache.debug), *desc)
for key := range refsMap { if err != nil {
keys = append(keys, key) return nil, err
}
sort.Strings(keys)
for i, key := range keys {
refs[i] = make([]interface{}, len(tableHeaders)+1)
refs[i][0] = key
ref := refsMap[key]
for j, k := range tableHeaders {
refs[i][j+1] = ref[k]
} }
bytes := make([]byte, desc.Size)
_, err = reader.ReadAt(bytes, 0)
if err != nil {
return nil, err
} }
return bytes, nil
return refs, err
} }

@ -0,0 +1,48 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package registry // import "helm.sh/helm/internal/experimental/registry"
import (
"io"
)
type (
// CacheOption allows specifying various settings configurable by the user for overriding the defaults
// used when creating a new default cache
CacheOption func(*Cache)
)
// CacheOptDebug returns a function that sets the debug setting on cache options set
func CacheOptDebug(debug bool) CacheOption {
return func(cache *Cache) {
cache.debug = debug
}
}
// CacheOptWriter returns a function that sets the writer setting on cache options set
func CacheOptWriter(out io.Writer) CacheOption {
return func(cache *Cache) {
cache.out = out
}
}
// CacheOptRoot returns a function that sets the root directory setting on cache options set
func CacheOptRoot(rootDir string) CacheOption {
return func(cache *Cache) {
cache.rootDir = rootDir
}
}

@ -20,147 +20,201 @@ import (
"context" "context"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"path/filepath"
"sort"
orascontent "github.com/deislabs/oras/pkg/content" auth "github.com/deislabs/oras/pkg/auth/docker"
orascontext "github.com/deislabs/oras/pkg/context"
"github.com/deislabs/oras/pkg/oras" "github.com/deislabs/oras/pkg/oras"
"github.com/gosuri/uitable" "github.com/gosuri/uitable"
"github.com/sirupsen/logrus" ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
"helm.sh/helm/pkg/chart" "helm.sh/helm/pkg/chart"
"helm.sh/helm/pkg/helmpath"
) )
const ( const (
// CredentialsFileBasename is the filename for auth credentials file
CredentialsFileBasename = "config.json" CredentialsFileBasename = "config.json"
) )
type ( type (
// ClientOptions is used to construct a new client
ClientOptions struct {
Debug bool
Out io.Writer
Authorizer Authorizer
Resolver Resolver
CacheRootDir string
}
// Client works with OCI-compliant registries and local Helm chart cache // Client works with OCI-compliant registries and local Helm chart cache
Client struct { Client struct {
debug bool debug bool
out io.Writer out io.Writer
authorizer Authorizer authorizer *Authorizer
resolver Resolver resolver *Resolver
cache *filesystemCache // TODO: something more robust cache *Cache
} }
) )
// NewClient returns a new registry client with config // NewClient returns a new registry client with config
func NewClient(options *ClientOptions) *Client { func NewClient(opts ...ClientOption) (*Client, error) {
return &Client{ client := &Client{
debug: options.Debug, out: ioutil.Discard,
out: options.Out, }
resolver: options.Resolver, for _, opt := range opts {
authorizer: options.Authorizer, opt(client)
cache: &filesystemCache{ }
out: options.Out, // set defaults if fields are missing
rootDir: options.CacheRootDir, if client.authorizer == nil {
store: orascontent.NewMemoryStore(), credentialsFile := filepath.Join(helmpath.Registry(), CredentialsFileBasename)
}, authClient, err := auth.NewClient(credentialsFile)
if err != nil {
return nil, err
}
client.authorizer = &Authorizer{
Client: authClient,
}
}
if client.resolver == nil {
resolver, err := client.authorizer.Resolver(context.Background())
if err != nil {
return nil, err
} }
client.resolver = &Resolver{
Resolver: resolver,
}
}
if client.cache == nil {
cache, err := NewCache(
CacheOptDebug(client.debug),
CacheOptWriter(client.out),
CacheOptRoot(filepath.Join(helmpath.Registry(), CacheRootDir)),
)
if err != nil {
return nil, err
}
client.cache = cache
}
return client, nil
} }
// Login logs into a registry // Login logs into a registry
func (c *Client) Login(hostname string, username string, password string) error { func (c *Client) Login(hostname string, username string, password string) error {
err := c.authorizer.Login(c.newContext(), hostname, username, password) err := c.authorizer.Login(ctx(c.out, c.debug), hostname, username, password)
if err != nil { if err != nil {
return err return err
} }
fmt.Fprint(c.out, "Login succeeded\n") fmt.Fprintf(c.out, "Login succeeded\n")
return nil return nil
} }
// Logout logs out of a registry // Logout logs out of a registry
func (c *Client) Logout(hostname string) error { func (c *Client) Logout(hostname string) error {
err := c.authorizer.Logout(c.newContext(), hostname) err := c.authorizer.Logout(ctx(c.out, c.debug), hostname)
if err != nil { if err != nil {
return err return err
} }
fmt.Fprint(c.out, "Logout succeeded\n") fmt.Fprintln(c.out, "Logout succeeded")
return nil return nil
} }
// PushChart uploads a chart to a registry // PushChart uploads a chart to a registry
func (c *Client) PushChart(ref *Reference) error { func (c *Client) PushChart(ref *Reference) error {
fmt.Fprintf(c.out, "The push refers to repository [%s]\n", ref.Repo) r, err := c.cache.FetchReference(ref)
layers, err := c.cache.LoadReference(ref)
if err != nil { if err != nil {
return err return err
} }
_, err = oras.Push(c.newContext(), c.resolver, ref.String(), c.cache.store, layers, if !r.Exists {
oras.WithConfigMediaType(HelmChartConfigMediaType)) return errors.New(fmt.Sprintf("Chart not found: %s", r.Name))
}
fmt.Fprintf(c.out, "The push refers to repository [%s]\n", r.Repo)
c.printCacheRefSummary(r)
layers := []ocispec.Descriptor{*r.ContentLayer}
_, err = oras.Push(ctx(c.out, c.debug), c.resolver, r.Name, c.cache.Provider(), layers,
oras.WithConfig(*r.Config), oras.WithNameValidation(nil))
if err != nil { if err != nil {
return err return err
} }
var totalSize int64 s := ""
for _, layer := range layers { numLayers := len(layers)
totalSize += layer.Size if 1 < numLayers {
s = "s"
} }
fmt.Fprintf(c.out, fmt.Fprintf(c.out,
"%s: pushed to remote (%d layers, %s total)\n", ref.Tag, len(layers), byteCountBinary(totalSize)) "%s: pushed to remote (%d layer%s, %s total)\n", r.Tag, numLayers, s, byteCountBinary(r.Size))
return nil return nil
} }
// PullChart downloads a chart from a registry // PullChart downloads a chart from a registry
func (c *Client) PullChart(ref *Reference) error { func (c *Client) PullChart(ref *Reference) error {
if ref.Tag == "" {
return errors.New("tag explicitly required")
}
existing, err := c.cache.FetchReference(ref)
if err != nil {
return err
}
fmt.Fprintf(c.out, "%s: Pulling from %s\n", ref.Tag, ref.Repo) fmt.Fprintf(c.out, "%s: Pulling from %s\n", ref.Tag, ref.Repo)
_, layers, err := oras.Pull(c.newContext(), c.resolver, ref.String(), c.cache.store, oras.WithAllowedMediaTypes(KnownMediaTypes())) manifest, _, err := oras.Pull(ctx(c.out, c.debug), c.resolver, ref.FullName(), c.cache.Ingester(),
oras.WithPullEmptyNameAllowed(),
oras.WithAllowedMediaTypes(KnownMediaTypes()),
oras.WithContentProvideIngester(c.cache.ProvideIngester()))
if err != nil { if err != nil {
return err return err
} }
exists, err := c.cache.StoreReference(ref, layers) err = c.cache.AddManifest(ref, &manifest)
if err != nil { if err != nil {
return err return err
} }
if !exists { r, err := c.cache.FetchReference(ref)
fmt.Fprintf(c.out, "Status: Downloaded newer chart for %s:%s\n", ref.Repo, ref.Tag) if err != nil {
return err
}
if !r.Exists {
return errors.New(fmt.Sprintf("Chart not found: %s", r.Name))
}
c.printCacheRefSummary(r)
if !existing.Exists {
fmt.Fprintf(c.out, "Status: Downloaded newer chart for %s\n", ref.FullName())
} else { } else {
fmt.Fprintf(c.out, "Status: Chart is up to date for %s:%s\n", ref.Repo, ref.Tag) fmt.Fprintf(c.out, "Status: Chart is up to date for %s\n", ref.FullName())
} }
return nil return err
} }
// SaveChart stores a copy of chart in local cache // SaveChart stores a copy of chart in local cache
func (c *Client) SaveChart(ch *chart.Chart, ref *Reference) error { func (c *Client) SaveChart(ch *chart.Chart, ref *Reference) error {
layers, err := c.cache.ChartToLayers(ch) r, err := c.cache.StoreReference(ref, ch)
if err != nil { if err != nil {
return err return err
} }
_, err = c.cache.StoreReference(ref, layers) c.printCacheRefSummary(r)
err = c.cache.AddManifest(ref, r.Manifest)
if err != nil { if err != nil {
return err return err
} }
fmt.Fprintf(c.out, "%s: saved\n", ref.Tag) fmt.Fprintf(c.out, "%s: saved\n", r.Tag)
return nil return nil
} }
// LoadChart retrieves a chart object by reference // LoadChart retrieves a chart object by reference
func (c *Client) LoadChart(ref *Reference) (*chart.Chart, error) { func (c *Client) LoadChart(ref *Reference) (*chart.Chart, error) {
layers, err := c.cache.LoadReference(ref) r, err := c.cache.FetchReference(ref)
if err != nil { if err != nil {
return nil, err return nil, err
} }
ch, err := c.cache.LayersToChart(layers) if !r.Exists {
return ch, err return nil, errors.New(fmt.Sprintf("Chart not found: %s", ref.FullName()))
}
c.printCacheRefSummary(r)
return r.Chart, nil
} }
// RemoveChart deletes a locally saved chart // RemoveChart deletes a locally saved chart
func (c *Client) RemoveChart(ref *Reference) error { func (c *Client) RemoveChart(ref *Reference) error {
err := c.cache.DeleteReference(ref) r, err := c.cache.DeleteReference(ref)
if err != nil { if err != nil {
return err return err
} }
fmt.Fprintf(c.out, "%s: removed\n", ref.Tag) if !r.Exists {
return err return errors.New(fmt.Sprintf("Chart not found: %s", ref.FullName()))
}
fmt.Fprintf(c.out, "%s: removed\n", r.Tag)
return nil
} }
// PrintChartTable prints a list of locally stored charts // PrintChartTable prints a list of locally stored charts
@ -168,7 +222,7 @@ func (c *Client) PrintChartTable() error {
table := uitable.New() table := uitable.New()
table.MaxColWidth = 60 table.MaxColWidth = 60
table.AddRow("REF", "NAME", "VERSION", "DIGEST", "SIZE", "CREATED") table.AddRow("REF", "NAME", "VERSION", "DIGEST", "SIZE", "CREATED")
rows, err := c.cache.TableRows() rows, err := c.getChartTableRows()
if err != nil { if err != nil {
return err return err
} }
@ -179,12 +233,45 @@ func (c *Client) PrintChartTable() error {
return nil return nil
} }
// disable verbose logging coming from ORAS unless debug is enabled // printCacheRefSummary prints out chart ref summary
func (c *Client) newContext() context.Context { func (c *Client) printCacheRefSummary(r *CacheRefSummary) {
if !c.debug { fmt.Fprintf(c.out, "ref: %s\n", r.Name)
return orascontext.Background() fmt.Fprintf(c.out, "digest: %s\n", r.Digest.Hex())
fmt.Fprintf(c.out, "size: %s\n", byteCountBinary(r.Size))
fmt.Fprintf(c.out, "name: %s\n", r.Chart.Metadata.Name)
fmt.Fprintf(c.out, "version: %s\n", r.Chart.Metadata.Version)
}
// getChartTableRows returns rows in uitable-friendly format
func (c *Client) getChartTableRows() ([][]interface{}, error) {
rr, err := c.cache.ListReferences()
if err != nil {
return nil, err
}
refsMap := map[string]map[string]string{}
for _, r := range rr {
refsMap[r.Name] = map[string]string{
"name": r.Chart.Metadata.Name,
"version": r.Chart.Metadata.Version,
"digest": shortDigest(r.Digest.Hex()),
"size": byteCountBinary(r.Size),
"created": timeAgo(r.CreatedAt),
}
}
// Sort and convert to format expected by uitable
rows := make([][]interface{}, len(refsMap))
keys := make([]string, 0, len(refsMap))
for key := range refsMap {
keys = append(keys, key)
}
sort.Strings(keys)
for i, key := range keys {
rows[i] = make([]interface{}, 6)
rows[i][0] = key
ref := refsMap[key]
for j, k := range []string{"name", "version", "digest", "size", "created"} {
rows[i][j+1] = ref[k]
}
} }
ctx := orascontext.WithLoggerFromWriter(context.Background(), c.out) return rows, nil
orascontext.GetLogger(ctx).Logger.SetLevel(logrus.DebugLevel)
return ctx
} }

@ -0,0 +1,62 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package registry // import "helm.sh/helm/internal/experimental/registry"
import (
"io"
)
type (
// ClientOption allows specifying various settings configurable by the user for overriding the defaults
// used when creating a new default client
ClientOption func(*Client)
)
// ClientOptDebug returns a function that sets the debug setting on client options set
func ClientOptDebug(debug bool) ClientOption {
return func(client *Client) {
client.debug = debug
}
}
// ClientOptWriter returns a function that sets the writer setting on client options set
func ClientOptWriter(out io.Writer) ClientOption {
return func(client *Client) {
client.out = out
}
}
// ClientOptResolver returns a function that sets the resolver setting on client options set
func ClientOptResolver(resolver *Resolver) ClientOption {
return func(client *Client) {
client.resolver = resolver
}
}
// ClientOptAuthorizer returns a function that sets the authorizer setting on client options set
func ClientOptAuthorizer(authorizer *Authorizer) ClientOption {
return func(client *Client) {
client.authorizer = authorizer
}
}
// ClientOptCache returns a function that sets the cache setting on a client options set
func ClientOptCache(cache *Cache) ClientOption {
return func(client *Client) {
client.cache = cache
}
}

@ -69,17 +69,27 @@ func (suite *RegistryClientTestSuite) SetupSuite() {
resolver, err := client.Resolver(context.Background()) resolver, err := client.Resolver(context.Background())
suite.Nil(err, "no error creating resolver") suite.Nil(err, "no error creating resolver")
// Init test client // create cache
suite.RegistryClient = NewClient(&ClientOptions{ cache, err := NewCache(
Out: suite.Out, CacheOptDebug(true),
Authorizer: Authorizer{ CacheOptWriter(suite.Out),
CacheOptRoot(filepath.Join(suite.CacheRootDir, CacheRootDir)),
)
suite.Nil(err, "no error creating cache")
// init test client
suite.RegistryClient, err = NewClient(
ClientOptDebug(true),
ClientOptWriter(suite.Out),
ClientOptAuthorizer(&Authorizer{
Client: client, Client: client,
}, }),
Resolver: Resolver{ ClientOptResolver(&Resolver{
Resolver: resolver, Resolver: resolver,
}, }),
CacheRootDir: suite.CacheRootDir, ClientOptCache(cache),
}) )
suite.Nil(err, "no error creating registry client")
// create htpasswd file (w BCrypt, which is required) // create htpasswd file (w BCrypt, which is required)
pwBytes, err := bcrypt.GenerateFromPassword([]byte(testPassword), bcrypt.DefaultCost) pwBytes, err := bcrypt.GenerateFromPassword([]byte(testPassword), bcrypt.DefaultCost)

@ -20,29 +20,14 @@ const (
// HelmChartConfigMediaType is the reserved media type for the Helm chart manifest config // HelmChartConfigMediaType is the reserved media type for the Helm chart manifest config
HelmChartConfigMediaType = "application/vnd.cncf.helm.config.v1+json" HelmChartConfigMediaType = "application/vnd.cncf.helm.config.v1+json"
// HelmChartMetaLayerMediaType is the reserved media type for Helm chart metadata
HelmChartMetaLayerMediaType = "application/vnd.cncf.helm.chart.meta.layer.v1+json"
// HelmChartContentLayerMediaType is the reserved media type for Helm chart package content // HelmChartContentLayerMediaType is the reserved media type for Helm chart package content
HelmChartContentLayerMediaType = "application/vnd.cncf.helm.chart.content.layer.v1+tar" HelmChartContentLayerMediaType = "application/vnd.cncf.helm.chart.content.layer.v1+tar"
// HelmChartMetaFileName is the reserved file name for Helm chart metadata
HelmChartMetaFileName = "chart-meta.json"
// HelmChartContentFileName is the reserved file name for Helm chart package content
HelmChartContentFileName = "chart-content.tgz"
// HelmChartNameAnnotation is the reserved annotation key for Helm chart name
HelmChartNameAnnotation = "sh.helm.chart.name"
// HelmChartVersionAnnotation is the reserved annotation key for Helm chart version
HelmChartVersionAnnotation = "sh.helm.chart.version"
) )
// KnownMediaTypes returns a list of layer mediaTypes that the Helm client knows about // KnownMediaTypes returns a list of layer mediaTypes that the Helm client knows about
func KnownMediaTypes() []string { func KnownMediaTypes() []string {
return []string{ return []string{
HelmChartMetaLayerMediaType, HelmChartConfigMediaType,
HelmChartContentLayerMediaType, HelmChartContentLayerMediaType,
} }
} }

@ -24,6 +24,6 @@ import (
func TestConstants(t *testing.T) { func TestConstants(t *testing.T) {
knownMediaTypes := KnownMediaTypes() knownMediaTypes := KnownMediaTypes()
assert.Contains(t, knownMediaTypes, HelmChartMetaLayerMediaType) assert.Contains(t, knownMediaTypes, HelmChartConfigMediaType)
assert.Contains(t, knownMediaTypes, HelmChartContentLayerMediaType) assert.Contains(t, knownMediaTypes, HelmChartContentLayerMediaType)
} }

@ -18,6 +18,7 @@ package registry // import "helm.sh/helm/internal/experimental/registry"
import ( import (
"errors" "errors"
"fmt"
"regexp" "regexp"
"strings" "strings"
@ -59,6 +60,14 @@ func ParseReference(s string) (*Reference, error) {
return &ref, nil return &ref, nil
} }
// FullName the full name of a reference (repo:tag)
func (ref *Reference) FullName() string {
if ref.Tag == "" {
return ref.Repo
}
return fmt.Sprintf("%s:%s", ref.Repo, ref.Tag)
}
// setExtraFields adds the Repo and Tag fields to a Reference // setExtraFields adds the Repo and Tag fields to a Reference
func (ref *Reference) setExtraFields() { func (ref *Reference) setExtraFields() {
ref.Tag = ref.Object ref.Tag = ref.Object

@ -44,46 +44,54 @@ func TestParseReference(t *testing.T) {
is.NoError(err) is.NoError(err)
is.Equal("mychart", ref.Repo) is.Equal("mychart", ref.Repo)
is.Equal("", ref.Tag) is.Equal("", ref.Tag)
is.Equal("mychart", ref.FullName())
s = "mychart:1.5.0" s = "mychart:1.5.0"
ref, err = ParseReference(s) ref, err = ParseReference(s)
is.NoError(err) is.NoError(err)
is.Equal("mychart", ref.Repo) is.Equal("mychart", ref.Repo)
is.Equal("1.5.0", ref.Tag) is.Equal("1.5.0", ref.Tag)
is.Equal("mychart:1.5.0", ref.FullName())
s = "myrepo/mychart" s = "myrepo/mychart"
ref, err = ParseReference(s) ref, err = ParseReference(s)
is.NoError(err) is.NoError(err)
is.Equal("myrepo/mychart", ref.Repo) is.Equal("myrepo/mychart", ref.Repo)
is.Equal("", ref.Tag) is.Equal("", ref.Tag)
is.Equal("myrepo/mychart", ref.FullName())
s = "myrepo/mychart:1.5.0" s = "myrepo/mychart:1.5.0"
ref, err = ParseReference(s) ref, err = ParseReference(s)
is.NoError(err) is.NoError(err)
is.Equal("myrepo/mychart", ref.Repo) is.Equal("myrepo/mychart", ref.Repo)
is.Equal("1.5.0", ref.Tag) is.Equal("1.5.0", ref.Tag)
is.Equal("myrepo/mychart:1.5.0", ref.FullName())
s = "mychart:5001:1.5.0" s = "mychart:5001:1.5.0"
ref, err = ParseReference(s) ref, err = ParseReference(s)
is.NoError(err) is.NoError(err)
is.Equal("mychart:5001", ref.Repo) is.Equal("mychart:5001", ref.Repo)
is.Equal("1.5.0", ref.Tag) is.Equal("1.5.0", ref.Tag)
is.Equal("mychart:5001:1.5.0", ref.FullName())
s = "myrepo:5001/mychart:1.5.0" s = "myrepo:5001/mychart:1.5.0"
ref, err = ParseReference(s) ref, err = ParseReference(s)
is.NoError(err) is.NoError(err)
is.Equal("myrepo:5001/mychart", ref.Repo) is.Equal("myrepo:5001/mychart", ref.Repo)
is.Equal("1.5.0", ref.Tag) is.Equal("1.5.0", ref.Tag)
is.Equal("myrepo:5001/mychart:1.5.0", ref.FullName())
s = "localhost:5000/mychart:latest" s = "localhost:5000/mychart:latest"
ref, err = ParseReference(s) ref, err = ParseReference(s)
is.NoError(err) is.NoError(err)
is.Equal("localhost:5000/mychart", ref.Repo) is.Equal("localhost:5000/mychart", ref.Repo)
is.Equal("latest", ref.Tag) is.Equal("latest", ref.Tag)
is.Equal("localhost:5000/mychart:latest", ref.FullName())
s = "my.host.com/my/nested/repo:1.2.3" s = "my.host.com/my/nested/repo:1.2.3"
ref, err = ParseReference(s) ref, err = ParseReference(s)
is.NoError(err) is.NoError(err)
is.Equal("my.host.com/my/nested/repo", ref.Repo) is.Equal("my.host.com/my/nested/repo", ref.Repo)
is.Equal("1.2.3", ref.Tag) is.Equal("1.2.3", ref.Tag)
is.Equal("my.host.com/my/nested/repo:1.2.3", ref.FullName())
} }

@ -0,0 +1,66 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package registry // import "helm.sh/helm/internal/experimental/registry"
import (
"context"
"fmt"
"io"
"time"
orascontext "github.com/deislabs/oras/pkg/context"
units "github.com/docker/go-units"
"github.com/sirupsen/logrus"
)
// byteCountBinary produces a human-readable file size
func byteCountBinary(b int64) string {
const unit = 1024
if b < unit {
return fmt.Sprintf("%d B", b)
}
div, exp := int64(unit), 0
for n := b / unit; n >= unit; n /= unit {
div *= unit
exp++
}
return fmt.Sprintf("%.1f %ciB", float64(b)/float64(div), "KMGTPE"[exp])
}
// shortDigest returns first 7 characters of a sha256 digest
func shortDigest(digest string) string {
if len(digest) == 64 {
return digest[:7]
}
return digest
}
// timeAgo returns a human-readable timestamp respresenting time that has passed
func timeAgo(t time.Time) string {
return units.HumanDuration(time.Now().UTC().Sub(t))
}
// ctx retrieves a fresh context.
// disable verbose logging coming from ORAS (unless debug is enabled)
func ctx(out io.Writer, debug bool) context.Context {
if !debug {
return orascontext.Background()
}
ctx := orascontext.WithLoggerFromWriter(context.Background(), out)
orascontext.GetLogger(ctx).Logger.SetLevel(logrus.DebugLevel)
return ctx
}

@ -19,6 +19,7 @@ import (
"context" "context"
"flag" "flag"
"io/ioutil" "io/ioutil"
"path/filepath"
"testing" "testing"
"time" "time"
@ -54,20 +55,32 @@ func actionConfigFixture(t *testing.T) *Configuration {
t.Fatal(err) t.Fatal(err)
} }
cache, err := registry.NewCache(
registry.CacheOptDebug(true),
registry.CacheOptRoot(filepath.Join(tdir, registry.CacheRootDir)),
)
if err != nil {
t.Fatal(err)
}
registryClient, err := registry.NewClient(
registry.ClientOptAuthorizer(&registry.Authorizer{
Client: client,
}),
registry.ClientOptResolver(&registry.Resolver{
Resolver: resolver,
}),
registry.ClientOptCache(cache),
)
if err != nil {
t.Fatal(err)
}
return &Configuration{ return &Configuration{
Releases: storage.Init(driver.NewMemory()), Releases: storage.Init(driver.NewMemory()),
KubeClient: &kubefake.FailingKubeClient{PrintingKubeClient: kubefake.PrintingKubeClient{Out: ioutil.Discard}}, KubeClient: &kubefake.FailingKubeClient{PrintingKubeClient: kubefake.PrintingKubeClient{Out: ioutil.Discard}},
Capabilities: chartutil.DefaultCapabilities, Capabilities: chartutil.DefaultCapabilities,
RegistryClient: registry.NewClient(&registry.ClientOptions{ RegistryClient: registryClient,
Out: ioutil.Discard,
Authorizer: registry.Authorizer{
Client: client,
},
Resolver: registry.Resolver{
Resolver: resolver,
},
CacheRootDir: tdir,
}),
Log: func(format string, v ...interface{}) { Log: func(format string, v ...interface{}) {
t.Helper() t.Helper()
if *verbose { if *verbose {

Loading…
Cancel
Save