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 }