-
Notifications
You must be signed in to change notification settings - Fork 40k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
add support to create webhook configuration when webhooks is enabled #120905
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -22,10 +22,15 @@ import ( | |
"fmt" | ||
"math/rand" | ||
"os" | ||
"strings" | ||
"time" | ||
|
||
"github.com/spf13/cobra" | ||
|
||
v1 "k8s.io/api/admissionregistration/v1" | ||
apierrors "k8s.io/apimachinery/pkg/api/errors" | ||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
utilerrors "k8s.io/apimachinery/pkg/util/errors" | ||
utilruntime "k8s.io/apimachinery/pkg/util/runtime" | ||
"k8s.io/apimachinery/pkg/util/sets" | ||
"k8s.io/apimachinery/pkg/util/uuid" | ||
|
@@ -42,6 +47,7 @@ import ( | |
"k8s.io/client-go/tools/leaderelection/resourcelock" | ||
cloudprovider "k8s.io/cloud-provider" | ||
cloudcontrollerconfig "k8s.io/cloud-provider/app/config" | ||
cloudproviderconfig "k8s.io/cloud-provider/config" | ||
"k8s.io/cloud-provider/names" | ||
"k8s.io/cloud-provider/options" | ||
cliflag "k8s.io/component-base/cli/flag" | ||
|
@@ -96,7 +102,7 @@ the cloud specific control loops shipped with Kubernetes.`, | |
return err | ||
} | ||
|
||
c, err := s.Config(ControllerNames(controllerInitFuncConstructors), ControllersDisabledByDefault.List(), controllerAliases, AllWebhooks, DisabledByDefaultWebhooks) | ||
c, err := s.Config(ControllerNames(controllerInitFuncConstructors), ControllersDisabledByDefault.List(), controllerAliases, AllWebhooks, AllWebhooks, DisabledByDefaultWebhooks) | ||
if err != nil { | ||
fmt.Fprintf(os.Stderr, "%v\n", err) | ||
return err | ||
|
@@ -162,6 +168,97 @@ the cloud specific control loops shipped with Kubernetes.`, | |
return cmd | ||
} | ||
|
||
func createOrUpdateWebhookConfiguration(ctx context.Context, webhooks map[string]WebhookHandler, webhooksConfig cloudproviderconfig.WebhookConfiguration, clientBuilder clientbuilder.SimpleControllerClientBuilder) error { | ||
errors := []error{} | ||
if webhooksConfig.ValidatingWebhookConfiguration != nil { | ||
for i, webhook := range webhooksConfig.ValidatingWebhookConfiguration.Webhooks { | ||
webhookconfig, ok := webhooks[webhook.Name] | ||
if !ok { | ||
errors = append(errors, fmt.Errorf("webhook configuration not found for webhook %s", webhook.Name)) | ||
continue | ||
} | ||
url := fmt.Sprintf("https://%s:%d/%s", webhooksConfig.WebhookAddress, webhooksConfig.WebhookPort, strings.TrimPrefix(webhookconfig.Path, "/")) | ||
|
||
webhooksConfig.ValidatingWebhookConfiguration.Webhooks[i].ClientConfig = v1.WebhookClientConfig{ | ||
URL: &url, | ||
CABundle: []byte(webhooksConfig.CaBundle), | ||
} | ||
} | ||
} | ||
if webhooksConfig.MutatingWebhookConfiguration != nil { | ||
for i, webhook := range webhooksConfig.MutatingWebhookConfiguration.Webhooks { | ||
webhookconfig, ok := webhooks[webhook.Name] | ||
if !ok { | ||
errors = append(errors, fmt.Errorf("webhook configuration not found for webhook %s", webhook.Name)) | ||
continue | ||
} | ||
url := fmt.Sprintf("https://%s:%d/%s", webhooksConfig.WebhookAddress, webhooksConfig.WebhookPort, strings.TrimPrefix(webhookconfig.Path, "/")) | ||
|
||
webhooksConfig.MutatingWebhookConfiguration.Webhooks[i].ClientConfig = v1.WebhookClientConfig{ | ||
URL: &url, | ||
CABundle: []byte(webhooksConfig.CaBundle), | ||
} | ||
} | ||
} | ||
if len(errors) != 0 { | ||
return utilerrors.NewAggregate(errors) | ||
} | ||
|
||
kubeClient := clientBuilder.ClientOrDie("ccm-webhook") | ||
|
||
if webhooksConfig.ValidatingWebhookConfiguration != nil { | ||
_, err := kubeClient.AdmissionregistrationV1().ValidatingWebhookConfigurations().Create(ctx, webhooksConfig.ValidatingWebhookConfiguration, metav1.CreateOptions{}) | ||
if err == nil { | ||
klog.Infoln("validating webhook configuration successfully created") | ||
} else { | ||
if !apierrors.IsAlreadyExists(err) { | ||
klog.ErrorS(err, "Unable to create validating webhook configuration with API server", "webhookconfiguration", klog.KObj(webhooksConfig.ValidatingWebhookConfiguration)) | ||
return fmt.Errorf("unable to create validating webhook configuration with API server %w", err) | ||
} | ||
|
||
currentConfiguration, err := kubeClient.AdmissionregistrationV1().ValidatingWebhookConfigurations().Get(ctx, webhooksConfig.ValidatingWebhookConfiguration.Name, metav1.GetOptions{}) | ||
if err != nil { | ||
klog.ErrorS(err, "Unable to create validating webhook configuration with API server, error getting existing webhook configuration", "webhookconfiguration", webhooksConfig.ValidatingWebhookConfiguration.Name) | ||
return fmt.Errorf("unable to get validating webhook configuration from API server %w", err) | ||
} | ||
currentConfiguration.Webhooks = webhooksConfig.ValidatingWebhookConfiguration.Webhooks | ||
|
||
_, err = kubeClient.AdmissionregistrationV1().ValidatingWebhookConfigurations().Update(ctx, currentConfiguration, metav1.UpdateOptions{}) | ||
Comment on lines
+219
to
+226
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. how does this avoid dueling registration from other CCM instances? |
||
if err != nil { | ||
klog.ErrorS(err, "Unable to update validating webhook configuration with API server", "webhookconfiguration", klog.KObj(webhooksConfig.ValidatingWebhookConfiguration)) | ||
return fmt.Errorf("unable to update validating webhook configuration with API server %w", err) | ||
} | ||
} | ||
} | ||
|
||
if webhooksConfig.MutatingWebhookConfiguration != nil { | ||
_, err := kubeClient.AdmissionregistrationV1().MutatingWebhookConfigurations().Create(ctx, webhooksConfig.MutatingWebhookConfiguration, metav1.CreateOptions{}) | ||
if err == nil { | ||
klog.Infoln("mutating webhook configuration successfully created") | ||
} else { | ||
if !apierrors.IsAlreadyExists(err) { | ||
klog.ErrorS(err, "Unable to create mutating webhook configuration with API server", "webhookconfiguration", klog.KObj(webhooksConfig.MutatingWebhookConfiguration)) | ||
return fmt.Errorf("unable to create mutating webhook configuration with API server %w", err) | ||
} | ||
|
||
currentConfiguration, err := kubeClient.AdmissionregistrationV1().MutatingWebhookConfigurations().Get(ctx, webhooksConfig.MutatingWebhookConfiguration.Name, metav1.GetOptions{}) | ||
if err != nil { | ||
klog.ErrorS(err, "Unable to create mutating webhook configuration with API server, error fetching existing webhook configuration", "webhookconfiguration", webhooksConfig.MutatingWebhookConfiguration.Name) | ||
return fmt.Errorf("unable to get mutating webhook configuration from API server %w", err) | ||
} | ||
currentConfiguration.Webhooks = webhooksConfig.MutatingWebhookConfiguration.Webhooks | ||
|
||
_, err = kubeClient.AdmissionregistrationV1().MutatingWebhookConfigurations().Update(ctx, currentConfiguration, metav1.UpdateOptions{}) | ||
if err != nil { | ||
klog.ErrorS(err, "Unable to update mutating webhook configuration with API server", "webhookconfiguration", klog.KObj(webhooksConfig.MutatingWebhookConfiguration)) | ||
return fmt.Errorf("unable to update mutating webhook configuration with API server %w", err) | ||
} | ||
} | ||
} | ||
|
||
return nil | ||
} | ||
|
||
// Run runs the ExternalCMServer. This should never exit. | ||
func Run(c *cloudcontrollerconfig.CompletedConfig, cloud cloudprovider.Interface, controllerInitializers map[string]InitFunc, webhooks map[string]WebhookHandler, | ||
stopCh <-chan struct{}) error { | ||
|
@@ -220,6 +317,14 @@ func Run(c *cloudcontrollerconfig.CompletedConfig, cloud cloudprovider.Interface | |
if err != nil { | ||
klog.Fatalf("error building controller context: %v", err) | ||
} | ||
if utilfeature.DefaultFeatureGate.Enabled(cmfeatures.CloudControllerManagerWebhook) { | ||
if len(webhooks) > 0 { | ||
klog.Info("creating/updating webhook configuration: ", webhooks) | ||
if err := createOrUpdateWebhookConfiguration(ctx, webhooks, c.ComponentConfig.Webhook, clientBuilder); err != nil { | ||
klog.Fatalf("error creating/updating webhook configuration: %v", err) | ||
} | ||
} | ||
} | ||
if err := startControllers(ctx, cloud, controllerContext, c, ctx.Done(), controllerInitializers, healthzHandler); err != nil { | ||
klog.Fatalf("error running controllers: %v", err) | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -19,6 +19,7 @@ package v1alpha1 | |
import ( | ||
"time" | ||
|
||
admissionregistrationv1 "k8s.io/api/admissionregistration/v1" | ||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
"k8s.io/apimachinery/pkg/runtime" | ||
nodeconfigv1alpha1 "k8s.io/cloud-provider/controllers/node/config/v1alpha1" | ||
|
@@ -69,3 +70,78 @@ func SetDefaults_KubeCloudSharedConfiguration(obj *KubeCloudSharedConfiguration) | |
obj.RouteReconciliationPeriod = metav1.Duration{Duration: 10 * time.Second} | ||
} | ||
} | ||
|
||
// SetDefaults_ValidatingWebhook sets defaults for webhook validating. This function | ||
// is duplicated from "k8s.io/kubernetes/pkg/apis/admissionregistration/v1/defaults.go" | ||
// in order for in-tree cloud providers to not depend on internal packages. | ||
func SetDefaults_ValidatingWebhook(obj *admissionregistrationv1.ValidatingWebhook) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think any defaulting is required here... the defaulting gets applied when creating against the server |
||
if obj.FailurePolicy == nil { | ||
policy := admissionregistrationv1.Fail | ||
obj.FailurePolicy = &policy | ||
} | ||
if obj.MatchPolicy == nil { | ||
policy := admissionregistrationv1.Equivalent | ||
obj.MatchPolicy = &policy | ||
} | ||
if obj.NamespaceSelector == nil { | ||
selector := metav1.LabelSelector{} | ||
obj.NamespaceSelector = &selector | ||
} | ||
if obj.ObjectSelector == nil { | ||
selector := metav1.LabelSelector{} | ||
obj.ObjectSelector = &selector | ||
} | ||
if obj.TimeoutSeconds == nil { | ||
obj.TimeoutSeconds = new(int32) | ||
*obj.TimeoutSeconds = 10 | ||
} | ||
} | ||
|
||
// SetDefaults_MutatingWebhook sets defaults for webhook mutating This function | ||
// is duplicated from "k8s.io/kubernetes/pkg/apis/admissionregistration/v1/defaults.go" | ||
// in order for in-tree cloud providers to not depend on internal packages. | ||
func SetDefaults_MutatingWebhook(obj *admissionregistrationv1.MutatingWebhook) { | ||
if obj.FailurePolicy == nil { | ||
policy := admissionregistrationv1.Fail | ||
obj.FailurePolicy = &policy | ||
} | ||
if obj.MatchPolicy == nil { | ||
policy := admissionregistrationv1.Equivalent | ||
obj.MatchPolicy = &policy | ||
} | ||
if obj.NamespaceSelector == nil { | ||
selector := metav1.LabelSelector{} | ||
obj.NamespaceSelector = &selector | ||
} | ||
if obj.ObjectSelector == nil { | ||
selector := metav1.LabelSelector{} | ||
obj.ObjectSelector = &selector | ||
} | ||
if obj.TimeoutSeconds == nil { | ||
obj.TimeoutSeconds = new(int32) | ||
*obj.TimeoutSeconds = 10 | ||
} | ||
if obj.ReinvocationPolicy == nil { | ||
never := admissionregistrationv1.NeverReinvocationPolicy | ||
obj.ReinvocationPolicy = &never | ||
} | ||
} | ||
|
||
// SetDefaults_Rule sets defaults for webhook rule This function | ||
// is duplicated from "k8s.io/kubernetes/pkg/apis/admissionregistration/v1/defaults.go" | ||
// in order for in-tree cloud providers to not depend on internal packages. | ||
func SetDefaults_Rule(obj *admissionregistrationv1.Rule) { | ||
if obj.Scope == nil { | ||
s := admissionregistrationv1.AllScopes | ||
obj.Scope = &s | ||
} | ||
} | ||
|
||
// SetDefaults_ServiceReference sets defaults for Webhook's ServiceReference This function | ||
// is duplicated from "k8s.io/kubernetes/pkg/apis/admissionregistration/v1/defaults.go" | ||
// in order for in-tree cloud providers to not depend on internal packages. | ||
func SetDefaults_ServiceReference(obj *admissionregistrationv1.ServiceReference) { | ||
if obj.Port == nil { | ||
obj.Port = utilpointer.Int32(443) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This self-registration of webhooks is really tricky to get right... I had no idea this was being contemplated to be added into cloud-controller-manager
some quick observations:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
great questions @liggitt , i was not aware of the complexity around the self-registration but it makes perfect sense once you've laid it out. i think this might cause us to reconsider the methodology here, it seems like using a manifest would make a better experience for users and help with some of the ordering/dueling issues.
thanks for the review!