github.com/openshift/installer@v1.4.17/pkg/validate/validate.go (about)

     1  // Package validate contains validation utilities for installer types.
     2  package validate
     3  
     4  import (
     5  	"crypto/x509"
     6  	"encoding/json"
     7  	"encoding/pem"
     8  	"errors"
     9  	"fmt"
    10  	"net"
    11  	"net/url"
    12  	"regexp"
    13  	"strings"
    14  
    15  	uuid "github.com/google/uuid"
    16  	"golang.org/x/crypto/ssh"
    17  	k8serrors "k8s.io/apimachinery/pkg/util/errors"
    18  	"k8s.io/apimachinery/pkg/util/validation"
    19  )
    20  
    21  var (
    22  	// DockerBridgeCIDR is the network range that is used by default network for docker.
    23  	DockerBridgeCIDR = func() *net.IPNet {
    24  		_, cidr, _ := net.ParseCIDR("172.17.0.0/16")
    25  		return cidr
    26  	}()
    27  )
    28  
    29  // CABundle checks if the given string contains valid certificate(s) and returns an error if not.
    30  func CABundle(v string) error {
    31  	rest := []byte(v)
    32  	for {
    33  		var block *pem.Block
    34  		block, rest = pem.Decode(rest)
    35  		if block == nil {
    36  			return fmt.Errorf("invalid block")
    37  		}
    38  		_, err := x509.ParseCertificate(block.Bytes)
    39  		if err != nil {
    40  			return err
    41  		}
    42  		if len(rest) == 0 {
    43  			break
    44  		}
    45  	}
    46  	return nil
    47  }
    48  func validateSubdomain(v string) error {
    49  	validationMessages := validation.IsDNS1123Subdomain(v)
    50  	if len(validationMessages) == 0 {
    51  		return nil
    52  	}
    53  
    54  	errs := make([]error, len(validationMessages))
    55  	for i, m := range validationMessages {
    56  		errs[i] = errors.New(m)
    57  	}
    58  	return k8serrors.NewAggregate(errs)
    59  }
    60  
    61  // DomainName checks if the given string is a valid domain name and returns an error if not.
    62  func DomainName(v string, acceptTrailingDot bool) error {
    63  	if acceptTrailingDot {
    64  		v = strings.TrimSuffix(v, ".")
    65  	}
    66  	return validateSubdomain(v)
    67  }
    68  
    69  // NoProxyDomainName checks if the given string is a valid proxy noProxy domain name
    70  // and returns an error if not. Example valid noProxy domains are ".foo.com", "bar.com",
    71  // "bar.com." but not "*.foo.com".
    72  func NoProxyDomainName(v string) error {
    73  	v = strings.TrimSuffix(strings.TrimPrefix(v, "."), ".")
    74  	return validateSubdomain(v)
    75  }
    76  
    77  type imagePullSecret struct {
    78  	Auths map[string]map[string]interface{} `json:"auths"`
    79  }
    80  
    81  // ImagePullSecret checks if the given string is a valid image pull secret and returns an error if not.
    82  func ImagePullSecret(secret string) error {
    83  	var s imagePullSecret
    84  	err := json.Unmarshal([]byte(secret), &s)
    85  	if err != nil {
    86  		return err
    87  	}
    88  	if len(s.Auths) == 0 {
    89  		return fmt.Errorf("auths required")
    90  	}
    91  	errs := []error{}
    92  	for d, a := range s.Auths {
    93  		_, authPresent := a["auth"]
    94  		_, credsStorePresnet := a["credsStore"]
    95  		if !authPresent && !credsStorePresnet {
    96  			errs = append(errs, fmt.Errorf("%q requires either auth or credsStore", d))
    97  		}
    98  	}
    99  	return k8serrors.NewAggregate(errs)
   100  }
   101  
   102  // ClusterName1035 checks the provided cluster name matches RFC1035 naming requirements.
   103  // Some platform resource names must comply with RFC1035 "[a-z]([-a-z0-9]*[a-z0-9])?". They
   104  // are based on the InfraID, which is a truncated version of the cluster name where all non-
   105  // alphanumeric characters "[^A-Za-z0-9-]" have been replaced with dashes "-". As a result,
   106  // if we first verify the name starts with a lower-case letter "^[a-z]" then we can rely on
   107  // the ClusterName function to confirm compliance with the rest. The resulting name will
   108  // therefore match RFC1035 with the exception of possible periods ".", which will be
   109  // translated into dashes "-" in the InfraID before being used to create cloud resources.
   110  func ClusterName1035(v string) error {
   111  	re := regexp.MustCompile("^[a-z]")
   112  	if !re.MatchString(v) {
   113  		return errors.New("cluster name must begin with a lower-case letter")
   114  	}
   115  	return ClusterName(v)
   116  }
   117  
   118  // GCPClusterName checks if the provided cluster name has words similar to the word 'google'
   119  // since resources with that name are not allowed in GCP.
   120  func GCPClusterName(v string) error {
   121  	reStartsWith := regexp.MustCompile("^goog")
   122  	reContains := regexp.MustCompile(".*g[o0]{2}gle.*")
   123  	if reStartsWith.MatchString(v) || reContains.MatchString(v) {
   124  		return errors.New("cluster name must not start with \"goog\" or contain variations of \"google\"")
   125  	}
   126  	return nil
   127  }
   128  
   129  // ClusterNameMaxLength validates if the string provided length is
   130  // greater than maxlen argument.
   131  func ClusterNameMaxLength(v string, maxlen int) error {
   132  	if len(v) > maxlen {
   133  		return errors.New(validation.MaxLenError(maxlen))
   134  	}
   135  	return nil
   136  }
   137  
   138  // ClusterName checks if the given string is a valid name for a cluster and returns an error if not.
   139  // The max length of the DNS label is `DNS1123LabelMaxLength + 9` because the public DNS zones have records
   140  // `api.clustername`, `*.apps.clustername`, and *.apps is rendered as the nine-character \052.apps in DNS records.
   141  func ClusterName(v string) error {
   142  	const maxlen = validation.DNS1123LabelMaxLength - 9
   143  	err := ClusterNameMaxLength(v, maxlen)
   144  	if err != nil {
   145  		return err
   146  	}
   147  	return validateSubdomain(v)
   148  }
   149  
   150  // SubnetCIDR checks if the given IP net is a valid CIDR.
   151  func SubnetCIDR(cidr *net.IPNet) error {
   152  	if cidr.IP.IsUnspecified() {
   153  		return errors.New("address must be specified")
   154  	}
   155  	nip := cidr.IP.Mask(cidr.Mask)
   156  	if nip.String() != cidr.IP.String() {
   157  		return fmt.Errorf("invalid network address. got %s, expecting %s", cidr.String(), (&net.IPNet{IP: nip, Mask: cidr.Mask}).String())
   158  	}
   159  	return nil
   160  }
   161  
   162  // ServiceSubnetCIDR checks if the given IP net is a valid CIDR for the Kubernetes service network
   163  func ServiceSubnetCIDR(cidr *net.IPNet) error {
   164  	if cidr.IP.IsUnspecified() {
   165  		return errors.New("address must be specified")
   166  	}
   167  	nip := cidr.IP.Mask(cidr.Mask)
   168  	if nip.String() != cidr.IP.String() {
   169  		return fmt.Errorf("invalid network address. got %s, expecting %s", cidr.String(), (&net.IPNet{IP: nip, Mask: cidr.Mask}).String())
   170  	}
   171  	maskLen, addrLen := cidr.Mask.Size()
   172  	if addrLen == 32 && maskLen < 12 {
   173  		return fmt.Errorf("subnet size for IPv4 service network must be /12 or greater (/16 is recommended)")
   174  	} else if addrLen == 128 && maskLen < 108 {
   175  		// Kubernetes allows any length greater than 108 (and so do we, for
   176  		// backward compat), but for various reasons there is no point in
   177  		// using any value other than 112.
   178  		return fmt.Errorf("subnet size for IPv6 service network should be /112")
   179  	}
   180  	return nil
   181  }
   182  
   183  // DoCIDRsOverlap returns true if one of the CIDRs is a subset of the other.
   184  func DoCIDRsOverlap(acidr, bcidr *net.IPNet) bool {
   185  	return acidr.Contains(bcidr.IP) || bcidr.Contains(acidr.IP)
   186  }
   187  
   188  // SSHPublicKey checks if the given string is a valid SSH public key
   189  // and returns an error if not.
   190  func SSHPublicKey(v string) error {
   191  	_, _, _, _, err := ssh.ParseAuthorizedKey([]byte(v))
   192  	return err
   193  }
   194  
   195  // URI validates if the URI is a valid absolute URI.
   196  func URI(uri string) error {
   197  	parsed, err := url.Parse(uri)
   198  	if err != nil {
   199  		return err
   200  	}
   201  	if !parsed.IsAbs() {
   202  		return fmt.Errorf("invalid URI %q (no scheme)", uri)
   203  	}
   204  	return nil
   205  }
   206  
   207  // URIWithProtocol validates that the URI specifies a certain
   208  // protocol scheme (e.g. "https")
   209  func URIWithProtocol(uri string, protocol string) error {
   210  	parsed, err := url.Parse(uri)
   211  	if err != nil {
   212  		return err
   213  	}
   214  	if parsed.Scheme != protocol {
   215  		return fmt.Errorf("must use %s protocol", protocol)
   216  	}
   217  	return nil
   218  }
   219  
   220  // IP validates if a string is a valid IP.
   221  func IP(ip string) error {
   222  	addr := net.ParseIP(ip)
   223  	if addr == nil {
   224  		return fmt.Errorf("%q is not a valid IP", ip)
   225  	}
   226  	return nil
   227  }
   228  
   229  // MAC validates that a value is a valid unicast EUI-48 MAC address
   230  func MAC(addr string) error {
   231  	hwAddr, err := net.ParseMAC(addr)
   232  	if err != nil {
   233  		return err
   234  	}
   235  
   236  	// net.ParseMAC checks for any valid mac, including 20-octet infiniband
   237  	// MAC's. Let's make sure we have an EUI-48 MAC, consisting of 6 octets
   238  	if len(hwAddr) != 6 {
   239  		return fmt.Errorf("invalid MAC address")
   240  	}
   241  
   242  	// We also need to check that the MAC is a valid unicast address. A multicast
   243  	// address is an address where the least significant bit of the most significant
   244  	// byte is 1.
   245  	//
   246  	//      Example 1: Multicast MAC
   247  	//      ------------------------
   248  	//      7D:CE:E3:29:35:6F
   249  	//       ^--> most significant byte
   250  	//
   251  	//      0x7D is 0b11111101
   252  	//                       ^--> this is a multicast MAC
   253  	//
   254  	//      Example 2: Unicast MAC
   255  	//      ----------------------
   256  	//      7A:CE:E3:29:35:6F
   257  	//       ^--> most significant byte
   258  	//
   259  	//      0x7A is 0b11111010
   260  	//                       ^--> this is a unicast MAC
   261  	if hwAddr[0]&1 == 1 {
   262  		return fmt.Errorf("expected unicast mac address, found multicast")
   263  	}
   264  
   265  	return nil
   266  }
   267  
   268  // UUID validates that a uuid is non-empty and a valid uuid.
   269  func UUID(val string) error {
   270  	_, err := uuid.Parse(val)
   271  	return err
   272  }
   273  
   274  // Host validates that a given string is a valid URI host.
   275  func Host(v string) error {
   276  	proxyIP := net.ParseIP(v)
   277  	if proxyIP != nil {
   278  		return nil
   279  	}
   280  	return validateSubdomain(v)
   281  }
   282  
   283  // OnPremClusterName verifies if the cluster name contains a '.' and returns an error if it does.
   284  func OnPremClusterName(v string) error {
   285  	if strings.Contains(v, ".") {
   286  		return errors.New("cluster name must not contain '.'")
   287  	}
   288  	return ClusterName(v)
   289  }