Webhook framework for cloud controller manager

Provides framework for CCMs to host webhooks.
This commit is contained in:
Nick Turner 2022-03-18 22:09:36 +00:00
parent fcfe5dfc21
commit 86f4136003
23 changed files with 1267 additions and 23 deletions

View File

@ -316,6 +316,7 @@ API rule violation: list_type_missing,k8s.io/apiserver/pkg/apis/audit/v1,PolicyR
API rule violation: list_type_missing,k8s.io/apiserver/pkg/apis/audit/v1,PolicyRule,UserGroups API rule violation: list_type_missing,k8s.io/apiserver/pkg/apis/audit/v1,PolicyRule,UserGroups
API rule violation: list_type_missing,k8s.io/apiserver/pkg/apis/audit/v1,PolicyRule,Users API rule violation: list_type_missing,k8s.io/apiserver/pkg/apis/audit/v1,PolicyRule,Users
API rule violation: list_type_missing,k8s.io/apiserver/pkg/apis/audit/v1,PolicyRule,Verbs API rule violation: list_type_missing,k8s.io/apiserver/pkg/apis/audit/v1,PolicyRule,Verbs
API rule violation: list_type_missing,k8s.io/cloud-provider/config/v1alpha1,WebhookConfiguration,Webhooks
API rule violation: list_type_missing,k8s.io/controller-manager/config/v1alpha1,GenericControllerManagerConfiguration,Controllers API rule violation: list_type_missing,k8s.io/controller-manager/config/v1alpha1,GenericControllerManagerConfiguration,Controllers
API rule violation: list_type_missing,k8s.io/controller-manager/config/v1alpha1,LeaderMigrationConfiguration,ControllerLeaders API rule violation: list_type_missing,k8s.io/controller-manager/config/v1alpha1,LeaderMigrationConfiguration,ControllerLeaders
API rule violation: list_type_missing,k8s.io/kube-controller-manager/config/v1alpha1,GarbageCollectorControllerConfiguration,GCIgnoredResources API rule violation: list_type_missing,k8s.io/kube-controller-manager/config/v1alpha1,GarbageCollectorControllerConfiguration,GCIgnoredResources
@ -442,6 +443,7 @@ API rule violation: names_match,k8s.io/cloud-provider/config/v1alpha1,CloudContr
API rule violation: names_match,k8s.io/cloud-provider/config/v1alpha1,CloudControllerManagerConfiguration,NodeController API rule violation: names_match,k8s.io/cloud-provider/config/v1alpha1,CloudControllerManagerConfiguration,NodeController
API rule violation: names_match,k8s.io/cloud-provider/config/v1alpha1,CloudControllerManagerConfiguration,NodeStatusUpdateFrequency API rule violation: names_match,k8s.io/cloud-provider/config/v1alpha1,CloudControllerManagerConfiguration,NodeStatusUpdateFrequency
API rule violation: names_match,k8s.io/cloud-provider/config/v1alpha1,CloudControllerManagerConfiguration,ServiceController API rule violation: names_match,k8s.io/cloud-provider/config/v1alpha1,CloudControllerManagerConfiguration,ServiceController
API rule violation: names_match,k8s.io/cloud-provider/config/v1alpha1,CloudControllerManagerConfiguration,Webhook
API rule violation: names_match,k8s.io/cloud-provider/config/v1alpha1,CloudProviderConfiguration,CloudConfigFile API rule violation: names_match,k8s.io/cloud-provider/config/v1alpha1,CloudProviderConfiguration,CloudConfigFile
API rule violation: names_match,k8s.io/cloud-provider/config/v1alpha1,CloudProviderConfiguration,Name API rule violation: names_match,k8s.io/cloud-provider/config/v1alpha1,CloudProviderConfiguration,Name
API rule violation: names_match,k8s.io/cloud-provider/config/v1alpha1,KubeCloudSharedConfiguration,AllocateNodeCIDRs API rule violation: names_match,k8s.io/cloud-provider/config/v1alpha1,KubeCloudSharedConfiguration,AllocateNodeCIDRs
@ -456,6 +458,7 @@ API rule violation: names_match,k8s.io/cloud-provider/config/v1alpha1,KubeCloudS
API rule violation: names_match,k8s.io/cloud-provider/config/v1alpha1,KubeCloudSharedConfiguration,NodeSyncPeriod API rule violation: names_match,k8s.io/cloud-provider/config/v1alpha1,KubeCloudSharedConfiguration,NodeSyncPeriod
API rule violation: names_match,k8s.io/cloud-provider/config/v1alpha1,KubeCloudSharedConfiguration,RouteReconciliationPeriod API rule violation: names_match,k8s.io/cloud-provider/config/v1alpha1,KubeCloudSharedConfiguration,RouteReconciliationPeriod
API rule violation: names_match,k8s.io/cloud-provider/config/v1alpha1,KubeCloudSharedConfiguration,UseServiceAccountCredentials API rule violation: names_match,k8s.io/cloud-provider/config/v1alpha1,KubeCloudSharedConfiguration,UseServiceAccountCredentials
API rule violation: names_match,k8s.io/cloud-provider/config/v1alpha1,WebhookConfiguration,Webhooks
API rule violation: names_match,k8s.io/controller-manager/config/v1alpha1,GenericControllerManagerConfiguration,Address API rule violation: names_match,k8s.io/controller-manager/config/v1alpha1,GenericControllerManagerConfiguration,Address
API rule violation: names_match,k8s.io/controller-manager/config/v1alpha1,GenericControllerManagerConfiguration,ClientConnection API rule violation: names_match,k8s.io/controller-manager/config/v1alpha1,GenericControllerManagerConfiguration,ClientConnection
API rule violation: names_match,k8s.io/controller-manager/config/v1alpha1,GenericControllerManagerConfiguration,ControllerStartInterval API rule violation: names_match,k8s.io/controller-manager/config/v1alpha1,GenericControllerManagerConfiguration,ControllerStartInterval

View File

