k8s.io/apiserver@v0.31.1/pkg/server/egressselector/config.go (about) 1 /* 2 Copyright 2019 The Kubernetes 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 egressselector 18 19 import ( 20 "fmt" 21 "os" 22 "strings" 23 24 "k8s.io/apimachinery/pkg/runtime" 25 "k8s.io/apimachinery/pkg/util/sets" 26 "k8s.io/apimachinery/pkg/util/validation/field" 27 "k8s.io/apiserver/pkg/apis/apiserver" 28 "k8s.io/apiserver/pkg/apis/apiserver/install" 29 "k8s.io/apiserver/pkg/apis/apiserver/v1beta1" 30 "k8s.io/utils/path" 31 "sigs.k8s.io/yaml" 32 ) 33 34 var cfgScheme = runtime.NewScheme() 35 36 // validEgressSelectorNames contains the set of valid egress selctor names. 37 var validEgressSelectorNames = sets.NewString("controlplane", "cluster", "etcd") 38 39 func init() { 40 install.Install(cfgScheme) 41 } 42 43 // ReadEgressSelectorConfiguration reads the egress selector configuration at the specified path. 44 // It returns the loaded egress selector configuration if the input file aligns with the required syntax. 45 // If it does not align with the provided syntax, it returns a default configuration which should function as a no-op. 46 // It does this by returning a nil configuration, which preserves backward compatibility. 47 // This works because prior to this there was no egress selector configuration. 48 // It returns an error if the file did not exist. 49 func ReadEgressSelectorConfiguration(configFilePath string) (*apiserver.EgressSelectorConfiguration, error) { 50 if configFilePath == "" { 51 return nil, nil 52 } 53 // a file was provided, so we just read it. 54 data, err := os.ReadFile(configFilePath) 55 if err != nil { 56 return nil, fmt.Errorf("unable to read egress selector configuration from %q [%v]", configFilePath, err) 57 } 58 var decodedConfig v1beta1.EgressSelectorConfiguration 59 err = yaml.Unmarshal(data, &decodedConfig) 60 if err != nil { 61 // we got an error where the decode wasn't related to a missing type 62 return nil, err 63 } 64 if decodedConfig.Kind != "EgressSelectorConfiguration" { 65 return nil, fmt.Errorf("invalid service configuration object %q", decodedConfig.Kind) 66 } 67 internalConfig := &apiserver.EgressSelectorConfiguration{} 68 if err := cfgScheme.Convert(&decodedConfig, internalConfig, nil); err != nil { 69 // we got an error where the decode wasn't related to a missing type 70 return nil, err 71 } 72 return internalConfig, nil 73 } 74 75 // ValidateEgressSelectorConfiguration checks the apiserver.EgressSelectorConfiguration for 76 // common configuration errors. It will return error for problems such as configuring mtls/cert 77 // settings for protocol which do not support security. It will also try to catch errors such as 78 // incorrect file paths. It will return nil if it does not find anything wrong. 79 func ValidateEgressSelectorConfiguration(config *apiserver.EgressSelectorConfiguration) field.ErrorList { 80 allErrs := field.ErrorList{} 81 if config == nil { 82 return allErrs // Treating a nil configuration as valid 83 } 84 for _, service := range config.EgressSelections { 85 fldPath := field.NewPath("service", "connection") 86 switch service.Connection.ProxyProtocol { 87 case apiserver.ProtocolDirect: 88 allErrs = append(allErrs, validateDirectConnection(service.Connection, fldPath)...) 89 case apiserver.ProtocolHTTPConnect: 90 allErrs = append(allErrs, validateHTTPConnectTransport(service.Connection.Transport, fldPath)...) 91 case apiserver.ProtocolGRPC: 92 allErrs = append(allErrs, validateGRPCTransport(service.Connection.Transport, fldPath)...) 93 default: 94 allErrs = append(allErrs, field.NotSupported( 95 fldPath.Child("protocol"), 96 service.Connection.ProxyProtocol, 97 []string{ 98 string(apiserver.ProtocolDirect), 99 string(apiserver.ProtocolHTTPConnect), 100 string(apiserver.ProtocolGRPC), 101 })) 102 } 103 } 104 105 seen := sets.String{} 106 for i, service := range config.EgressSelections { 107 canonicalName := strings.ToLower(service.Name) 108 fldPath := field.NewPath("service", "connection") 109 // no duplicate check 110 if seen.Has(canonicalName) { 111 allErrs = append(allErrs, field.Duplicate(fldPath.Index(i), canonicalName)) 112 continue 113 } 114 seen.Insert(canonicalName) 115 116 if !validEgressSelectorNames.Has(canonicalName) { 117 allErrs = append(allErrs, field.NotSupported(fldPath, canonicalName, validEgressSelectorNames.List())) 118 continue 119 } 120 } 121 122 return allErrs 123 } 124 125 func validateHTTPConnectTransport(transport *apiserver.Transport, fldPath *field.Path) field.ErrorList { 126 allErrs := field.ErrorList{} 127 if transport == nil { 128 allErrs = append(allErrs, field.Required( 129 fldPath.Child("transport"), 130 "transport must be set for HTTPConnect")) 131 return allErrs 132 } 133 134 if transport.TCP != nil && transport.UDS != nil { 135 allErrs = append(allErrs, field.Invalid( 136 fldPath.Child("tcp"), 137 transport.TCP, 138 "TCP and UDS cannot both be set")) 139 } else if transport.TCP == nil && transport.UDS == nil { 140 allErrs = append(allErrs, field.Required( 141 fldPath.Child("tcp"), 142 "One of TCP or UDS must be set")) 143 } else if transport.TCP != nil { 144 allErrs = append(allErrs, validateTCPConnection(transport.TCP, fldPath)...) 145 } else if transport.UDS != nil { 146 allErrs = append(allErrs, validateUDSConnection(transport.UDS, fldPath)...) 147 } 148 return allErrs 149 } 150 151 func validateGRPCTransport(transport *apiserver.Transport, fldPath *field.Path) field.ErrorList { 152 allErrs := field.ErrorList{} 153 if transport == nil { 154 allErrs = append(allErrs, field.Required( 155 fldPath.Child("transport"), 156 "transport must be set for GRPC")) 157 return allErrs 158 } 159 160 if transport.UDS != nil { 161 allErrs = append(allErrs, validateUDSConnection(transport.UDS, fldPath)...) 162 } else { 163 allErrs = append(allErrs, field.Required( 164 fldPath.Child("uds"), 165 "UDS must be set with GRPC")) 166 } 167 return allErrs 168 } 169 170 func validateDirectConnection(connection apiserver.Connection, fldPath *field.Path) field.ErrorList { 171 if connection.Transport != nil { 172 return field.ErrorList{field.Invalid( 173 fldPath.Child("transport"), 174 "direct", 175 "Transport config should be absent for direct connect"), 176 } 177 } 178 179 return nil 180 } 181 182 func validateUDSConnection(udsConfig *apiserver.UDSTransport, fldPath *field.Path) field.ErrorList { 183 allErrs := field.ErrorList{} 184 if udsConfig.UDSName == "" { 185 allErrs = append(allErrs, field.Invalid( 186 fldPath.Child("udsName"), 187 "nil", 188 "UDSName should be present for UDS connections")) 189 } 190 return allErrs 191 } 192 193 func validateTCPConnection(tcpConfig *apiserver.TCPTransport, fldPath *field.Path) field.ErrorList { 194 allErrs := field.ErrorList{} 195 196 if strings.HasPrefix(tcpConfig.URL, "http://") { 197 if tcpConfig.TLSConfig != nil { 198 allErrs = append(allErrs, field.Invalid( 199 fldPath.Child("tlsConfig"), 200 "nil", 201 "TLSConfig config should not be present when using HTTP")) 202 } 203 } else if strings.HasPrefix(tcpConfig.URL, "https://") { 204 return validateTLSConfig(tcpConfig.TLSConfig, fldPath) 205 } else { 206 allErrs = append(allErrs, field.Invalid( 207 fldPath.Child("url"), 208 tcpConfig.URL, 209 "supported connection protocols are http:// and https://")) 210 } 211 return allErrs 212 } 213 214 func validateTLSConfig(tlsConfig *apiserver.TLSConfig, fldPath *field.Path) field.ErrorList { 215 allErrs := field.ErrorList{} 216 217 if tlsConfig == nil { 218 allErrs = append(allErrs, field.Required( 219 fldPath.Child("tlsConfig"), 220 "TLSConfig must be present when using HTTPS")) 221 return allErrs 222 } 223 if tlsConfig.CABundle != "" { 224 if exists, err := path.Exists(path.CheckFollowSymlink, tlsConfig.CABundle); !exists || err != nil { 225 allErrs = append(allErrs, field.Invalid( 226 fldPath.Child("tlsConfig", "caBundle"), 227 tlsConfig.CABundle, 228 "TLS config ca bundle does not exist")) 229 } 230 } 231 if tlsConfig.ClientCert == "" { 232 allErrs = append(allErrs, field.Invalid( 233 fldPath.Child("tlsConfig", "clientCert"), 234 "nil", 235 "Using TLS requires clientCert")) 236 } else if exists, err := path.Exists(path.CheckFollowSymlink, tlsConfig.ClientCert); !exists || err != nil { 237 allErrs = append(allErrs, field.Invalid( 238 fldPath.Child("tlsConfig", "clientCert"), 239 tlsConfig.ClientCert, 240 "TLS client cert does not exist")) 241 } 242 if tlsConfig.ClientKey == "" { 243 allErrs = append(allErrs, field.Invalid( 244 fldPath.Child("tlsConfig", "clientKey"), 245 "nil", 246 "Using TLS requires requires clientKey")) 247 } else if exists, err := path.Exists(path.CheckFollowSymlink, tlsConfig.ClientKey); !exists || err != nil { 248 allErrs = append(allErrs, field.Invalid( 249 fldPath.Child("tlsConfig", "clientKey"), 250 tlsConfig.ClientKey, 251 "TLS client key does not exist")) 252 } 253 return allErrs 254 }