diff --git a/cmd/kube-apiserver/app/options/options.go b/cmd/kube-apiserver/app/options/options.go index c61f6bbfc28..25cc10fbc41 100644 --- a/cmd/kube-apiserver/app/options/options.go +++ b/cmd/kube-apiserver/app/options/options.go @@ -82,9 +82,6 @@ type ServerRunOptions struct { MasterCount int EndpointReconcilerType string - IdentityLeaseDurationSeconds int - IdentityLeaseRenewIntervalSeconds int - ServiceAccountSigningKeyFile string ServiceAccountIssuer serviceaccount.TokenGenerator ServiceAccountTokenMaxExpiration time.Duration @@ -110,12 +107,10 @@ func NewServerRunOptions() *ServerRunOptions { Logs: logs.NewOptions(), Traces: genericoptions.NewTracingOptions(), - EnableLogsHandler: true, - EventTTL: 1 * time.Hour, - MasterCount: 1, - EndpointReconcilerType: string(reconcilers.LeaseEndpointReconcilerType), - IdentityLeaseDurationSeconds: 3600, - IdentityLeaseRenewIntervalSeconds: 10, + EnableLogsHandler: true, + EventTTL: 1 * time.Hour, + MasterCount: 1, + EndpointReconcilerType: string(reconcilers.LeaseEndpointReconcilerType), KubeletConfig: kubeletclient.KubeletClientConfig{ Port: ports.KubeletPort, ReadOnlyPort: ports.KubeletReadOnlyPort, @@ -185,12 +180,6 @@ func (s *ServerRunOptions) Flags() (fss cliflag.NamedFlagSets) { fs.StringVar(&s.EndpointReconcilerType, "endpoint-reconciler-type", s.EndpointReconcilerType, "Use an endpoint reconciler ("+strings.Join(reconcilers.AllTypes.Names(), ", ")+") master-count is deprecated, and will be removed in a future version.") - fs.IntVar(&s.IdentityLeaseDurationSeconds, "identity-lease-duration-seconds", s.IdentityLeaseDurationSeconds, - "The duration of kube-apiserver lease in seconds, must be a positive number. (In use when the APIServerIdentity feature gate is enabled.)") - - fs.IntVar(&s.IdentityLeaseRenewIntervalSeconds, "identity-lease-renew-interval-seconds", s.IdentityLeaseRenewIntervalSeconds, - "The interval of kube-apiserver renewing its lease in seconds, must be a positive number. (In use when the APIServerIdentity feature gate is enabled.)") - // See #14282 for details on how to test/try this option out. // TODO: remove this comment once this option is tested in CI. fs.IntVar(&s.KubernetesServiceNodePort, "kubernetes-service-node-port", s.KubernetesServiceNodePort, ""+ diff --git a/cmd/kube-apiserver/app/options/options_test.go b/cmd/kube-apiserver/app/options/options_test.go index 137490ed5f5..b44a784065c 100644 --- a/cmd/kube-apiserver/app/options/options_test.go +++ b/cmd/kube-apiserver/app/options/options_test.go @@ -318,8 +318,6 @@ func TestAddFlags(t *testing.T) { Traces: &apiserveroptions.TracingOptions{ ConfigFile: "/var/run/kubernetes/tracing_config.yaml", }, - IdentityLeaseDurationSeconds: 3600, - IdentityLeaseRenewIntervalSeconds: 10, AggregatorRejectForwardingRedirects: true, } diff --git a/cmd/kube-apiserver/app/options/validation.go b/cmd/kube-apiserver/app/options/validation.go index d96a6a41b17..a2e587e3847 100644 --- a/cmd/kube-apiserver/app/options/validation.go +++ b/cmd/kube-apiserver/app/options/validation.go @@ -142,17 +142,6 @@ func validateAPIPriorityAndFairness(options *ServerRunOptions) []error { return nil } -func validateAPIServerIdentity(options *ServerRunOptions) []error { - var errs []error - if options.IdentityLeaseDurationSeconds <= 0 { - errs = append(errs, fmt.Errorf("--identity-lease-duration-seconds should be a positive number, but value '%d' provided", options.IdentityLeaseDurationSeconds)) - } - if options.IdentityLeaseRenewIntervalSeconds <= 0 { - errs = append(errs, fmt.Errorf("--identity-lease-renew-interval-seconds should be a positive number, but value '%d' provided", options.IdentityLeaseRenewIntervalSeconds)) - } - return errs -} - // Validate checks ServerRunOptions and return a slice of found errs. func (s *ServerRunOptions) Validate() []error { var errs []error @@ -171,7 +160,6 @@ func (s *ServerRunOptions) Validate() []error { errs = append(errs, s.APIEnablement.Validate(legacyscheme.Scheme, apiextensionsapiserver.Scheme, aggregatorscheme.Scheme)...) errs = append(errs, validateTokenRequest(s)...) errs = append(errs, s.Metrics.Validate()...) - errs = append(errs, validateAPIServerIdentity(s)...) return errs } diff --git a/cmd/kube-apiserver/app/server.go b/cmd/kube-apiserver/app/server.go index fc36d044dbe..b30b8624617 100644 --- a/cmd/kube-apiserver/app/server.go +++ b/cmd/kube-apiserver/app/server.go @@ -283,9 +283,6 @@ func CreateKubeAPIServerConfig(s completedServerRunOptions) ( ExtendExpiration: s.Authentication.ServiceAccounts.ExtendExpiration, VersionedInformers: versionedInformers, - - IdentityLeaseDurationSeconds: s.IdentityLeaseDurationSeconds, - IdentityLeaseRenewIntervalSeconds: s.IdentityLeaseRenewIntervalSeconds, }, } diff --git a/pkg/controlplane/instance.go b/pkg/controlplane/instance.go index 5ebcd4529fd..da41a5c3565 100644 --- a/pkg/controlplane/instance.go +++ b/pkg/controlplane/instance.go @@ -130,6 +130,18 @@ const ( repairLoopInterval = 3 * time.Minute ) +var ( + // IdentityLeaseGCPeriod is the interval which the lease GC controller checks for expired leases + // IdentityLeaseGCPeriod is exposed so integration tests can tune this value. + IdentityLeaseGCPeriod = 3600 * time.Second + // IdentityLeaseDurationSeconds is the duration of kube-apiserver lease in seconds + // IdentityLeaseDurationSeconds is exposed so integration tests can tune this value. + IdentityLeaseDurationSeconds = 3600 + // IdentityLeaseRenewIntervalSeconds is the interval of kube-apiserver renewing its lease in seconds + // IdentityLeaseRenewIntervalSeconds is exposed so integration tests can tune this value. + IdentityLeaseRenewIntervalPeriod = 10 * time.Second +) + // ExtraConfig defines extra configuration for the master type ExtraConfig struct { ClusterAuthenticationInfo clusterauthenticationtrust.ClusterAuthenticationInfo @@ -194,9 +206,6 @@ type ExtraConfig struct { VersionedInformers informers.SharedInformerFactory - IdentityLeaseDurationSeconds int - IdentityLeaseRenewIntervalSeconds int - // RepairServicesInterval interval used by the repair loops for // the Services NodePort and ClusterIP resources RepairServicesInterval time.Duration @@ -489,9 +498,9 @@ func (c completedConfig) New(delegationTarget genericapiserver.DelegationTarget) clock.RealClock{}, kubeClient, holderIdentity, - int32(c.ExtraConfig.IdentityLeaseDurationSeconds), + int32(IdentityLeaseDurationSeconds), nil, - time.Duration(c.ExtraConfig.IdentityLeaseRenewIntervalSeconds)*time.Second, + IdentityLeaseRenewIntervalPeriod, leaseName, metav1.NamespaceSystem, labelAPIServerHeartbeat) @@ -505,7 +514,7 @@ func (c completedConfig) New(delegationTarget genericapiserver.DelegationTarget) } go apiserverleasegc.NewAPIServerLeaseGC( kubeClient, - time.Duration(c.ExtraConfig.IdentityLeaseDurationSeconds)*time.Second, + IdentityLeaseGCPeriod, metav1.NamespaceSystem, KubeAPIServerIdentityLeaseLabelSelector, ).Run(hookContext.StopCh) diff --git a/staging/src/k8s.io/apiserver/pkg/features/kube_features.go b/staging/src/k8s.io/apiserver/pkg/features/kube_features.go index 8a57165c3f0..6da3fcc7ce8 100644 --- a/staging/src/k8s.io/apiserver/pkg/features/kube_features.go +++ b/staging/src/k8s.io/apiserver/pkg/features/kube_features.go @@ -216,7 +216,7 @@ var defaultKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureS APIResponseCompression: {Default: true, PreRelease: featuregate.Beta}, - APIServerIdentity: {Default: false, PreRelease: featuregate.Alpha}, + APIServerIdentity: {Default: true, PreRelease: featuregate.Beta}, APIServerTracing: {Default: false, PreRelease: featuregate.Alpha}, diff --git a/test/e2e/apimachinery/apiserver_identity.go b/test/e2e/apimachinery/apiserver_identity.go new file mode 100644 index 00000000000..2a54ad9e793 --- /dev/null +++ b/test/e2e/apimachinery/apiserver_identity.go @@ -0,0 +1,169 @@ +/* +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 apimachinery + +import ( + "context" + "crypto/sha256" + "encoding/base32" + "errors" + "fmt" + "net" + "strings" + "time" + + "github.com/onsi/ginkgo/v2" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/kubernetes/test/e2e/framework" + e2enode "k8s.io/kubernetes/test/e2e/framework/node" + e2eskipper "k8s.io/kubernetes/test/e2e/framework/skipper" + e2essh "k8s.io/kubernetes/test/e2e/framework/ssh" + admissionapi "k8s.io/pod-security-admission/api" +) + +func getControlPlaneHostname(node *v1.Node) (string, error) { + nodeAddresses := e2enode.GetAddresses(node, v1.NodeExternalIP) + if len(nodeAddresses) == 0 { + return "", errors.New("no valid addresses to use for SSH") + } + + controlPlaneAddress := nodeAddresses[0] + + host := controlPlaneAddress + ":" + e2essh.SSHPort + result, err := e2essh.SSH("hostname", host, framework.TestContext.Provider) + if err != nil { + return "", err + } + + if result.Code != 0 { + return "", fmt.Errorf("encountered non-zero exit code when running hostname command: %d", result.Code) + } + + return strings.TrimSpace(result.Stdout), nil +} + +// restartAPIServer attempts to restart the kube-apiserver on a node +func restartAPIServer(node *v1.Node) error { + nodeAddresses := e2enode.GetAddresses(node, v1.NodeExternalIP) + if len(nodeAddresses) == 0 { + return errors.New("no valid addresses to use for SSH") + } + + controlPlaneAddress := nodeAddresses[0] + cmd := "pidof kube-apiserver | xargs sudo kill" + framework.Logf("Restarting kube-apiserver via ssh, running: %v", cmd) + result, err := e2essh.SSH(cmd, net.JoinHostPort(controlPlaneAddress, e2essh.SSHPort), framework.TestContext.Provider) + if err != nil || result.Code != 0 { + e2essh.LogResult(result) + return fmt.Errorf("couldn't restart kube-apiserver: %v", err) + } + return nil +} + +// This test requires that --feature-gates=APIServerIdentity=true be set on the apiserver +var _ = SIGDescribe("kube-apiserver identity [Feature:APIServerIdentity]", func() { + f := framework.NewDefaultFramework("kube-apiserver-identity") + f.NamespacePodSecurityEnforceLevel = admissionapi.LevelPrivileged + + ginkgo.It("kube-apiserver identity should persist after restart [Disruptive]", func() { + e2eskipper.SkipUnlessProviderIs("gce") + + client := f.ClientSet + + var controlPlaneNodes []v1.Node + nodes, err := client.CoreV1().Nodes().List(context.TODO(), metav1.ListOptions{}) + framework.ExpectNoError(err) + + for _, node := range nodes.Items { + if _, ok := node.Labels["node-role.kubernetes.io/control-plane"]; ok { + controlPlaneNodes = append(controlPlaneNodes, node) + continue + } + + if _, ok := node.Labels["node-role.kubernetes.io/master"]; ok { + controlPlaneNodes = append(controlPlaneNodes, node) + continue + } + + for _, taint := range node.Spec.Taints { + if taint.Key == "node-role.kubernetes.io/master" { + controlPlaneNodes = append(controlPlaneNodes, node) + break + } + + if taint.Key == "node-role.kubernetes.io/control-plane" { + controlPlaneNodes = append(controlPlaneNodes, node) + break + } + } + } + + leases, err := client.CoordinationV1().Leases(metav1.NamespaceSystem).List(context.TODO(), metav1.ListOptions{ + LabelSelector: "k8s.io/component=kube-apiserver", + }) + framework.ExpectNoError(err) + framework.ExpectEqual(len(leases.Items), len(controlPlaneNodes), "unexpected number of leases") + + for _, node := range controlPlaneNodes { + hostname, err := getControlPlaneHostname(&node) + framework.ExpectNoError(err) + + hash := sha256.Sum256([]byte(hostname)) + leaseName := "kube-apiserver-" + strings.ToLower(base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(hash[:16])) + + lease, err := client.CoordinationV1().Leases(metav1.NamespaceSystem).Get(context.TODO(), leaseName, metav1.GetOptions{}) + framework.ExpectNoError(err) + oldHolderIdentity := lease.Spec.HolderIdentity + lastRenewedTime := lease.Spec.RenewTime + + err = restartAPIServer(&node) + framework.ExpectNoError(err) + + err = wait.PollImmediate(time.Second, wait.ForeverTestTimeout, func() (bool, error) { + lease, err = client.CoordinationV1().Leases(metav1.NamespaceSystem).Get(context.TODO(), leaseName, metav1.GetOptions{}) + if err != nil { + return false, nil + } + + // expect only the holder identity to change after a restart + newHolderIdentity := lease.Spec.HolderIdentity + if newHolderIdentity == oldHolderIdentity { + return false, nil + } + + // wait for at least one lease heart beat after the holder identity changes + if !lease.Spec.RenewTime.After(lastRenewedTime.Time) { + return false, nil + } + + return true, nil + + }) + framework.ExpectNoError(err, "holder identity did not change after a restart") + } + + // As long as the hostname of kube-apiserver is unchanged, a restart should not result in new Lease objects. + // Check that the number of lease objects remains the same after restarting kube-apiserver. + leases, err = client.CoordinationV1().Leases(metav1.NamespaceSystem).List(context.TODO(), metav1.ListOptions{ + LabelSelector: "k8s.io/component=kube-apiserver", + }) + framework.ExpectNoError(err) + framework.ExpectEqual(len(leases.Items), len(controlPlaneNodes), "unexpected number of leases") + }) +}) diff --git a/test/integration/controlplane/apiserver_identity_test.go b/test/integration/controlplane/apiserver_identity_test.go index 170b1167263..1924994051d 100644 --- a/test/integration/controlplane/apiserver_identity_test.go +++ b/test/integration/controlplane/apiserver_identity_test.go @@ -99,13 +99,23 @@ func TestCreateLeaseOnStart(t *testing.T) { } func TestLeaseGarbageCollection(t *testing.T) { + oldIdentityLeaseDurationSeconds := controlplane.IdentityLeaseDurationSeconds + oldIdentityLeaseGCPeriod := controlplane.IdentityLeaseGCPeriod + oldIdentityLeaseRenewIntervalPeriod := controlplane.IdentityLeaseRenewIntervalPeriod + defer func() { + // reset the default values for leases after this test + controlplane.IdentityLeaseDurationSeconds = oldIdentityLeaseDurationSeconds + controlplane.IdentityLeaseGCPeriod = oldIdentityLeaseGCPeriod + controlplane.IdentityLeaseRenewIntervalPeriod = oldIdentityLeaseRenewIntervalPeriod + }() + + // Shorten lease parameters so GC behavior can be exercised in integration tests + controlplane.IdentityLeaseDurationSeconds = 1 + controlplane.IdentityLeaseGCPeriod = time.Second + controlplane.IdentityLeaseRenewIntervalPeriod = time.Second + defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.APIServerIdentity, true)() - result := kubeapiservertesting.StartTestServerOrDie(t, nil, - // This shorten the GC check period to make the test run faster. - // Since we are testing GC behavior on leases we create, what happens to - // the real apiserver lease doesn't matter. - []string{"--identity-lease-duration-seconds=1"}, - framework.SharedEtcd()) + result := kubeapiservertesting.StartTestServerOrDie(t, nil, nil, framework.SharedEtcd()) defer result.TearDownFn() kubeclient, err := kubernetes.NewForConfig(result.ClientConfig)