@ -655,8 +655,9 @@ function start_cloud_controller_manager {
fi fi
CLOUD_CTLRMGR_LOG=${LOG_DIR}/cloud-controller-manager.log CLOUD_CTLRMGR_LOG=${LOG_DIR}/cloud-controller-manager.log
# shellcheck disable=SC2086
${CONTROLPLANE_SUDO} "${EXTERNAL_CLOUD_PROVIDER_BINARY:-"${GO_OUT}/cloud-controller-manager"}" \ ${CONTROLPLANE_SUDO} "${EXTERNAL_CLOUD_PROVIDER_BINARY:-"${GO_OUT}/cloud-controller-manager"}" \
"${CLOUD_CTLRMGR_FLAGS}" \ ${CLOUD_CTLRMGR_FLAGS} \
--v="${LOG_LEVEL}" \ --v="${LOG_LEVEL}" \
--vmodule="${LOG_SPEC}" \ --vmodule="${LOG_SPEC}" \
--feature-gates="${FEATURE_GATES}" \ --feature-gates="${FEATURE_GATES}" \

View File

@ -16,6 +16,10 @@ limitations under the License.
package ports package ports
import (
cpoptions "k8s.io/cloud-provider/options"
)
// In this file, we can see all default port of cluster. // In this file, we can see all default port of cluster.
// It's also an important documentation for us. So don't remove them easily. // It's also an important documentation for us. So don't remove them easily.
const ( const (
@ -43,4 +47,8 @@ const (
// CloudControllerManagerPort is the default port for the cloud controller manager server. // CloudControllerManagerPort is the default port for the cloud controller manager server.
// This value may be overridden by a flag at startup. // This value may be overridden by a flag at startup.
CloudControllerManagerPort = 10258 CloudControllerManagerPort = 10258
// CloudControllerManagerWebhookPort is the default port for the cloud
// controller manager webhook server. May be overridden by a flag at
// startup.
CloudControllerManagerWebhookPort = cpoptions.CloudControllerManagerWebhookPort
) )

View File

@ -172,6 +172,12 @@ const (
// Enables kubelet to detect CSI volume condition and send the event of the abnormal volume to the corresponding pod that is using it. // Enables kubelet to detect CSI volume condition and send the event of the abnormal volume to the corresponding pod that is using it.
CSIVolumeHealth featuregate.Feature = "CSIVolumeHealth" CSIVolumeHealth featuregate.Feature = "CSIVolumeHealth"
// owner: @nckturner
// kep: http://kep.k8s.io/2699
// alpha: v1.27
// Enable webhooks in cloud controller manager
CloudControllerManagerWebhook featuregate.Feature = "CloudControllerManagerWebhook"
// owner: @adrianreber // owner: @adrianreber
// kep: https://kep.k8s.io/2008 // kep: https://kep.k8s.io/2008
// alpha: v1.25 // alpha: v1.25
@ -915,6 +921,8 @@ var defaultKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureS
CSIVolumeHealth: {Default: false, PreRelease: featuregate.Alpha}, CSIVolumeHealth: {Default: false, PreRelease: featuregate.Alpha},
CloudControllerManagerWebhook: {Default: false, PreRelease: featuregate.Alpha},
ContainerCheckpoint: {Default: false, PreRelease: featuregate.Alpha}, ContainerCheckpoint: {Default: false, PreRelease: featuregate.Alpha},
ConsistentHTTPGetHandlers: {Default: true, PreRelease: featuregate.GA}, ConsistentHTTPGetHandlers: {Default: true, PreRelease: featuregate.GA},

View File

@ -1016,6 +1016,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA
"k8s.io/cloud-provider/config/v1alpha1.CloudControllerManagerConfiguration": schema_k8sio_cloud_provider_config_v1alpha1_CloudControllerManagerConfiguration(ref), "k8s.io/cloud-provider/config/v1alpha1.CloudControllerManagerConfiguration": schema_k8sio_cloud_provider_config_v1alpha1_CloudControllerManagerConfiguration(ref),
"k8s.io/cloud-provider/config/v1alpha1.CloudProviderConfiguration": schema_k8sio_cloud_provider_config_v1alpha1_CloudProviderConfiguration(ref), "k8s.io/cloud-provider/config/v1alpha1.CloudProviderConfiguration": schema_k8sio_cloud_provider_config_v1alpha1_CloudProviderConfiguration(ref),
"k8s.io/cloud-provider/config/v1alpha1.KubeCloudSharedConfiguration": schema_k8sio_cloud_provider_config_v1alpha1_KubeCloudSharedConfiguration(ref), "k8s.io/cloud-provider/config/v1alpha1.KubeCloudSharedConfiguration": schema_k8sio_cloud_provider_config_v1alpha1_KubeCloudSharedConfiguration(ref),
"k8s.io/cloud-provider/config/v1alpha1.WebhookConfiguration": schema_k8sio_cloud_provider_config_v1alpha1_WebhookConfiguration(ref),
"k8s.io/controller-manager/config/v1alpha1.ControllerLeaderConfiguration": schema_k8sio_controller_manager_config_v1alpha1_ControllerLeaderConfiguration(ref), "k8s.io/controller-manager/config/v1alpha1.ControllerLeaderConfiguration": schema_k8sio_controller_manager_config_v1alpha1_ControllerLeaderConfiguration(ref),
"k8s.io/controller-manager/config/v1alpha1.GenericControllerManagerConfiguration": schema_k8sio_controller_manager_config_v1alpha1_GenericControllerManagerConfiguration(ref), "k8s.io/controller-manager/config/v1alpha1.GenericControllerManagerConfiguration": schema_k8sio_controller_manager_config_v1alpha1_GenericControllerManagerConfiguration(ref),
"k8s.io/controller-manager/config/v1alpha1.LeaderMigrationConfiguration": schema_k8sio_controller_manager_config_v1alpha1_LeaderMigrationConfiguration(ref), "k8s.io/controller-manager/config/v1alpha1.LeaderMigrationConfiguration": schema_k8sio_controller_manager_config_v1alpha1_LeaderMigrationConfiguration(ref),
@ -50661,7 +50662,8 @@ func schema_k8sio_cloud_provider_config_v1alpha1_CloudControllerManagerConfigura
return common.OpenAPIDefinition{ return common.OpenAPIDefinition{
Schema: spec.Schema{ Schema: spec.Schema{
SchemaProps: spec.SchemaProps{ SchemaProps: spec.SchemaProps{
Type: []string{"object"}, Description: "CloudControllerManagerConfiguration contains elements describing cloud-controller manager.",
Type: []string{"object"},
Properties: map[string]spec.Schema{ Properties: map[string]spec.Schema{
"kind": { "kind": {
SchemaProps: spec.SchemaProps{ SchemaProps: spec.SchemaProps{
@ -50712,12 +50714,19 @@ func schema_k8sio_cloud_provider_config_v1alpha1_CloudControllerManagerConfigura
Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.Duration"), Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.Duration"),
}, },
}, },
"Webhook": {
SchemaProps: spec.SchemaProps{
Description: "Webhook is the configuration for cloud-controller-manager hosted webhooks",
Default: map[string]interface{}{},
Ref: ref("k8s.io/cloud-provider/config/v1alpha1.WebhookConfiguration"),
},
},
}, },
Required: []string{"Generic", "KubeCloudShared", "NodeController", "ServiceController", "NodeStatusUpdateFrequency"}, Required: []string{"Generic", "KubeCloudShared", "NodeController", "ServiceController", "NodeStatusUpdateFrequency", "Webhook"},
}, },
}, },
Dependencies: []string{ Dependencies: []string{
"k8s.io/apimachinery/pkg/apis/meta/v1.Duration", "k8s.io/cloud-provider/config/v1alpha1.KubeCloudSharedConfiguration", "k8s.io/cloud-provider/controllers/node/config/v1alpha1.NodeControllerConfiguration", "k8s.io/cloud-provider/controllers/service/config/v1alpha1.ServiceControllerConfiguration", "k8s.io/controller-manager/config/v1alpha1.GenericControllerManagerConfiguration"}, "k8s.io/apimachinery/pkg/apis/meta/v1.Duration", "k8s.io/cloud-provider/config/v1alpha1.KubeCloudSharedConfiguration", "k8s.io/cloud-provider/config/v1alpha1.WebhookConfiguration", "k8s.io/cloud-provider/controllers/node/config/v1alpha1.NodeControllerConfiguration", "k8s.io/cloud-provider/controllers/service/config/v1alpha1.ServiceControllerConfiguration", "k8s.io/controller-manager/config/v1alpha1.GenericControllerManagerConfiguration"},
} }
} }
@ -50858,6 +50867,35 @@ func schema_k8sio_cloud_provider_config_v1alpha1_KubeCloudSharedConfiguration(re
} }
} }
func schema_k8sio_cloud_provider_config_v1alpha1_WebhookConfiguration(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Description: "WebhookConfiguration contains configuration related to cloud-controller-manager hosted webhooks",
Type: []string{"object"},
Properties: map[string]spec.Schema{
"Webhooks": {
SchemaProps: spec.SchemaProps{
Description: "Webhooks is the list of webhooks to enable or disable '*' means \"all enabled by default webhooks\" 'foo' means \"enable 'foo'\" '-foo' means \"disable 'foo'\" first item for a particular name wins",
Type: []string{"array"},
Items: &spec.SchemaOrArray{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Default: "",
Type: []string{"string"},
Format: "",
},
},
},
},
},
},
Required: []string{"Webhooks"},
},
},
}
}
func schema_k8sio_controller_manager_config_v1alpha1_ControllerLeaderConfiguration(ref common.ReferenceCallback) common.OpenAPIDefinition { func schema_k8sio_controller_manager_config_v1alpha1_ControllerLeaderConfiguration(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{ return common.OpenAPIDefinition{
Schema: spec.Schema{ Schema: spec.Schema{

View File

@ -0,0 +1,183 @@
/*
Copyright 2022 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 app
import (
"fmt"
"os"
"github.com/spf13/cobra"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/cloud-provider/options"
cliflag "k8s.io/component-base/cli/flag"
"k8s.io/component-base/cli/globalflag"
"k8s.io/component-base/term"
"k8s.io/component-base/version/verflag"
)
type CommandBuilder struct {
webhookConfigs map[string]WebhookConfig
controllerInitFuncConstructors map[string]ControllerInitFuncConstructor
additionalFlags cliflag.NamedFlagSets
options *options.CloudControllerManagerOptions
cloudInitializer InitCloudFunc
stopCh <-chan struct{}
cmdName string
long string
defaults *options.ProviderDefaults
}
func NewBuilder() *CommandBuilder {
cb := CommandBuilder{}
cb.webhookConfigs = make(map[string]WebhookConfig)
cb.controllerInitFuncConstructors = make(map[string]ControllerInitFuncConstructor)
return &cb
}
func (cb *CommandBuilder) SetOptions(options *options.CloudControllerManagerOptions) {
cb.options = options
}
func (cb *CommandBuilder) AddFlags(additionalFlags cliflag.NamedFlagSets) {
cb.additionalFlags = additionalFlags
}
func (cb *CommandBuilder) RegisterController(name string, constructor ControllerInitFuncConstructor) {
cb.controllerInitFuncConstructors[name] = constructor
}
func (cb *CommandBuilder) RegisterDefaultControllers() {
for key, val := range DefaultInitFuncConstructors {
cb.controllerInitFuncConstructors[key] = val
}
}
func (cb *CommandBuilder) RegisterWebhook(name string, config WebhookConfig) {
cb.webhookConfigs[name] = config
}
func (cb *CommandBuilder) SetCloudInitializer(cloudInitializer InitCloudFunc) {
cb.cloudInitializer = cloudInitializer
}
func (cb *CommandBuilder) SetStopChannel(stopCh <-chan struct{}) {
cb.stopCh = stopCh
}
func (cb *CommandBuilder) SetCmdName(name string) {
cb.cmdName = name
}
func (cb *CommandBuilder) SetLongDesc(long string) {
cb.long = long
}
// SetProviderDefaults can be called to change the default values for some
// options when a flag is not set
func (cb *CommandBuilder) SetProviderDefaults(defaults options.ProviderDefaults) {
cb.defaults = &defaults
}
func (cb *CommandBuilder) setdefaults() {
if cb.stopCh == nil {
cb.stopCh = wait.NeverStop
}
if cb.cmdName == "" {
cb.cmdName = "cloud-controller-manager"
}
if cb.long == "" {
cb.long = `The Cloud controller manager is a daemon that embeds the cloud specific control loops shipped with Kubernetes.`
}
if cb.defaults == nil {
cb.defaults = &options.ProviderDefaults{}
}
if cb.options == nil {
opts, err := options.NewCloudControllerManagerOptionsWithProviderDefaults(*cb.defaults)
if err != nil {
fmt.Fprintf(os.Stderr, "unable to initialize command options: %v\n", err)
os.Exit(1)
}
cb.options = opts
}
}
func (cb *CommandBuilder) BuildCommand() *cobra.Command {
cb.setdefaults()
cmd := &cobra.Command{
Use: cb.cmdName,
Long: cb.long,
RunE: func(cmd *cobra.Command, args []string) error {
verflag.PrintAndExitIfRequested()
cliflag.PrintFlags(cmd.Flags())
config, err := cb.options.Config(ControllerNames(cb.controllerInitFuncConstructors), ControllersDisabledByDefault.List(),
WebhookNames(cb.webhookConfigs), WebhooksDisabledByDefault.List())
if err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
return err
}
completedConfig := config.Complete()
cloud := cb.cloudInitializer(completedConfig)
controllerInitializers := ConstructControllerInitializers(cb.controllerInitFuncConstructors, completedConfig, cloud)
webhooks := newWebhookHandlers(cb.webhookConfigs, completedConfig, cloud)
if err := Run(completedConfig, cloud, controllerInitializers, webhooks, cb.stopCh); err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
return err
}
return nil
},
Args: func(cmd *cobra.Command, args []string) error {
for _, arg := range args {
if len(arg) > 0 {
return fmt.Errorf("%q does not take any arguments, got %q", cmd.CommandPath(), args)
}
}
return nil
},
}
fs := cmd.Flags()
namedFlagSets := cb.options.Flags(ControllerNames(cb.controllerInitFuncConstructors), ControllersDisabledByDefault.List(), WebhookNames(cb.webhookConfigs), WebhooksDisabledByDefault.List())
verflag.AddFlags(namedFlagSets.FlagSet("global"))
globalflag.AddGlobalFlags(namedFlagSets.FlagSet("global"), cmd.Name())
for _, f := range namedFlagSets.FlagSets {
fs.AddFlagSet(f)
}
for _, f := range cb.additionalFlags.FlagSets {
fs.AddFlagSet(f)
}
usageFmt := "Usage:\n %s\n"
cols, _, _ := term.TerminalSize(cmd.OutOrStdout())
cmd.SetUsageFunc(func(cmd *cobra.Command) error {
fmt.Fprintf(cmd.OutOrStderr(), usageFmt, cmd.UseLine())
cliflag.PrintSections(cmd.OutOrStderr(), namedFlagSets, cols)
return nil
})
cmd.SetHelpFunc(func(cmd *cobra.Command, args []string) {
fmt.Fprintf(cmd.OutOrStdout(), "%s\n\n"+usageFmt, cmd.Long, cmd.UseLine())
cliflag.PrintSections(cmd.OutOrStdout(), namedFlagSets, cols)
})
return cmd
}

View File

@ -37,6 +37,10 @@ type Config struct {
Authentication apiserver.AuthenticationInfo Authentication apiserver.AuthenticationInfo
Authorization apiserver.AuthorizationInfo Authorization apiserver.AuthorizationInfo
// WebhookSecureServing is a separate SecureServing configuration from
// healthz, configz, and metrics.
WebhookSecureServing *apiserver.SecureServingInfo
// the general kube client // the general kube client
Client *clientset.Clientset Client *clientset.Clientset

View File

@ -58,6 +58,7 @@ import (
genericcontrollermanager "k8s.io/controller-manager/app" genericcontrollermanager "k8s.io/controller-manager/app"
"k8s.io/controller-manager/controller" "k8s.io/controller-manager/controller"
"k8s.io/controller-manager/pkg/clientbuilder" "k8s.io/controller-manager/pkg/clientbuilder"
cmfeatures "k8s.io/controller-manager/pkg/features"
controllerhealthz "k8s.io/controller-manager/pkg/healthz" controllerhealthz "k8s.io/controller-manager/pkg/healthz"
"k8s.io/controller-manager/pkg/informerfactory" "k8s.io/controller-manager/pkg/informerfactory"
"k8s.io/controller-manager/pkg/leadermigration" "k8s.io/controller-manager/pkg/leadermigration"
@ -95,7 +96,7 @@ the cloud specific control loops shipped with Kubernetes.`,
return err return err
} }
c, err := s.Config(ControllerNames(controllerInitFuncConstructors), ControllersDisabledByDefault.List()) c, err := s.Config(ControllerNames(controllerInitFuncConstructors), ControllersDisabledByDefault.List(), AllWebhooks, DisabledByDefaultWebhooks)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err) fmt.Fprintf(os.Stderr, "%v\n", err)
return err return err
@ -105,7 +106,7 @@ the cloud specific control loops shipped with Kubernetes.`,
cloud := cloudInitializer(completedConfig) cloud := cloudInitializer(completedConfig)
controllerInitializers := ConstructControllerInitializers(controllerInitFuncConstructors, completedConfig, cloud) controllerInitializers := ConstructControllerInitializers(controllerInitFuncConstructors, completedConfig, cloud)
if err := Run(completedConfig, cloud, controllerInitializers, stopCh); err != nil { if err := Run(completedConfig, cloud, controllerInitializers, make(map[string]webhookHandler), stopCh); err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err) fmt.Fprintf(os.Stderr, "%v\n", err)
return err return err
} }
@ -122,7 +123,7 @@ the cloud specific control loops shipped with Kubernetes.`,
} }
fs := cmd.Flags() fs := cmd.Flags()
namedFlagSets := s.Flags(ControllerNames(controllerInitFuncConstructors), ControllersDisabledByDefault.List()) namedFlagSets := s.Flags(ControllerNames(controllerInitFuncConstructors), ControllersDisabledByDefault.List(), AllWebhooks, DisabledByDefaultWebhooks)
globalFlagSet := namedFlagSets.FlagSet("global") globalFlagSet := namedFlagSets.FlagSet("global")
verflag.AddFlags(globalFlagSet) verflag.AddFlags(globalFlagSet)
@ -160,7 +161,8 @@ the cloud specific control loops shipped with Kubernetes.`,
} }
// Run runs the ExternalCMServer. This should never exit. // Run runs the ExternalCMServer. This should never exit.
func Run(c *cloudcontrollerconfig.CompletedConfig, cloud cloudprovider.Interface, controllerInitializers map[string]InitFunc, stopCh <-chan struct{}) error { func Run(c *cloudcontrollerconfig.CompletedConfig, cloud cloudprovider.Interface, controllerInitializers map[string]InitFunc, webhooks map[string]webhookHandler,
stopCh <-chan struct{}) error {
// To help debugging, immediately log version // To help debugging, immediately log version
klog.Infof("Version: %+v", version.Get()) klog.Infof("Version: %+v", version.Get())
@ -184,6 +186,16 @@ func Run(c *cloudcontrollerconfig.CompletedConfig, cloud cloudprovider.Interface
checks = append(checks, electionChecker) checks = append(checks, electionChecker)
} }
if utilfeature.DefaultFeatureGate.Enabled(cmfeatures.CloudControllerManagerWebhook) {
if len(webhooks) > 0 {
klog.Info("Webhook Handlers enabled: ", webhooks)
handler := newHandler(webhooks)
if _, _, err := c.WebhookSecureServing.Serve(handler, 0, stopCh); err != nil {
return err
}
}
}
healthzHandler := controllerhealthz.NewMutableHealthzHandler(checks...) healthzHandler := controllerhealthz.NewMutableHealthzHandler(checks...)
// Start the controller manager HTTP server // Start the controller manager HTTP server
if c.SecureServing != nil { if c.SecureServing != nil {
@ -362,8 +374,20 @@ func ControllerNames(controllerInitFuncConstructors map[string]ControllerInitFun
return ret.List() return ret.List()
} }
// ControllersDisabledByDefault is the controller disabled default when starting cloud-controller managers. var (
var ControllersDisabledByDefault = sets.NewString() // ControllersDisabledByDefault is the controller disabled default when starting cloud-controller managers.
ControllersDisabledByDefault = sets.NewString()
// AllWebhooks represents the list of all webhook options configured in
// this package. This is empty because no webhooks are currently
// configured in this package.
AllWebhooks = []string{}
// DisabledByDefaultWebhooks represents the list of webhooks which must be
// explicitly enabled. This is empty because no webhooks are currently
// configured in this package.
DisabledByDefaultWebhooks = []string{}
)
// ConstructControllerInitializers is a map of controller name(as defined by controllers flag in https://kubernetes.io/docs/reference/command-line-tools-reference/kube-controller-manager/#options) to their InitFuncConstructor. // ConstructControllerInitializers is a map of controller name(as defined by controllers flag in https://kubernetes.io/docs/reference/command-line-tools-reference/kube-controller-manager/#options) to their InitFuncConstructor.
// paired to their InitFunc. This allows for structured downstream composition and subdivision. // paired to their InitFunc. This allows for structured downstream composition and subdivision.

View File

@ -112,14 +112,22 @@ func StartTestServer(ctx context.Context, customFlags []string) (result TestServ
commandArgs := []string{} commandArgs := []string{}
listeners := []net.Listener{} listeners := []net.Listener{}
disableSecure := false disableSecure := false
webhookServing := false
for _, arg := range customFlags { for _, arg := range customFlags {
if strings.HasPrefix(arg, "--secure-port=") { // This block collects all custom flags other than secure serving flags,
// which are added after creating a listener.
if strings.HasPrefix(arg, "--secure-port=") || strings.HasPrefix(arg, "--cert-dir=") {
if arg == "--secure-port=0" { if arg == "--secure-port=0" {
commandArgs = append(commandArgs, arg) commandArgs = append(commandArgs, arg)
disableSecure = true disableSecure = true
} }
} else if strings.HasPrefix(arg, "--cert-dir=") { } else if strings.HasPrefix(arg, "--webhook-secure-port=") || strings.HasPrefix(arg, "--webhook-cert-dir=") {
// skip it if arg == "--webhook-secure-port=0" {
commandArgs = append(commandArgs, arg)
webhookServing = false
} else {
webhookServing = true
}
} else { } else {
commandArgs = append(commandArgs, arg) commandArgs = append(commandArgs, arg)
} }
@ -136,6 +144,19 @@ func StartTestServer(ctx context.Context, customFlags []string) (result TestServ
logger.Info("cloud-controller-manager will listen securely", "port", bindPort) logger.Info("cloud-controller-manager will listen securely", "port", bindPort)
} }
if webhookServing {
listener, bindPort, err := createListenerOnFreePort()
if err != nil {
return result, fmt.Errorf("failed to create listener: %v", err)
}
listeners = append(listeners, listener)
commandArgs = append(commandArgs, fmt.Sprintf("--webhook-secure-port=%d", bindPort))
commandArgs = append(commandArgs, fmt.Sprintf("--webhook-cert-dir=%s", result.TmpDir))
logger.Info("cloud-controller-manager (webhook endpoint) will listen securely", "port", bindPort)
}
for _, listener := range listeners { for _, listener := range listeners {
listener.Close() listener.Close()
} }

View File

@ -0,0 +1,76 @@
/*
Copyright 2023 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 app
import (
"context"
"k8s.io/component-base/metrics"
"k8s.io/component-base/metrics/legacyregistry"
)
const (
// subSystemName is the name of this subsystem name used for prometheus metrics.
subSystemName = "cloud_provider_webhook"
)
type registerables []metrics.Registerable
// init registers all metrics
func init() {
for _, metric := range toRegister {
legacyregistry.MustRegister(metric)
}
}
var (
requestTotal = metrics.NewCounterVec(
&metrics.CounterOpts{
Name: "request_total",
Subsystem: subSystemName,
Help: "Number of HTTP requests partitioned by status code.",
StabilityLevel: metrics.ALPHA,
},
[]string{"code", "webhook"},
)
requestLatency = metrics.NewHistogramVec(
&metrics.HistogramOpts{
Name: "request_duration_seconds",
Subsystem: subSystemName,
Help: "Request latency in seconds. Broken down by status code.",
Buckets: []float64{0.25, 0.5, 0.7, 1, 1.5, 3, 5, 10},
StabilityLevel: metrics.ALPHA,
},
[]string{"code", "webhook"},
)
toRegister = registerables{
requestTotal,
requestLatency,
}
)
// RecordRequestTotal increments the total number of requests for the webhook.
func recordRequestTotal(ctx context.Context, code string, webhookName string) {
requestTotal.WithContext(ctx).With(map[string]string{"code": code, "webhook": webhookName}).Add(1)
}
// RecordRequestLatency measures request latency in seconds for the delegated authorization. Broken down by status code.
func recordRequestLatency(ctx context.Context, code string, webhookName string, latency float64) {
requestLatency.WithContext(ctx).With(map[string]string{"code": code, "webhook": webhookName}).Observe(latency)
}

View File

@ -0,0 +1,200 @@
/*
Copyright 2022 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 app
import (
"bytes"
"context"
"fmt"
"net/http"
"strconv"
"time"
admissionv1 "k8s.io/api/admission/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apiserver/pkg/server/mux"
cloudprovider "k8s.io/cloud-provider"
"k8s.io/cloud-provider/app/config"
genericcontrollermanager "k8s.io/controller-manager/app"
"k8s.io/klog/v2"
)
var (
runtimeScheme = runtime.NewScheme()
codecs = serializer.NewCodecFactory(runtimeScheme)
deserializer = codecs.UniversalDeserializer()
encoder runtime.Encoder
)
func init() {
_ = corev1.AddToScheme(runtimeScheme)
_ = admissionv1.AddToScheme(runtimeScheme)
serializerInfo, _ := runtime.SerializerInfoForMediaType(codecs.SupportedMediaTypes(), runtime.ContentTypeJSON)
encoder = serializerInfo.Serializer
}
// WebhooksDisabledByDefault is the webhooks disabled default when starting cloud-controller managers.
var WebhooksDisabledByDefault = sets.NewString()
type WebhookConfig struct {
Path string
AdmissionHandler func(*admissionv1.AdmissionRequest) (*admissionv1.AdmissionResponse, error)
}
type webhookHandler struct {
name string
path string
http.Handler
admissionHandler func(*admissionv1.AdmissionRequest) (*admissionv1.AdmissionResponse, error)
completedConfig *config.CompletedConfig
cloud cloudprovider.Interface
}
func newWebhookHandlers(webhookConfigs map[string]WebhookConfig, completedConfig *config.CompletedConfig, cloud cloudprovider.Interface) map[string]webhookHandler {
webhookHandlers := make(map[string]webhookHandler)
for name, config := range webhookConfigs {
if !genericcontrollermanager.IsControllerEnabled(name, WebhooksDisabledByDefault, completedConfig.ComponentConfig.Webhook.Webhooks) {
klog.Warningf("Webhook %q is disabled", name)
continue
}
klog.Infof("Webhook enabled: %q", name)
webhookHandlers[name] = webhookHandler{
name: name,
path: config.Path,
admissionHandler: config.AdmissionHandler,
completedConfig: completedConfig,
cloud: cloud,
}
}
return webhookHandlers
}
func WebhookNames(webhooks map[string]WebhookConfig) []string {
ret := sets.StringKeySet(webhooks)
return ret.List()
}
func newHandler(webhooks map[string]webhookHandler) *mux.PathRecorderMux {
mux := mux.NewPathRecorderMux("controller-manager-webhook")
for _, handler := range webhooks {
mux.Handle(handler.path, handler)
}
return mux
}
func (h webhookHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx := context.Background()
klog.Infof("Received validation request: %q", r.RequestURI)
start := time.Now()
var (
statusCode int
err error
in *admissionv1.AdmissionReview
admissionResponse *admissionv1.AdmissionResponse
)
defer func() {
latency := time.Since(start)
if statusCode != 0 {
recordRequestTotal(ctx, strconv.Itoa(statusCode), h.name)
recordRequestLatency(ctx, strconv.Itoa(statusCode), h.name, latency.Seconds())
return
}
if err != nil {
recordRequestTotal(ctx, "<error>", h.name)
recordRequestLatency(ctx, "<error>", h.name, latency.Seconds())
}
}()
in, err = parseRequest(r)
if err != nil {
klog.Error(err)
statusCode = http.StatusBadRequest
http.Error(w, err.Error(), statusCode)
return
}
admissionResponse, err = h.admissionHandler(in.Request)
if err != nil {
e := fmt.Sprintf("error generating admission response: %v", err)
klog.Errorf(e)
statusCode = http.StatusInternalServerError
http.Error(w, e, statusCode)
return
} else if admissionResponse == nil {
e := fmt.Sprintf("admission response cannot be nil")
klog.Error(e)
statusCode = http.StatusInternalServerError
http.Error(w, e, statusCode)
return
}
admissionReview := &admissionv1.AdmissionReview{
Response: admissionResponse,
}
admissionReview.Response.UID = in.Request.UID
w.Header().Set("Content-Type", "application/json")
codec := codecs.EncoderForVersion(encoder, admissionv1.SchemeGroupVersion)
out, err := runtime.Encode(codec, admissionReview)
if err != nil {
e := fmt.Sprintf("error parsing admission response: %v", err)
klog.Error(e)
statusCode = http.StatusInternalServerError
http.Error(w, e, statusCode)
return
}
klog.Infof("%s", out)
fmt.Fprintf(w, "%s", out)
}
// parseRequest extracts an AdmissionReview from an http.Request if possible
func parseRequest(r *http.Request) (*admissionv1.AdmissionReview, error) {
review := &admissionv1.AdmissionReview{}
if r.Header.Get("Content-Type") != "application/json" {
return nil, fmt.Errorf("Content-Type: %q should be %q",
r.Header.Get("Content-Type"), "application/json")
}
bodybuf := new(bytes.Buffer)
bodybuf.ReadFrom(r.Body)
body := bodybuf.Bytes()
if len(body) == 0 {
return nil, fmt.Errorf("admission request HTTP body is empty")
}
if _, _, err := deserializer.Decode(body, nil, review); err != nil {
return nil, fmt.Errorf("could not deserialize incoming admission review: %v", err)
}
if review.Request == nil {
return nil, fmt.Errorf("admission review can't be used: Request field is nil")
}
return review, nil
}

View File

@ -0,0 +1,134 @@
/*
Copyright 2023 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 app
import (
"testing"
"github.com/davecgh/go-spew/spew"
admissionv1 "k8s.io/api/admission/v1"
"k8s.io/cloud-provider/app/config"
cpconfig "k8s.io/cloud-provider/config"
"k8s.io/cloud-provider/fake"
)
func TestWebhookEnableDisable(t *testing.T) {
var (
cloud = &fake.Cloud{}
noOpAdmissionHandler = func(req *admissionv1.AdmissionRequest) (*admissionv1.AdmissionResponse, error) {
return &admissionv1.AdmissionResponse{}, nil
}
)
cases := []struct {
desc string
webhookConfigs map[string]WebhookConfig
completedConfig *config.CompletedConfig
expected map[string]webhookHandler
}{
{
"Webhooks Enabled",
map[string]WebhookConfig{
"webhook-a": {Path: "/path/a", AdmissionHandler: noOpAdmissionHandler},
"webhook-b": {Path: "/path/b", AdmissionHandler: noOpAdmissionHandler},
},
newConfig(cpconfig.WebhookConfiguration{Webhooks: []string{"webhook-a", "webhook-b"}}),
map[string]webhookHandler{
"webhook-a": {path: "/path/a", admissionHandler: noOpAdmissionHandler},
"webhook-b": {path: "/path/b", admissionHandler: noOpAdmissionHandler},
},
},
{
"Webhook Not Enabled",
map[string]WebhookConfig{
"webhook-a": {Path: "/path/a", AdmissionHandler: noOpAdmissionHandler},
"webhook-b": {Path: "/path/b", AdmissionHandler: noOpAdmissionHandler},
},
newConfig(cpconfig.WebhookConfiguration{Webhooks: []string{"webhook-a"}}),
map[string]webhookHandler{
"webhook-a": {path: "/path/a", admissionHandler: noOpAdmissionHandler},
},
},
{
"Webhook Disabled (1)",
map[string]WebhookConfig{
"webhook-a": {Path: "/path/a", AdmissionHandler: noOpAdmissionHandler},
"webhook-b": {Path: "/path/b", AdmissionHandler: noOpAdmissionHandler},
},
newConfig(cpconfig.WebhookConfiguration{Webhooks: []string{"webhook-a", "-webhook-b"}}),
map[string]webhookHandler{
"webhook-a": {path: "/path/a", admissionHandler: noOpAdmissionHandler},
},
},
{
"Webhook Disabled (2)",
map[string]WebhookConfig{
"webhook-a": {Path: "/path/a", AdmissionHandler: noOpAdmissionHandler},
"webhook-b": {Path: "/path/b", AdmissionHandler: noOpAdmissionHandler},
},
newConfig(cpconfig.WebhookConfiguration{Webhooks: []string{"-webhook-b"}}),
map[string]webhookHandler{},
},
{
"Webhooks Enabled Glob",
map[string]WebhookConfig{
"webhook-a": {Path: "/path/a", AdmissionHandler: noOpAdmissionHandler},
"webhook-b": {Path: "/path/b", AdmissionHandler: noOpAdmissionHandler},
},
newConfig(cpconfig.WebhookConfiguration{Webhooks: []string{"*"}}),
map[string]webhookHandler{
"webhook-a": {path: "/path/a", admissionHandler: noOpAdmissionHandler},
"webhook-b": {path: "/path/b", admissionHandler: noOpAdmissionHandler},
},
},
}
for _, tc := range cases {
t.Logf("Running %q", tc.desc)
actual := newWebhookHandlers(tc.webhookConfigs, tc.completedConfig, cloud)
if !webhookHandlersEqual(actual, tc.expected) {
t.Fatalf(
"FAILED: %q\n---\nActual:\n%s\nExpected:\n%s\ntc.webhookConfigs:\n%s\ntc.completedConfig:\n%s\n",
tc.desc,
spew.Sdump(actual),
spew.Sdump(tc.expected),
spew.Sdump(tc.webhookConfigs),
spew.Sdump(tc.completedConfig),
)
}
}
}
func newConfig(webhookConfig cpconfig.WebhookConfiguration) *config.CompletedConfig {
cfg := &config.Config{
ComponentConfig: cpconfig.CloudControllerManagerConfiguration{
Webhook: webhookConfig,
},
}
return cfg.Complete()
}
func webhookHandlersEqual(actual, expected map[string]webhookHandler) bool {
if len(actual) != len(expected) {
return false
}
for k := range expected {
if _, ok := actual[k]; !ok {
return false
}
}
return true
}

View File

@ -45,6 +45,9 @@ type CloudControllerManagerConfiguration struct {
// NodeStatusUpdateFrequency is the frequency at which the controller updates nodes' status // NodeStatusUpdateFrequency is the frequency at which the controller updates nodes' status
NodeStatusUpdateFrequency metav1.Duration NodeStatusUpdateFrequency metav1.Duration
// Webhook is the configuration for cloud-controller-manager hosted webhooks
Webhook WebhookConfiguration
} }
// KubeCloudSharedConfiguration contains elements shared by both kube-controller manager // KubeCloudSharedConfiguration contains elements shared by both kube-controller manager
@ -89,3 +92,12 @@ type CloudProviderConfiguration struct {
// cloudConfigFile is the path to the cloud provider configuration file. // cloudConfigFile is the path to the cloud provider configuration file.
CloudConfigFile string CloudConfigFile string
} }
type WebhookConfiguration struct {
// Webhooks is the list of webhooks to enable or disable
// '*' means "all enabled by default webhooks"
// 'foo' means "enable 'foo'"
// '-foo' means "disable 'foo'"
// first item for a particular name wins
Webhooks []string
}

View File

@ -25,6 +25,7 @@ import (
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// CloudControllerManagerConfiguration contains elements describing cloud-controller manager.
type CloudControllerManagerConfiguration struct { type CloudControllerManagerConfiguration struct {
metav1.TypeMeta `json:",inline"` metav1.TypeMeta `json:",inline"`
@ -41,6 +42,8 @@ type CloudControllerManagerConfiguration struct {
ServiceController serviceconfigv1alpha1.ServiceControllerConfiguration ServiceController serviceconfigv1alpha1.ServiceControllerConfiguration
// NodeStatusUpdateFrequency is the frequency at which the controller updates nodes' status // NodeStatusUpdateFrequency is the frequency at which the controller updates nodes' status
NodeStatusUpdateFrequency metav1.Duration NodeStatusUpdateFrequency metav1.Duration
// Webhook is the configuration for cloud-controller-manager hosted webhooks
Webhook WebhookConfiguration
} }
// KubeCloudSharedConfiguration contains elements shared by both kube-controller manager // KubeCloudSharedConfiguration contains elements shared by both kube-controller manager
@ -85,3 +88,14 @@ type CloudProviderConfiguration struct {
// cloudConfigFile is the path to the cloud provider configuration file. // cloudConfigFile is the path to the cloud provider configuration file.
CloudConfigFile string CloudConfigFile string
} }
// WebhookConfiguration contains configuration related to
// cloud-controller-manager hosted webhooks
type WebhookConfiguration struct {
// Webhooks is the list of webhooks to enable or disable
// '*' means "all enabled by default webhooks"
// 'foo' means "enable 'foo'"
// '-foo' means "disable 'foo'"
// first item for a particular name wins
Webhooks []string
}

View File

@ -22,6 +22,8 @@ limitations under the License.
package v1alpha1 package v1alpha1
import ( import (
unsafe "unsafe"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
conversion "k8s.io/apimachinery/pkg/conversion" conversion "k8s.io/apimachinery/pkg/conversion"
runtime "k8s.io/apimachinery/pkg/runtime" runtime "k8s.io/apimachinery/pkg/runtime"
@ -48,6 +50,16 @@ func RegisterConversions(s *runtime.Scheme) error {
}); err != nil { }); err != nil {
return err return err
} }
if err := s.AddGeneratedConversionFunc((*WebhookConfiguration)(nil), (*config.WebhookConfiguration)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_v1alpha1_WebhookConfiguration_To_config_WebhookConfiguration(a.(*WebhookConfiguration), b.(*config.WebhookConfiguration), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*config.WebhookConfiguration)(nil), (*WebhookConfiguration)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_config_WebhookConfiguration_To_v1alpha1_WebhookConfiguration(a.(*config.WebhookConfiguration), b.(*WebhookConfiguration), scope)
}); err != nil {
return err
}
if err := s.AddConversionFunc((*config.CloudProviderConfiguration)(nil), (*CloudProviderConfiguration)(nil), func(a, b interface{}, scope conversion.Scope) error { if err := s.AddConversionFunc((*config.CloudProviderConfiguration)(nil), (*CloudProviderConfiguration)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_config_CloudProviderConfiguration_To_v1alpha1_CloudProviderConfiguration(a.(*config.CloudProviderConfiguration), b.(*CloudProviderConfiguration), scope) return Convert_config_CloudProviderConfiguration_To_v1alpha1_CloudProviderConfiguration(a.(*config.CloudProviderConfiguration), b.(*CloudProviderConfiguration), scope)
}); err != nil { }); err != nil {
@ -85,6 +97,9 @@ func autoConvert_v1alpha1_CloudControllerManagerConfiguration_To_config_CloudCon
return err return err
} }
out.NodeStatusUpdateFrequency = in.NodeStatusUpdateFrequency out.NodeStatusUpdateFrequency = in.NodeStatusUpdateFrequency
if err := Convert_v1alpha1_WebhookConfiguration_To_config_WebhookConfiguration(&in.Webhook, &out.Webhook, s); err != nil {
return err
}
return nil return nil
} }
@ -107,6 +122,9 @@ func autoConvert_config_CloudControllerManagerConfiguration_To_v1alpha1_CloudCon
return err return err
} }
out.NodeStatusUpdateFrequency = in.NodeStatusUpdateFrequency out.NodeStatusUpdateFrequency = in.NodeStatusUpdateFrequency
if err := Convert_config_WebhookConfiguration_To_v1alpha1_WebhookConfiguration(&in.Webhook, &out.Webhook, s); err != nil {
return err
}
return nil return nil
} }
@ -166,3 +184,23 @@ func autoConvert_config_KubeCloudSharedConfiguration_To_v1alpha1_KubeCloudShared
out.NodeSyncPeriod = in.NodeSyncPeriod out.NodeSyncPeriod = in.NodeSyncPeriod
return nil return nil
} }
func autoConvert_v1alpha1_WebhookConfiguration_To_config_WebhookConfiguration(in *WebhookConfiguration, out *config.WebhookConfiguration, s conversion.Scope) error {
out.Webhooks = *(*[]string)(unsafe.Pointer(&in.Webhooks))
return nil
}
// Convert_v1alpha1_WebhookConfiguration_To_config_WebhookConfiguration is an autogenerated conversion function.
func Convert_v1alpha1_WebhookConfiguration_To_config_WebhookConfiguration(in *WebhookConfiguration, out *config.WebhookConfiguration, s conversion.Scope) error {
return autoConvert_v1alpha1_WebhookConfiguration_To_config_WebhookConfiguration(in, out, s)
}
func autoConvert_config_WebhookConfiguration_To_v1alpha1_WebhookConfiguration(in *config.WebhookConfiguration, out *WebhookConfiguration, s conversion.Scope) error {
out.Webhooks = *(*[]string)(unsafe.Pointer(&in.Webhooks))
return nil
}
// Convert_config_WebhookConfiguration_To_v1alpha1_WebhookConfiguration is an autogenerated conversion function.
func Convert_config_WebhookConfiguration_To_v1alpha1_WebhookConfiguration(in *config.WebhookConfiguration, out *WebhookConfiguration, s conversion.Scope) error {
return autoConvert_config_WebhookConfiguration_To_v1alpha1_WebhookConfiguration(in, out, s)
}

View File

@ -34,6 +34,7 @@ func (in *CloudControllerManagerConfiguration) DeepCopyInto(out *CloudController
out.NodeController = in.NodeController out.NodeController = in.NodeController
out.ServiceController = in.ServiceController out.ServiceController = in.ServiceController
out.NodeStatusUpdateFrequency = in.NodeStatusUpdateFrequency out.NodeStatusUpdateFrequency = in.NodeStatusUpdateFrequency
in.Webhook.DeepCopyInto(&out.Webhook)
return return
} }
@ -95,3 +96,24 @@ func (in *KubeCloudSharedConfiguration) DeepCopy() *KubeCloudSharedConfiguration
in.DeepCopyInto(out) in.DeepCopyInto(out)
return out return out
} }
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *WebhookConfiguration) DeepCopyInto(out *WebhookConfiguration) {
*out = *in
if in.Webhooks != nil {
in, out := &in.Webhooks, &out.Webhooks
*out = make([]string, len(*in))
copy(*out, *in)
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WebhookConfiguration.
func (in *WebhookConfiguration) DeepCopy() *WebhookConfiguration {
if in == nil {
return nil
}
out := new(WebhookConfiguration)
in.DeepCopyInto(out)
return out
}

View File

@ -34,6 +34,7 @@ func (in *CloudControllerManagerConfiguration) DeepCopyInto(out *CloudController
out.NodeController = in.NodeController out.NodeController = in.NodeController
out.ServiceController = in.ServiceController out.ServiceController = in.ServiceController
out.NodeStatusUpdateFrequency = in.NodeStatusUpdateFrequency out.NodeStatusUpdateFrequency = in.NodeStatusUpdateFrequency
in.Webhook.DeepCopyInto(&out.Webhook)
return return
} }
@ -90,3 +91,24 @@ func (in *KubeCloudSharedConfiguration) DeepCopy() *KubeCloudSharedConfiguration
in.DeepCopyInto(out) in.DeepCopyInto(out)
return out return out
} }
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *WebhookConfiguration) DeepCopyInto(out *WebhookConfiguration) {
*out = *in
if in.Webhooks != nil {
in, out := &in.Webhooks, &out.Webhooks
*out = make([]string, len(*in))
copy(*out, *in)
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WebhookConfiguration.
func (in *WebhookConfiguration) DeepCopy() *WebhookConfiguration {
if in == nil {
return nil
}
out := new(WebhookConfiguration)
in.DeepCopyInto(out)
return out
}

View File

@ -5,6 +5,7 @@ module k8s.io/cloud-provider
go 1.19 go 1.19
require ( require (
github.com/davecgh/go-spew v1.1.1
github.com/google/go-cmp v0.5.9 github.com/google/go-cmp v0.5.9
github.com/spf13/cobra v1.6.0 github.com/spf13/cobra v1.6.0
github.com/spf13/pflag v1.0.5 github.com/spf13/pflag v1.0.5
@ -30,7 +31,6 @@ require (
github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/coreos/go-semver v0.3.0 // indirect github.com/coreos/go-semver v0.3.0 // indirect
github.com/coreos/go-systemd/v22 v22.4.0 // indirect github.com/coreos/go-systemd/v22 v22.4.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/emicklei/go-restful/v3 v3.9.0 // indirect github.com/emicklei/go-restful/v3 v3.9.0 // indirect
github.com/evanphx/json-patch v4.12.0+incompatible // indirect github.com/evanphx/json-patch v4.12.0+incompatible // indirect
github.com/felixge/httpsnoop v1.0.3 // indirect github.com/felixge/httpsnoop v1.0.3 // indirect

View File

@ -65,12 +65,32 @@ type CloudControllerManagerOptions struct {
Master string Master string
WebhookServing *WebhookServingOptions
Webhook *WebhookOptions
// NodeStatusUpdateFrequency is the frequency at which the controller updates nodes' status // NodeStatusUpdateFrequency is the frequency at which the controller updates nodes' status
NodeStatusUpdateFrequency metav1.Duration NodeStatusUpdateFrequency metav1.Duration
} }
// ProviderDefaults are provided by the consumer when calling
// NewCloudControllerManagerOptions(), so that they can customize certain flag
// default values.
type ProviderDefaults struct {
// WebhookBindAddress is the default address. It can be overridden by "--webhook-bind-address".
WebhookBindAddress *net.IP
// WebhookBindPort is the default port. It can be overridden by "--webhook-bind-port".
WebhookBindPort *int
}
// NewCloudControllerManagerOptions creates a new ExternalCMServer with a default config. // NewCloudControllerManagerOptions creates a new ExternalCMServer with a default config.
func NewCloudControllerManagerOptions() (*CloudControllerManagerOptions, error) { func NewCloudControllerManagerOptions() (*CloudControllerManagerOptions, error) {
return NewCloudControllerManagerOptionsWithProviderDefaults(ProviderDefaults{})
}
// NewCloudControllerManagerOptionsWithProviderDefaults creates a new
// ExternalCMServer with a default config, but allows the cloud provider to
// override a select number of default option values.
func NewCloudControllerManagerOptionsWithProviderDefaults(defaults ProviderDefaults) (*CloudControllerManagerOptions, error) {
componentConfig, err := NewDefaultComponentConfig() componentConfig, err := NewDefaultComponentConfig()
if err != nil { if err != nil {
return nil, err return nil, err
@ -86,6 +106,8 @@ func NewCloudControllerManagerOptions() (*CloudControllerManagerOptions, error)
ServiceControllerConfiguration: &componentConfig.ServiceController, ServiceControllerConfiguration: &componentConfig.ServiceController,
}, },
SecureServing: apiserveroptions.NewSecureServingOptions().WithLoopback(), SecureServing: apiserveroptions.NewSecureServingOptions().WithLoopback(),
Webhook: NewWebhookOptions(),
WebhookServing: NewWebhookServingOptions(defaults),
Authentication: apiserveroptions.NewDelegatingAuthenticationOptions(), Authentication: apiserveroptions.NewDelegatingAuthenticationOptions(),
Authorization: apiserveroptions.NewDelegatingAuthorizationOptions(), Authorization: apiserveroptions.NewDelegatingAuthorizationOptions(),
NodeStatusUpdateFrequency: componentConfig.NodeStatusUpdateFrequency, NodeStatusUpdateFrequency: componentConfig.NodeStatusUpdateFrequency,
@ -119,12 +141,18 @@ func NewDefaultComponentConfig() (*ccmconfig.CloudControllerManagerConfiguration
} }
// Flags returns flags for a specific CloudController by section name // Flags returns flags for a specific CloudController by section name
func (o *CloudControllerManagerOptions) Flags(allControllers, disabledByDefaultControllers []string) cliflag.NamedFlagSets { func (o *CloudControllerManagerOptions) Flags(allControllers, disabledByDefaultControllers, allWebhooks, disabledByDefaultWebhooks []string) cliflag.NamedFlagSets {
fss := cliflag.NamedFlagSets{} fss := cliflag.NamedFlagSets{}
o.Generic.AddFlags(&fss, allControllers, disabledByDefaultControllers) o.Generic.AddFlags(&fss, allControllers, disabledByDefaultControllers)
o.KubeCloudShared.AddFlags(fss.FlagSet("generic")) o.KubeCloudShared.AddFlags(fss.FlagSet("generic"))
o.NodeController.AddFlags(fss.FlagSet("node controller")) o.NodeController.AddFlags(fss.FlagSet("node controller"))
o.ServiceController.AddFlags(fss.FlagSet("service controller")) o.ServiceController.AddFlags(fss.FlagSet("service controller"))
if o.Webhook != nil {
o.Webhook.AddFlags(fss.FlagSet("webhook"), allWebhooks, disabledByDefaultWebhooks)
}
if o.WebhookServing != nil {
o.WebhookServing.AddFlags(fss.FlagSet("webhook serving"))
}
o.SecureServing.AddFlags(fss.FlagSet("secure serving")) o.SecureServing.AddFlags(fss.FlagSet("secure serving"))
o.Authentication.AddFlags(fss.FlagSet("authentication")) o.Authentication.AddFlags(fss.FlagSet("authentication"))
@ -134,7 +162,6 @@ func (o *CloudControllerManagerOptions) Flags(allControllers, disabledByDefaultC
fs.StringVar(&o.Master, "master", o.Master, "The address of the Kubernetes API server (overrides any value in kubeconfig).") fs.StringVar(&o.Master, "master", o.Master, "The address of the Kubernetes API server (overrides any value in kubeconfig).")
fs.StringVar(&o.Generic.ClientConnection.Kubeconfig, "kubeconfig", o.Generic.ClientConnection.Kubeconfig, "Path to kubeconfig file with authorization and master location information (the master location can be overridden by the master flag).") fs.StringVar(&o.Generic.ClientConnection.Kubeconfig, "kubeconfig", o.Generic.ClientConnection.Kubeconfig, "Path to kubeconfig file with authorization and master location information (the master location can be overridden by the master flag).")
fs.DurationVar(&o.NodeStatusUpdateFrequency.Duration, "node-status-update-frequency", o.NodeStatusUpdateFrequency.Duration, "Specifies how often the controller updates nodes' status.") fs.DurationVar(&o.NodeStatusUpdateFrequency.Duration, "node-status-update-frequency", o.NodeStatusUpdateFrequency.Duration, "Specifies how often the controller updates nodes' status.")
utilfeature.DefaultMutableFeatureGate.AddFlag(fss.FlagSet("generic")) utilfeature.DefaultMutableFeatureGate.AddFlag(fss.FlagSet("generic"))
return fss return fss
@ -166,6 +193,16 @@ func (o *CloudControllerManagerOptions) ApplyTo(c *config.Config, userAgent stri
if err = o.ServiceController.ApplyTo(&c.ComponentConfig.ServiceController); err != nil { if err = o.ServiceController.ApplyTo(&c.ComponentConfig.ServiceController); err != nil {
return err return err
} }
if o.Webhook != nil {
if err = o.Webhook.ApplyTo(&c.ComponentConfig.Webhook); err != nil {
return err
}
}
if o.WebhookServing != nil {
if err = o.WebhookServing.ApplyTo(&c.WebhookSecureServing); err != nil {
return err
}
}
if err = o.SecureServing.ApplyTo(&c.SecureServing, &c.LoopbackClientConfig); err != nil { if err = o.SecureServing.ApplyTo(&c.SecureServing, &c.LoopbackClientConfig); err != nil {
return err return err
} }
@ -209,7 +246,7 @@ func (o *CloudControllerManagerOptions) ApplyTo(c *config.Config, userAgent stri
} }
// Validate is used to validate config before launching the cloud controller manager // Validate is used to validate config before launching the cloud controller manager
func (o *CloudControllerManagerOptions) Validate(allControllers, disabledByDefaultControllers []string) error { func (o *CloudControllerManagerOptions) Validate(allControllers, disabledByDefaultControllers, allWebhooks, disabledByDefaultWebhooks []string) error {
errors := []error{} errors := []error{}
errors = append(errors, o.Generic.Validate(allControllers, disabledByDefaultControllers)...) errors = append(errors, o.Generic.Validate(allControllers, disabledByDefaultControllers)...)
@ -219,6 +256,16 @@ func (o *CloudControllerManagerOptions) Validate(allControllers, disabledByDefau
errors = append(errors, o.Authentication.Validate()...) errors = append(errors, o.Authentication.Validate()...)
errors = append(errors, o.Authorization.Validate()...) errors = append(errors, o.Authorization.Validate()...)
if o.Webhook != nil {
errors = append(errors, o.Webhook.Validate(allWebhooks, disabledByDefaultWebhooks)...)
}
if o.WebhookServing != nil {
errors = append(errors, o.WebhookServing.Validate()...)
if o.WebhookServing.BindPort == o.SecureServing.BindPort {
errors = append(errors, fmt.Errorf("--webhook-secure-port cannot be the same value as --secure-port"))
}
}
if len(o.KubeCloudShared.CloudProvider.Name) == 0 { if len(o.KubeCloudShared.CloudProvider.Name) == 0 {
errors = append(errors, fmt.Errorf("--cloud-provider cannot be empty")) errors = append(errors, fmt.Errorf("--cloud-provider cannot be empty"))
} }
@ -235,8 +282,8 @@ func resyncPeriod(c *config.Config) func() time.Duration {
} }
// Config return a cloud controller manager config objective // Config return a cloud controller manager config objective
func (o *CloudControllerManagerOptions) Config(allControllers, disabledByDefaultControllers []string) (*config.Config, error) { func (o *CloudControllerManagerOptions) Config(allControllers, disabledByDefaultControllers, allWebhooks, disabledByDefaultWebhooks []string) (*config.Config, error) {
if err := o.Validate(allControllers, disabledByDefaultControllers); err != nil { if err := o.Validate(allControllers, disabledByDefaultControllers, allWebhooks, disabledByDefaultWebhooks); err != nil {
return nil, err return nil, err
} }
@ -244,6 +291,12 @@ func (o *CloudControllerManagerOptions) Config(allControllers, disabledByDefault
return nil, fmt.Errorf("error creating self-signed certificates: %v", err) return nil, fmt.Errorf("error creating self-signed certificates: %v", err)
} }
if o.WebhookServing != nil {
if err := o.WebhookServing.MaybeDefaultWithSelfSignedCerts("localhost", nil, []net.IP{netutils.ParseIPSloppy("127.0.0.1")}); err != nil {
return nil, fmt.Errorf("error creating self-signed certificates for webhook: %v", err)
}
}
c := &config.Config{} c := &config.Config{}
if err := o.ApplyTo(c, CloudControllerManagerUserAgent); err != nil { if err := o.ApplyTo(c, CloudControllerManagerUserAgent); err != nil {
return nil, err return nil, err

View File

@ -17,6 +17,8 @@ limitations under the License.
package options package options
import ( import (
"fmt"
"os"
"reflect" "reflect"
"testing" "testing"
"time" "time"
@ -24,7 +26,9 @@ import (
"github.com/spf13/pflag" "github.com/spf13/pflag"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/diff" "k8s.io/apimachinery/pkg/util/diff"
apiserver "k8s.io/apiserver/pkg/server"
apiserveroptions "k8s.io/apiserver/pkg/server/options" apiserveroptions "k8s.io/apiserver/pkg/server/options"
appconfig "k8s.io/cloud-provider/app/config"
cpconfig "k8s.io/cloud-provider/config" cpconfig "k8s.io/cloud-provider/config"
nodeconfig "k8s.io/cloud-provider/controllers/node/config" nodeconfig "k8s.io/cloud-provider/controllers/node/config"
serviceconfig "k8s.io/cloud-provider/controllers/service/config" serviceconfig "k8s.io/cloud-provider/controllers/service/config"
@ -33,10 +37,15 @@ import (
cmoptions "k8s.io/controller-manager/options" cmoptions "k8s.io/controller-manager/options"
migration "k8s.io/controller-manager/pkg/leadermigration/options" migration "k8s.io/controller-manager/pkg/leadermigration/options"
netutils "k8s.io/utils/net" netutils "k8s.io/utils/net"
"github.com/stretchr/testify/assert"
) )
func TestDefaultFlags(t *testing.T) { func TestDefaultFlags(t *testing.T) {
s, _ := NewCloudControllerManagerOptions() s, err := NewCloudControllerManagerOptions()
if err != nil {
t.Errorf("unexpected err: %v", err)
}
expected := &CloudControllerManagerOptions{ expected := &CloudControllerManagerOptions{
Generic: &cmoptions.GenericControllerManagerConfigurationOptions{ Generic: &cmoptions.GenericControllerManagerConfigurationOptions{
@ -96,6 +105,17 @@ func TestDefaultFlags(t *testing.T) {
ConcurrentServiceSyncs: 1, ConcurrentServiceSyncs: 1,
}, },
}, },
Webhook: &WebhookOptions{},
WebhookServing: &WebhookServingOptions{
SecureServingOptions: &apiserveroptions.SecureServingOptions{
ServerCert: apiserveroptions.GeneratableKeyCert{
CertDirectory: "",
PairName: "cloud-controller-manager-webhook",
},
BindPort: 10260,
BindAddress: netutils.ParseIPSloppy("0.0.0.0"),
},
},
SecureServing: (&apiserveroptions.SecureServingOptions{ SecureServing: (&apiserveroptions.SecureServingOptions{
BindPort: 10258, BindPort: 10258,
BindAddress: netutils.ParseIPSloppy("0.0.0.0"), BindAddress: netutils.ParseIPSloppy("0.0.0.0"),
@ -136,12 +156,13 @@ func TestDefaultFlags(t *testing.T) {
func TestAddFlags(t *testing.T) { func TestAddFlags(t *testing.T) {
fs := pflag.NewFlagSet("addflagstest", pflag.ContinueOnError) fs := pflag.NewFlagSet("addflagstest", pflag.ContinueOnError)
s, err := NewCloudControllerManagerOptions() s, err := NewCloudControllerManagerOptions()
if err != nil { if err != nil {
t.Errorf("unexpected err: %v", err) t.Errorf("unexpected err: %v", err)
} }
for _, f := range s.Flags([]string{""}, []string{""}).FlagSets { for _, f := range s.Flags([]string{""}, []string{""}, []string{""}, []string{""}).FlagSets {
fs.AddFlagSet(f) fs.AddFlagSet(f)
} }
@ -176,6 +197,7 @@ func TestAddFlags(t *testing.T) {
"--secure-port=10001", "--secure-port=10001",
"--use-service-account-credentials=false", "--use-service-account-credentials=false",
"--concurrent-node-syncs=5", "--concurrent-node-syncs=5",
"--webhooks=foo,bar,-baz",
} }
err = fs.Parse(args) err = fs.Parse(args)
if err != nil { if err != nil {
@ -240,6 +262,19 @@ func TestAddFlags(t *testing.T) {
ConcurrentServiceSyncs: 1, ConcurrentServiceSyncs: 1,
}, },
}, },
Webhook: &WebhookOptions{
Webhooks: []string{"foo", "bar", "-baz"},
},
WebhookServing: &WebhookServingOptions{
SecureServingOptions: &apiserveroptions.SecureServingOptions{
ServerCert: apiserveroptions.GeneratableKeyCert{
CertDirectory: "",
PairName: "cloud-controller-manager-webhook",
},
BindPort: 10260,
BindAddress: netutils.ParseIPSloppy("0.0.0.0"),
},
},
SecureServing: (&apiserveroptions.SecureServingOptions{ SecureServing: (&apiserveroptions.SecureServingOptions{
BindPort: 10001, BindPort: 10001,
BindAddress: netutils.ParseIPSloppy("192.168.4.21"), BindAddress: netutils.ParseIPSloppy("192.168.4.21"),
@ -277,3 +312,138 @@ func TestAddFlags(t *testing.T) {
t.Errorf("Got different run options than expected.\nDifference detected on:\n%s", diff.ObjectReflectDiff(expected, s)) t.Errorf("Got different run options than expected.\nDifference detected on:\n%s", diff.ObjectReflectDiff(expected, s))
} }
} }
func TestCreateConfig(t *testing.T) {
fs := pflag.NewFlagSet("addflagstest", pflag.ContinueOnError)
s, err := NewCloudControllerManagerOptions()
if err != nil {
t.Errorf("unexpected err: %v", err)
}
for _, f := range s.Flags([]string{""}, []string{""}, []string{""}, []string{""}).FlagSets {
fs.AddFlagSet(f)
}
tmpdir, err := os.MkdirTemp("", "options_test")
if err != nil {
t.Fatalf("%s", err)
}
defer os.RemoveAll(tmpdir)
args := []string{
"--webhooks=foo,bar,-baz",
"--allocate-node-cidrs=true",
"--authorization-always-allow-paths=",
"--bind-address=0.0.0.0",
"--secure-port=10200",
fmt.Sprintf("--cert-dir=%s/certs", tmpdir),
"--cloud-provider=aws",
"--cluster-cidr=1.2.3.4/24",
"--cluster-name=k8s",
"--configure-cloud-routes=false",
"--contention-profiling=true",
"--controller-start-interval=2m",
"--controllers=foo,bar",
"--concurrent-node-syncs=1",
"--http2-max-streams-per-connection=47",
"--kube-api-burst=101",
"--kube-api-content-type=application/vnd.kubernetes.protobuf",
"--kube-api-qps=50.0",
"--leader-elect=false",
"--leader-elect-lease-duration=30s",
"--leader-elect-renew-deadline=15s",
"--leader-elect-resource-lock=configmap",
"--leader-elect-retry-period=5s",
"--master=192.168.4.20",
"--min-resync-period=100m",
"--node-status-update-frequency=10m",
"--profiling=false",
"--route-reconciliation-period=30s",
"--use-service-account-credentials=false",
"--webhook-bind-address=0.0.0.0",
"--webhook-secure-port=10300",
}
err = fs.Parse(args)
assert.Nil(t, err, "unexpected error: %s", err)
fs.VisitAll(func(f *pflag.Flag) {
fmt.Printf("%s: %s\n", f.Name, f.Value)
})
c, err := s.Config([]string{"foo", "bar"}, []string{}, []string{"foo", "bar", "baz"}, []string{})
assert.Nil(t, err, "unexpected error: %s", err)
expected := &appconfig.Config{
ComponentConfig: cpconfig.CloudControllerManagerConfiguration{
Generic: cmconfig.GenericControllerManagerConfiguration{
Address: "0.0.0.0",
MinResyncPeriod: metav1.Duration{Duration: 100 * time.Minute},
ClientConnection: componentbaseconfig.ClientConnectionConfiguration{
ContentType: "application/vnd.kubernetes.protobuf",
QPS: 50.0,
Burst: 101,
},
ControllerStartInterval: metav1.Duration{Duration: 2 * time.Minute},
LeaderElection: componentbaseconfig.LeaderElectionConfiguration{
ResourceLock: "configmap",
LeaderElect: false,
LeaseDuration: metav1.Duration{Duration: 30 * time.Second},
RenewDeadline: metav1.Duration{Duration: 15 * time.Second},
RetryPeriod: metav1.Duration{Duration: 5 * time.Second},
ResourceName: "cloud-controller-manager",
ResourceNamespace: "kube-system",
},
Controllers: []string{"foo", "bar"},
Debugging: componentbaseconfig.DebuggingConfiguration{
EnableProfiling: false,
EnableContentionProfiling: true,
},
LeaderMigration: cmconfig.LeaderMigrationConfiguration{},
},
KubeCloudShared: cpconfig.KubeCloudSharedConfiguration{
RouteReconciliationPeriod: metav1.Duration{Duration: 30 * time.Second},
NodeMonitorPeriod: metav1.Duration{Duration: 5 * time.Second},
ClusterName: "k8s",
ClusterCIDR: "1.2.3.4/24",
AllocateNodeCIDRs: true,
CIDRAllocatorType: "RangeAllocator",
ConfigureCloudRoutes: false,
CloudProvider: cpconfig.CloudProviderConfiguration{
Name: "aws",
CloudConfigFile: "",
},
},
ServiceController: serviceconfig.ServiceControllerConfiguration{
ConcurrentServiceSyncs: 1,
},
NodeController: nodeconfig.NodeControllerConfiguration{ConcurrentNodeSyncs: 1},
NodeStatusUpdateFrequency: metav1.Duration{Duration: 10 * time.Minute},
Webhook: cpconfig.WebhookConfiguration{
Webhooks: []string{"foo", "bar", "-baz"},
},
},
SecureServing: nil,
WebhookSecureServing: nil,
Authentication: apiserver.AuthenticationInfo{},
Authorization: apiserver.AuthorizationInfo{},
}
// Don't check
c.SecureServing = nil
c.WebhookSecureServing = nil
c.Authentication = apiserver.AuthenticationInfo{}
c.Authorization = apiserver.AuthorizationInfo{}
c.SharedInformers = nil
c.VersionedClient = nil
c.ClientBuilder = nil
c.EventRecorder = nil
c.EventBroadcaster = nil
c.Kubeconfig = nil
c.Client = nil
c.LoopbackClientConfig = nil
if !reflect.DeepEqual(expected, c) {
t.Errorf("Got different config than expected.\nDifference detected on:\n%s", diff.ObjectReflectDiff(expected, c))
}
}

View File

@ -0,0 +1,206 @@
/*
Copyright 2022 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 options
import (
"context"
"fmt"
"net"
"strconv"
"strings"
"github.com/spf13/pflag"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apiserver/pkg/server"
"k8s.io/apiserver/pkg/server/dynamiccertificates"
apiserveroptions "k8s.io/apiserver/pkg/server/options"
"k8s.io/cloud-provider/config"
netutils "k8s.io/utils/net"
)
const (
CloudControllerManagerWebhookPort = 10260
)
type WebhookOptions struct {
// Webhooks is the list of webhook names that should be enabled or disabled
Webhooks []string
}
func NewWebhookOptions() *WebhookOptions {
o := &WebhookOptions{}
return o
}
func (o *WebhookOptions) AddFlags(fs *pflag.FlagSet, allWebhooks, disabledByDefaultWebhooks []string) {
fs.StringSliceVar(&o.Webhooks, "webhooks", o.Webhooks, fmt.Sprintf(""+
"A list of webhooks to enable. '*' enables all on-by-default webhooks, 'foo' enables the webhook "+
"named 'foo', '-foo' disables the webhook named 'foo'.\nAll webhooks: %s\nDisabled-by-default webhooks: %s",
strings.Join(allWebhooks, ", "), strings.Join(disabledByDefaultWebhooks, ", ")))
}
func (o *WebhookOptions) Validate(allWebhooks, disabledByDefaultWebhooks []string) []error {
allErrors := []error{}
allWebhooksSet := sets.NewString(allWebhooks...)
toValidate := sets.NewString(o.Webhooks...)
toValidate.Insert(disabledByDefaultWebhooks...)
for _, webhook := range toValidate.List() {
if webhook == "*" {
continue
}
webhook = strings.TrimPrefix(webhook, "-")
if !allWebhooksSet.Has(webhook) {
allErrors = append(allErrors, fmt.Errorf("%q is not in the list of known webhooks", webhook))
}
}
return allErrors
}
func (o *WebhookOptions) ApplyTo(cfg *config.WebhookConfiguration) error {
if o == nil {
return nil
}
cfg.Webhooks = o.Webhooks
return nil
}
type WebhookServingOptions struct {
*apiserveroptions.SecureServingOptions
}
func NewWebhookServingOptions(defaults ProviderDefaults) *WebhookServingOptions {
var (
bindAddress net.IP
bindPort int
)
if defaults.WebhookBindAddress != nil {
bindAddress = *defaults.WebhookBindAddress
} else {
bindAddress = netutils.ParseIPSloppy("0.0.0.0")
}
if defaults.WebhookBindPort != nil {
bindPort = *defaults.WebhookBindPort
} else {
bindPort = CloudControllerManagerWebhookPort
}
return &WebhookServingOptions{
SecureServingOptions: &apiserveroptions.SecureServingOptions{
BindAddress: bindAddress,
BindPort: bindPort,
ServerCert: apiserveroptions.GeneratableKeyCert{
CertDirectory: "",
PairName: "cloud-controller-manager-webhook",
},
},
}
}
func (o *WebhookServingOptions) AddFlags(fs *pflag.FlagSet) {
fs.IPVar(&o.BindAddress, "webhook-bind-address", o.BindAddress, ""+
"The IP address on which to listen for the --webhook-secure-port port. The "+
"associated interface(s) must be reachable by the rest of the cluster, and by CLI/web "+
fmt.Sprintf("clients. If set to an unspecified address (0.0.0.0 or ::), all interfaces will be used. If unset, defaults to %v.", o.BindAddress))
fs.IntVar(&o.BindPort, "webhook-secure-port", o.BindPort, fmt.Sprintf("Secure port to serve cloud provider webhooks. If unset, defaults to %d.", o.BindPort))
fs.StringVar(&o.ServerCert.CertDirectory, "webhook-cert-dir", o.ServerCert.CertDirectory, ""+
"The directory where the TLS certs are located. "+
"If --tls-cert-file and --tls-private-key-file are provided, this flag will be ignored.")
fs.StringVar(&o.ServerCert.CertKey.CertFile, "webhook-tls-cert-file", o.ServerCert.CertKey.CertFile, ""+
"File containing the default x509 Certificate for HTTPS. (CA cert, if any, concatenated "+
"after server cert). If HTTPS serving is enabled, and --tls-cert-file and "+
"--tls-private-key-file are not provided, a self-signed certificate and key "+
"are generated for the public address and saved to the directory specified by --cert-dir.")
fs.StringVar(&o.ServerCert.CertKey.KeyFile, "webhook-tls-private-key-file", o.ServerCert.CertKey.KeyFile,
"File containing the default x509 private key matching --tls-cert-file.")
}
func (o *WebhookServingOptions) Validate() []error {
allErrors := []error{}
if o.BindPort < 0 || o.BindPort > 65535 {
allErrors = append(allErrors, fmt.Errorf("--webhook-secure-port %v must be between 0 and 65535, inclusive. A value of 0 disables the webhook endpoint entirely.", o.BindPort))
}
if (len(o.ServerCert.CertKey.CertFile) != 0 || len(o.ServerCert.CertKey.KeyFile) != 0) && o.ServerCert.GeneratedCert != nil {
allErrors = append(allErrors, fmt.Errorf("cert/key file and in-memory certificate cannot both be set"))
}
return allErrors
}
func (o *WebhookServingOptions) ApplyTo(cfg **server.SecureServingInfo) error {
if o == nil {
return nil
}
if o.BindPort <= 0 {
return nil
}
var err error
var listener net.Listener
addr := net.JoinHostPort(o.BindAddress.String(), strconv.Itoa(o.BindPort))
l := net.ListenConfig{}
listener, o.BindPort, err = createListener(addr, l)
if err != nil {
return fmt.Errorf("failed to create listener: %v", err)
}
*cfg = &server.SecureServingInfo{
Listener: listener,
}
serverCertFile, serverKeyFile := o.ServerCert.CertKey.CertFile, o.ServerCert.CertKey.KeyFile
if len(serverCertFile) != 0 || len(serverKeyFile) != 0 {
var err error
(*cfg).Cert, err = dynamiccertificates.NewDynamicServingContentFromFiles("serving-cert", serverCertFile, serverKeyFile)
if err != nil {
return err
}
} else if o.ServerCert.GeneratedCert != nil {
(*cfg).Cert = o.ServerCert.GeneratedCert
}
return nil
}
func createListener(addr string, config net.ListenConfig) (net.Listener, int, error) {
ln, err := config.Listen(context.TODO(), "tcp", addr)
if err != nil {
return nil, 0, fmt.Errorf("failed to listen on %v: %v", addr, err)
}
// get port
tcpAddr, ok := ln.Addr().(*net.TCPAddr)
if !ok {
ln.Close()
return nil, 0, fmt.Errorf("invalid listen address: %q", ln.Addr().String())
}
return ln, tcpAddr.Port, nil
}

View File

@ -39,6 +39,12 @@ const (
// Enables less load balancer re-configurations by the service controller // Enables less load balancer re-configurations by the service controller
// (KCCM) as an effect of changing node state. // (KCCM) as an effect of changing node state.
StableLoadBalancerNodeSet featuregate.Feature = "StableLoadBalancerNodeSet" StableLoadBalancerNodeSet featuregate.Feature = "StableLoadBalancerNodeSet"
// owner: @nckturner
// kep: http://kep.k8s.io/2699
// alpha: v1.27
// Enable webhook in cloud controller manager
CloudControllerManagerWebhook featuregate.Feature = "CloudControllerManagerWebhook"
) )
func SetupCurrentKubernetesSpecificFeatureGates(featuregates featuregate.MutableFeatureGate) error { func SetupCurrentKubernetesSpecificFeatureGates(featuregates featuregate.MutableFeatureGate) error {
@ -48,5 +54,6 @@ func SetupCurrentKubernetesSpecificFeatureGates(featuregates featuregate.Mutable
// cloudPublicFeatureGates consists of cloud-specific feature keys. // cloudPublicFeatureGates consists of cloud-specific feature keys.
// To add a new feature, define a key for it at k8s.io/api/pkg/features and add it here. // To add a new feature, define a key for it at k8s.io/api/pkg/features and add it here.
var cloudPublicFeatureGates = map[featuregate.Feature]featuregate.FeatureSpec{ var cloudPublicFeatureGates = map[featuregate.Feature]featuregate.FeatureSpec{
StableLoadBalancerNodeSet: {Default: true, PreRelease: featuregate.Beta}, StableLoadBalancerNodeSet: {Default: true, PreRelease: featuregate.Beta},
CloudControllerManagerWebhook: {Default: false, PreRelease: featuregate.Alpha},
} }

View File

@ -165,7 +165,7 @@ users:
extraFlags []string extraFlags []string
}{ }{
{"kube-controller-manager", kubeControllerManagerTester{}, nil}, {"kube-controller-manager", kubeControllerManagerTester{}, nil},
{"cloud-controller-manager", cloudControllerManagerTester{}, []string{"--cloud-provider=fake"}}, {"cloud-controller-manager", cloudControllerManagerTester{}, []string{"--cloud-provider=fake", "--webhook-secure-port=0"}},
{"kube-scheduler", kubeSchedulerTester{}, nil}, {"kube-scheduler", kubeSchedulerTester{}, nil},
} }