@ -18,26 +18,31 @@ package registry // import "helm.sh/helm/v4/pkg/registry"
import (
"context"
"encoding/base64"
"crypto/tls"
"crypto/x509"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"sort"
"strings"
"sync"
"github.com/Masterminds/semver/v3"
"github.com/containerd/containerd/remotes"
"github.com/opencontainers/image-spec/specs-go"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
"oras.land/oras-go/pkg/auth"
dockerauth "oras.land/oras-go/pkg/auth/docker"
"oras.land/oras-go/pkg/content"
"oras.land/oras-go/pkg/oras"
"oras.land/oras-go/pkg/registry"
registryremote "oras.land/oras-go/pkg/registry/remote"
registryauth "oras.land/oras-go/pkg/registry/remote/auth"
"oras.land/oras-go/v2"
"oras.land/oras-go/v2/content"
"oras.land/oras-go/v2/content/memory"
"oras.land/oras-go/v2/registry"
"oras.land/oras-go/v2/registry/remote"
"oras.land/oras-go/v2/registry/remote/auth"
"oras.land/oras-go/v2/registry/remote/credentials"
"oras.land/oras-go/v2/registry/remote/retry"
"helm.sh/helm/v4/internal/version"
"helm.sh/helm/v4/pkg/chart"
@ -51,6 +56,8 @@ storing semantic versions, Helm adopts the convention of changing plus (+) to
an underscore ( _ ) in chart version tags when pushing to a registry and back to
a plus ( + ) when pulling from a registry . `
var errDeprecatedRemote = errors . New ( "providing github.com/containerd/containerd/remotes.Resolver via ClientOptResolver is no longer suported" )
type (
// RemoteClient shadows the ORAS remote.Client interface
// (hiding the ORAS type from Helm client visibility)
@ -68,11 +75,12 @@ type (
username string
password string
out io . Writer
authorizer auth . Client
authorizer * auth . Client
registryAuthorizer RemoteClient
resolver func ( ref registry . Reference ) ( remotes . Resolver , error )
credentialsStore credentials . Store
httpClient * http . Client
plainHTTP bool
err error // pass any errors from the ClientOption functions
}
// ClientOption allows specifying various settings configurable by the user for overriding the defaults
@ -87,101 +95,70 @@ func NewClient(options ...ClientOption) (*Client, error) {
}
for _ , option := range options {
option ( client )
if client . err != nil {
return nil , client . err
}
}
if client . credentialsFile == "" {
client . credentialsFile = helmpath . ConfigPath ( CredentialsFileBasename )
}
if client . authorizer == nil {
authClient , err := dockerauth . NewClientWithDockerFallback ( client . credentialsFile )
if err != nil {
return nil , err
}
client . authorizer = authClient
}
resolverFn := client . resolver // copy for avoiding recursive call
client . resolver = func ( ref registry . Reference ) ( remotes . Resolver , error ) {
if resolverFn != nil {
// validate if the resolverFn returns a valid resolver
if resolver , err := resolverFn ( ref ) ; resolver != nil && err == nil {
return resolver , nil
}
}
headers := http . Header { }
headers . Set ( "User-Agent" , version . GetUserAgent ( ) )
opts := [ ] auth . ResolverOption { auth . WithResolverHeaders ( headers ) }
if client . httpClient != nil {
opts = append ( opts , auth . WithResolverClient ( client . httpClient ) )
}
if client . plainHTTP {
opts = append ( opts , auth . WithResolverPlainHTTP ( ) )
if client . httpClient == nil {
type cloner [ T any ] interface {
Clone ( ) T
}
// if username and password are set, use them for authentication
// by adding the basic auth Authorization header to the resolver
if client . username != "" && client . password != "" {
concat := client . username + ":" + client . password
encodedAuth := base64 . StdEncoding . EncodeToString ( [ ] byte ( concat ) )
opts = append ( opts , auth . WithResolverHeaders (
http . Header {
"Authorization" : [ ] string { "Basic " + encodedAuth } ,
} ,
) )
// try to copy (clone) the http.DefaultTransport so any mutations we
// perform on it (e.g. TLS config) are not reflected globally
// follow https://github.com/golang/go/issues/39299 for a more elegant
// solution in the future
transport := http . DefaultTransport
if t , ok := transport . ( cloner [ * http . Transport ] ) ; ok {
transport = t . Clone ( )
} else if t , ok := transport . ( cloner [ http . RoundTripper ] ) ; ok {
// this branch will not be used with go 1.20, it was added
// optimistically to try to clone if the http.DefaultTransport
// implementation changes, still the Clone method in that case
// might not return http.RoundTripper...
transport = t . Clone ( )
}
resolver , err := client . authorizer . ResolverWithOpts ( opts ... )
if err != nil {
return nil , err
client . httpClient = & http . Client {
Transport : retry . NewTransport ( transport ) ,
}
return resolver , nil
}
// allocate a cache if option is set
var cache registryauth . Cache
if client . enableCache {
cache = registryauth . DefaultCache
storeOptions := credentials . StoreOptions {
AllowPlaintextPut : true ,
DetectDefaultNativeStore : true ,
}
store , err := credentials . NewStore ( client . credentialsFile , storeOptions )
if err != nil {
return nil , err
}
dockerStore , err := credentials . NewStoreFromDocker ( storeOptions )
if err != nil {
// should only fail if user home directory can't be determined
client . credentialsStore = store
} else {
// use Helm credentials with fallback to Docker
client . credentialsStore = credentials . NewStoreWithFallbacks ( store , dockerStore )
}
if client . registryAuthorizer == nil {
client . registryAuthorizer = & registryauth . Client {
Client : client . httpClient ,
Header : http . Header {
"User-Agent" : { version . GetUserAgent ( ) } ,
} ,
Cache : cache ,
Credential : func ( _ context . Context , reg string ) ( registryauth . Credential , error ) {
if client . username != "" && client . password != "" {
return registryauth . Credential {
Username : client . username ,
Password : client . password ,
} , nil
}
dockerClient , ok := client . authorizer . ( * dockerauth . Client )
if ! ok {
return registryauth . EmptyCredential , errors . New ( "unable to obtain docker client" )
}
username , password , err := dockerClient . Credential ( reg )
if err != nil {
return registryauth . EmptyCredential , errors . New ( "unable to retrieve credentials" )
}
// A blank returned username and password value is a bearer token
if username == "" && password != "" {
return registryauth . Credential {
RefreshToken : password ,
} , nil
}
if client . authorizer == nil {
authorizer := auth . Client {
Client : client . httpClient ,
}
authorizer . SetUserAgent ( version . GetUserAgent ( ) )
return registryauth . Credential {
Username : username ,
Password : password ,
} , nil
authorizer . Credential = credentials . Credential ( client . credentialsStore )
} ,
if client . enableCache {
authorizer . Cache = auth . NewCache ( )
}
client . authorizer = & authorizer
}
return client , nil
}
@ -220,7 +197,7 @@ func ClientOptWriter(out io.Writer) ClientOption {
// Depending on the use-case you may need to set both ClientOptAuthorizer and ClientOptRegistryAuthorizer.
func ClientOptAuthorizer ( authorizer auth . Client ) ClientOption {
return func ( client * Client ) {
client . authorizer = authorizer
client . authorizer = & authorizer
}
}
@ -254,12 +231,9 @@ func ClientOptPlainHTTP() ClientOption {
}
}
// ClientOptResolver returns a function that sets the resolver setting on a client options set
func ClientOptResolver ( resolver remotes . Resolver ) ClientOption {
return func ( client * Client ) {
client . resolver = func ( _ registry . Reference ) ( remotes . Resolver , error ) {
return resolver , nil
}
func ClientOptResolver ( _ remotes . Resolver ) ClientOption {
return func ( c * Client ) {
c . err = errDeprecatedRemote
}
}
@ -268,60 +242,128 @@ type (
LoginOption func ( * loginOperation )
loginOperation struct {
username string
password string
insecure bool
certFile string
keyFile string
caFile string
host string
client * Client
}
)
// Login logs into a registry
func ( c * Client ) Login ( host string , options ... LoginOption ) error {
operation := & loginOperation { }
for _ , option := range options {
option ( operation )
option ( & loginOperation { host , c } )
}
reg , err := remote . NewRegistry ( host )
if err != nil {
return err
}
authorizerLoginOpts := [ ] auth . LoginOption {
auth . WithLoginContext ( ctx ( c . out , c . debug ) ) ,
auth . WithLoginHostname ( host ) ,
auth . WithLoginUsername ( operation . username ) ,
auth . WithLoginSecret ( operation . password ) ,
auth . WithLoginUserAgent ( version . GetUserAgent ( ) ) ,
auth . WithLoginTLS ( operation . certFile , operation . keyFile , operation . caFile ) ,
reg. PlainHTTP = c . plainHTTP
reg . Client = c . authorizer
ctx := context . Background ( )
cred , err := c . authorizer . Credential ( ctx , host )
if err != nil {
return fmt . Errorf ( "fetching credentials for %q: %w" , host , err )
}
if operation . insecure {
authorizerLoginOpts = append ( authorizerLoginOpts , auth . WithLoginInsecure ( ) )
if err := reg . Ping ( ctx ) ; err != nil {
return fmt . Errorf ( "authenticating to %q: %w" , host , err )
}
if err := c . authorizer . LoginWithOpts ( authorizerLoginOpts ... ) ; err != nil {
key := credentials . ServerAddressFromRegistry ( host )
if err := c . credentialsStore . Put ( ctx , key , cred ) ; err != nil {
return err
}
fmt . Fprintln ( c . out , "Login Succeeded" )
return nil
}
// LoginOptBasicAuth returns a function that sets the username/password settings on login
func LoginOptBasicAuth ( username string , password string ) LoginOption {
return func ( operation * loginOperation ) {
operation . username = username
operation . password = password
return func ( o * loginOperation ) {
o . client . username = username
o . client . password = password
o . client . authorizer . Credential = auth . StaticCredential ( o . host , auth . Credential { Username : username , Password : password } )
}
}
// LoginOptPlainText returns a function that allows plaintext (HTTP) login
func LoginOptPlainText ( isPlainText bool ) LoginOption {
return func ( o * loginOperation ) {
o . client . plainHTTP = isPlainText
}
}
func ensureTLSConfig ( client * auth . Client ) ( * tls . Config , error ) {
var transport * http . Transport
switch t := client . Client . Transport . ( type ) {
case * http . Transport :
transport = t
case * retry . Transport :
switch t := t . Base . ( type ) {
case * http . Transport :
transport = t
}
}
if transport == nil {
// we don't know how to access the http.Transport, most likely the
// auth.Client.Client was provided by API user
return nil , fmt . Errorf ( "unable to access TLS client configuration, the provided HTTP Transport is not supported, given: %T" , client . Client . Transport )
}
if transport . TLSClientConfig == nil {
transport . TLSClientConfig = & tls . Config { }
}
return transport . TLSClientConfig , nil
}
// LoginOptInsecure returns a function that sets the insecure setting on login
func LoginOptInsecure ( insecure bool ) LoginOption {
return func ( operation * loginOperation ) {
operation . insecure = insecure
return func ( o * loginOperation ) {
tlsConfig , err := ensureTLSConfig ( o . client . authorizer )
if err != nil {
panic ( err )
}
tlsConfig . InsecureSkipVerify = insecure
}
}
// LoginOptTLSClientConfig returns a function that sets the TLS settings on login.
func LoginOptTLSClientConfig ( certFile , keyFile , caFile string ) LoginOption {
return func ( operation * loginOperation ) {
operation . certFile = certFile
operation . keyFile = keyFile
operation . caFile = caFile
return func ( o * loginOperation ) {
if ( certFile == "" || keyFile == "" ) && caFile == "" {
return
}
tlsConfig , err := ensureTLSConfig ( o . client . authorizer )
if err != nil {
panic ( err )
}
if certFile != "" && keyFile != "" {
authCert , err := tls . LoadX509KeyPair ( certFile , keyFile )
if err != nil {
panic ( err )
}
tlsConfig . Certificates = [ ] tls . Certificate { authCert }
}
if caFile != "" {
certPool := x509 . NewCertPool ( )
ca , err := os . ReadFile ( caFile )
if err != nil {
panic ( err )
}
if ! certPool . AppendCertsFromPEM ( ca ) {
panic ( fmt . Errorf ( "unable to parse CA file: %q" , caFile ) )
}
tlsConfig . RootCAs = certPool
}
}
}
@ -338,7 +380,8 @@ func (c *Client) Logout(host string, opts ...LogoutOption) error {
for _ , opt := range opts {
opt ( operation )
}
if err := c . authorizer . Logout ( ctx ( c . out , c . debug ) , host ) ; err != nil {
if err := credentials . Logout ( context . Background ( ) , c . credentialsStore , host ) ; err != nil {
return err
}
fmt . Fprintf ( c . out , "Removing login credentials for %s\n" , host )
@ -393,8 +436,9 @@ func (c *Client) Pull(ref string, options ...PullOption) (*PullResult, error) {
return nil , errors . New (
"must specify at least one layer to pull (chart/prov)" )
}
memoryStore := content. NewMemory ( )
memoryStore := memory. New ( )
allowedMediaTypes := [ ] string {
ocispec . MediaTypeImageManifest ,
ConfigMediaType ,
}
minNumDescriptors := 1 // 1 for the config
@ -410,18 +454,34 @@ func (c *Client) Pull(ref string, options ...PullOption) (*PullResult, error) {
}
var descriptors , layers [ ] ocispec . Descriptor
remotesResolver , err := c . resolver ( parsedRef . orasReference )
repository , err := remote . NewRepository ( parsedRef . String ( ) )
if err != nil {
return nil , err
}
registryStore := content . Registry { Resolver : remotesResolver }
repository . PlainHTTP = c . plainHTTP
repository . Client = c . authorizer
manifest , err := oras . Copy ( ctx ( c . out , c . debug ) , registryStore , parsedRef . String ( ) , memoryStore , "" ,
oras . WithPullEmptyNameAllowed ( ) ,
oras . WithAllowedMediaTypes ( allowedMediaTypes ) ,
oras . WithLayerDescriptors ( func ( l [ ] ocispec . Descriptor ) {
layers = l
} ) )
ctx := context . Background ( )
sort . Strings ( allowedMediaTypes )
var mu sync . Mutex
manifest , err := oras . Copy ( ctx , repository , parsedRef . String ( ) , memoryStore , "" , oras . CopyOptions {
CopyGraphOptions : oras . CopyGraphOptions {
PreCopy : func ( _ context . Context , desc ocispec . Descriptor ) error {
mediaType := desc . MediaType
if i := sort . SearchStrings ( allowedMediaTypes , mediaType ) ; i >= len ( allowedMediaTypes ) || allowedMediaTypes [ i ] != mediaType {
return errors . Errorf ( "media type %q is not allowed, found in descriptor with digest: %q" , mediaType , desc . Digest )
}
mu . Lock ( )
layers = append ( layers , desc )
mu . Unlock ( )
return nil
} ,
} ,
} )
if err != nil {
return nil , err
}
@ -480,54 +540,37 @@ func (c *Client) Pull(ref string, options ...PullOption) (*PullResult, error) {
Prov : & DescriptorPullSummary { } ,
Ref : parsedRef . String ( ) ,
}
var getManifestErr error
if _ , manifestData , ok := memoryStore . Get ( manifest ) ; ! ok {
getManifestErr = errors . Errorf ( "Unable to retrieve blob with digest %s" , manifest . Digest )
} else {
result . Manifest . Data = manifestData
}
if getManifestErr != nil {
return nil , getManifestErr
result . Manifest . Data , err = content . FetchAll ( ctx , memoryStore , manifest )
if err != nil {
return nil , fmt . Errorf ( "unable to retrieve blob with digest %s: %w" , manifest . Digest , err )
}
var getConfigDescriptorErr error
if _ , configData , ok := memoryStore . Get ( * configDescriptor ) ; ! ok {
getConfigDescriptorErr = errors . Errorf ( "Unable to retrieve blob with digest %s" , configDescriptor . Digest )
} else {
result . Config . Data = configData
var meta * chart . Metadata
if err := json . Unmarshal ( configData , & meta ) ; err != nil {
return nil , err
}
result . Chart . Meta = meta
result . Config . Data , err = content . FetchAll ( ctx , memoryStore , * configDescriptor )
if err != nil {
return nil , fmt . Errorf ( "unable to retrieve blob with digest %s: %w" , configDescriptor . Digest , err )
}
if getConfigDescriptorErr != nil {
return nil , getConfigDescriptorErr
if err := json . Unmarshal ( result . Config . Data , & result . Chart . Meta ) ; err != nil {
return nil , err
}
if operation . withChart {
var getChartDescriptorErr error
if _ , chartData , ok := memoryStore . Get ( * chartDescriptor ) ; ! ok {
getChartDescriptorErr = errors . Errorf ( "Unable to retrieve blob with digest %s" , chartDescriptor . Digest )
} else {
result . Chart . Data = chartData
result . Chart . Digest = chartDescriptor . Digest . String ( )
result . Chart . Size = chartDescriptor . Size
}
if getChartDescriptorErr != nil {
return nil , getChartDescriptorErr
result . Chart . Data , err = content . FetchAll ( ctx , memoryStore , * chartDescriptor )
if err != nil {
return nil , fmt . Errorf ( "unable to retrieve blob with digest %s: %w" , chartDescriptor . Digest , err )
}
result . Chart . Digest = chartDescriptor . Digest . String ( )
result . Chart . Size = chartDescriptor . Size
}
if operation . withProv && ! provMissing {
var getProvDescriptorErr error
if _ , provData , ok := memoryStore . Get ( * provDescriptor ) ; ! ok {
getProvDescriptorErr = errors . Errorf ( "Unable to retrieve blob with digest %s" , provDescriptor . Digest )
} else {
result . Prov . Data = provData
result . Prov . Digest = provDescriptor . Digest . String ( )
result . Prov . Size = provDescriptor . Size
}
if getProvDescriptorErr != nil {
return nil , getProvDescriptorErr
result . Prov . Data , err = content . FetchAll ( ctx , memoryStore , * provDescriptor )
if err != nil {
return nil , fmt . Errorf ( "unable to retrieve blob with digest %s: %w" , provDescriptor . Digest , err )
}
result . Prov . Digest = provDescriptor . Digest . String ( )
result . Prov . Size = provDescriptor . Size
}
fmt . Fprintf ( c . out , "Pulled: %s\n" , result . Ref )
@ -615,8 +658,11 @@ func (c *Client) Push(data []byte, ref string, options ...PushOption) (*PushResu
"strict mode enabled, ref basename and tag must match the chart name and version" )
}
}
memoryStore := content . NewMemory ( )
chartDescriptor , err := memoryStore . Add ( "" , ChartLayerMediaType , data )
ctx := context . Background ( )
memoryStore := memory . New ( )
chartDescriptor , err := oras . PushBytes ( ctx , memoryStore , ChartLayerMediaType , data )
if err != nil {
return nil , err
}
@ -626,43 +672,57 @@ func (c *Client) Push(data []byte, ref string, options ...PushOption) (*PushResu
return nil , err
}
configDescriptor , err := memoryStore. Add ( "" , ConfigMediaType , configData )
configDescriptor , err := oras. PushBytes ( ctx , memoryStore, ConfigMediaType , configData )
if err != nil {
return nil , err
}
descripto rs := [ ] ocispec . Descriptor { chartDescriptor }
laye rs := [ ] ocispec . Descriptor { chartDescriptor }
var provDescriptor ocispec . Descriptor
if operation . provData != nil {
provDescriptor , err = memoryStore. Add ( "" , ProvLayerMediaType , operation . provData )
provDescriptor , err = oras. PushBytes ( ctx , memoryStore, ProvLayerMediaType , operation . provData )
if err != nil {
return nil , err
}
descriptors = append ( descripto rs, provDescriptor )
layers = append ( laye rs, provDescriptor )
}
// sort layers for determinism, similar to how ORAS v1 does it
sort . Slice ( layers , func ( i , j int ) bool {
return layers [ i ] . Digest < layers [ j ] . Digest
} )
ociAnnotations := generateOCIAnnotations ( meta , operation . creationTime )
manifest := ocispec . Manifest {
Versioned : specs . Versioned { SchemaVersion : 2 } ,
Config : configDescriptor ,
Layers : layers ,
Annotations : ociAnnotations ,
}
manifestData , manifest , err := content . GenerateManifest ( & configDescriptor , ociAnnotations , descriptors ... )
manifestData , err := json . Marshal ( manifest )
if err != nil {
return nil , err
}
if err := memoryStore . StoreManifest ( parsedRef . String ( ) , manifest , manifestData ) ; err != nil {
manifestDescriptor , err := oras . TagBytes ( ctx , memoryStore , ocispec . MediaTypeImageManifest , manifestData , ref )
if err != nil {
return nil , err
}
re motesResolver, err := c . resolver ( parsedRef . orasReference )
re pository, err := remote . NewRepository ( parsedRef . String ( ) )
if err != nil {
return nil , err
}
registryStore := content . Registry { Resolver : remotesResolver }
_ , err = oras . Copy ( ctx ( c . out , c . debug ) , memoryStore , parsedRef . orasReference . String ( ) , registryStore , "" ,
oras . WithNameValidation ( nil ) )
repository . PlainHTTP = c . plainHTTP
repository . Client = c . authorizer
manifestDescriptor , err = oras . ExtendedCopy ( ctx , memoryStore , parsedRef . String ( ) , repository , parsedRef . String ( ) , oras . DefaultExtendedCopyOptions )
if err != nil {
return nil , err
}
chartSummary := & descriptorPushSummaryWithMeta {
Meta : meta ,
}
@ -670,8 +730,8 @@ func (c *Client) Push(data []byte, ref string, options ...PushOption) (*PushResu
chartSummary . Size = chartDescriptor . Size
result := & PushResult {
Manifest : & descriptorPushSummary {
Digest : manifest . Digest . String ( ) ,
Size : manifest . Size ,
Digest : manifest Descriptor . Digest . String ( ) ,
Size : manifest Descriptor . Size ,
} ,
Config : & descriptorPushSummary {
Digest : configDescriptor . Digest . String ( ) ,
@ -725,27 +785,29 @@ func (c *Client) Tags(ref string) ([]string, error) {
return nil , err
}
repository := registryremote . Repository {
Reference : parsedReference ,
Client : c . registryAuthorizer ,
PlainHTTP : c . plainHTTP ,
}
var registryTags [ ] string
registryTags , err = registry . Tags ( ctx ( c . out , c . debug ) , & repository )
ctx := context . Background ( )
repository , err := remote . NewRepository ( parsedReference . String ( ) )
if err != nil {
return nil , err
}
repository . PlainHTTP = c . plainHTTP
repository . Client = c . authorizer
var tagVersions [ ] * semver . Version
for _ , tag := range registryTags {
// Change underscore (_) back to plus (+) for Helm
// See https://github.com/helm/helm/issues/10166
tagVersion , err := semver . StrictNewVersion ( strings . ReplaceAll ( tag , "_" , "+" ) )
if err == nil {
tagVersions = append ( tagVersions , tagVersion )
err = repository . Tags ( ctx , "" , func ( tags [ ] string ) error {
for _ , tag := range tags {
// Change underscore (_) back to plus (+) for Helm
// See https://github.com/helm/helm/issues/10166
tagVersion , err := semver . StrictNewVersion ( strings . ReplaceAll ( tag , "_" , "+" ) )
if err == nil {
tagVersions = append ( tagVersions , tagVersion )
}
}
return nil
} )
if err != nil {
return nil , err
}
// Sort the collection
@ -762,30 +824,28 @@ func (c *Client) Tags(ref string) ([]string, error) {
}
// Resolve a reference to a descriptor.
func ( c * Client ) Resolve ( ref string ) ( * ocispec . Descriptor , error ) {
ctx := context . Background ( )
parsedRef , err := newReference ( ref )
func ( c * Client ) Resolve ( ref string ) ( desc ocispec . Descriptor , err error ) {
remoteRepository , err := remote . NewRepository ( ref )
if err != nil {
return nil , err
}
if parsedRef . Registry == "" {
return nil , nil
return desc , err
}
remoteRepository . PlainHTTP = c . plainHTTP
remotesResolver, err := c . resolver ( parsedRef . orasReference )
parsedReference, err := newReference ( ref )
if err != nil {
return nil , err
return desc , err
}
_ , desc , err := remotesResolver . Resolve ( ctx , ref )
return & desc , err
ctx := context . Background ( )
parsedString := parsedReference . String ( )
return remoteRepository . Resolve ( ctx , parsedString )
}
// ValidateReference for path and version
func ( c * Client ) ValidateReference ( ref , version string , u * url . URL ) ( * url . URL , error ) {
var tag string
registryReference , err := newReference ( u . Path)
registryReference , err := newReference ( u . Host + u . Path)
if err != nil {
return nil , err
}
@ -800,19 +860,20 @@ func (c *Client) ValidateReference(ref, version string, u *url.URL) (*url.URL, e
}
if registryReference . Digest != "" {
if registryReference. Tag == "" {
if version == "" {
// Install by digest only
return u , nil
}
u . Path = fmt . Sprintf ( "%s@%s" , registryReference . Repository , registryReference . Digest )
// Validate the tag if it was specified
path := registryReference . Registry + "/" + registryReference . Repository + ":" + registryReference. Tag
path := registryReference . Registry + "/" + registryReference . Repository + ":" + version
desc , err := c . Resolve ( path )
if err != nil {
// The resource does not have to be tagged when digest is specified
return u , nil
}
if desc != nil && desc . Digest . String ( ) != registryReference . Digest {
if desc . Digest . String ( ) != registryReference . Digest {
return nil , errors . Errorf ( "chart reference digest mismatch: %s is not %s" , desc . Digest . String ( ) , registryReference . Digest )
}
return u , nil
@ -842,7 +903,7 @@ func (c *Client) ValidateReference(ref, version string, u *url.URL) (*url.URL, e
}
}
u . Path = fmt . Sprintf ( "%s /%s :%s", registryReference . Registry , registryReference . Repository , tag )
u . Path = fmt . Sprintf ( "%s :%s", registryReference . Repository , tag )
return u , err
}