generic-controlplane: add generic-controlplane apiserver sample
Signed-off-by: Dr. Stefan Schimanski <stefan.schimanski@gmail.com> generic Signed-off-by: Dr. Stefan Schimanski <stefan.schimanski@gmail.com>
This commit is contained in:
		
							
								
								
									
										23
									
								
								pkg/controlplane/apiserver/samples/doc.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								pkg/controlplane/apiserver/samples/doc.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| /* | ||||
| Copyright 2024 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 samples contains two kube-like generic control plane apiserver, one | ||||
| // with CRDs (generic) and one without (minimum). | ||||
| // | ||||
| // They are here mainly to preserve the feasibility to construct these kind of | ||||
| // control planes. Eventually, we might promote them to be example for 3rd parties | ||||
| // to follow. | ||||
| package samples | ||||
							
								
								
									
										13
									
								
								pkg/controlplane/apiserver/samples/generic/OWNERS
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								pkg/controlplane/apiserver/samples/generic/OWNERS
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | ||||
| # See the OWNERS docs at https://go.k8s.io/owners | ||||
|  | ||||
| approvers: | ||||
|   - sttts | ||||
|   - deads2k | ||||
|   - jpbetz | ||||
| reviewers: | ||||
|   - sttts | ||||
|   - deads2k | ||||
|   - jpbetz | ||||
| labels: | ||||
|   - sig/api-machinery | ||||
|   - area/apiserver | ||||
							
								
								
									
										22
									
								
								pkg/controlplane/apiserver/samples/generic/doc.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								pkg/controlplane/apiserver/samples/generic/doc.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | ||||
| /* | ||||
| Copyright 2024 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. | ||||
| */ | ||||
|  | ||||
| // sample-generic-controlplane is a kube-like generic control plane | ||||
| // - with CRDs | ||||
| // - with generic Kube native APIs | ||||
| // - with aggregation | ||||
| // - without the container domain specific APIs. | ||||
| package main | ||||
							
								
								
									
										36
									
								
								pkg/controlplane/apiserver/samples/generic/main.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								pkg/controlplane/apiserver/samples/generic/main.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,36 @@ | ||||
