Skip to content

Commit 2aed50c

Browse files
committed
OSD-24275: Read Install Config during WH creation. Allow unit test params, and shared test flag.
1 parent 9297e5a commit 2aed50c

File tree

6 files changed

+114
-148
lines changed

6 files changed

+114
-148
lines changed

cmd/main.go

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,15 @@ import (
2121
"github.com/openshift/managed-cluster-validating-webhooks/pkg/k8sutil"
2222
"github.com/openshift/managed-cluster-validating-webhooks/pkg/localmetrics"
2323
"github.com/openshift/managed-cluster-validating-webhooks/pkg/webhooks"
24+
"github.com/openshift/managed-cluster-validating-webhooks/pkg/webhooks/utils"
2425
)
2526

2627
var log = logf.Log.WithName("handler")
2728

2829
var (
2930
listenAddress = flag.String("listen", "0.0.0.0", "listen address")
3031
listenPort = flag.String("port", "5000", "port to listen on")
31-
testHooks = flag.Bool("testhooks", false, "Test webhook URI uniqueness and quit?")
32+
metricsAddr string
3233

3334
useTLS = flag.Bool("tls", false, "Use TLS? Must specify -tlskey, -tlscert, -cacert")
3435
tlsKey = flag.String("tlskey", "", "TLS Key for TLS")
@@ -39,15 +40,18 @@ var (
3940
metricsPort = "8080"
4041
)
4142

42-
func main() {
43-
var metricsAddr string
43+
func init() {
44+
// Allow export webhook var to share flag value...
45+
flag.BoolVar(&utils.TestHooks, "testhooks", false, "Test webhook URI uniqueness and quit?")
4446
flag.StringVar(&metricsAddr, "metrics-bind-address", ":"+metricsPort, "The address the metric endpoint binds to.")
4547
flag.Parse()
46-
klog.SetOutput(os.Stdout)
48+
}
4749

50+
func main() {
51+
klog.SetOutput(os.Stdout)
4852
logf.SetLogger(klogr.New())
4953

50-
if !*testHooks {
54+
if !utils.TestHooks {
5155
log.Info("HTTP server running at", "listen", net.JoinHostPort(*listenAddress, *listenPort))
5256
}
5357
dispatcher := dispatcher.NewDispatcher(webhooks.Webhooks)
@@ -58,12 +62,12 @@ func main() {
5862
panic(fmt.Errorf("Duplicate webhook trying to listen on %s", realHook.GetURI()))
5963
}
6064
seen[name] = true
61-
if !*testHooks {
65+
if !utils.TestHooks {
6266
log.Info("Listening", "webhookName", name, "URI", realHook.GetURI())
6367
}
6468
http.HandleFunc(realHook.GetURI(), dispatcher.HandleRequest)
6569
}
66-
if *testHooks {
70+
if utils.TestHooks {
6771
os.Exit(0)
6872
}
6973

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ require (
5151
github.com/onsi/gomega v1.29.0 // indirect
5252
github.com/openshift/custom-resource-status v1.1.3-0.20220503160415-f2fdb4999d87 // indirect
5353
github.com/openshift/elasticsearch-operator v0.0.0-20220613183908-e1648e67c298 // indirect
54+
github.com/openshift/installer v0.16.1 // indirect
5455
github.com/pkg/errors v0.9.1 // indirect
5556
github.com/prometheus/client_model v0.4.0 // indirect
5657
github.com/prometheus/common v0.44.0 // indirect

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,8 @@ github.com/openshift/elasticsearch-operator v0.0.0-20220613183908-e1648e67c298 h
181181
github.com/openshift/elasticsearch-operator v0.0.0-20220613183908-e1648e67c298/go.mod h1:6dxhWPY3Wr/0b0eGrFpV7gcyeS+ne48Mo9OQ9dxrLNI=
182182
github.com/openshift/hive/apis v0.0.0-20230327212335-7fd70848a6d5 h1:adHXZ1WFqCvXpargpTa6divneeUuvV2xr/D6NWgbqS8=
183183
github.com/openshift/hive/apis v0.0.0-20230327212335-7fd70848a6d5/go.mod h1:VIxA5HhvBmsqVn7aUVQYs004B9K4U5A+HrFwvRq2nK8=
184+
github.com/openshift/installer v0.16.1 h1:PmjALN9x1NVNVi3SCqfz0ZwVCgOkQLQWo2nHYXREq/A=
185+
github.com/openshift/installer v0.16.1/go.mod h1:VWGgpJgF8DGCKQjbccnigglhZnHtRLCZ6cxqkXN4Ck0=
184186
github.com/openshift/operator-custom-metrics v0.5.1 h1:1pk4YMUV+cmqfV0f2fyxY62cl7Gc76kwudJT+EdcfYM=
185187
github.com/openshift/operator-custom-metrics v0.5.1/go.mod h1:0dYDHi/ubKRWzsC9MmW6bRMdBgo1QSOuAh3GupTe0Sw=
186188
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=

pkg/webhooks/ingresscontroller/ingresscontroller.go

Lines changed: 73 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"strings"
1212

1313
operatorv1 "github.com/openshift/api/operator/v1"
14+
installer "github.com/openshift/installer/pkg/types"
1415
"github.com/openshift/managed-cluster-validating-webhooks/pkg/k8sutil"
1516
"github.com/openshift/managed-cluster-validating-webhooks/pkg/webhooks/utils"
1617
"github.com/pkg/errors"
@@ -54,24 +55,11 @@ var (
5455
)
5556

5657
type IngressControllerWebhook struct {
57-
s runtime.Scheme
58-
kubeClient client.Client
58+
s runtime.Scheme
5959
// Allow caching install config and machineCIDR values...
60-
machineCIDRIP net.IP
6160
machineCIDRNet *net.IPNet
6261
}
6362

64-
// installConfig struct to load the min values needed from the install-config data.
65-
// Alternative would be to vendor all of github.com/openshift/installer/pkg/types to import the proper struct.
66-
// Required values: machineCidr info, anything else we gather from this? hcp vs classic, privatelink info, etc?
67-
type installConfig struct {
68-
metav1.TypeMeta `json:",inline"`
69-
metav1.ObjectMeta `json:"metadata"`
70-
Networking struct {
71-
MachineCIDR string `json:"machineCIDR"`
72-
} `json:"networking"`
73-
}
74-
7563
// ObjectSelector implements Webhook interface
7664
func (wh *IngressControllerWebhook) ObjectSelector() *metav1.LabelSelector { return nil }
7765

@@ -178,12 +166,10 @@ func (wh *IngressControllerWebhook) authorized(request admissionctl.Request) adm
178166

179167
/* TODO:
180168
* - HypershiftEnabled is currently set to false/disabled.
181-
* If HCP is to be enabled for allowed source ranges, should this part of a 2nd ingress validator to
182-
* allow separation of validations between cluster install types? ...or if there's a reliable run time method available
183-
* to this validator to determine classic vs hcp they can remain in the single webhook(?)
184169
* - Classic vs HCP could likely share some of the network funcions, but will need slightly
185-
* different logic as well as permissions fetching the network config info from different
186-
* source (configmap) locations and config formats(?).
170+
* different logic for the different minimum CIDR sets required, as well as different
171+
* permissions fetching the network config info from different
172+
* source (configmap) locations and config formats(?).
187173
*/
188174
// Only check for machine cidr in allowed ranges if creating or updating resource...
189175
reqOp := request.AdmissionRequest.Operation
@@ -204,58 +190,46 @@ func (wh *IngressControllerWebhook) authorized(request admissionctl.Request) adm
204190
return ret
205191
}
206192

207-
func (wh *IngressControllerWebhook) getMachineCIDR() (net.IP, *net.IPNet, error) {
208-
if wh.machineCIDRIP == nil || wh.machineCIDRNet == nil {
209-
instConf, err := wh.getClusterConfig()
210-
if err != nil {
211-
log.Error(err, "Failed to fetch machineCIDR", "namespace", installConfigNamespace, "configmap", installConfigMap)
212-
return nil, nil, err
213-
}
193+
func (wh *IngressControllerWebhook) getMachineCIDR(instConf *installer.InstallConfig) (*net.IPNet, error) {
194+
if wh.machineCIDRNet == nil {
214195
if instConf == nil {
215196
err := fmt.Errorf("can not fetch machineCIDR from empty '%s' install config", installConfigMap)
216197
log.Error(err, "getMachineCIDR failed to find CIDR value")
217-
return nil, nil, err
218-
}
219-
if len(instConf.Networking.MachineCIDR) <= 0 {
220-
err := fmt.Errorf("empty machineCIDR string value parsed from '%s' install config", installConfigMap)
221-
log.Error(err, "getMachineCIDR found empty CIDR value")
222-
return nil, nil, err
198+
return nil, err
223199
}
224-
machIP, machNet, err := net.ParseCIDR(string(instConf.Networking.MachineCIDR))
225-
if err != nil {
226-
log.Error(err, "err parsing machineCIDR into network cidr", "machineCIDR", string(instConf.Networking.MachineCIDR))
227-
return nil, nil, err
200+
if instConf.Networking.MachineCIDR == nil {
201+
err := fmt.Errorf("nil installConfig.machineCIDR value found")
202+
log.Error(err, "nil installConfig.machineCIDR value found")
203+
return nil, err
228204
}
229-
if machIP == nil || machNet == nil {
230-
err := fmt.Errorf("failed to parse machineCIDR string:'%s' into network structures", string(instConf.Networking.MachineCIDR))
231-
log.Error(err, "failed to parse install-config machineCIDR")
232-
return nil, nil, err
205+
if len(instConf.Networking.MachineCIDR.Network()) <= 0 || len(instConf.Networking.MachineCIDR.IPNet.Network()) <= 0 {
206+
err := fmt.Errorf("empty machineCIDR network() value parsed from '%s' install config", installConfigMap)
207+
log.Error(err, "getMachineCIDR found empty network value")
208+
return nil, err
233209
}
234210
// Successfully fetched, parsed, and converted the machineCIDR string into net structures...
235-
wh.machineCIDRIP = machIP
236-
wh.machineCIDRNet = machNet
211+
wh.machineCIDRNet = &instConf.Networking.MachineCIDR.IPNet
237212
}
238-
return wh.machineCIDRIP, wh.machineCIDRNet, nil
213+
return wh.machineCIDRNet, nil
239214
}
240215

241216
/* Fetch the install-config from the kube-system config map's data.
242217
* this requires proper role, rolebinding for this service account's get() request
243218
* to succeed. (see toplevel selectorsyncset). This config should not change during runtime so
244-
* this operation should cache this if possible.
219+
* this operation should cache the value(s) if possible.
245220
* TODO: Should it retry fetching the config if there are any failures/errors encountered while
246221
* parsing out the the desired values?
247222
*/
248-
func (wh *IngressControllerWebhook) getClusterConfig() (*installConfig, error) {
223+
func (wh *IngressControllerWebhook) getClusterConfig() (*installer.InstallConfig, error) {
249224
var err error
250-
if wh.kubeClient == nil {
251-
wh.kubeClient, err = k8sutil.KubeClient(&wh.s)
252-
if err != nil {
253-
log.Error(err, "Fail creating KubeClient for IngressControllerWebhook")
254-
return nil, err
255-
}
225+
226+
kubeClient, err := k8sutil.KubeClient(&wh.s)
227+
if err != nil {
228+
log.Error(err, "Fail creating KubeClient for IngressControllerWebhook")
229+
return nil, err
256230
}
257231
clusterConfig := &corev1.ConfigMap{}
258-
err = wh.kubeClient.Get(context.Background(), client.ObjectKey{Name: installConfigMap, Namespace: installConfigNamespace}, clusterConfig)
232+
err = kubeClient.Get(context.Background(), client.ObjectKey{Name: installConfigMap, Namespace: installConfigNamespace}, clusterConfig)
259233
if err != nil {
260234
log.Error(err, "Failed to fetch configmap: 'cluster-config-v1' for cluster config")
261235
return nil, err
@@ -264,8 +238,7 @@ func (wh *IngressControllerWebhook) getClusterConfig() (*installConfig, error) {
264238
if !ok {
265239
return nil, fmt.Errorf("did not find key %s in configmap %s/%s", installConfigKeyName, installConfigNamespace, installConfigMap)
266240
}
267-
instConf := &installConfig{}
268-
241+
instConf := &installer.InstallConfig{}
269242
decoder := yaml.NewYAMLOrJSONDecoder(bytes.NewReader([]byte(data)), 4096)
270243
if err := decoder.Decode(instConf); err != nil {
271244
return nil, errors.Wrap(err, "failed to decode install config")
@@ -280,16 +253,12 @@ func (wh *IngressControllerWebhook) checkAllowsMachineCIDR(ipRanges []operatorv1
280253
// where the IGC will remaining in progressing state indefinitely.
281254
// For now return Allowed, but with a warning?
282255
if ipRanges == nil || len(ipRanges) <= 0 {
283-
return admissionctl.Allowed("Allowing empty 'AllowedSourceRanges'")
284-
}
285-
machIP, machNet, err := wh.getMachineCIDR()
286-
if err != nil {
287-
// This represents a fault in either the webhook itself, webhook permissions, or install config.
288-
// Might be nice to have an env var etc we can set to allow proceeding w/o the immediate need to roll new code?
289-
return admissionctl.Errored(http.StatusInternalServerError, err)
256+
return admissionctl.Allowed("Allowing empty 'AllowedSourceRanges'.")
290257
}
291-
machNetSize, machNetBits := machNet.Mask.Size()
292-
log.Info("Checking AllowedSourceRanges", "MachineCIDR", fmt.Sprintf("%s/%d", machIP.String(), machNetSize), "NetBits", machNetBits, "AllowedSourceRanges", ipRanges)
258+
259+
machNetSize, machNetBits := wh.machineCIDRNet.Mask.Size()
260+
machineCIDRIP := wh.machineCIDRNet.IP
261+
log.Info("Checking AllowedSourceRanges", "MachineCIDR", fmt.Sprintf("%s/%d", machineCIDRIP.String(), machNetSize), "NetBits", machNetBits, "AllowedSourceRanges", ipRanges)
293262
for _, OpV1CIDR := range ipRanges {
294263
// Clean up the operatorV1.CIDR value into trimmed CIDR 'a.b.c.d/x' string
295264
ASRstring := strings.TrimSpace(string(OpV1CIDR))
@@ -304,20 +273,19 @@ func (wh *IngressControllerWebhook) checkAllowsMachineCIDR(ipRanges []operatorv1
304273
return admissionctl.Errored(http.StatusBadRequest, fmt.Errorf("failed to parse AllowedSourceRanges value: '%s'. Err: %s", string(ASRstring), err))
305274
}
306275
// First check if this AlloweSourceRange entry network contains the machine cidr ip...
307-
if !ASRNet.Contains(machIP) {
308-
log.Info(fmt.Sprintf("AllowedSourceRange:'%s' does not contain machine CIDR:'%s/%d'", ASRstring, machIP.String(), machNetSize))
276+
if !ASRNet.Contains(machineCIDRIP) {
277+
//log.Info(fmt.Sprintf("AllowedSourceRange:'%s' does not contain machine CIDR:'%s/%d'", ASRstring, machineCIDRIP.String(), machNetSize))
309278
continue
310279
}
311280
// Check if this AlloweSourceRange entry mask includes the network.
312281
ASRNetSize, ASRNetBits := ASRNet.Mask.Size()
313282
if machNetBits == ASRNetBits && ASRNetSize <= machNetSize {
314-
log.Info(fmt.Sprintf("Found machineCidr:'%s/%d' within AllowedSourceRange:'%s'", machIP.String(), machNetSize, ASRstring))
315-
return admissionctl.Allowed(fmt.Sprintf("Found machineCidr:'%s/%d' within AllowedSourceRange:'%s'", machIP.String(), machNetSize, ASRstring))
316-
//return admissionctl.Allowed("IngressController operation is allowed. Minimum AllowedSourceRanges are met.")
283+
log.Info(fmt.Sprintf("Found machineCidr:'%s/%d' within AllowedSourceRange:'%s'", machineCIDRIP.String(), machNetSize, ASRstring))
284+
return admissionctl.Allowed(fmt.Sprintf("Found machineCidr:'%s/%d' within AllowedSourceRange:'%s'", machineCIDRIP.String(), machNetSize, ASRstring))
317285
}
318286
}
319-
log.Info(fmt.Sprintf("machineCidr:'%s/%d' not found within networks provided by AllowedSourceRanges:'%v'", machIP.String(), machNetSize, ipRanges))
320-
return admissionctl.Denied(fmt.Sprintf("At least one AllowedSourceRange must allow machine cidr:'%s/%d'", machIP.String(), machNetSize))
287+
log.Info(fmt.Sprintf("machineCidr:'%s/%d' not found within networks provided by AllowedSourceRanges:'%v'", machineCIDRIP.String(), machNetSize, ipRanges))
288+
return admissionctl.Denied(fmt.Sprintf("At least one AllowedSourceRange must allow machine cidr:'%s/%d'", machineCIDRIP.String(), machNetSize))
321289
}
322290

323291
// isAllowedUser checks if the user is allowed to perform the action
@@ -362,19 +330,48 @@ func (s *IngressControllerWebhook) ClassicEnabled() bool { return true }
362330
func (s *IngressControllerWebhook) HypershiftEnabled() bool { return false }
363331

364332
// NewWebhook creates a new webhook
365-
func NewWebhook() *IngressControllerWebhook {
333+
// Allow variadic args so unit tests can provide optional test values...
334+
func NewWebhook(params ...interface{}) *IngressControllerWebhook {
366335
scheme := runtime.NewScheme()
367336
err := corev1.AddToScheme(scheme)
368337
if err != nil {
369338
log.Error(err, "Fail adding corev1 scheme to IngressControllerWebhook")
370339
os.Exit(1)
371340
}
372341
wh := &IngressControllerWebhook{
373-
s: *scheme,
374-
kubeClient: nil,
342+
s: *scheme,
343+
}
344+
// utils.TestHooks maps to cli flag 'testhooks' and is used during 'make test' to "test webhook URI uniqueness".
345+
// 'make test' does not require this hook to build runtime clients/config at this time...
346+
if utils.TestHooks {
347+
return wh
348+
}
349+
350+
if len(params) > 0 {
351+
param := params[0]
352+
// As of know only *IPNet values can be provided by unit tests to set machineCIDR, normal webhook factory
353+
// calls NewWebhook() without arguments...
354+
if cidr, ok := param.(*net.IPNet); ok {
355+
log.Info(fmt.Sprintf("Got test net.IPNet param network() for machineCIDR:'%s'\n", cidr.Network()))
356+
wh.machineCIDRNet = cidr
357+
} else {
358+
log.Error(fmt.Errorf("invalid test param provided, expected *net.IPNet machineCIDR value"), "invalid test param provided, expected *net.IPNet machineCIDR value")
359+
os.Exit(1)
360+
}
361+
} else {
362+
// This is not a test run.
363+
// Try to populate machine cidr at init. Exit with error if this fails...
364+
instConf, err := wh.getClusterConfig()
365+
if err != nil {
366+
log.Error(err, "Failed to fetch configmap for machineCIDR", "namespace", installConfigNamespace, "configmap", installConfigMap)
367+
os.Exit(1)
368+
}
369+
370+
_, err = wh.getMachineCIDR(instConf)
371+
if err != nil || wh.machineCIDRNet == nil {
372+
log.Error(err, "Failed to fetch cluster machineCIDR.")
373+
os.Exit(1)
374+
}
375375
}
376-
// Try to populate machine cidr at init. If this fails it will try again on the
377-
// first update/create request involving the default ingress controller.
378-
wh.getMachineCIDR()
379376
return wh
380377
}

0 commit comments

Comments
 (0)