2019-09-27 21:51:53 +00:00
/ *
Copyright 2019 The Kubernetes Authors .
Licensed under the Apache License , Version 2.0 ( the "License" ) ;
you may not use this file except in compliance with the License .
You may obtain a copy of the License at
http : //www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing , software
distributed under the License is distributed on an "AS IS" BASIS ,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND , either express or implied .
See the License for the specific language governing permissions and
limitations under the License .
* /
package egressselector
import (
"fmt"
"io/ioutil"
"strings"
"k8s.io/apimachinery/pkg/runtime"
2020-12-01 01:06:26 +00:00
"k8s.io/apimachinery/pkg/util/sets"
2019-09-27 21:51:53 +00:00
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/apiserver/pkg/apis/apiserver"
"k8s.io/apiserver/pkg/apis/apiserver/install"
2020-03-26 21:07:15 +00:00
"k8s.io/apiserver/pkg/apis/apiserver/v1beta1"
2019-09-27 21:51:53 +00:00
"k8s.io/utils/path"
"sigs.k8s.io/yaml"
)
var cfgScheme = runtime . NewScheme ( )
2020-12-01 01:06:26 +00:00
// validEgressSelectorNames contains the set of valid egress selctor names.
// 'master' is deprecated in favor of 'controlplane' and will be removed in v1.22.
var validEgressSelectorNames = sets . NewString ( "master" , "controlplane" , "cluster" , "etcd" )
2019-09-27 21:51:53 +00:00
func init ( ) {
install . Install ( cfgScheme )
}
// ReadEgressSelectorConfiguration reads the egress selector configuration at the specified path.
// It returns the loaded egress selector configuration if the input file aligns with the required syntax.
// If it does not align with the provided syntax, it returns a default configuration which should function as a no-op.
// It does this by returning a nil configuration, which preserves backward compatibility.
// This works because prior to this there was no egress selector configuration.
// It returns an error if the file did not exist.
func ReadEgressSelectorConfiguration ( configFilePath string ) ( * apiserver . EgressSelectorConfiguration , error ) {
if configFilePath == "" {
return nil , nil
}
// a file was provided, so we just read it.
data , err := ioutil . ReadFile ( configFilePath )
if err != nil {
return nil , fmt . Errorf ( "unable to read egress selector configuration from %q [%v]" , configFilePath , err )
}
2020-03-26 21:07:15 +00:00
var decodedConfig v1beta1 . EgressSelectorConfiguration
2019-09-27 21:51:53 +00:00
err = yaml . Unmarshal ( data , & decodedConfig )
if err != nil {
// we got an error where the decode wasn't related to a missing type
return nil , err
}
if decodedConfig . Kind != "EgressSelectorConfiguration" {
return nil , fmt . Errorf ( "invalid service configuration object %q" , decodedConfig . Kind )
}
2019-12-12 01:27:03 +00:00
internalConfig := & apiserver . EgressSelectorConfiguration { }
if err := cfgScheme . Convert ( & decodedConfig , internalConfig , nil ) ; err != nil {
2019-09-27 21:51:53 +00:00
// we got an error where the decode wasn't related to a missing type
return nil , err
}
2019-12-12 01:27:03 +00:00
return internalConfig , nil
2019-09-27 21:51:53 +00:00
}
// ValidateEgressSelectorConfiguration checks the apiserver.EgressSelectorConfiguration for
// common configuration errors. It will return error for problems such as configuring mtls/cert
// settings for protocol which do not support security. It will also try to catch errors such as
// incorrect file paths. It will return nil if it does not find anything wrong.
func ValidateEgressSelectorConfiguration ( config * apiserver . EgressSelectorConfiguration ) field . ErrorList {
allErrs := field . ErrorList { }
if config == nil {
return allErrs // Treating a nil configuration as valid
}
for _ , service := range config . EgressSelections {
2020-03-26 21:07:15 +00:00
fldPath := field . NewPath ( "service" , "connection" )
switch service . Connection . ProxyProtocol {
case apiserver . ProtocolDirect :
allErrs = append ( allErrs , validateDirectConnection ( service . Connection , fldPath ) ... )
case apiserver . ProtocolHTTPConnect :
allErrs = append ( allErrs , validateHTTPConnectTransport ( service . Connection . Transport , fldPath ) ... )
case apiserver . ProtocolGRPC :
allErrs = append ( allErrs , validateGRPCTransport ( service . Connection . Transport , fldPath ) ... )
2019-09-27 21:51:53 +00:00
default :
allErrs = append ( allErrs , field . NotSupported (
2020-03-26 21:07:15 +00:00
fldPath . Child ( "protocol" ) ,
service . Connection . ProxyProtocol ,
[ ] string {
string ( apiserver . ProtocolDirect ) ,
string ( apiserver . ProtocolHTTPConnect ) ,
string ( apiserver . ProtocolGRPC ) ,
} ) )
2019-09-27 21:51:53 +00:00
}
}
2020-12-01 01:06:26 +00:00
var foundControlPlane , foundMaster bool
for _ , service := range config . EgressSelections {
canonicalName := strings . ToLower ( service . Name )
if ! validEgressSelectorNames . Has ( canonicalName ) {
allErrs = append ( allErrs , field . NotSupported ( field . NewPath ( "egressSelection" , "name" ) , canonicalName , validEgressSelectorNames . List ( ) ) )
continue
}
if canonicalName == "master" {
foundMaster = true
}
if canonicalName == "controlplane" {
foundControlPlane = true
}
}
// error if both master and controlplane egress selectors are set
if foundMaster && foundControlPlane {
allErrs = append ( allErrs , field . Forbidden ( field . NewPath ( "egressSelection" , "name" ) , "both egressSelection names 'master' and 'controlplane' are specified, only one is allowed" ) )
}
2020-03-26 21:07:15 +00:00
return allErrs
}
2019-09-27 21:51:53 +00:00
2020-03-26 21:07:15 +00:00
func validateHTTPConnectTransport ( transport * apiserver . Transport , fldPath * field . Path ) field . ErrorList {
allErrs := field . ErrorList { }
if transport == nil {
allErrs = append ( allErrs , field . Required (
fldPath . Child ( "transport" ) ,
"transport must be set for HTTPConnect" ) )
return allErrs
}
if transport . TCP != nil && transport . UDS != nil {
allErrs = append ( allErrs , field . Invalid (
fldPath . Child ( "tcp" ) ,
transport . TCP ,
"TCP and UDS cannot both be set" ) )
} else if transport . TCP == nil && transport . UDS == nil {
allErrs = append ( allErrs , field . Required (
fldPath . Child ( "tcp" ) ,
"One of TCP or UDS must be set" ) )
} else if transport . TCP != nil {
allErrs = append ( allErrs , validateTCPConnection ( transport . TCP , fldPath ) ... )
} else if transport . UDS != nil {
allErrs = append ( allErrs , validateUDSConnection ( transport . UDS , fldPath ) ... )
}
return allErrs
}
func validateGRPCTransport ( transport * apiserver . Transport , fldPath * field . Path ) field . ErrorList {
allErrs := field . ErrorList { }
if transport == nil {
allErrs = append ( allErrs , field . Required (
fldPath . Child ( "transport" ) ,
"transport must be set for GRPC" ) )
return allErrs
}
if transport . UDS != nil {
allErrs = append ( allErrs , validateUDSConnection ( transport . UDS , fldPath ) ... )
} else {
allErrs = append ( allErrs , field . Required (
fldPath . Child ( "uds" ) ,
"UDS must be set with GRPC" ) )
}
2019-09-27 21:51:53 +00:00
return allErrs
}
func validateDirectConnection ( connection apiserver . Connection , fldPath * field . Path ) field . ErrorList {
2020-03-26 21:07:15 +00:00
if connection . Transport != nil {
2019-09-27 21:51:53 +00:00
return field . ErrorList { field . Invalid (
2020-03-26 21:07:15 +00:00
fldPath . Child ( "transport" ) ,
2019-09-27 21:51:53 +00:00
"direct" ,
2020-03-26 21:07:15 +00:00
"Transport config should be absent for direct connect" ) ,
2019-09-27 21:51:53 +00:00
}
}
2020-03-26 21:07:15 +00:00
2019-09-27 21:51:53 +00:00
return nil
}
2020-03-26 21:07:15 +00:00
func validateUDSConnection ( udsConfig * apiserver . UDSTransport , fldPath * field . Path ) field . ErrorList {
2019-09-27 21:51:53 +00:00
allErrs := field . ErrorList { }
2020-03-26 21:07:15 +00:00
if udsConfig . UDSName == "" {
2019-09-27 21:51:53 +00:00
allErrs = append ( allErrs , field . Invalid (
2020-03-26 21:07:15 +00:00
fldPath . Child ( "udsName" ) ,
2019-09-27 21:51:53 +00:00
"nil" ,
2020-03-26 21:07:15 +00:00
"UDSName should be present for UDS connections" ) )
}
return allErrs
}
func validateTCPConnection ( tcpConfig * apiserver . TCPTransport , fldPath * field . Path ) field . ErrorList {
allErrs := field . ErrorList { }
if strings . HasPrefix ( tcpConfig . URL , "http://" ) {
if tcpConfig . TLSConfig != nil {
2019-09-27 21:51:53 +00:00
allErrs = append ( allErrs , field . Invalid (
2020-03-26 21:07:15 +00:00
fldPath . Child ( "tlsConfig" ) ,
2019-09-27 21:51:53 +00:00
"nil" ,
2020-03-26 21:07:15 +00:00
"TLSConfig config should not be present when using HTTP" ) )
2019-09-27 21:51:53 +00:00
}
2020-03-26 21:07:15 +00:00
} else if strings . HasPrefix ( tcpConfig . URL , "https://" ) {
return validateTLSConfig ( tcpConfig . TLSConfig , fldPath )
2019-09-27 21:51:53 +00:00
} else {
allErrs = append ( allErrs , field . Invalid (
2020-03-26 21:07:15 +00:00
fldPath . Child ( "url" ) ,
tcpConfig . URL ,
2019-09-27 21:51:53 +00:00
"supported connection protocols are http:// and https://" ) )
}
return allErrs
}
2020-03-26 21:07:15 +00:00
func validateTLSConfig ( tlsConfig * apiserver . TLSConfig , fldPath * field . Path ) field . ErrorList {
allErrs := field . ErrorList { }
if tlsConfig == nil {
allErrs = append ( allErrs , field . Required (
fldPath . Child ( "tlsConfig" ) ,
"TLSConfig must be present when using HTTPS" ) )
return allErrs
}
if tlsConfig . CABundle != "" {
2020-12-01 01:06:26 +00:00
if exists , err := path . Exists ( path . CheckFollowSymlink , tlsConfig . CABundle ) ; ! exists || err != nil {
2020-03-26 21:07:15 +00:00
allErrs = append ( allErrs , field . Invalid (
fldPath . Child ( "tlsConfig" , "caBundle" ) ,
tlsConfig . CABundle ,
"TLS config ca bundle does not exist" ) )
}
}
if tlsConfig . ClientCert == "" {
allErrs = append ( allErrs , field . Invalid (
fldPath . Child ( "tlsConfig" , "clientCert" ) ,
"nil" ,
"Using TLS requires clientCert" ) )
2020-12-01 01:06:26 +00:00
} else if exists , err := path . Exists ( path . CheckFollowSymlink , tlsConfig . ClientCert ) ; ! exists || err != nil {
2020-03-26 21:07:15 +00:00
allErrs = append ( allErrs , field . Invalid (
fldPath . Child ( "tlsConfig" , "clientCert" ) ,
tlsConfig . ClientCert ,
"TLS client cert does not exist" ) )
}
if tlsConfig . ClientKey == "" {
allErrs = append ( allErrs , field . Invalid (
fldPath . Child ( "tlsConfig" , "clientKey" ) ,
"nil" ,
"Using TLS requires requires clientKey" ) )
2020-12-01 01:06:26 +00:00
} else if exists , err := path . Exists ( path . CheckFollowSymlink , tlsConfig . ClientKey ) ; ! exists || err != nil {
2020-03-26 21:07:15 +00:00
allErrs = append ( allErrs , field . Invalid (
fldPath . Child ( "tlsConfig" , "clientKey" ) ,
tlsConfig . ClientKey ,
"TLS client key does not exist" ) )
}
return allErrs
}