...

Source file src/helm.sh/helm/v3/pkg/registry/util.go

Documentation: helm.sh/helm/v3/pkg/registry

     1  /*
     2  Copyright The Helm Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package registry // import "helm.sh/helm/v3/pkg/registry"
    18  
    19  import (
    20  	"bytes"
    21  	"context"
    22  	"fmt"
    23  	"io"
    24  	"net/http"
    25  	"strings"
    26  	"time"
    27  
    28  	helmtime "helm.sh/helm/v3/pkg/time"
    29  
    30  	"github.com/Masterminds/semver/v3"
    31  	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
    32  	"github.com/pkg/errors"
    33  	"github.com/sirupsen/logrus"
    34  	orascontext "oras.land/oras-go/pkg/context"
    35  	"oras.land/oras-go/pkg/registry"
    36  
    37  	"helm.sh/helm/v3/internal/tlsutil"
    38  	"helm.sh/helm/v3/pkg/chart"
    39  	"helm.sh/helm/v3/pkg/chart/loader"
    40  )
    41  
    42  var immutableOciAnnotations = []string{
    43  	ocispec.AnnotationVersion,
    44  	ocispec.AnnotationTitle,
    45  }
    46  
    47  // IsOCI determines whether or not a URL is to be treated as an OCI URL
    48  func IsOCI(url string) bool {
    49  	return strings.HasPrefix(url, fmt.Sprintf("%s://", OCIScheme))
    50  }
    51  
    52  // ContainsTag determines whether a tag is found in a provided list of tags
    53  func ContainsTag(tags []string, tag string) bool {
    54  	for _, t := range tags {
    55  		if tag == t {
    56  			return true
    57  		}
    58  	}
    59  	return false
    60  }
    61  
    62  func GetTagMatchingVersionOrConstraint(tags []string, versionString string) (string, error) {
    63  	var constraint *semver.Constraints
    64  	if versionString == "" {
    65  		// If string is empty, set wildcard constraint
    66  		constraint, _ = semver.NewConstraint("*")
    67  	} else {
    68  		// when customer input exact version, check whether have exact match
    69  		// one first
    70  		for _, v := range tags {
    71  			if versionString == v {
    72  				return v, nil
    73  			}
    74  		}
    75  
    76  		// Otherwise set constraint to the string given
    77  		var err error
    78  		constraint, err = semver.NewConstraint(versionString)
    79  		if err != nil {
    80  			return "", err
    81  		}
    82  	}
    83  
    84  	// Otherwise try to find the first available version matching the string,
    85  	// in case it is a constraint
    86  	for _, v := range tags {
    87  		test, err := semver.NewVersion(v)
    88  		if err != nil {
    89  			continue
    90  		}
    91  		if constraint.Check(test) {
    92  			return v, nil
    93  		}
    94  	}
    95  
    96  	return "", errors.Errorf("Could not locate a version matching provided version string %s", versionString)
    97  }
    98  
    99  // extractChartMeta is used to extract a chart metadata from a byte array
   100  func extractChartMeta(chartData []byte) (*chart.Metadata, error) {
   101  	ch, err := loader.LoadArchive(bytes.NewReader(chartData))
   102  	if err != nil {
   103  		return nil, err
   104  	}
   105  	return ch.Metadata, nil
   106  }
   107  
   108  // ctx retrieves a fresh context.
   109  // disable verbose logging coming from ORAS (unless debug is enabled)
   110  func ctx(out io.Writer, debug bool) context.Context {
   111  	if !debug {
   112  		return orascontext.Background()
   113  	}
   114  	ctx := orascontext.WithLoggerFromWriter(context.Background(), out)
   115  	orascontext.GetLogger(ctx).Logger.SetLevel(logrus.DebugLevel)
   116  	return ctx
   117  }
   118  
   119  // parseReference will parse and validate the reference, and clean tags when
   120  // applicable tags are only cleaned when plus (+) signs are present, and are
   121  // converted to underscores (_) before pushing
   122  // See https://github.com/helm/helm/issues/10166
   123  func parseReference(raw string) (registry.Reference, error) {
   124  	// The sole possible reference modification is replacing plus (+) signs
   125  	// present in tags with underscores (_). To do this properly, we first
   126  	// need to identify a tag, and then pass it on to the reference parser
   127  	// NOTE: Passing immediately to the reference parser will fail since (+)
   128  	// signs are an invalid tag character, and simply replacing all plus (+)
   129  	// occurrences could invalidate other portions of the URI
   130  	parts := strings.Split(raw, ":")
   131  	if len(parts) > 1 && !strings.Contains(parts[len(parts)-1], "/") {
   132  		tag := parts[len(parts)-1]
   133  
   134  		if tag != "" {
   135  			// Replace any plus (+) signs with known underscore (_) conversion
   136  			newTag := strings.ReplaceAll(tag, "+", "_")
   137  			raw = strings.ReplaceAll(raw, tag, newTag)
   138  		}
   139  	}
   140  
   141  	return registry.ParseReference(raw)
   142  }
   143  
   144  // NewRegistryClientWithTLS is a helper function to create a new registry client with TLS enabled.
   145  func NewRegistryClientWithTLS(out io.Writer, certFile, keyFile, caFile string, insecureSkipTLSverify bool, registryConfig string, debug bool) (*Client, error) {
   146  	tlsConf, err := tlsutil.NewClientTLS(certFile, keyFile, caFile, insecureSkipTLSverify)
   147  	if err != nil {
   148  		return nil, fmt.Errorf("can't create TLS config for client: %s", err)
   149  	}
   150  	// Create a new registry client
   151  	registryClient, err := NewClient(
   152  		ClientOptDebug(debug),
   153  		ClientOptEnableCache(true),
   154  		ClientOptWriter(out),
   155  		ClientOptCredentialsFile(registryConfig),
   156  		ClientOptHTTPClient(&http.Client{
   157  			Transport: &http.Transport{
   158  				TLSClientConfig: tlsConf,
   159  			},
   160  		}),
   161  	)
   162  	if err != nil {
   163  		return nil, err
   164  	}
   165  	return registryClient, nil
   166  }
   167  
   168  // generateOCIAnnotations will generate OCI annotations to include within the OCI manifest
   169  func generateOCIAnnotations(meta *chart.Metadata, creationTime string) map[string]string {
   170  
   171  	// Get annotations from Chart attributes
   172  	ociAnnotations := generateChartOCIAnnotations(meta, creationTime)
   173  
   174  	// Copy Chart annotations
   175  annotations:
   176  	for chartAnnotationKey, chartAnnotationValue := range meta.Annotations {
   177  
   178  		// Avoid overriding key properties
   179  		for _, immutableOciKey := range immutableOciAnnotations {
   180  			if immutableOciKey == chartAnnotationKey {
   181  				continue annotations
   182  			}
   183  		}
   184  
   185  		// Add chart annotation
   186  		ociAnnotations[chartAnnotationKey] = chartAnnotationValue
   187  	}
   188  
   189  	return ociAnnotations
   190  }
   191  
   192  // getChartOCIAnnotations will generate OCI annotations from the provided chart
   193  func generateChartOCIAnnotations(meta *chart.Metadata, creationTime string) map[string]string {
   194  	chartOCIAnnotations := map[string]string{}
   195  
   196  	chartOCIAnnotations = addToMap(chartOCIAnnotations, ocispec.AnnotationDescription, meta.Description)
   197  	chartOCIAnnotations = addToMap(chartOCIAnnotations, ocispec.AnnotationTitle, meta.Name)
   198  	chartOCIAnnotations = addToMap(chartOCIAnnotations, ocispec.AnnotationVersion, meta.Version)
   199  	chartOCIAnnotations = addToMap(chartOCIAnnotations, ocispec.AnnotationURL, meta.Home)
   200  
   201  	if len(creationTime) == 0 {
   202  		creationTime = helmtime.Now().UTC().Format(time.RFC3339)
   203  	}
   204  
   205  	chartOCIAnnotations = addToMap(chartOCIAnnotations, ocispec.AnnotationCreated, creationTime)
   206  
   207  	if len(meta.Sources) > 0 {
   208  		chartOCIAnnotations = addToMap(chartOCIAnnotations, ocispec.AnnotationSource, meta.Sources[0])
   209  	}
   210  
   211  	if meta.Maintainers != nil && len(meta.Maintainers) > 0 {
   212  		var maintainerSb strings.Builder
   213  
   214  		for maintainerIdx, maintainer := range meta.Maintainers {
   215  
   216  			if len(maintainer.Name) > 0 {
   217  				maintainerSb.WriteString(maintainer.Name)
   218  			}
   219  
   220  			if len(maintainer.Email) > 0 {
   221  				maintainerSb.WriteString(" (")
   222  				maintainerSb.WriteString(maintainer.Email)
   223  				maintainerSb.WriteString(")")
   224  			}
   225  
   226  			if maintainerIdx < len(meta.Maintainers)-1 {
   227  				maintainerSb.WriteString(", ")
   228  			}
   229  
   230  		}
   231  
   232  		chartOCIAnnotations = addToMap(chartOCIAnnotations, ocispec.AnnotationAuthors, maintainerSb.String())
   233  
   234  	}
   235  
   236  	return chartOCIAnnotations
   237  }
   238  
   239  // addToMap takes an existing map and adds an item if the value is not empty
   240  func addToMap(inputMap map[string]string, newKey string, newValue string) map[string]string {
   241  
   242  	// Add item to map if its
   243  	if len(strings.TrimSpace(newValue)) > 0 {
   244  		inputMap[newKey] = newValue
   245  	}
   246  
   247  	return inputMap
   248  
   249  }
   250  

View as plain text