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  }