github.com/diafour/helm@v3.0.0-beta.3+incompatible/internal/experimental/registry/reference.go (about)

     1  /*
     2  Copyright The Helm Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package registry // import "helm.sh/helm/internal/experimental/registry"
    18  
    19  import (
    20  	"errors"
    21  	"fmt"
    22  	"net/url"
    23  	"regexp"
    24  	"strconv"
    25  	"strings"
    26  )
    27  
    28  var (
    29  	validPortRegEx = regexp.MustCompile(`^([1-9]\d{0,3}|0|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$`) // adapted from https://stackoverflow.com/a/12968117
    30  	// TODO: Currently we don't support digests, so we are only splitting on the
    31  	// colon. However, when we add support for digests, we'll need to use the
    32  	// regexp anyway to split on both colons and @, so leaving it like this for
    33  	// now
    34  	referenceDelimiter = regexp.MustCompile(`[:]`)
    35  	errEmptyRepo       = errors.New("parsed repo was empty")
    36  	errTooManyColons   = errors.New("ref may only contain a single colon character (:) unless specifying a port number")
    37  )
    38  
    39  type (
    40  	// Reference defines the main components of a reference specification
    41  	Reference struct {
    42  		Tag  string
    43  		Repo string
    44  	}
    45  )
    46  
    47  // ParseReference converts a string to a Reference
    48  func ParseReference(s string) (*Reference, error) {
    49  	if s == "" {
    50  		return nil, errEmptyRepo
    51  	}
    52  	// Split the components of the string on the colon or @, if it is more than 3,
    53  	// immediately return an error. Other validation will be performed later in
    54  	// the function
    55  	splitComponents := fixSplitComponents(referenceDelimiter.Split(s, -1))
    56  	if len(splitComponents) > 3 {
    57  		return nil, errTooManyColons
    58  	}
    59  
    60  	var ref *Reference
    61  	switch len(splitComponents) {
    62  	case 1:
    63  		ref = &Reference{Repo: splitComponents[0]}
    64  	case 2:
    65  		ref = &Reference{Repo: splitComponents[0], Tag: splitComponents[1]}
    66  	case 3:
    67  		ref = &Reference{Repo: strings.Join(splitComponents[:2], ":"), Tag: splitComponents[2]}
    68  	}
    69  
    70  	// ensure the reference is valid
    71  	err := ref.validate()
    72  	if err != nil {
    73  		return nil, err
    74  	}
    75  
    76  	return ref, nil
    77  }
    78  
    79  // FullName the full name of a reference (repo:tag)
    80  func (ref *Reference) FullName() string {
    81  	if ref.Tag == "" {
    82  		return ref.Repo
    83  	}
    84  	return fmt.Sprintf("%s:%s", ref.Repo, ref.Tag)
    85  }
    86  
    87  // validate makes sure the ref meets our criteria
    88  func (ref *Reference) validate() error {
    89  	err := ref.validateRepo()
    90  	if err != nil {
    91  		return err
    92  	}
    93  	return ref.validateNumColons()
    94  }
    95  
    96  // validateRepo checks that the Repo field is non-empty
    97  func (ref *Reference) validateRepo() error {
    98  	if ref.Repo == "" {
    99  		return errEmptyRepo
   100  	}
   101  	// Makes sure the repo results in a parsable URL (similar to what is done
   102  	// with containerd reference parsing)
   103  	_, err := url.Parse(ref.Repo)
   104  	return err
   105  }
   106  
   107  // validateNumColon ensures the ref only contains a single colon character (:)
   108  // (or potentially two, there might be a port number specified i.e. :5000)
   109  func (ref *Reference) validateNumColons() error {
   110  	if strings.Contains(ref.Tag, ":") {
   111  		return errTooManyColons
   112  	}
   113  	parts := strings.Split(ref.Repo, ":")
   114  	lastIndex := len(parts) - 1
   115  	if 1 < lastIndex {
   116  		return errTooManyColons
   117  	}
   118  	if 0 < lastIndex {
   119  		port := strings.Split(parts[lastIndex], "/")[0]
   120  		if !isValidPort(port) {
   121  			return errTooManyColons
   122  		}
   123  	}
   124  	return nil
   125  }
   126  
   127  // isValidPort returns whether or not a string looks like a valid port
   128  func isValidPort(s string) bool {
   129  	return validPortRegEx.MatchString(s)
   130  }
   131  
   132  // fixSplitComponents this will modify reference parts based on presence of port
   133  // Example: {localhost, 5000/x/y/z, 0.1.0} => {localhost:5000/x/y/z, 0.1.0}
   134  func fixSplitComponents(c []string) []string {
   135  	if len(c) <= 1 {
   136  		return c
   137  	}
   138  	possiblePortParts := strings.Split(c[1], "/")
   139  	if _, err := strconv.Atoi(possiblePortParts[0]); err == nil {
   140  		components := []string{strings.Join(c[:2], ":")}
   141  		components = append(components, c[2:]...)
   142  		return components
   143  	}
   144  	return c
   145  }