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
metadata := chart.Metadata{}
err = json.Unmarshal(metaJSONRaw, &metadata)
if err != nil {
return nil, err
}
metadata.APIVersion = chart.APIVersionV1
metadata.Name = name
metadata.Version = version
// Obtain raw chart content
_, contentRaw, ok := cache.store.Get(contentLayer)
if !ok {
return nil, errors.New("error retrieving meta layer")
} }
// Construct chart object and attach metadata // FetchReference retrieves a chart ref from cache
ch, err := loader.LoadArchive(bytes.NewBuffer(contentRaw)) func (cache *Cache) FetchReference(ref *Reference) (*CacheRefSummary, error) {
if err != nil { if err := cache.init(); err != nil {
return nil, err return nil, err
} }
ch.Metadata = &metadata r := CacheRefSummary{
Name: ref.FullName(),
return ch, nil Repo: ref.Repo,
Tag: ref.Tag,
} }
for _, desc := range cache.ociStore.ListReferences() {
func (cache *filesystemCache) ChartToLayers(ch *chart.Chart) ([]ocispec.Descriptor, error) { if desc.Annotations[ocispec.AnnotationRefName] == r.Name {
r.Exists = true
// extract/separate the name and version from other metadata manifestBytes, err := cache.fetchBlob(&desc)
if err := ch.Validate(); err != nil {
return nil, err
}
name := ch.Metadata.Name
version := ch.Metadata.Version
// Create meta layer, clear name and version from Chart.yaml and convert to json
ch.Metadata.Name = ""
ch.Metadata.Version = ""
metaJSONRaw, err := json.Marshal(ch.Metadata)
if err != nil { if err != nil {
return nil, err return &r, err
} }
metaLayer := cache.store.Add(HelmChartMetaFileName, HelmChartMetaLayerMediaType, metaJSONRaw) var manifest ocispec.Manifest
err = json.Unmarshal(manifestBytes, &manifest)
// Create content layer
// TODO: something better than this hack. Currently needed for chartutil.Save()
// 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) r.Manifest = &desc
if err != nil { r.Config = &manifest.Config
return nil, err numLayers := len(manifest.Layers)
if numLayers != 1 {
return &r, errors.New(
fmt.Sprintf("manifest does not contain exactly 1 layer (total: %d)", numLayers))
} }
contentLayer := cache.store.Add(HelmChartContentFileName, HelmChartContentLayerMediaType, contentRaw) var contentLayer *ocispec.Descriptor
for _, layer := range manifest.Layers {
// Set annotations switch layer.MediaType {
contentLayer.Annotations[HelmChartNameAnnotation] = name case HelmChartContentLayerMediaType:
contentLayer.Annotations[HelmChartVersionAnnotation] = version contentLayer = &layer
layers := []ocispec.Descriptor{metaLayer, contentLayer}
return layers, nil
} }
func (cache *filesystemCache) LoadReference(ref *Reference) ([]ocispec.Descriptor, error) {
tagDir := filepath.Join(cache.rootDir, "refs", escape(ref.Repo), "tags", ref.Tag)
// add meta layer
metaJSONRaw, err := getSymlinkDestContent(filepath.Join(tagDir, "meta"))
if err != nil {
return nil, err
} }
metaLayer := cache.store.Add(HelmChartMetaFileName, HelmChartMetaLayerMediaType, metaJSONRaw) if contentLayer.Size == 0 {
return &r, errors.New(
// add content layer fmt.Sprintf("manifest does not contain a layer with mediatype %s", HelmChartContentLayerMediaType))
contentRaw, err := getSymlinkDestContent(filepath.Join(tagDir, "content"))
if err != nil {
return nil, err
} }
contentLayer := cache.store.Add(HelmChartContentFileName, HelmChartContentLayerMediaType, contentRaw) r.ContentLayer = contentLayer
info, err := cache.ociStore.Info(ctx(cache.out, cache.debug), contentLayer.Digest)
// set annotations on content layer (chart name and version)
err = setLayerAnnotationsFromChartLink(contentLayer, filepath.Join(tagDir, "chart"))
if err != nil { if err != nil {
return nil, err return &r, err
}
printChartSummary(cache.out, metaLayer, contentLayer)
layers := []ocispec.Descriptor{metaLayer, contentLayer}
return layers, nil
} }
r.Size = info.Size
func (cache *filesystemCache) StoreReference(ref *Reference, layers []ocispec.Descriptor) (bool, error) { r.Digest = info.Digest
tagDir := mkdir(filepath.Join(cache.rootDir, "refs", escape(ref.Repo), "tags", ref.Tag)) r.CreatedAt = info.CreatedAt
contentBytes, err := cache.fetchBlob(contentLayer)
// Retrieve just the meta and content layers
metaLayer, contentLayer, err := extractLayers(layers)
if err != nil { if err != nil {
return false, err return &r, err
} }
ch, err := loader.LoadArchive(bytes.NewBuffer(contentBytes))
// Extract chart name and version
name, version, err := extractChartNameVersionFromLayer(contentLayer)
if err != nil { if err != nil {
return false, err return &r, err
} }
r.Chart = ch
// Create chart file
chartPath, err := createChartFile(filepath.Join(cache.rootDir, "charts"), name, version)
if err != nil {
return false, err
} }
}
// Create chart symlink return &r, nil
err = createSymlink(chartPath, filepath.Join(tagDir, "chart"))
if err != nil {
return false, err
} }
// Save meta blob // StoreReference stores a chart ref in cache
metaExists, metaPath := digestPath(filepath.Join(cache.rootDir, "blobs"), metaLayer.Digest) func (cache *Cache) StoreReference(ref *Reference, ch *chart.Chart) (*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
} }
r := CacheRefSummary{
Name: ref.FullName(),
Repo: ref.Repo,
Tag: ref.Tag,
Chart: ch,
} }
existing, _ := cache.FetchReference(ref)
// Create meta symlink r.Exists = existing.Exists
err = createSymlink(metaPath, filepath.Join(tagDir, "meta")) config, _, err := cache.saveChartConfig(ch)
if err != nil { if err != nil {
return false, err return &r, err
} }
r.Config = config
// Save content blob contentLayer, _, err := cache.saveChartContentLayer(ch)
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 &r, err
}
} }
r.ContentLayer = contentLayer
// Create content symlink info, err := cache.ociStore.Info(ctx(cache.out, cache.debug), contentLayer.Digest)
err = createSymlink(contentPath, filepath.Join(tagDir, "content"))
if err != nil { if err != nil {
return false, err return &r, err
}
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) r.Size = info.Size
r.Digest = info.Digest
r.CreatedAt = info.CreatedAt
manifest, _, err := cache.saveChartManifest(config, contentLayer)
if err != nil {
return &r, err
} }
r.Manifest = manifest
func (cache *filesystemCache) TableRows() ([][]interface{}, error) { return &r, nil
return getRefsSorted(filepath.Join(cache.rootDir, "refs"))
} }
// escape sanitizes a registry URL to remove characters such as ":" // DeleteReference deletes a chart ref from cache
// which are illegal on windows // TODO: garbage collection, only manifest removed
func escape(s string) string { func (cache *Cache) DeleteReference(ref *Reference) (*CacheRefSummary, error) {
return strings.ReplaceAll(s, ":", "_") if err := cache.init(); err != nil {
return nil, err
} }
r, err := cache.FetchReference(ref)
// escape reverses escape if err != nil || !r.Exists {
func unescape(s string) string { return r, err
return strings.ReplaceAll(s, "_", ":")
} }
cache.ociStore.DeleteReference(r.Name)
// printChartSummary prints details about a chart layers err = cache.ociStore.SaveIndex()
func printChartSummary(out io.Writer, metaLayer ocispec.Descriptor, contentLayer ocispec.Descriptor) { return r, err
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 // ListReferences lists all chart refs in a cache
func fileExists(path string) bool { func (cache *Cache) ListReferences() ([]*CacheRefSummary, error) {
if _, err := os.Stat(path); os.IsNotExist(err) { if err := cache.init(); err != nil {
return false return nil, err
} }
return true 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
// mkdir will create a directory (no error check) and return the path
func mkdir(dir string) string {
os.MkdirAll(dir, 0755)
return dir
} }
ref, err := ParseReference(name)
// createSymlink creates a symbolic link, deleting existing one if exists
func createSymlink(src string, dest string) error {
os.Remove(dest)
err := os.Symlink(src, dest)
return err
}
// getSymlinkDestContent returns the file contents of a symlink's destination
func getSymlinkDestContent(linkPath string) ([]byte, error) {
src, err := os.Readlink(linkPath)
if err != nil { if err != nil {
return nil, err return rr, err
} }
return ioutil.ReadFile(src) r, err := cache.FetchReference(ref)
}
// setLayerAnnotationsFromChartLink will set chart name/version annotations on a layer
// based on the path of the chart link destination
func setLayerAnnotationsFromChartLink(layer ocispec.Descriptor, chartLinkPath string) error {
src, err := os.Readlink(chartLinkPath)
if err != nil { if err != nil {
return err return rr, err
} }
// example path: /some/path/charts/mychart/versions/1.2.0 rr = append(rr, r)
chartName := filepath.Base(filepath.Dir(filepath.Dir(src)))
chartVersion := filepath.Base(src)
layer.Annotations[HelmChartNameAnnotation] = chartName
layer.Annotations[HelmChartVersionAnnotation] = chartVersion
return nil
} }
return rr, 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 { // AddManifest provides a manifest to the cache index.json
switch layer.MediaType { func (cache *Cache) AddManifest(ref *Reference, manifest *ocispec.Descriptor) error {
case HelmChartMetaLayerMediaType: if err := cache.init(); err != nil {
metaLayer = layer return err
case HelmChartContentLayerMediaType:
contentLayer = layer
} }
cache.ociStore.AddReference(ref.FullName(), *manifest)
err := cache.ociStore.SaveIndex()
return err
} }
if metaLayer.Size == 0 { // Provider provides a valid containerd Provider
return metaLayer, contentLayer, errors.New("manifest does not contain a Helm chart meta layer") func (cache *Cache) Provider() content.Provider {
return content.Provider(cache.ociStore)
} }
if contentLayer.Size == 0 { // Ingester provides a valid containerd Ingester
return metaLayer, contentLayer, errors.New("manifest does not contain a Helm chart content layer") func (cache *Cache) Ingester() content.Ingester {
return content.Ingester(cache.ociStore)
} }
return metaLayer, contentLayer, nil // ProvideIngester provides a valid oras ProvideIngester
func (cache *Cache) ProvideIngester() orascontent.ProvideIngester {
return orascontent.ProvideIngester(cache.ociStore)
} }
// extractChartNameVersionFromLayer retrieves the chart name and version from layer annotations // init creates files needed necessary for OCI layout store
func extractChartNameVersionFromLayer(layer ocispec.Descriptor) (string, string, error) { func (cache *Cache) init() error {
name, ok := layer.Annotations[HelmChartNameAnnotation] if cache.ociStore == nil {
if !ok { ociStore, err := orascontent.NewOCIStore(cache.rootDir)
return "", "", errors.New("could not find chart name in annotations") if err != nil {
return err
} }
version, ok := layer.Annotations[HelmChartVersionAnnotation] cache.ociStore = ociStore
if !ok { cache.memoryStore = orascontent.NewMemoryStore()
return "", "", errors.New("could not find chart version in annotations")
} }
return name, version, nil return nil
} }
// createChartFile creates a file under "<chartsdir>" dir which is linked to by ref // saveChartConfig stores the Chart.yaml as json blob and returns a descriptor
func createChartFile(chartsRootDir string, name string, version string) (string, error) { func (cache *Cache) saveChartConfig(ch *chart.Chart) (*ocispec.Descriptor, bool, error) {
chartPathDir := filepath.Join(chartsRootDir, name, "versions") configBytes, err := json.Marshal(ch.Metadata)
chartPath := filepath.Join(chartPathDir, version)
if _, err := os.Stat(chartPath); err != nil && os.IsNotExist(err) {
os.MkdirAll(chartPathDir, 0755)
err := ioutil.WriteFile(chartPath, []byte("-"), 0644)
if err != nil { if err != nil {
return "", err return nil, false, err
} }
configExists, err := cache.storeBlob(configBytes)
if err != nil {
return nil, configExists, err
} }
return chartPath, nil descriptor := cache.memoryStore.Add("", HelmChartConfigMediaType, configBytes)
} return &descriptor, configExists, nil
// 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 // saveChartContentLayer stores the chart as tarball blob and returns a descriptor
func byteCountBinary(b int64) string { func (cache *Cache) saveChartContentLayer(ch *chart.Chart) (*ocispec.Descriptor, bool, error) {
const unit = 1024 destDir := filepath.Join(cache.rootDir, ".build")
if b < unit { os.MkdirAll(destDir, 0755)
return fmt.Sprintf("%d B", b) tmpFile, err := chartutil.Save(ch, destDir)
} defer os.Remove(tmpFile)
div, exp := int64(unit), 0 if err != nil {
for n := b / unit; n >= unit; n /= unit { return nil, false, errors.Wrap(err, "failed to save")
div *= unit
exp++
} }
return fmt.Sprintf("%.1f %ciB", float64(b)/float64(div), "KMGTPE"[exp]) contentBytes, err := ioutil.ReadFile(tmpFile)
if err != nil {
return nil, false, err
} }
contentExists, err := cache.storeBlob(contentBytes)
// shortDigest returns first 7 characters of a sha256 digest if err != nil {
func shortDigest(digest string) string { return nil, contentExists, err
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{}
} }
manifestBytes, err := json.Marshal(manifest)
// Add data to entry based on file name (symlink name) if err != nil {
base := filepath.Base(path) return nil, false, err
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()))
} }
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 _, err = writer.Write(blobBytes)
break if err != nil {
return exists, err
} }
err = writer.Commit(ctx(cache.out, cache.debug), 0, writer.Digest())
if err != nil {
if !errdefs.IsAlreadyExists(err) {
return exists, err
} }
if !allKeysFound { exists = true
delete(refsMap, k)
} }
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 {
return err
}
err = c.cache.AddManifest(ref, &manifest)
if err != nil { if err != nil {
return err return err
} }
exists, err := c.cache.StoreReference(ref, layers) r, err := c.cache.FetchReference(ref)
if err != nil { if err != nil {
return err return err
} }
if !exists { if !r.Exists {
fmt.Fprintf(c.out, "Status: Downloaded newer chart for %s:%s\n", ref.Repo, ref.Tag) 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