1/*
2Copyright 2019 The Kubernetes Authors.
3
4Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8    http://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15*/
16
17package egressselector
18
19import (
20	"fmt"
21	"io/ioutil"
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
34var cfgScheme = runtime.NewScheme()
35
36// validEgressSelectorNames contains the set of valid egress selctor names.
37// 'master' is deprecated in favor of 'controlplane' and will be removed in v1.22.
38var validEgressSelectorNames = sets.NewString("master", "controlplane", "cluster", "etcd")
39
40func init() {
41	install.Install(cfgScheme)
42}
43
44// ReadEgressSelectorConfiguration reads the egress selector configuration at the specified path.
45// It returns the loaded egress selector configuration if the input file aligns with the required syntax.
46// If it does not align with the provided syntax, it returns a default configuration which should function as a no-op.
47// It does this by returning a nil configuration, which preserves backward compatibility.
48// This works because prior to this there was no egress selector configuration.
49// It returns an error if the file did not exist.
50func ReadEgressSelectorConfiguration(configFilePath string) (*apiserver.EgressSelectorConfiguration, error) {
51	if configFilePath == "" {
52		return nil, nil
53	}
54	// a file was provided, so we just read it.
55	data, err := ioutil.ReadFile(configFilePath)
56	if err != nil {
57		return nil, fmt.Errorf("unable to read egress selector configuration from %q [%v]", configFilePath, err)
58	}
59	var decodedConfig v1beta1.EgressSelectorConfiguration
60	err = yaml.Unmarshal(data, &decodedConfig)
61	if err != nil {
62		// we got an error where the decode wasn't related to a missing type
63		return nil, err
64	}
65	if decodedConfig.Kind != "EgressSelectorConfiguration" {
66		return nil, fmt.Errorf("invalid service configuration object %q", decodedConfig.Kind)
67	}
68	internalConfig := &apiserver.EgressSelectorConfiguration{}
69	if err := cfgScheme.Convert(&decodedConfig, internalConfig, nil); err != nil {
70		// we got an error where the decode wasn't related to a missing type
71		return nil, err
72	}
73	return internalConfig, nil
74}
75
76// ValidateEgressSelectorConfiguration checks the apiserver.EgressSelectorConfiguration for
77// common configuration errors. It will return error for problems such as configuring mtls/cert
78// settings for protocol which do not support security. It will also try to catch errors such as
79// incorrect file paths. It will return nil if it does not find anything wrong.
80func ValidateEgressSelectorConfiguration(config *apiserver.EgressSelectorConfiguration) field.ErrorList {
81	allErrs := field.ErrorList{}
82	if config == nil {
83		return allErrs // Treating a nil configuration as valid
84	}
85	for _, service := range config.EgressSelections {
86		fldPath := field.NewPath("service", "connection")
87		switch service.Connection.ProxyProtocol {
88		case apiserver.ProtocolDirect:
89			allErrs = append(allErrs, validateDirectConnection(service.Connection, fldPath)...)
90		case apiserver.ProtocolHTTPConnect:
91			allErrs = append(allErrs, validateHTTPConnectTransport(service.Connection.Transport, fldPath)...)
92		case apiserver.ProtocolGRPC:
93			allErrs = append(allErrs, validateGRPCTransport(service.Connection.Transport, fldPath)...)
94		default:
95			allErrs = append(allErrs, field.NotSupported(
96				fldPath.Child("protocol"),
97				service.Connection.ProxyProtocol,
98				[]string{
99					string(apiserver.ProtocolDirect),
100					string(apiserver.ProtocolHTTPConnect),
101					string(apiserver.ProtocolGRPC),
102				}))
103		}
104	}
105
106	var foundControlPlane, foundMaster bool
107	for _, service := range config.EgressSelections {
108		canonicalName := strings.ToLower(service.Name)
109
110		if !validEgressSelectorNames.Has(canonicalName) {
111			allErrs = append(allErrs, field.NotSupported(field.NewPath("egressSelection", "name"), canonicalName, validEgressSelectorNames.List()))
112			continue
113		}
114
115		if canonicalName == "master" {
116			foundMaster = true
117		}
118
119		if canonicalName == "controlplane" {
120			foundControlPlane = true
121		}
122	}
123
124	// error if both master and controlplane egress selectors are set
125	if foundMaster && foundControlPlane {
126		allErrs = append(allErrs, field.Forbidden(field.NewPath("egressSelection", "name"), "both egressSelection names 'master' and 'controlplane' are specified, only one is allowed"))
127	}
128
129	return allErrs
130}
131
132func validateHTTPConnectTransport(transport *apiserver.Transport, fldPath *field.Path) field.ErrorList {
133	allErrs := field.ErrorList{}
134	if transport == nil {
135		allErrs = append(allErrs, field.Required(
136			fldPath.Child("transport"),
137			"transport must be set for HTTPConnect"))
138		return allErrs
139	}
140
141	if transport.TCP != nil && transport.UDS != nil {
142		allErrs = append(allErrs, field.Invalid(
143			fldPath.Child("tcp"),
144			transport.TCP,
145			"TCP and UDS cannot both be set"))
146	} else if transport.TCP == nil && transport.UDS == nil {
147		allErrs = append(allErrs, field.Required(
148			fldPath.Child("tcp"),
149			"One of TCP or UDS must be set"))
150	} else if transport.TCP != nil {
151		allErrs = append(allErrs, validateTCPConnection(transport.TCP, fldPath)...)
152	} else if transport.UDS != nil {
153		allErrs = append(allErrs, validateUDSConnection(transport.UDS, fldPath)...)
154	}
155	return allErrs
156}
157
158func validateGRPCTransport(transport *apiserver.Transport, fldPath *field.Path) field.ErrorList {
159	allErrs := field.ErrorList{}
160	if transport == nil {
161		allErrs = append(allErrs, field.Required(
162			fldPath.Child("transport"),
163			"transport must be set for GRPC"))
164		return allErrs
165	}
166
167	if transport.UDS != nil {
168		allErrs = append(allErrs, validateUDSConnection(transport.UDS, fldPath)...)
169	} else {
170		allErrs = append(allErrs, field.Required(
171			fldPath.Child("uds"),
172			"UDS must be set with GRPC"))
173	}
174	return allErrs
175}
176
177func validateDirectConnection(connection apiserver.Connection, fldPath *field.Path) field.ErrorList {
178	if connection.Transport != nil {
179		return field.ErrorList{field.Invalid(
180			fldPath.Child("transport"),
181			"direct",
182			"Transport config should be absent for direct connect"),
183		}
184	}
185
186	return nil
187}
188
189func validateUDSConnection(udsConfig *apiserver.UDSTransport, fldPath *field.Path) field.ErrorList {
190	allErrs := field.ErrorList{}
191	if udsConfig.UDSName == "" {
192		allErrs = append(allErrs, field.Invalid(
193			fldPath.Child("udsName"),
194			"nil",
195			"UDSName should be present for UDS connections"))
196	}
197	return allErrs
198}
199
200func validateTCPConnection(tcpConfig *apiserver.TCPTransport, fldPath *field.Path) field.ErrorList {
201	allErrs := field.ErrorList{}
202
203	if strings.HasPrefix(tcpConfig.URL, "http://") {
204		if tcpConfig.TLSConfig != nil {
205			allErrs = append(allErrs, field.Invalid(
206				fldPath.Child("tlsConfig"),
207				"nil",
208				"TLSConfig config should not be present when using HTTP"))
209		}
210	} else if strings.HasPrefix(tcpConfig.URL, "https://") {
211		return validateTLSConfig(tcpConfig.TLSConfig, fldPath)
212	} else {
213		allErrs = append(allErrs, field.Invalid(
214			fldPath.Child("url"),
215			tcpConfig.URL,
216			"supported connection protocols are http:// and https://"))
217	}
218	return allErrs
219}
220
221func validateTLSConfig(tlsConfig *apiserver.TLSConfig, fldPath *field.Path) field.ErrorList {
222	allErrs := field.ErrorList{}
223
224	if tlsConfig == nil {
225		allErrs = append(allErrs, field.Required(
226			fldPath.Child("tlsConfig"),
227			"TLSConfig must be present when using HTTPS"))
228		return allErrs
229	}
230	if tlsConfig.CABundle != "" {
231		if exists, err := path.Exists(path.CheckFollowSymlink, tlsConfig.CABundle); !exists || err != nil {
232			allErrs = append(allErrs, field.Invalid(
233				fldPath.Child("tlsConfig", "caBundle"),
234				tlsConfig.CABundle,
235				"TLS config ca bundle does not exist"))
236		}
237	}
238	if tlsConfig.ClientCert == "" {
239		allErrs = append(allErrs, field.Invalid(
240			fldPath.Child("tlsConfig", "clientCert"),
241			"nil",
242			"Using TLS requires clientCert"))
243	} else if exists, err := path.Exists(path.CheckFollowSymlink, tlsConfig.ClientCert); !exists || err != nil {
244		allErrs = append(allErrs, field.Invalid(
245			fldPath.Child("tlsConfig", "clientCert"),
246			tlsConfig.ClientCert,
247			"TLS client cert does not exist"))
248	}
249	if tlsConfig.ClientKey == "" {
250		allErrs = append(allErrs, field.Invalid(
251			fldPath.Child("tlsConfig", "clientKey"),
252			"nil",
253			"Using TLS requires requires clientKey"))
254	} else if exists, err := path.Exists(path.CheckFollowSymlink, tlsConfig.ClientKey); !exists || err != nil {
255		allErrs = append(allErrs, field.Invalid(
256			fldPath.Child("tlsConfig", "clientKey"),
257			tlsConfig.ClientKey,
258			"TLS client key does not exist"))
259	}
260	return allErrs
261}
262