| /* | ||||
| 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. | ||||
| */ | ||||
|  | ||||
| // sample-generic-controlplane is a kube-like generic control plane | ||||
| // It is compatible to kube-apiserver, but lacks the container domain | ||||
| // specific APIs. | ||||
| package main | ||||
|  | ||||
| import ( | ||||
| 	"os" | ||||
|  | ||||
| 	"k8s.io/component-base/cli" | ||||
| 	_ "k8s.io/component-base/logs/json/register" | ||||
| 	_ "k8s.io/component-base/metrics/prometheus/clientgo" | ||||
| 	_ "k8s.io/component-base/metrics/prometheus/version" | ||||
| 	"k8s.io/kubernetes/pkg/controlplane/apiserver/samples/generic/server" | ||||
| ) | ||||
|  | ||||
| func main() { | ||||
| 	command := server.NewCommand() | ||||
| 	code := cli.Run(command) | ||||
| 	os.Exit(code) | ||||
| } | ||||
| @@ -0,0 +1,52 @@ | ||||
| /* | ||||
| Copyright 2024 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 server | ||||
|  | ||||
| import ( | ||||
| 	"k8s.io/apimachinery/pkg/util/sets" | ||||
| 	"k8s.io/apiserver/pkg/admission/plugin/namespace/lifecycle" | ||||
| 	validatingadmissionpolicy "k8s.io/apiserver/pkg/admission/plugin/policy/validating" | ||||
| 	"k8s.io/apiserver/pkg/admission/plugin/resourcequota" | ||||
| 	mutatingwebhook "k8s.io/apiserver/pkg/admission/plugin/webhook/mutating" | ||||
| 	validatingwebhook "k8s.io/apiserver/pkg/admission/plugin/webhook/validating" | ||||
| 	"k8s.io/kubernetes/pkg/kubeapiserver/options" | ||||
| 	certapproval "k8s.io/kubernetes/plugin/pkg/admission/certificates/approval" | ||||
| 	"k8s.io/kubernetes/plugin/pkg/admission/certificates/ctbattest" | ||||
| 	certsigning "k8s.io/kubernetes/plugin/pkg/admission/certificates/signing" | ||||
| 	certsubjectrestriction "k8s.io/kubernetes/plugin/pkg/admission/certificates/subjectrestriction" | ||||
| 	"k8s.io/kubernetes/plugin/pkg/admission/defaulttolerationseconds" | ||||
| 	"k8s.io/kubernetes/plugin/pkg/admission/serviceaccount" | ||||
| ) | ||||
|  | ||||
| // DefaultOffAdmissionPlugins get admission plugins off by default for kube-apiserver. | ||||
| func DefaultOffAdmissionPlugins() sets.Set[string] { | ||||
| 	defaultOnPlugins := sets.New[string]( | ||||
| 		lifecycle.PluginName,                 // NamespaceLifecycle | ||||
| 		serviceaccount.PluginName,            // ServiceAccount | ||||
| 		defaulttolerationseconds.PluginName,  // DefaultTolerationSeconds | ||||
| 		mutatingwebhook.PluginName,           // MutatingAdmissionWebhook | ||||
| 		validatingwebhook.PluginName,         // ValidatingAdmissionWebhook | ||||
| 		resourcequota.PluginName,             // ResourceQuota | ||||
| 		certapproval.PluginName,              // CertificateApproval | ||||
| 		certsigning.PluginName,               // CertificateSigning | ||||
| 		ctbattest.PluginName,                 // ClusterTrustBundleAttest | ||||
| 		certsubjectrestriction.PluginName,    // CertificateSubjectRestriction | ||||
| 		validatingadmissionpolicy.PluginName, // ValidatingAdmissionPolicy, only active when feature gate ValidatingAdmissionPolicy is enabled | ||||
| 	) | ||||
|  | ||||
| 	return sets.New(options.AllOrderedPlugins...).Difference(defaultOnPlugins) | ||||
| } | ||||
| @@ -0,0 +1,55 @@ | ||||
| /* | ||||
| Copyright 2024 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 server | ||||
|  | ||||
| import ( | ||||
| 	"testing" | ||||
|  | ||||
| 	"k8s.io/apimachinery/pkg/util/sets" | ||||
| 	kubeoptions "k8s.io/kubernetes/pkg/kubeapiserver/options" | ||||
| 	"k8s.io/kubernetes/plugin/pkg/admission/limitranger" | ||||
| 	"k8s.io/kubernetes/plugin/pkg/admission/network/defaultingressclass" | ||||
| 	"k8s.io/kubernetes/plugin/pkg/admission/nodetaint" | ||||
| 	podpriority "k8s.io/kubernetes/plugin/pkg/admission/priority" | ||||
| 	"k8s.io/kubernetes/plugin/pkg/admission/runtimeclass" | ||||
| 	"k8s.io/kubernetes/plugin/pkg/admission/security/podsecurity" | ||||
| 	"k8s.io/kubernetes/plugin/pkg/admission/storage/persistentvolume/resize" | ||||
| 	"k8s.io/kubernetes/plugin/pkg/admission/storage/storageclass/setdefault" | ||||
| 	"k8s.io/kubernetes/plugin/pkg/admission/storage/storageobjectinuseprotection" | ||||
| ) | ||||
|  | ||||
| var intentionallyOffPlugins = sets.New[string]( | ||||
| 	limitranger.PluginName,                  // LimitRanger | ||||
| 	setdefault.PluginName,                   // DefaultStorageClass | ||||
| 	resize.PluginName,                       // PersistentVolumeClaimResize | ||||
| 	storageobjectinuseprotection.PluginName, // StorageObjectInUseProtection | ||||
| 	podpriority.PluginName,                  // Priority | ||||
| 	nodetaint.PluginName,                    // TaintNodesByCondition | ||||
| 	runtimeclass.PluginName,                 // RuntimeClass | ||||
| 	defaultingressclass.PluginName,          // DefaultIngressClass | ||||
| 	podsecurity.PluginName,                  // PodSecurity | ||||
| ) | ||||
|  | ||||
| func TestDefaultOffAdmissionPlugins(t *testing.T) { | ||||
| 	expectedOff := kubeoptions.DefaultOffAdmissionPlugins().Union(intentionallyOffPlugins) | ||||
| 	if missing := DefaultOffAdmissionPlugins().Difference(expectedOff); missing.Len() > 0 { | ||||
| 		t.Fatalf("generic DefaultOffAdmissionPlugins() is incomplete, double check: %v", missing) | ||||
| 	} | ||||
| 	if unexpected := expectedOff.Difference(DefaultOffAdmissionPlugins()); unexpected.Len() > 0 { | ||||
| 		t.Fatalf("generic DefaultOffAdmissionPlugins() has unepxeced plugins, double check: %v", unexpected) | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										112
									
								
								pkg/controlplane/apiserver/samples/generic/server/config.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								pkg/controlplane/apiserver/samples/generic/server/config.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,112 @@ | ||||
| /* | ||||
| 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 server | ||||
|  | ||||
| import ( | ||||
| 	apiextensionsapiserver "k8s.io/apiextensions-apiserver/pkg/apiserver" | ||||
| 	"k8s.io/apimachinery/pkg/runtime" | ||||
| 	"k8s.io/apiserver/pkg/util/webhook" | ||||
| 	aggregatorapiserver "k8s.io/kube-aggregator/pkg/apiserver" | ||||
| 	aggregatorscheme "k8s.io/kube-aggregator/pkg/apiserver/scheme" | ||||
| 	"k8s.io/kubernetes/pkg/controlplane" | ||||
|  | ||||
| 	"k8s.io/kubernetes/pkg/api/legacyscheme" | ||||
| 	controlplaneapiserver "k8s.io/kubernetes/pkg/controlplane/apiserver" | ||||
| 	"k8s.io/kubernetes/pkg/controlplane/apiserver/options" | ||||
| 	generatedopenapi "k8s.io/kubernetes/pkg/generated/openapi" | ||||
| ) | ||||
|  | ||||
| type Config struct { | ||||
| 	Options options.CompletedOptions | ||||
|  | ||||
| 	Aggregator    *aggregatorapiserver.Config | ||||
| 	ControlPlane  *controlplaneapiserver.Config | ||||
| 	APIExtensions *apiextensionsapiserver.Config | ||||
|  | ||||
| 	ExtraConfig | ||||
| } | ||||
|  | ||||
| type ExtraConfig struct { | ||||
| } | ||||
|  | ||||
| type completedConfig struct { | ||||
| 	Options options.CompletedOptions | ||||
|  | ||||
| 	Aggregator    aggregatorapiserver.CompletedConfig | ||||
| 	ControlPlane  controlplaneapiserver.CompletedConfig | ||||
| 	APIExtensions apiextensionsapiserver.CompletedConfig | ||||
|  | ||||
| 	ExtraConfig | ||||
| } | ||||
|  | ||||
| type CompletedConfig struct { | ||||
| 	// Embed a private pointer that cannot be instantiated outside of this package. | ||||
| 	*completedConfig | ||||
| } | ||||
|  | ||||
| func (c *Config) Complete() (CompletedConfig, error) { | ||||
| 	return CompletedConfig{&completedConfig{ | ||||
| 		Options: c.Options, | ||||
|  | ||||
| 		Aggregator:    c.Aggregator.Complete(), | ||||
| 		ControlPlane:  c.ControlPlane.Complete(), | ||||
| 		APIExtensions: c.APIExtensions.Complete(), | ||||
|  | ||||
| 		ExtraConfig: c.ExtraConfig, | ||||
| 	}}, nil | ||||
| } | ||||
|  | ||||
| // NewConfig creates all the self-contained pieces making up an | ||||
| // sample-generic-controlplane, but does not wire them yet into a server object. | ||||
| func NewConfig(opts options.CompletedOptions) (*Config, error) { | ||||
| 	c := &Config{ | ||||
| 		Options: opts, | ||||
| 	} | ||||
|  | ||||
| 	genericConfig, versionedInformers, storageFactory, err := controlplaneapiserver.BuildGenericConfig( | ||||
| 		opts, | ||||
| 		[]*runtime.Scheme{legacyscheme.Scheme, apiextensionsapiserver.Scheme, aggregatorscheme.Scheme}, | ||||
| 		controlplane.DefaultAPIResourceConfigSource(), | ||||
| 		generatedopenapi.GetOpenAPIDefinitions, | ||||
| 	) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	serviceResolver := webhook.NewDefaultServiceResolver() | ||||
| 	kubeAPIs, pluginInitializer, err := controlplaneapiserver.CreateConfig(opts, genericConfig, versionedInformers, storageFactory, serviceResolver, nil) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	c.ControlPlane = kubeAPIs | ||||
|  | ||||
| 	authInfoResolver := webhook.NewDefaultAuthenticationInfoResolverWrapper(kubeAPIs.ProxyTransport, kubeAPIs.Generic.EgressSelector, kubeAPIs.Generic.LoopbackClientConfig, kubeAPIs.Generic.TracerProvider) | ||||
| 	apiExtensions, err := controlplaneapiserver.CreateAPIExtensionsConfig(*kubeAPIs.Generic, kubeAPIs.VersionedInformers, pluginInitializer, opts, 3, serviceResolver, authInfoResolver) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	c.APIExtensions = apiExtensions | ||||
|  | ||||
| 	aggregator, err := controlplaneapiserver.CreateAggregatorConfig(*kubeAPIs.Generic, opts, kubeAPIs.VersionedInformers, serviceResolver, kubeAPIs.ProxyTransport, kubeAPIs.Extra.PeerProxy, pluginInitializer) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	c.Aggregator = aggregator | ||||
| 	c.Aggregator.ExtraConfig.DisableRemoteAvailableConditionController = true | ||||
|  | ||||
| 	return c, nil | ||||
| } | ||||
							
								
								
									
										202
									
								
								pkg/controlplane/apiserver/samples/generic/server/server.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										202
									
								
								pkg/controlplane/apiserver/samples/generic/server/server.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,202 @@ | ||||
| /* | ||||
| 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 server | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"net" | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
|  | ||||
| 	"github.com/spf13/cobra" | ||||
| 	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" | ||||
| 	utilerrors "k8s.io/apimachinery/pkg/util/errors" | ||||
| 	utilruntime "k8s.io/apimachinery/pkg/util/runtime" | ||||
| 	_ "k8s.io/apiserver/pkg/admission" | ||||
| 	genericapifilters "k8s.io/apiserver/pkg/endpoints/filters" | ||||
| 	genericapiserver "k8s.io/apiserver/pkg/server" | ||||
| 	utilfeature "k8s.io/apiserver/pkg/util/feature" | ||||
| 	"k8s.io/apiserver/pkg/util/notfoundhandler" | ||||
| 	"k8s.io/client-go/kubernetes" | ||||
| 	"k8s.io/client-go/rest" | ||||
| 	cliflag "k8s.io/component-base/cli/flag" | ||||
| 	"k8s.io/component-base/cli/globalflag" | ||||
| 	"k8s.io/component-base/logs" | ||||
| 	logsapi "k8s.io/component-base/logs/api/v1" | ||||
| 	_ "k8s.io/component-base/metrics/prometheus/workqueue" | ||||
| 	"k8s.io/component-base/term" | ||||
| 	"k8s.io/component-base/version" | ||||
| 	"k8s.io/component-base/version/verflag" | ||||
| 	"k8s.io/klog/v2" | ||||
| 	aggregatorapiserver "k8s.io/kube-aggregator/pkg/apiserver" | ||||
|  | ||||
| 	controlplaneapiserver "k8s.io/kubernetes/pkg/controlplane/apiserver" | ||||
| 	"k8s.io/kubernetes/pkg/controlplane/apiserver/options" | ||||
| 	_ "k8s.io/kubernetes/pkg/features" | ||||
| 	// add the kubernetes feature gates | ||||
| ) | ||||
|  | ||||
| func init() { | ||||
| 	utilruntime.Must(logsapi.AddFeatureGates(utilfeature.DefaultMutableFeatureGate)) | ||||
| } | ||||
|  | ||||
| // NewCommand creates a *cobra.Command object with default parameters | ||||
| func NewCommand() *cobra.Command { | ||||
| 	s := NewOptions() | ||||
| 	cmd := &cobra.Command{ | ||||
| 		Use: "sample-generic-apiserver", | ||||
| 		Long: `The sample generic apiserver is part of a generic controlplane, | ||||
| a system serving APIs like Kubernetes, but without the container domain specific | ||||
| APIs.`, | ||||
|  | ||||
| 		// stop printing usage when the command errors | ||||
| 		SilenceUsage: true, | ||||
| 		PersistentPreRunE: func(*cobra.Command, []string) error { | ||||
| 			// silence client-go warnings. | ||||
| 			// kube-apiserver loopback clients should not log self-issued warnings. | ||||
| 			rest.SetDefaultWarningHandler(rest.NoWarnings{}) | ||||
| 			return nil | ||||
| 		}, | ||||
| 		RunE: func(cmd *cobra.Command, args []string) error { | ||||
| 			verflag.PrintAndExitIfRequested() | ||||
| 			fs := cmd.Flags() | ||||
|  | ||||
| 			// Activate logging as soon as possible, after that | ||||
| 			// show flags with the final logging configuration. | ||||
| 			if err := logsapi.ValidateAndApply(s.Logs, utilfeature.DefaultFeatureGate); err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 			cliflag.PrintFlags(fs) | ||||
|  | ||||
| 			completedOptions, err := s.Complete([]string{}, []net.IP{}) | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
|  | ||||
| 			if errs := completedOptions.Validate(); len(errs) != 0 { | ||||
| 				return utilerrors.NewAggregate(errs) | ||||
| 			} | ||||
|  | ||||
| 			// add feature enablement metrics | ||||
| 			utilfeature.DefaultMutableFeatureGate.AddMetrics() | ||||
| 			ctx := genericapiserver.SetupSignalContext() | ||||
| 			return Run(ctx, completedOptions) | ||||
| 		}, | ||||
| 		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 | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	var namedFlagSets cliflag.NamedFlagSets | ||||
| 	s.AddFlags(&namedFlagSets) | ||||
| 	verflag.AddFlags(namedFlagSets.FlagSet("global")) | ||||
| 	globalflag.AddGlobalFlags(namedFlagSets.FlagSet("global"), cmd.Name(), logs.SkipLoggingConfigurationFlags()) | ||||
|  | ||||
| 	fs := cmd.Flags() | ||||
| 	for _, f := range namedFlagSets.FlagSets { | ||||
| 		fs.AddFlagSet(f) | ||||
| 	} | ||||
|  | ||||
| 	cols, _, _ := term.TerminalSize(cmd.OutOrStdout()) | ||||
| 	cliflag.SetUsageAndHelpFunc(cmd, namedFlagSets, cols) | ||||
|  | ||||
| 	return cmd | ||||
| } | ||||
|  | ||||
| func NewOptions() *options.Options { | ||||
| 	s := options.NewOptions() | ||||
| 	s.Admission.GenericAdmission.DefaultOffPlugins = DefaultOffAdmissionPlugins() | ||||
|  | ||||
| 	wd, _ := os.Getwd() | ||||
| 	s.SecureServing.ServerCert.CertDirectory = filepath.Join(wd, ".sample-minimal-controlplane") | ||||
|  | ||||
| 	// Wire ServiceAccount authentication without relying on pods and nodes. | ||||
| 	s.Authentication.ServiceAccounts.OptionalTokenGetter = genericTokenGetter | ||||
|  | ||||
| 	return s | ||||
| } | ||||
|  | ||||
| // Run runs the specified APIServer. This should never exit. | ||||
| func Run(ctx context.Context, opts options.CompletedOptions) error { | ||||
| 	// To help debugging, immediately log version | ||||
| 	klog.Infof("Version: %+v", version.Get()) | ||||
|  | ||||
| 	klog.InfoS("Golang settings", "GOGC", os.Getenv("GOGC"), "GOMAXPROCS", os.Getenv("GOMAXPROCS"), "GOTRACEBACK", os.Getenv("GOTRACEBACK")) | ||||
|  | ||||
| 	config, err := NewConfig(opts) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	completed, err := config.Complete() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	server, err := CreateServerChain(completed) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	prepared, err := server.PrepareRun() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	return prepared.Run(ctx) | ||||
| } | ||||
|  | ||||
| // CreateServerChain creates the apiservers connected via delegation. | ||||
| func CreateServerChain(config CompletedConfig) (*aggregatorapiserver.APIAggregator, error) { | ||||
| 	// 1. CRDs | ||||
| 	notFoundHandler := notfoundhandler.New(config.ControlPlane.Generic.Serializer, genericapifilters.NoMuxAndDiscoveryIncompleteKey) | ||||
| 	apiExtensionsServer, err := config.APIExtensions.New(genericapiserver.NewEmptyDelegateWithCustomHandler(notFoundHandler)) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("failed to create apiextensions-apiserver: %w", err) | ||||
| 	} | ||||
| 	crdAPIEnabled := config.APIExtensions.GenericConfig.MergedResourceConfig.ResourceEnabled(apiextensionsv1.SchemeGroupVersion.WithResource("customresourcedefinitions")) | ||||
|  | ||||
| 	// 2. Natively implemented resources | ||||
| 	nativeAPIs, err := config.ControlPlane.New("sample-generic-controlplane", apiExtensionsServer.GenericAPIServer) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("failed to create generic controlplane apiserver: %w", err) | ||||
| 	} | ||||
| 	client, err := kubernetes.NewForConfig(config.ControlPlane.Generic.LoopbackClientConfig) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	storageProviders, err := config.ControlPlane.GenericStorageProviders(client.Discovery()) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("failed to create storage providers: %w", err) | ||||
| 	} | ||||
| 	if err := nativeAPIs.InstallAPIs(storageProviders...); err != nil { | ||||
| 		return nil, fmt.Errorf("failed to install APIs: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	// 3. Aggregator for APIServices, discovery and OpenAPI | ||||
| 	aggregatorServer, err := controlplaneapiserver.CreateAggregatorServer(config.Aggregator, nativeAPIs.GenericAPIServer, apiExtensionsServer.Informers.Apiextensions().V1().CustomResourceDefinitions(), crdAPIEnabled, controlplaneapiserver.DefaultGenericAPIServicePriorities()) | ||||
| 	if err != nil { | ||||
| 		// we don't need special handling for innerStopCh because the aggregator server doesn't create any go routines | ||||
| 		return nil, fmt.Errorf("failed to create kube-aggregator: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	return aggregatorServer, nil | ||||
| } | ||||
| @@ -0,0 +1,53 @@ | ||||
| /* | ||||
| Copyright 2024 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 server | ||||
|  | ||||
| import ( | ||||
| 	v1 "k8s.io/api/core/v1" | ||||
| 	apierrors "k8s.io/apimachinery/pkg/api/errors" | ||||
| 	"k8s.io/client-go/informers" | ||||
| 	v1listers "k8s.io/client-go/listers/core/v1" | ||||
| 	"k8s.io/kubernetes/pkg/serviceaccount" | ||||
| ) | ||||
|  | ||||
| // clientGetter implements ServiceAccountTokenGetter using a factory function | ||||
| type clientGetter struct { | ||||
| 	secretLister         v1listers.SecretLister | ||||
| 	serviceAccountLister v1listers.ServiceAccountLister | ||||
| } | ||||
|  | ||||
| // genericTokenGetter returns a ServiceAccountTokenGetter that does not depend | ||||
| // on pods and nodes. | ||||
| func genericTokenGetter(factory informers.SharedInformerFactory) serviceaccount.ServiceAccountTokenGetter { | ||||
| 	return clientGetter{secretLister: factory.Core().V1().Secrets().Lister(), serviceAccountLister: factory.Core().V1().ServiceAccounts().Lister()} | ||||
| } | ||||
|  | ||||
| func (c clientGetter) GetServiceAccount(namespace, name string) (*v1.ServiceAccount, error) { | ||||
| 	return c.serviceAccountLister.ServiceAccounts(namespace).Get(name) | ||||
| } | ||||
|  | ||||
| func (c clientGetter) GetPod(namespace, name string) (*v1.Pod, error) { | ||||
| 	return nil, apierrors.NewNotFound(v1.Resource("pods"), name) | ||||
| } | ||||
|  | ||||
| func (c clientGetter) GetSecret(namespace, name string) (*v1.Secret, error) { | ||||
| 	return c.secretLister.Secrets(namespace).Get(name) | ||||
| } | ||||
|  | ||||
| func (c clientGetter) GetNode(name string) (*v1.Node, error) { | ||||
| 	return nil, apierrors.NewNotFound(v1.Resource("nodes"), name) | ||||
| } | ||||
							
								
								
									
										39
									
								
								pkg/controlplane/apiserver/samples/generic/server/testing/testdata/127.0.0.1__localhost.crt
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								pkg/controlplane/apiserver/samples/generic/server/testing/testdata/127.0.0.1__localhost.crt
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,39 @@ | ||||
| -----BEGIN CERTIFICATE----- | ||||
| MIIDNzCCAh+gAwIBAgIITYKwSTTKZ+owDQYJKoZIhvcNAQELBQAwIjEgMB4GA1UE | ||||
| AwwXMTI3LjAuMC4xLWNhQDE3MjExOTkxMTUwIBcNMjQwNzE3MDU1MTU1WhgPMjEy | ||||
| NDA2MjMwNTUxNTVaMB8xHTAbBgNVBAMMFDEyNy4wLjAuMUAxNzIxMTk5MTE1MIIB | ||||
| IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAw8v0LpHRUswuOxhfQbuf5xIJ | ||||
| gBd/b5+66gxUaXUtNm1NvOhh9NylhGeYN241JvPRruAhFdK8SJ8+FcteniMyw1O4 | ||||
| Hg03v8KsGerJbxaucXe0ascWwklunkZiTwqqInPxbCWnlu7v6pfpG3mC0UXFRWrA | ||||
| Qs6uZNCr7gxjg1rdyU1bM2VMF6menQYfNV0AT7R1BmehcHRT7feHa3Sc1tPvbCUt | ||||
| FQeh1gV33WU6OoxRzVtYOi4mHAeP0+v1o1wZN4AEz8DruE3+rnWVpAQypBGEpPYK | ||||
| YcHk5SfUM5wwn/nA7F1IhWsVciUdB6u8j3gBTMHeMs4IbSr1aD8mjnC4xZAF4wID | ||||
| AQABo3IwcDAOBgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDAYD | ||||
| VR0TAQH/BAIwADAfBgNVHSMEGDAWgBTJYQZnq9uWaGVS5ECwCtMdlD2ONjAaBgNV | ||||
| HREEEzARgglsb2NhbGhvc3SHBH8AAAEwDQYJKoZIhvcNAQELBQADggEBABRm6GiR | ||||
| /eV5ojx17tY669LFQjHBS9hruODWxn25PGjsYAGS7YX0w8fmBvOzIYnTHvcxvsnz | ||||
| MPVjqNhdRXOyiXyI1w83Gzt9YNoF0Uht2ymrMUnSkxCkNEkayQlFzlqgWr8hB3QM | ||||
| K44eOTE0md1Oz/A4hxeTdwEjKNlHeAFjVs5l+Gm61A6NqMu2jFLdDYPK6qEE0a9f | ||||
| wP6yAgwt/mfZ+GpUAHCB3N9R0tbtwULcROGZbRRuz4dmgC1FjZv8enltMcd7Gl5e | ||||
| yS+tsM+vHLdUZRB4nE5X5vt+IWXpbWR7Cd3lxtRLX2gHOlOLw73+Zqk2pmPwJzZS | ||||
| agAevWg4PfYZPbM= | ||||
| -----END CERTIFICATE----- | ||||
| -----BEGIN CERTIFICATE----- | ||||
| MIIDCjCCAfKgAwIBAgIIKaeGif6ywP8wDQYJKoZIhvcNAQELBQAwIjEgMB4GA1UE | ||||
| AwwXMTI3LjAuMC4xLWNhQDE3MjExOTkxMTUwIBcNMjQwNzE3MDU1MTU1WhgPMjEy | ||||
| NDA2MjMwNTUxNTVaMCIxIDAeBgNVBAMMFzEyNy4wLjAuMS1jYUAxNzIxMTk5MTE1 | ||||
| MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAupei9GWWmYyDwqlwggjn | ||||
| vDgWQIEqQHRQOJnuFCEDcSPp1LEOskqH4KTbDi0kMO9+Yi1BsKnJowq5edvy8om9 | ||||
| nyCOR/cejEnE5I+tOSpHcC6u2ZQhtkoQ8c3+a7j6YsSiq2htck3YJylons+4i0GS | ||||
| NE00XdxhXrLV2UaXeBR/hJ7hN+2vsrOb4wvZG7DHn+HX42pet0kpu6xlGPwEpKvr | ||||
| DKQJ0DlLKXGE2pe/FlKfRJTHO2HWSZdYEu9AzfU+TY33xoEC6xJy1Xiw9JO2Tl1+ | ||||
| 0KZjq3X962R5zaRnsuVd6fLolaw4+Ku4mZtwkugyTbL4ave0QprZ4KA27K0vUtST | ||||
| ZQIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAqQwDwYDVR0TAQH/BAUwAwEB/zAdBgNV | ||||
| HQ4EFgQUyWEGZ6vblmhlUuRAsArTHZQ9jjYwDQYJKoZIhvcNAQELBQADggEBADjA | ||||
| gK8EeZcqiLPKz2WjglqnWe0SuSKY4WjOtEEp/IxW6+IjcHUmXEjHWBVpYVqJGEAH | ||||
| 4vwQ9RGMZUJx8ZB28/EPqrSMxq6snSO3L4UBRjGK2zYZG/6eADpEIXAviTrrqRRR | ||||
| +Xf0pimuQgU6hmEakAFqKBcuQ+TVwN3PnlErs+31QfErDgNrnuxeTODeQtYrNvc8 | ||||
| 4nZ/z4LI5Jn0rJ8rJ2n3wkiT8hokjG5hYQjhqzDwZCCM7Eh4v9mVh0/XmzJ5D/zL | ||||
| r9CVVBTcTiianOZ2aDlz85MvlAcwQtej/YerLzpnKiZfjdo/s0qLstjDUePuj3QH | ||||
| rF7apSTJlh/oef0+UJU= | ||||
| -----END CERTIFICATE----- | ||||
							
								
								
									
										27
									
								
								pkg/controlplane/apiserver/samples/generic/server/testing/testdata/127.0.0.1__localhost.key
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								pkg/controlplane/apiserver/samples/generic/server/testing/testdata/127.0.0.1__localhost.key
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | ||||
| -----BEGIN RSA PRIVATE KEY----- | ||||
| MIIEpAIBAAKCAQEAw8v0LpHRUswuOxhfQbuf5xIJgBd/b5+66gxUaXUtNm1NvOhh | ||||
| 9NylhGeYN241JvPRruAhFdK8SJ8+FcteniMyw1O4Hg03v8KsGerJbxaucXe0ascW | ||||
| wklunkZiTwqqInPxbCWnlu7v6pfpG3mC0UXFRWrAQs6uZNCr7gxjg1rdyU1bM2VM | ||||
| F6menQYfNV0AT7R1BmehcHRT7feHa3Sc1tPvbCUtFQeh1gV33WU6OoxRzVtYOi4m | ||||
| HAeP0+v1o1wZN4AEz8DruE3+rnWVpAQypBGEpPYKYcHk5SfUM5wwn/nA7F1IhWsV | ||||
| ciUdB6u8j3gBTMHeMs4IbSr1aD8mjnC4xZAF4wIDAQABAoIBAE77hjwG/H6++OND | ||||
| 2KFGk6F96DEwyWp478icQqzr5Nowy4wp3eIN5AL+Wyv5HB3jezFlHlOUV/mfq0bV | ||||
| bAy0vDSJIBuXT2bem9g0mx9h8eq51CDCwQ6M2r+kOuIRtkIBrWDn66v6JPPoZdN8 | ||||
| d+X9lC+FeZs5jqYCe2iivL3vOMqL4bxO9micdEvE90vv6+SpOQ2/wc8wP75LNem1 | ||||
| q/lbrJ60yVDQOsrz5jat/Njp0+ETEmyavLETx0goRwmQqTMvJUVM+K7EsB7pOWOt | ||||
| +I2+wodqwqvhixQIYeJZTKkPSMXFRFyRPK+fQQtpIbBFPo3cWeJm/+HC8kZNFjsG | ||||
| AuKCsIECgYEA83H9PGzKeb9Yo09EZTp5xsuRuVZyhOIsXvrBq5EprZ3Igz+5LPml | ||||
| zpVcanZlLNub/7IgizMWX4rn23GuXs6fKKLARjP6Qw/OdefPTJeR1dbNd1NfhP+e | ||||
| lFVjkMW84Ed14nTsMSYKOMy+ZJnTsxmIaTPYnSPMCmTRA8uetM986CMCgYEAzeTm | ||||
| KUFQc0ojjZJrdWYw5Lf041t04w5wBoedKuXSe2srxY+IRSsXzw92SXfXWG6RIb70 | ||||
| 2frNr5B+UwSMFx66rwcIFWRH9/h2h56e0DmmP8iPBiCiqb8PPU33QHBA6E2eC7Dd | ||||
| xuZctnq4OqxM56HtDDvPbalOTQu0vfIy6wpxZ0ECgYEAss2rSKFDCa7PpIsI2izb | ||||
| 2nYUHwNuc0lHe69DZgbljL4R0syP7oeiD5xGV2+EGjFmX6RuIK8yJJR6fQP/JWUv | ||||
| IwJ+pFFy46SNaK4M5N2CYIQ3Pwg+ZQn2aE5bJa8GbdgurlhgTiz5XwSKZotRIP+E | ||||
| 4HgTBj+Pkqa/mcEJXRX0UO8CgYAx5hatyul/d2lUZzbp1eFlnPuZmlGisZ4OxxEd | ||||
| E2PGi3upPpbtBHuZsAqf1Y54HRvJTOk0ZucwdFlZL1HwTH876f1YidwzSaEYTyX4 | ||||
| GvCipq2a84/YibhcyCdzE4F3i1ART0UAblXr16QMfDOLM6AqhdhIoG6cl4ivPCKA | ||||
| +h/vwQKBgQDB5NtqNR/0F/V7uSM4jA2e93TOSsViZT0jleGbAznKiEFGfBKdC9t3 | ||||
| hcOSzZm/mnN2LCCGHvdrbpjL6Vzwn730o0DkblN//m9ExcYrh56StGaMfPCpD9H3 | ||||
| uQJnpwQ4VjJPqMwqcwHouqTyUA6SYyoFv7/rKVE1nWCAp+A5i3A/YA== | ||||
| -----END RSA PRIVATE KEY----- | ||||
							
								
								
									
										1
									
								
								pkg/controlplane/apiserver/samples/generic/server/testing/testdata/README.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								pkg/controlplane/apiserver/samples/generic/server/testing/testdata/README.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| Keys in this directory are generated for testing purposes only. | ||||
| @@ -0,0 +1,348 @@ | ||||
| /* | ||||
| Copyright 2017 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 testing | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"net" | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 	"runtime" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/spf13/pflag" | ||||
| 	"go.etcd.io/etcd/client/pkg/v3/transport" | ||||
| 	clientv3 "go.etcd.io/etcd/client/v3" | ||||
| 	"google.golang.org/grpc" | ||||
| 	"k8s.io/kubernetes/pkg/controlplane/apiserver/samples/generic/server" | ||||
|  | ||||
| 	"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/wait" | ||||
| 	"k8s.io/apiserver/pkg/storage/storagebackend" | ||||
| 	"k8s.io/client-go/kubernetes" | ||||
| 	restclient "k8s.io/client-go/rest" | ||||
| 	cliflag "k8s.io/component-base/cli/flag" | ||||
| 	logsapi "k8s.io/component-base/logs/api/v1" | ||||
| 	"k8s.io/klog/v2" | ||||
| 	controlplaneapiserver "k8s.io/kubernetes/pkg/controlplane/apiserver/options" | ||||
| 	"k8s.io/kubernetes/test/utils/ktesting" | ||||
| ) | ||||
|  | ||||
| func init() { | ||||
| 	// If instantiated more than once or together with other servers, the | ||||
| 	// servers would try to modify the global logging state. This must get | ||||
| 	// ignored during testing. | ||||
| 	logsapi.ReapplyHandling = logsapi.ReapplyHandlingIgnoreUnchanged | ||||
| } | ||||
|  | ||||
| // This key is for testing purposes only and is not considered secure. | ||||
| const ecdsaPrivateKey = `-----BEGIN EC PRIVATE KEY----- | ||||
| MHcCAQEEIEZmTmUhuanLjPA2CLquXivuwBDHTt5XYwgIr/kA1LtRoAoGCCqGSM49 | ||||
| AwEHoUQDQgAEH6cuzP8XuD5wal6wf9M6xDljTOPLX2i8uIp/C/ASqiIGUeeKQtX0 | ||||
| /IR3qCXyThP/dbCiHrF3v1cuhBOHY8CLVg== | ||||
| -----END EC PRIVATE KEY-----` | ||||
|  | ||||
| // TearDownFunc is to be called to tear down a test server. | ||||
| type TearDownFunc func() | ||||
|  | ||||
| // TestServerInstanceOptions Instance options the TestServer | ||||
| type TestServerInstanceOptions struct { | ||||
| 	// SkipHealthzCheck returns without waiting for the server to become healthy. | ||||
| 	// Useful for testing server configurations expected to prevent /healthz from completing. | ||||
| 	SkipHealthzCheck bool | ||||
| } | ||||
|  | ||||
| // TestServer represents a running test server with everything to access it and | ||||
| // its backing store etcd. | ||||
| type TestServer struct { | ||||
| 	ClientConfig      *restclient.Config             // Rest client config | ||||
| 	ServerOpts        *controlplaneapiserver.Options // ServerOpts | ||||
| 	TearDownFn        TearDownFunc                   // TearDown function | ||||
| 	TmpDir            string                         // Temp Dir used, by the apiserver | ||||
| 	EtcdClient        *clientv3.Client               // used by tests that need to check data migrated from APIs that are no longer served | ||||
| 	EtcdStoragePrefix string                         // storage prefix in etcd | ||||
| } | ||||
|  | ||||
| // NewDefaultTestServerOptions default options for TestServer instances | ||||
| func NewDefaultTestServerOptions() *TestServerInstanceOptions { | ||||
| 	return &TestServerInstanceOptions{} | ||||
| } | ||||
|  | ||||
| // StartTestServer starts an etcd server and sample-generic-controlplane and | ||||
| // returns a TestServer struct with a tear-down func and clients to access it | ||||
| // and its backing store. | ||||
| func StartTestServer(t ktesting.TB, instanceOptions *TestServerInstanceOptions, customFlags []string, storageConfig *storagebackend.Config) (result TestServer, err error) { | ||||
| 	tCtx := ktesting.Init(t) | ||||
|  | ||||
| 	if instanceOptions == nil { | ||||
| 		instanceOptions = NewDefaultTestServerOptions() | ||||
| 	} | ||||
|  | ||||
| 	result.TmpDir, err = os.MkdirTemp("", "sample-generic-controlplane") | ||||
| 	if err != nil { | ||||
| 		return result, fmt.Errorf("failed to create temp dir: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	var errCh chan error | ||||
| 	tearDown := func() { | ||||
| 		// Cancel is stopping apiserver and cleaning up | ||||
| 		// after itself, including shutting down its storage layer. | ||||
| 		tCtx.Cancel("tearing down") | ||||
|  | ||||
| 		// If the apiserver was started, let's wait for it to | ||||
| 		// shutdown clearly. | ||||
| 		if errCh != nil { | ||||
| 			err, ok := <-errCh | ||||
| 			if ok && err != nil { | ||||
| 				klog.Errorf("Failed to shutdown test server clearly: %v", err) | ||||
| 			} | ||||
| 		} | ||||
| 		os.RemoveAll(result.TmpDir) //nolint:errcheck // best effort | ||||
| 	} | ||||
| 	defer func() { | ||||
| 		if result.TearDownFn == nil { | ||||
| 			tearDown() | ||||
| 		} | ||||
| 	}() | ||||
|  | ||||
| 	o := server.NewOptions() | ||||
| 	var fss cliflag.NamedFlagSets | ||||
| 	o.AddFlags(&fss) | ||||
|  | ||||
| 	fs := pflag.NewFlagSet("test", pflag.PanicOnError) | ||||
| 	for _, f := range fss.FlagSets { | ||||
| 		fs.AddFlagSet(f) | ||||
| 	} | ||||
|  | ||||
| 	o.SecureServing.Listener, o.SecureServing.BindPort, err = createLocalhostListenerOnFreePort() | ||||
| 	if err != nil { | ||||
| 		return result, fmt.Errorf("failed to create listener: %w", err) | ||||
| 	} | ||||
| 	o.SecureServing.ServerCert.CertDirectory = result.TmpDir | ||||
| 	o.SecureServing.ExternalAddress = o.SecureServing.Listener.Addr().(*net.TCPAddr).IP // use listener addr although it is a loopback device | ||||
|  | ||||
| 	pkgPath, err := pkgPath(t) | ||||
| 	if err != nil { | ||||
| 		return result, err | ||||
| 	} | ||||
| 	o.SecureServing.ServerCert.FixtureDirectory = filepath.Join(pkgPath, "testdata") | ||||
|  | ||||
| 	o.Etcd.StorageConfig = *storageConfig | ||||
| 	utilruntime.Must(o.APIEnablement.RuntimeConfig.Set("api/all=true")) | ||||
|  | ||||
| 	if err := fs.Parse(customFlags); err != nil { | ||||
| 		return result, err | ||||
| 	} | ||||
|  | ||||
| 	saSigningKeyFile, err := os.CreateTemp("/tmp", "insecure_test_key") | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("create temp file failed: %v", err) | ||||
| 	} | ||||
| 	defer os.RemoveAll(saSigningKeyFile.Name()) //nolint:errcheck // best effort | ||||
| 	if err = os.WriteFile(saSigningKeyFile.Name(), []byte(ecdsaPrivateKey), 0666); err != nil { | ||||
| 		t.Fatalf("write file %s failed: %v", saSigningKeyFile.Name(), err) | ||||
| 	} | ||||
| 	o.ServiceAccountSigningKeyFile = saSigningKeyFile.Name() | ||||
| 	o.Authentication.ServiceAccounts.Issuers = []string{"https://foo.bar.example.com"} | ||||
| 	o.Authentication.ServiceAccounts.KeyFiles = []string{saSigningKeyFile.Name()} | ||||
|  | ||||
| 	completedOptions, err := o.Complete(nil, nil) | ||||
| 	if err != nil { | ||||
| 		return result, fmt.Errorf("failed to set default ServerRunOptions: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	if errs := completedOptions.Validate(); len(errs) != 0 { | ||||
| 		return result, fmt.Errorf("failed to validate ServerRunOptions: %w", utilerrors.NewAggregate(errs)) | ||||
| 	} | ||||
|  | ||||
| 	t.Logf("runtime-config=%v", completedOptions.APIEnablement.RuntimeConfig) | ||||
| 	t.Logf("Starting sample-generic-controlplane on port %d...", o.SecureServing.BindPort) | ||||
|  | ||||
| 	config, err := server.NewConfig(completedOptions) | ||||
| 	if err != nil { | ||||
| 		return result, err | ||||
| 	} | ||||
| 	completed, err := config.Complete() | ||||
| 	if err != nil { | ||||
| 		return result, err | ||||
| 	} | ||||
| 	s, err := server.CreateServerChain(completed) | ||||
| 	if err != nil { | ||||
| 		return result, fmt.Errorf("failed to create server chain: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	errCh = make(chan error) | ||||
| 	go func() { | ||||
| 		defer close(errCh) | ||||
| 		prepared, err := s.PrepareRun() | ||||
| 		if err != nil { | ||||
| 			errCh <- err | ||||
| 		} else if err := prepared.Run(tCtx); err != nil { | ||||
| 			errCh <- err | ||||
| 		} | ||||
| 	}() | ||||
|  | ||||
| 	client, err := kubernetes.NewForConfig(s.GenericAPIServer.LoopbackClientConfig) | ||||
| 	if err != nil { | ||||
| 		return result, fmt.Errorf("failed to create a client: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	if !instanceOptions.SkipHealthzCheck { | ||||
| 		t.Logf("Waiting for /healthz to be ok...") | ||||
|  | ||||
| 		// wait until healthz endpoint returns ok | ||||
| 		err = wait.PollUntilContextTimeout(tCtx, 100*time.Millisecond, wait.ForeverTestTimeout, true, func(ctx context.Context) (bool, error) { | ||||
| 			select { | ||||
| 			case err := <-errCh: | ||||
| 				return false, err | ||||
| 			default: | ||||
| 			} | ||||
|  | ||||
| 			req := client.CoreV1().RESTClient().Get().AbsPath("/healthz") | ||||
| 			result := req.Do(ctx) | ||||
| 			status := 0 | ||||
| 			result.StatusCode(&status) | ||||
| 			if status == 200 { | ||||
| 				return true, nil | ||||
| 			} | ||||
| 			return false, nil | ||||
| 		}) | ||||
| 		if err != nil { | ||||
| 			return result, fmt.Errorf("failed to wait for /healthz to return ok: %w", err) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// wait until default namespace is created | ||||
| 	err = wait.PollUntilContextTimeout(tCtx, 100*time.Millisecond, wait.ForeverTestTimeout, true, func(ctx context.Context) (bool, error) { | ||||
| 		select { | ||||
| 		case err := <-errCh: | ||||
| 			return false, err | ||||
| 		default: | ||||
| 		} | ||||
|  | ||||
| 		if _, err := client.CoreV1().Namespaces().Get(ctx, "default", metav1.GetOptions{}); err != nil { | ||||
| 			if !errors.IsNotFound(err) { | ||||
| 				t.Logf("Unable to get default namespace: %v", err) | ||||
| 			} | ||||
| 			return false, nil | ||||
| 		} | ||||
| 		return true, nil | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		return result, fmt.Errorf("failed to wait for default namespace to be created: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	tlsInfo := transport.TLSInfo{ | ||||
| 		CertFile:      storageConfig.Transport.CertFile, | ||||
| 		KeyFile:       storageConfig.Transport.KeyFile, | ||||
| 		TrustedCAFile: storageConfig.Transport.TrustedCAFile, | ||||
| 	} | ||||
| 	tlsConfig, err := tlsInfo.ClientConfig() | ||||
| 	if err != nil { | ||||
| 		return result, err | ||||
| 	} | ||||
| 	etcdConfig := clientv3.Config{ | ||||
| 		Endpoints:   storageConfig.Transport.ServerList, | ||||
| 		DialTimeout: 20 * time.Second, | ||||
| 		DialOptions: []grpc.DialOption{ | ||||
| 			grpc.WithBlock(), // block until the underlying connection is up | ||||
| 		}, | ||||
| 		TLS: tlsConfig, | ||||
| 	} | ||||
| 	etcdClient, err := clientv3.New(etcdConfig) | ||||
| 	if err != nil { | ||||
| 		return result, err | ||||
| 	} | ||||
|  | ||||
| 	// from here the caller must call tearDown | ||||
| 	result.ClientConfig = restclient.CopyConfig(s.GenericAPIServer.LoopbackClientConfig) | ||||
| 	result.ClientConfig.QPS = 1000 | ||||
| 	result.ClientConfig.Burst = 10000 | ||||
| 	result.ServerOpts = o | ||||
| 	result.TearDownFn = func() { | ||||
| 		tearDown() | ||||
| 		etcdClient.Close() //nolint:errcheck // best effort | ||||
| 	} | ||||
| 	result.EtcdClient = etcdClient | ||||
| 	result.EtcdStoragePrefix = storageConfig.Prefix | ||||
|  | ||||
| 	return result, nil | ||||
| } | ||||
|  | ||||
| // StartTestServerOrDie calls StartTestServer t.Fatal if it does not succeed. | ||||
| func StartTestServerOrDie(t ktesting.TB, instanceOptions *TestServerInstanceOptions, flags []string, storageConfig *storagebackend.Config) *TestServer { | ||||
| 	result, err := StartTestServer(t, instanceOptions, flags, storageConfig) | ||||
| 	if err == nil { | ||||
| 		return &result | ||||
| 	} | ||||
|  | ||||
| 	t.Fatalf("failed to launch server: %v", err) | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func createLocalhostListenerOnFreePort() (net.Listener, int, error) { | ||||
| 	ln, err := net.Listen("tcp", "127.0.0.1:0") | ||||
| 	if err != nil { | ||||
| 		return nil, 0, err | ||||
| 	} | ||||
|  | ||||
| 	// get port | ||||
| 	tcpAddr, ok := ln.Addr().(*net.TCPAddr) | ||||
| 	if !ok { | ||||
| 		ln.Close() //nolint:errcheck // best effort | ||||
| 		return nil, 0, fmt.Errorf("invalid listen address: %q", ln.Addr().String()) | ||||
| 	} | ||||
|  | ||||
| 	return ln, tcpAddr.Port, nil | ||||
| } | ||||
|  | ||||
| // pkgPath returns the absolute file path to this package's directory. With go | ||||
| // test, we can just look at the runtime call stack. However, bazel compiles go | ||||
| // binaries with the -trimpath option so the simple approach fails however we | ||||
| // can consult environment variables to derive the path. | ||||
| // | ||||
| // The approach taken here works for both go test and bazel on the assumption | ||||
| // that if and only if trimpath is passed, we are running under bazel. | ||||
| func pkgPath(t ktesting.TB) (string, error) { | ||||
| 	_, thisFile, _, ok := runtime.Caller(0) | ||||
| 	if !ok { | ||||
| 		return "", fmt.Errorf("failed to get current file") | ||||
| 	} | ||||
|  | ||||
| 	pkgPath := filepath.Dir(thisFile) | ||||
|  | ||||
| 	// If we find bazel env variables, then -trimpath was passed so we need to | ||||
| 	// construct the path from the environment. | ||||
| 	if testSrcdir, testWorkspace := os.Getenv("TEST_SRCDIR"), os.Getenv("TEST_WORKSPACE"); testSrcdir != "" && testWorkspace != "" { | ||||
| 		t.Logf("Detected bazel env varaiables: TEST_SRCDIR=%q TEST_WORKSPACE=%q", testSrcdir, testWorkspace) | ||||
| 		pkgPath = filepath.Join(testSrcdir, testWorkspace, pkgPath) | ||||
| 	} | ||||
|  | ||||
| 	// If the path is still not absolute, something other than bazel compiled | ||||
| 	// with -trimpath. | ||||
| 	if !filepath.IsAbs(pkgPath) { | ||||
| 		return "", fmt.Errorf("can't construct an absolute path from %q", pkgPath) | ||||
| 	} | ||||
|  | ||||
| 	t.Logf("Resolved testserver package path to: %q", pkgPath) | ||||
|  | ||||
| 	return pkgPath, nil | ||||
| } | ||||
							
								
								
									
										136
									
								
								test/integration/controlplane/generic_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										136
									
								
								test/integration/controlplane/generic_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,136 @@ | ||||
| /* | ||||
| Copyright 2024 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 controlplane | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"strings" | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/google/go-cmp/cmp" | ||||
| 	"github.com/stretchr/testify/require" | ||||
| 	corev1 "k8s.io/api/core/v1" | ||||
| 	apiextensionsclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" | ||||
| 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||||
| 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" | ||||
| 	"k8s.io/apimachinery/pkg/runtime/schema" | ||||
| 	"k8s.io/apimachinery/pkg/util/sets" | ||||
| 	"k8s.io/client-go/dynamic" | ||||
| 	"k8s.io/client-go/kubernetes" | ||||
| 	genericcontrolplanetesting "k8s.io/kubernetes/pkg/controlplane/apiserver/samples/generic/server/testing" | ||||
| 	"k8s.io/kubernetes/test/integration/etcd" | ||||
| 	"k8s.io/kubernetes/test/integration/framework" | ||||
| ) | ||||
|  | ||||
| func TestGenericControlplaneStartUp(t *testing.T) { | ||||
| 	server, err := genericcontrolplanetesting.StartTestServer(t, genericcontrolplanetesting.NewDefaultTestServerOptions(), nil, framework.SharedEtcd()) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 	defer server.TearDownFn() | ||||
|  | ||||
| 	ctx, cancel := context.WithCancel(context.Background()) | ||||
| 	t.Cleanup(cancel) | ||||
|  | ||||
| 	client, err := kubernetes.NewForConfig(server.ClientConfig) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 	dynamicClient, err := dynamic.NewForConfig(server.ClientConfig) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
|  | ||||
| 	_, err = client.RESTClient().Get().AbsPath("/readyz").Do(ctx).Raw() | ||||
| 	require.NoError(t, err) | ||||
|  | ||||
| 	groups, err := client.Discovery().ServerPreferredResources() | ||||
| 	require.NoError(t, err) | ||||
|  | ||||
| 	t.Logf("Found %d API groups", len(groups)) | ||||
| 	grs := sets.New[string]() | ||||
| 	for _, g := range groups { | ||||
| 		var group string | ||||
| 		comps := strings.SplitN(g.GroupVersion, "/", 2) | ||||
| 		if len(comps) == 2 { | ||||
| 			group = comps[0] | ||||
| 		} | ||||
| 		for _, r := range g.APIResources { | ||||
| 			grs.Insert(schema.GroupResource{Group: group, Resource: r.Name}.String()) | ||||
| 		} | ||||
| 	} | ||||
| 	expected := sets.New[string]( | ||||
| 		"apiservices.apiregistration.k8s.io", | ||||
| 		"certificatesigningrequests.certificates.k8s.io", | ||||
| 		"clusterrolebindings.rbac.authorization.k8s.io", | ||||
| 		"clusterroles.rbac.authorization.k8s.io", | ||||
| 		"configmaps", | ||||
| 		"customresourcedefinitions.apiextensions.k8s.io", | ||||
| 		"events", | ||||
| 		"events.events.k8s.io", | ||||
| 		"flowschemas.flowcontrol.apiserver.k8s.io", | ||||
| 		"leases.coordination.k8s.io", | ||||
| 		"localsubjectaccessreviews.authorization.k8s.io", | ||||
| 		"mutatingwebhookconfigurations.admissionregistration.k8s.io", | ||||
| 		"namespaces", | ||||
| 		"prioritylevelconfigurations.flowcontrol.apiserver.k8s.io", | ||||
| 		"resourcequotas", | ||||
| 		"rolebindings.rbac.authorization.k8s.io", | ||||
| 		"roles.rbac.authorization.k8s.io", | ||||
| 		"secrets", | ||||
| 		"selfsubjectaccessreviews.authorization.k8s.io", | ||||
| 		"selfsubjectreviews.authentication.k8s.io", | ||||
| 		"selfsubjectrulesreviews.authorization.k8s.io", | ||||
| 		"serviceaccounts", | ||||
| 		"storageversions.internal.apiserver.k8s.io", | ||||
| 		"subjectaccessreviews.authorization.k8s.io", | ||||
| 		"tokenreviews.authentication.k8s.io", | ||||
| 		"validatingadmissionpolicies.admissionregistration.k8s.io", | ||||
| 		"validatingadmissionpolicybindings.admissionregistration.k8s.io", | ||||
| 		"validatingwebhookconfigurations.admissionregistration.k8s.io", | ||||
| 	) | ||||
| 	if diff := cmp.Diff(sets.List(expected), sets.List(grs)); diff != "" { | ||||
| 		t.Fatalf("unexpected API groups: +want, -got\n%s", diff) | ||||
| 	} | ||||
|  | ||||
| 	t.Logf("Create cluster scoped resource: namespace %q", "test") | ||||
| 	if _, err := client.CoreV1().Namespaces().Create(ctx, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "test"}}, metav1.CreateOptions{}); err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
|  | ||||
| 	t.Logf("Create namesapces resource: configmap %q", "config") | ||||
| 	if _, err := client.CoreV1().ConfigMaps("test").Create(ctx, &corev1.ConfigMap{ | ||||
| 		ObjectMeta: metav1.ObjectMeta{Namespace: "test", Name: "config"}, | ||||
| 		Data:       map[string]string{"foo": "bar"}, | ||||
| 	}, metav1.CreateOptions{}); err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
|  | ||||
| 	t.Logf("Create CRD") | ||||
| 	etcd.CreateTestCRDs(t, apiextensionsclientset.NewForConfigOrDie(server.ClientConfig), false, etcd.GetCustomResourceDefinitionData()...) | ||||
| 	if _, err := dynamicClient.Resource(schema.GroupVersionResource{Group: "awesome.bears.com", Version: "v1", Resource: "pandas"}).Create(ctx, &unstructured.Unstructured{ | ||||
| 		Object: map[string]interface{}{ | ||||
| 			"apiVersion": "awesome.bears.com/v1", | ||||
| 			"kind":       "Panda", | ||||
| 			"metadata": map[string]interface{}{ | ||||
| 				"name": "baobao", | ||||
| 			}, | ||||
| 		}, | ||||
| 	}, metav1.CreateOptions{}); err != nil { | ||||
| 		t.Error(err) | ||||
| 	} | ||||
| } | ||||
		Reference in New Issue
	
	Block a user
	 Dr. Stefan Schimanski
					Dr. Stefan Schimanski