mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-23 19:56:01 +00:00
Merge pull request #36101 from jbeda/bootstrap-signer
Automatic merge from submit-queue (batch tested with PRs 38252, 41122, 36101, 41017, 41264) BootstrapSigner and TokenCleaner controllers This is part of https://github.com/kubernetes/features/issues/130 and is an implementation of https://github.com/kubernetes/community/pull/189. Work that needs to be done yet in this PR: * [ ] ~~e2e tests~~ Will come in new PR. * [x] flag to disable this by default ```release-note Native support for token based bootstrap flow. This includes signing a well known ConfigMap in the `kube-public` namespace and cleaning out expired tokens. ``` @kubernetes/sig-cluster-lifecycle @dgoodwin @roberthbailey @mikedanese
This commit is contained in:
commit
866aa73591
@ -14,6 +14,7 @@ go_library(
|
|||||||
"apps.go",
|
"apps.go",
|
||||||
"autoscaling.go",
|
"autoscaling.go",
|
||||||
"batch.go",
|
"batch.go",
|
||||||
|
"bootstrap.go",
|
||||||
"certificates.go",
|
"certificates.go",
|
||||||
"controllermanager.go",
|
"controllermanager.go",
|
||||||
"core.go",
|
"core.go",
|
||||||
@ -42,6 +43,7 @@ go_library(
|
|||||||
"//pkg/cloudprovider/providers/photon:go_default_library",
|
"//pkg/cloudprovider/providers/photon:go_default_library",
|
||||||
"//pkg/cloudprovider/providers/vsphere:go_default_library",
|
"//pkg/cloudprovider/providers/vsphere:go_default_library",
|
||||||
"//pkg/controller:go_default_library",
|
"//pkg/controller:go_default_library",
|
||||||
|
"//pkg/controller/bootstrap:go_default_library",
|
||||||
"//pkg/controller/certificates:go_default_library",
|
"//pkg/controller/certificates:go_default_library",
|
||||||
"//pkg/controller/cronjob:go_default_library",
|
"//pkg/controller/cronjob:go_default_library",
|
||||||
"//pkg/controller/daemon:go_default_library",
|
"//pkg/controller/daemon:go_default_library",
|
||||||
|
35
cmd/kube-controller-manager/app/bootstrap.go
Normal file
35
cmd/kube-controller-manager/app/bootstrap.go
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2016 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 "k8s.io/kubernetes/pkg/controller/bootstrap"
|
||||||
|
|
||||||
|
func startBootstrapSignerController(ctx ControllerContext) (bool, error) {
|
||||||
|
go bootstrap.NewBootstrapSigner(
|
||||||
|
ctx.ClientBuilder.ClientGoClientOrDie("bootstrap-signer"),
|
||||||
|
bootstrap.DefaultBootstrapSignerOptions(),
|
||||||
|
).Run(ctx.Stop)
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func startTokenCleanerController(ctx ControllerContext) (bool, error) {
|
||||||
|
go bootstrap.NewTokenCleaner(
|
||||||
|
ctx.ClientBuilder.ClientGoClientOrDie("token-cleaner"),
|
||||||
|
bootstrap.DefaultTokenCleanerOptions(),
|
||||||
|
).Run(ctx.Stop)
|
||||||
|
return true, nil
|
||||||
|
}
|
@ -270,7 +270,10 @@ func KnownControllers() []string {
|
|||||||
return sets.StringKeySet(newControllerInitializers()).List()
|
return sets.StringKeySet(newControllerInitializers()).List()
|
||||||
}
|
}
|
||||||
|
|
||||||
var ControllersDisabledByDefault = sets.NewString()
|
var ControllersDisabledByDefault = sets.NewString(
|
||||||
|
"bootstrapsigner",
|
||||||
|
"tokencleaner",
|
||||||
|
)
|
||||||
|
|
||||||
func newControllerInitializers() map[string]InitFunc {
|
func newControllerInitializers() map[string]InitFunc {
|
||||||
controllers := map[string]InitFunc{}
|
controllers := map[string]InitFunc{}
|
||||||
@ -291,6 +294,8 @@ func newControllerInitializers() map[string]InitFunc {
|
|||||||
controllers["cronjob"] = startCronJobController
|
controllers["cronjob"] = startCronJobController
|
||||||
controllers["certificatesigningrequests"] = startCSRController
|
controllers["certificatesigningrequests"] = startCSRController
|
||||||
controllers["ttl"] = startTTLController
|
controllers["ttl"] = startTTLController
|
||||||
|
controllers["bootstrapsigner"] = startBootstrapSignerController
|
||||||
|
controllers["tokencleaner"] = startTokenCleanerController
|
||||||
|
|
||||||
return controllers
|
return controllers
|
||||||
}
|
}
|
||||||
|
@ -36,6 +36,7 @@ go_library(
|
|||||||
"//cmd/kubeadm/app/preflight:go_default_library",
|
"//cmd/kubeadm/app/preflight:go_default_library",
|
||||||
"//cmd/kubeadm/app/util:go_default_library",
|
"//cmd/kubeadm/app/util:go_default_library",
|
||||||
"//pkg/api:go_default_library",
|
"//pkg/api:go_default_library",
|
||||||
|
"//pkg/bootstrap/api:go_default_library",
|
||||||
"//pkg/kubectl:go_default_library",
|
"//pkg/kubectl:go_default_library",
|
||||||
"//pkg/kubectl/cmd/util:go_default_library",
|
"//pkg/kubectl/cmd/util:go_default_library",
|
||||||
"//pkg/util/initsystem:go_default_library",
|
"//pkg/util/initsystem:go_default_library",
|
||||||
@ -48,6 +49,7 @@ go_library(
|
|||||||
"//vendor:k8s.io/apimachinery/pkg/runtime",
|
"//vendor:k8s.io/apimachinery/pkg/runtime",
|
||||||
"//vendor:k8s.io/apimachinery/pkg/util/net",
|
"//vendor:k8s.io/apimachinery/pkg/util/net",
|
||||||
"//vendor:k8s.io/apiserver/pkg/util/flag",
|
"//vendor:k8s.io/apiserver/pkg/util/flag",
|
||||||
|
"//vendor:k8s.io/client-go/pkg/api",
|
||||||
"//vendor:k8s.io/client-go/util/cert",
|
"//vendor:k8s.io/client-go/util/cert",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@ -29,11 +29,12 @@ import (
|
|||||||
|
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/fields"
|
"k8s.io/apimachinery/pkg/fields"
|
||||||
|
"k8s.io/client-go/pkg/api"
|
||||||
kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm"
|
kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm"
|
||||||
kubemaster "k8s.io/kubernetes/cmd/kubeadm/app/master"
|
kubemaster "k8s.io/kubernetes/cmd/kubeadm/app/master"
|
||||||
"k8s.io/kubernetes/cmd/kubeadm/app/phases/kubeconfig"
|
"k8s.io/kubernetes/cmd/kubeadm/app/phases/kubeconfig"
|
||||||
kubeadmutil "k8s.io/kubernetes/cmd/kubeadm/app/util"
|
kubeadmutil "k8s.io/kubernetes/cmd/kubeadm/app/util"
|
||||||
"k8s.io/kubernetes/pkg/api"
|
bootstrapapi "k8s.io/kubernetes/pkg/bootstrap/api"
|
||||||
"k8s.io/kubernetes/pkg/kubectl"
|
"k8s.io/kubernetes/pkg/kubectl"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -164,7 +165,7 @@ func RunListTokens(out io.Writer, errW io.Writer, cmd *cobra.Command) error {
|
|||||||
|
|
||||||
tokenSelector := fields.SelectorFromSet(
|
tokenSelector := fields.SelectorFromSet(
|
||||||
map[string]string{
|
map[string]string{
|
||||||
api.SecretTypeField: string(api.SecretTypeBootstrapToken),
|
api.SecretTypeField: string(bootstrapapi.SecretTypeBootstrapToken),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
listOptions := metav1.ListOptions{
|
listOptions := metav1.ListOptions{
|
||||||
|
@ -21,8 +21,8 @@ go_library(
|
|||||||
"//cmd/kubeadm/app/apis/kubeadm:go_default_library",
|
"//cmd/kubeadm/app/apis/kubeadm:go_default_library",
|
||||||
"//cmd/kubeadm/app/apis/kubeadm/v1alpha1:go_default_library",
|
"//cmd/kubeadm/app/apis/kubeadm/v1alpha1:go_default_library",
|
||||||
"//cmd/kubeadm/app/preflight:go_default_library",
|
"//cmd/kubeadm/app/preflight:go_default_library",
|
||||||
"//pkg/api:go_default_library",
|
|
||||||
"//pkg/api/v1:go_default_library",
|
"//pkg/api/v1:go_default_library",
|
||||||
|
"//pkg/bootstrap/api:go_default_library",
|
||||||
"//pkg/client/clientset_generated/clientset:go_default_library",
|
"//pkg/client/clientset_generated/clientset:go_default_library",
|
||||||
"//vendor:k8s.io/apimachinery/pkg/api/errors",
|
"//vendor:k8s.io/apimachinery/pkg/api/errors",
|
||||||
"//vendor:k8s.io/apimachinery/pkg/apis/meta/v1",
|
"//vendor:k8s.io/apimachinery/pkg/apis/meta/v1",
|
||||||
|
@ -29,8 +29,8 @@ import (
|
|||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm"
|
kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm"
|
||||||
kubeadmapiext "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1alpha1"
|
kubeadmapiext "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1alpha1"
|
||||||
"k8s.io/kubernetes/pkg/api"
|
|
||||||
v1 "k8s.io/kubernetes/pkg/api/v1"
|
v1 "k8s.io/kubernetes/pkg/api/v1"
|
||||||
|
bootstrapapi "k8s.io/kubernetes/pkg/bootstrap/api"
|
||||||
"k8s.io/kubernetes/pkg/client/clientset_generated/clientset"
|
"k8s.io/kubernetes/pkg/client/clientset_generated/clientset"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -154,7 +154,9 @@ func UpdateOrCreateToken(client *clientset.Clientset, d *kubeadmapi.TokenDiscove
|
|||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
Name: secretName,
|
Name: secretName,
|
||||||
},
|
},
|
||||||
Type: api.SecretTypeBootstrapToken,
|
// TODO(jbeda): convert kubeadm to client-go
|
||||||
|
// https://github.com/kubernetes/kubeadm/issues/52
|
||||||
|
Type: v1.SecretType(bootstrapapi.SecretTypeBootstrapToken),
|
||||||
Data: encodeTokenSecretData(d, tokenDuration),
|
Data: encodeTokenSecretData(d, tokenDuration),
|
||||||
}
|
}
|
||||||
if _, err := client.Secrets(metav1.NamespaceSystem).Create(secret); err == nil {
|
if _, err := client.Secrets(metav1.NamespaceSystem).Create(secret); err == nil {
|
||||||
|
@ -87,6 +87,7 @@ pkg/apis/rbac/install
|
|||||||
pkg/apis/rbac/v1alpha1
|
pkg/apis/rbac/v1alpha1
|
||||||
pkg/apis/storage/install
|
pkg/apis/storage/install
|
||||||
pkg/apis/storage/validation
|
pkg/apis/storage/validation
|
||||||
|
pkg/bootstrap/api
|
||||||
pkg/client/conditions
|
pkg/client/conditions
|
||||||
pkg/client/informers/informers_generated/apps
|
pkg/client/informers/informers_generated/apps
|
||||||
pkg/client/informers/informers_generated/apps/internalversion
|
pkg/client/informers/informers_generated/apps/internalversion
|
||||||
|
@ -31,6 +31,7 @@ filegroup(
|
|||||||
"//pkg/apis/storage:all-srcs",
|
"//pkg/apis/storage:all-srcs",
|
||||||
"//pkg/auth/authorizer/abac:all-srcs",
|
"//pkg/auth/authorizer/abac:all-srcs",
|
||||||
"//pkg/auth/user:all-srcs",
|
"//pkg/auth/user:all-srcs",
|
||||||
|
"//pkg/bootstrap/api:all-srcs",
|
||||||
"//pkg/capabilities:all-srcs",
|
"//pkg/capabilities:all-srcs",
|
||||||
"//pkg/client/chaosclient:all-srcs",
|
"//pkg/client/chaosclient:all-srcs",
|
||||||
"//pkg/client/clientset_generated/clientset:all-srcs",
|
"//pkg/client/clientset_generated/clientset:all-srcs",
|
||||||
|
@ -189,6 +189,8 @@ const (
|
|||||||
NamespaceNone string = ""
|
NamespaceNone string = ""
|
||||||
// NamespaceSystem is the system namespace where we place system components.
|
// NamespaceSystem is the system namespace where we place system components.
|
||||||
NamespaceSystem string = "kube-system"
|
NamespaceSystem string = "kube-system"
|
||||||
|
// NamespacePublic is the namespace where we place public info (ConfigMaps)
|
||||||
|
NamespacePublic string = "kube-public"
|
||||||
// TerminationMessagePathDefault means the default path to capture the application termination message running in a container
|
// TerminationMessagePathDefault means the default path to capture the application termination message running in a container
|
||||||
TerminationMessagePathDefault string = "/dev/termination-log"
|
TerminationMessagePathDefault string = "/dev/termination-log"
|
||||||
)
|
)
|
||||||
@ -3344,9 +3346,6 @@ const (
|
|||||||
// - Secret.Data["token"] - a token that identifies the service account to the API
|
// - Secret.Data["token"] - a token that identifies the service account to the API
|
||||||
SecretTypeServiceAccountToken SecretType = "kubernetes.io/service-account-token"
|
SecretTypeServiceAccountToken SecretType = "kubernetes.io/service-account-token"
|
||||||
|
|
||||||
// SecretTypeBootstrapToken is the key for tokens used by kubeadm to validate cluster info during discovery.
|
|
||||||
SecretTypeBootstrapToken = "bootstrap.kubernetes.io/token"
|
|
||||||
|
|
||||||
// ServiceAccountNameKey is the key of the required annotation for SecretTypeServiceAccountToken secrets
|
// ServiceAccountNameKey is the key of the required annotation for SecretTypeServiceAccountToken secrets
|
||||||
ServiceAccountNameKey = "kubernetes.io/service-account.name"
|
ServiceAccountNameKey = "kubernetes.io/service-account.name"
|
||||||
// ServiceAccountUIDKey is the key of the required annotation for SecretTypeServiceAccountToken secrets
|
// ServiceAccountUIDKey is the key of the required annotation for SecretTypeServiceAccountToken secrets
|
||||||
|
31
pkg/bootstrap/api/BUILD
Normal file
31
pkg/bootstrap/api/BUILD
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
package(default_visibility = ["//visibility:public"])
|
||||||
|
|
||||||
|
licenses(["notice"])
|
||||||
|
|
||||||
|
load(
|
||||||
|
"@io_bazel_rules_go//go:def.bzl",
|
||||||
|
"go_library",
|
||||||
|
)
|
||||||
|
|
||||||
|
go_library(
|
||||||
|
name = "go_default_library",
|
||||||
|
srcs = [
|
||||||
|
"doc.go",
|
||||||
|
"types.go",
|
||||||
|
],
|
||||||
|
tags = ["automanaged"],
|
||||||
|
deps = ["//vendor:k8s.io/client-go/pkg/api/v1"],
|
||||||
|
)
|
||||||
|
|
||||||
|
filegroup(
|
||||||
|
name = "package-srcs",
|
||||||
|
srcs = glob(["**"]),
|
||||||
|
tags = ["automanaged"],
|
||||||
|
visibility = ["//visibility:private"],
|
||||||
|
)
|
||||||
|
|
||||||
|
filegroup(
|
||||||
|
name = "all-srcs",
|
||||||
|
srcs = [":package-srcs"],
|
||||||
|
tags = ["automanaged"],
|
||||||
|
)
|
20
pkg/bootstrap/api/doc.go
Normal file
20
pkg/bootstrap/api/doc.go
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
/*
|
||||||
|
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 api (pkg/bootstrap/api) contains constants and types needed for
|
||||||
|
// bootstrap tokens as maintained by the BootstrapSigner and TokenCleaner
|
||||||
|
// controllers (in pkg/controller/bootstrap)
|
||||||
|
package api // import "k8s.io/kubernetes/pkg/bootstrap/api"
|
48
pkg/bootstrap/api/types.go
Normal file
48
pkg/bootstrap/api/types.go
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
/*
|
||||||
|
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 api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"k8s.io/client-go/pkg/api/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// SecretTypeBootstrapToken is used during the automated bootstrap process (first
|
||||||
|
// implemented by kubeadm). It stores tokens that are used to sign well known
|
||||||
|
// ConfigMaps. They may also eventually be used for authentication.
|
||||||
|
SecretTypeBootstrapToken v1.SecretType = "bootstrap.kubernetes.io/token"
|
||||||
|
|
||||||
|
// BootstrapTokenIDKey is the id of this token. This can be transmitted in the
|
||||||
|
// clear and encoded in the name of the secret. It should be a random 6
|
||||||
|
// character string. Required
|
||||||
|
BootstrapTokenIDKey = "token-id"
|
||||||
|
|
||||||
|
// BootstrapTokenSecretKey is the actual secret. Typically this is a random 16
|
||||||
|
// character string. Required.
|
||||||
|
BootstrapTokenSecretKey = "token-secret"
|
||||||
|
|
||||||
|
// BootstrapTokenExpirationKey is when this token should be expired and no
|
||||||
|
// longer used. A controller will delete this resource after this time. This
|
||||||
|
// is an absolute UTC time using RFC3339. If this cannot be parsed, the token
|
||||||
|
// should be considered invalid. Optional.
|
||||||
|
BootstrapTokenExpirationKey = "expiration"
|
||||||
|
|
||||||
|
// BootstrapTokenUsageSigningKey signals that this token should be used to
|
||||||
|
// sign configs as part of the bootstrap process. Value must be "true". Any
|
||||||
|
// other value is assumed to be false. Optional.
|
||||||
|
BootstrapTokenUsageSigningKey = "usage-bootstrap-signing"
|
||||||
|
)
|
@ -84,6 +84,7 @@ filegroup(
|
|||||||
name = "all-srcs",
|
name = "all-srcs",
|
||||||
srcs = [
|
srcs = [
|
||||||
":package-srcs",
|
":package-srcs",
|
||||||
|
"//pkg/controller/bootstrap:all-srcs",
|
||||||
"//pkg/controller/certificates:all-srcs",
|
"//pkg/controller/certificates:all-srcs",
|
||||||
"//pkg/controller/cloud:all-srcs",
|
"//pkg/controller/cloud:all-srcs",
|
||||||
"//pkg/controller/cronjob:all-srcs",
|
"//pkg/controller/cronjob:all-srcs",
|
||||||
|
75
pkg/controller/bootstrap/BUILD
Normal file
75
pkg/controller/bootstrap/BUILD
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
package(default_visibility = ["//visibility:public"])
|
||||||
|
|
||||||
|
licenses(["notice"])
|
||||||
|
|
||||||
|
load(
|
||||||
|
"@io_bazel_rules_go//go:def.bzl",
|
||||||
|
"go_library",
|
||||||
|
"go_test",
|
||||||
|
)
|
||||||
|
|
||||||
|
go_test(
|
||||||
|
name = "go_default_test",
|
||||||
|
srcs = [
|
||||||
|
"bootstrapsigner_test.go",
|
||||||
|
"common_test.go",
|
||||||
|
"jws_test.go",
|
||||||
|
"tokencleaner_test.go",
|
||||||
|
"util_test.go",
|
||||||
|
],
|
||||||
|
library = ":go_default_library",
|
||||||
|
tags = ["automanaged"],
|
||||||
|
deps = [
|
||||||
|
"//pkg/bootstrap/api:go_default_library",
|
||||||
|
"//vendor:github.com/davecgh/go-spew/spew",
|
||||||
|
"//vendor:k8s.io/apimachinery/pkg/apis/meta/v1",
|
||||||
|
"//vendor:k8s.io/apimachinery/pkg/runtime/schema",
|
||||||
|
"//vendor:k8s.io/client-go/kubernetes/fake",
|
||||||
|
"//vendor:k8s.io/client-go/pkg/api",
|
||||||
|
"//vendor:k8s.io/client-go/pkg/api/v1",
|
||||||
|
"//vendor:k8s.io/client-go/testing",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
go_library(
|
||||||
|
name = "go_default_library",
|
||||||
|
srcs = [
|
||||||
|
"bootstrapsigner.go",
|
||||||
|
"doc.go",
|
||||||
|
"jws.go",
|
||||||
|
"tokencleaner.go",
|
||||||
|
"util.go",
|
||||||
|
],
|
||||||
|
tags = ["automanaged"],
|
||||||
|
deps = [
|
||||||
|
"//pkg/bootstrap/api:go_default_library",
|
||||||
|
"//pkg/util/metrics:go_default_library",
|
||||||
|
"//vendor:github.com/golang/glog",
|
||||||
|
"//vendor:github.com/square/go-jose",
|
||||||
|
"//vendor:k8s.io/apimachinery/pkg/api/errors",
|
||||||
|
"//vendor:k8s.io/apimachinery/pkg/apis/meta/v1",
|
||||||
|
"//vendor:k8s.io/apimachinery/pkg/fields",
|
||||||
|
"//vendor:k8s.io/apimachinery/pkg/runtime",
|
||||||
|
"//vendor:k8s.io/apimachinery/pkg/util/runtime",
|
||||||
|
"//vendor:k8s.io/apimachinery/pkg/util/wait",
|
||||||
|
"//vendor:k8s.io/apimachinery/pkg/watch",
|
||||||
|
"//vendor:k8s.io/client-go/kubernetes",
|
||||||
|
"//vendor:k8s.io/client-go/pkg/api",
|
||||||
|
"//vendor:k8s.io/client-go/pkg/api/v1",
|
||||||
|
"//vendor:k8s.io/client-go/tools/cache",
|
||||||
|
"//vendor:k8s.io/client-go/util/workqueue",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
filegroup(
|
||||||
|
name = "package-srcs",
|
||||||
|
srcs = glob(["**"]),
|
||||||
|
tags = ["automanaged"],
|
||||||
|
visibility = ["//visibility:private"],
|
||||||
|
)
|
||||||
|
|
||||||
|
filegroup(
|
||||||
|
name = "all-srcs",
|
||||||
|
srcs = [":package-srcs"],
|
||||||
|
tags = ["automanaged"],
|
||||||
|
)
|
303
pkg/controller/bootstrap/bootstrapsigner.go
Normal file
303
pkg/controller/bootstrap/bootstrapsigner.go
Normal file
@ -0,0 +1,303 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2016 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 bootstrap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/golang/glog"
|
||||||
|
|
||||||
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/fields"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/util/wait"
|
||||||
|
"k8s.io/apimachinery/pkg/watch"
|
||||||
|
clientset "k8s.io/client-go/kubernetes"
|
||||||
|
"k8s.io/client-go/pkg/api"
|
||||||
|
"k8s.io/client-go/pkg/api/v1"
|
||||||
|
"k8s.io/client-go/tools/cache"
|
||||||
|
"k8s.io/client-go/util/workqueue"
|
||||||
|
bootstrapapi "k8s.io/kubernetes/pkg/bootstrap/api"
|
||||||
|
"k8s.io/kubernetes/pkg/util/metrics"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
configMapClusterInfo = "cluster-info"
|
||||||
|
kubeConfigKey = "kubeconfig"
|
||||||
|
signaturePrefix = "jws-kubeconfig-"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BootstrapSignerOptions contains options for the BootstrapSigner
|
||||||
|
type BootstrapSignerOptions struct {
|
||||||
|
|
||||||
|
// ConfigMapNamespace is the namespace of the ConfigMap
|
||||||
|
ConfigMapNamespace string
|
||||||
|
|
||||||
|
// ConfigMapName is the name for the ConfigMap
|
||||||
|
ConfigMapName string
|
||||||
|
|
||||||
|
// TokenSecretNamespace string is the namespace for token Secrets.
|
||||||
|
TokenSecretNamespace string
|
||||||
|
|
||||||
|
// ConfigMapResynce is the time.Duration at which to fully re-list configmaps.
|
||||||
|
// If zero, re-list will be delayed as long as possible
|
||||||
|
ConfigMapResync time.Duration
|
||||||
|
|
||||||
|
// SecretResync is the time.Duration at which to fully re-list secrets.
|
||||||
|
// If zero, re-list will be delayed as long as possible
|
||||||
|
SecretResync time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultBootstrapSignerOptions returns a set of default options for creating a
|
||||||
|
// BootstrapSigner
|
||||||
|
func DefaultBootstrapSignerOptions() BootstrapSignerOptions {
|
||||||
|
return BootstrapSignerOptions{
|
||||||
|
ConfigMapNamespace: api.NamespacePublic,
|
||||||
|
ConfigMapName: configMapClusterInfo,
|
||||||
|
TokenSecretNamespace: api.NamespaceSystem,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BootstrapSigner is a controller that signs a ConfigMap with a set of tokens.
|
||||||
|
type BootstrapSigner struct {
|
||||||
|
client clientset.Interface
|
||||||
|
configMapKey string
|
||||||
|
secretNamespace string
|
||||||
|
|
||||||
|
configMaps cache.Store
|
||||||
|
secrets cache.Store
|
||||||
|
|
||||||
|
// syncQueue handles synchronizing updates to the ConfigMap. We'll only ever
|
||||||
|
// have one item (Named <ConfigMapName>) in this queue. We are using it
|
||||||
|
// serializes and collapses updates as they can come from both the ConfigMap
|
||||||
|
// and Secrets controllers.
|
||||||
|
syncQueue workqueue.Interface
|
||||||
|
|
||||||
|
// Since we join two objects, we'll watch both of them with controllers.
|
||||||
|
configMapsController cache.Controller
|
||||||
|
secretsController cache.Controller
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBootstrapSigner returns a new *BootstrapSigner.
|
||||||
|
//
|
||||||
|
// TODO: Switch to shared informers
|
||||||
|
func NewBootstrapSigner(cl clientset.Interface, options BootstrapSignerOptions) *BootstrapSigner {
|
||||||
|
e := &BootstrapSigner{
|
||||||
|
client: cl,
|
||||||
|
configMapKey: options.ConfigMapNamespace + "/" + options.ConfigMapName,
|
||||||
|
secretNamespace: options.TokenSecretNamespace,
|
||||||
|
syncQueue: workqueue.NewNamed("bootstrap_signer_queue"),
|
||||||
|
}
|
||||||
|
if cl.Core().RESTClient().GetRateLimiter() != nil {
|
||||||
|
metrics.RegisterMetricAndTrackRateLimiterUsage("bootstrap_signer", cl.Core().RESTClient().GetRateLimiter())
|
||||||
|
}
|
||||||
|
configMapSelector := fields.SelectorFromSet(map[string]string{api.ObjectNameField: options.ConfigMapName})
|
||||||
|
e.configMaps, e.configMapsController = cache.NewInformer(
|
||||||
|
&cache.ListWatch{
|
||||||
|
ListFunc: func(lo metav1.ListOptions) (runtime.Object, error) {
|
||||||
|
lo.FieldSelector = configMapSelector.String()
|
||||||
|
return e.client.Core().ConfigMaps(options.ConfigMapNamespace).List(lo)
|
||||||
|
},
|
||||||
|
WatchFunc: func(lo metav1.ListOptions) (watch.Interface, error) {
|
||||||
|
lo.FieldSelector = configMapSelector.String()
|
||||||
|
return e.client.Core().ConfigMaps(options.ConfigMapNamespace).Watch(lo)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&v1.ConfigMap{},
|
||||||
|
options.ConfigMapResync,
|
||||||
|
cache.ResourceEventHandlerFuncs{
|
||||||
|
AddFunc: func(_ interface{}) { e.pokeConfigMapSync() },
|
||||||
|
UpdateFunc: func(_, _ interface{}) { e.pokeConfigMapSync() },
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
secretSelector := fields.SelectorFromSet(map[string]string{api.SecretTypeField: string(bootstrapapi.SecretTypeBootstrapToken)})
|
||||||
|
e.secrets, e.secretsController = cache.NewInformer(
|
||||||
|
&cache.ListWatch{
|
||||||
|
ListFunc: func(lo metav1.ListOptions) (runtime.Object, error) {
|
||||||
|
lo.FieldSelector = secretSelector.String()
|
||||||
|
return e.client.Core().Secrets(e.secretNamespace).List(lo)
|
||||||
|
},
|
||||||
|
WatchFunc: func(lo metav1.ListOptions) (watch.Interface, error) {
|
||||||
|
lo.FieldSelector = secretSelector.String()
|
||||||
|
return e.client.Core().Secrets(e.secretNamespace).Watch(lo)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&v1.Secret{},
|
||||||
|
options.SecretResync,
|
||||||
|
cache.ResourceEventHandlerFuncs{
|
||||||
|
AddFunc: func(_ interface{}) { e.pokeConfigMapSync() },
|
||||||
|
UpdateFunc: func(_, _ interface{}) { e.pokeConfigMapSync() },
|
||||||
|
DeleteFunc: func(_ interface{}) { e.pokeConfigMapSync() },
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run runs controller loops and returns when they are done
|
||||||
|
func (e *BootstrapSigner) Run(stopCh <-chan struct{}) {
|
||||||
|
go e.configMapsController.Run(stopCh)
|
||||||
|
go e.secretsController.Run(stopCh)
|
||||||
|
go wait.Until(e.serviceConfigMapQueue, 0, stopCh)
|
||||||
|
<-stopCh
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *BootstrapSigner) pokeConfigMapSync() {
|
||||||
|
e.syncQueue.Add(e.configMapKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *BootstrapSigner) serviceConfigMapQueue() {
|
||||||
|
key, quit := e.syncQueue.Get()
|
||||||
|
if quit {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer e.syncQueue.Done(key)
|
||||||
|
|
||||||
|
e.signConfigMap()
|
||||||
|
}
|
||||||
|
|
||||||
|
// signConfigMap computes the signatures on our latest cached objects and writes
|
||||||
|
// back if necessary.
|
||||||
|
func (e *BootstrapSigner) signConfigMap() {
|
||||||
|
origCM := e.getConfigMap()
|
||||||
|
|
||||||
|
if origCM == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var needUpdate = false
|
||||||
|
|
||||||
|
newCM, err := copyConfigMap(origCM)
|
||||||
|
if err != nil {
|
||||||
|
utilruntime.HandleError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// First capture the config we are signing
|
||||||
|
content, ok := newCM.Data[kubeConfigKey]
|
||||||
|
if !ok {
|
||||||
|
glog.V(3).Infof("No %s key in %s/%s ConfigMap", kubeConfigKey, origCM.Namespace, origCM.Name)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next remove and save all existing signatures
|
||||||
|
sigs := map[string]string{}
|
||||||
|
for key, value := range newCM.Data {
|
||||||
|
if strings.HasPrefix(key, signaturePrefix) {
|
||||||
|
tokenID := strings.TrimPrefix(key, signaturePrefix)
|
||||||
|
sigs[tokenID] = value
|
||||||
|
delete(newCM.Data, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now recompute signatures and store them on the new map
|
||||||
|
tokens := e.getTokens()
|
||||||
|
for tokenID, tokenValue := range tokens {
|
||||||
|
sig, err := computeDetachedSig(content, tokenID, tokenValue)
|
||||||
|
if err != nil {
|
||||||
|
utilruntime.HandleError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check to see if this signature is changed or new.
|
||||||
|
oldSig, _ := sigs[tokenID]
|
||||||
|
if sig != oldSig {
|
||||||
|
needUpdate = true
|
||||||
|
}
|
||||||
|
delete(sigs, tokenID)
|
||||||
|
|
||||||
|
newCM.Data[signaturePrefix+tokenID] = sig
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have signatures left over we know that some signatures were
|
||||||
|
// removed. We now need to update the ConfigMap
|
||||||
|
if len(sigs) != 0 {
|
||||||
|
needUpdate = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if needUpdate {
|
||||||
|
e.updateConfigMap(newCM)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *BootstrapSigner) updateConfigMap(cm *v1.ConfigMap) {
|
||||||
|
_, err := e.client.Core().ConfigMaps(cm.Namespace).Update(cm)
|
||||||
|
if err != nil && !apierrors.IsConflict(err) && !apierrors.IsNotFound(err) {
|
||||||
|
glog.V(3).Infof("Error updating ConfigMap: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// getConfigMap gets the ConfigMap we are interested in
|
||||||
|
func (e *BootstrapSigner) getConfigMap() *v1.ConfigMap {
|
||||||
|
configMap, exists, err := e.configMaps.GetByKey(e.configMapKey)
|
||||||
|
|
||||||
|
// If we can't get the configmap just return nil. The resync will eventually
|
||||||
|
// sync things up.
|
||||||
|
if err != nil {
|
||||||
|
utilruntime.HandleError(err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if exists {
|
||||||
|
return configMap.(*v1.ConfigMap)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *BootstrapSigner) listSecrets() []*v1.Secret {
|
||||||
|
secrets := e.secrets.List()
|
||||||
|
|
||||||
|
items := []*v1.Secret{}
|
||||||
|
for _, obj := range secrets {
|
||||||
|
items = append(items, obj.(*v1.Secret))
|
||||||
|
}
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
|
// getTokens returns a map of tokenID->tokenSecret. It ensures the token is
|
||||||
|
// valid for signing.
|
||||||
|
func (e *BootstrapSigner) getTokens() map[string]string {
|
||||||
|
ret := map[string]string{}
|
||||||
|
secretObjs := e.listSecrets()
|
||||||
|
for _, secret := range secretObjs {
|
||||||
|
tokenID, tokenSecret, ok := validateSecretForSigning(secret)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check and warn for duplicate secrets. Behavior here will be undefined.
|
||||||
|
if _, ok := ret[tokenID]; ok {
|
||||||
|
glog.V(3).Infof("Duplicate bootstrap tokens found for id %s, ignoring on in %s/%s", tokenID, secret.Namespace, secret.Name)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// This secret looks good, add it to the list.
|
||||||
|
ret[tokenID] = tokenSecret
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyConfigMap(orig *v1.ConfigMap) (*v1.ConfigMap, error) {
|
||||||
|
newCMObj, err := api.Scheme.DeepCopy(orig)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return newCMObj.(*v1.ConfigMap), nil
|
||||||
|
}
|
137
pkg/controller/bootstrap/bootstrapsigner_test.go
Normal file
137
pkg/controller/bootstrap/bootstrapsigner_test.go
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2016 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 bootstrap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/davecgh/go-spew/spew"
|
||||||
|
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
"k8s.io/client-go/kubernetes/fake"
|
||||||
|
"k8s.io/client-go/pkg/api"
|
||||||
|
"k8s.io/client-go/pkg/api/v1"
|
||||||
|
core "k8s.io/client-go/testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
spew.Config.DisableMethods = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func newBootstrapSigner() (*BootstrapSigner, *fake.Clientset) {
|
||||||
|
options := DefaultBootstrapSignerOptions()
|
||||||
|
cl := fake.NewSimpleClientset()
|
||||||
|
return NewBootstrapSigner(cl, options), cl
|
||||||
|
}
|
||||||
|
|
||||||
|
func newConfigMap(tokenID, signature string) *v1.ConfigMap {
|
||||||
|
ret := &v1.ConfigMap{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Namespace: metav1.NamespacePublic,
|
||||||
|
Name: configMapClusterInfo,
|
||||||
|
ResourceVersion: "1",
|
||||||
|
},
|
||||||
|
Data: map[string]string{
|
||||||
|
kubeConfigKey: "payload",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if len(tokenID) > 0 {
|
||||||
|
ret.Data[signaturePrefix+tokenID] = signature
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNoConfigMap(t *testing.T) {
|
||||||
|
signer, cl := newBootstrapSigner()
|
||||||
|
signer.signConfigMap()
|
||||||
|
verifyActions(t, []core.Action{}, cl.Actions())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSimpleSign(t *testing.T) {
|
||||||
|
signer, cl := newBootstrapSigner()
|
||||||
|
|
||||||
|
cm := newConfigMap("", "")
|
||||||
|
signer.configMaps.Add(cm)
|
||||||
|
|
||||||
|
secret := newTokenSecret("tokenID", "tokenSecret")
|
||||||
|
addSecretSigningUsage(secret, "true")
|
||||||
|
signer.secrets.Add(secret)
|
||||||
|
|
||||||
|
signer.signConfigMap()
|
||||||
|
|
||||||
|
expected := []core.Action{
|
||||||
|
core.NewUpdateAction(schema.GroupVersionResource{Version: "v1", Resource: "configmaps"},
|
||||||
|
api.NamespacePublic,
|
||||||
|
newConfigMap("tokenID", "eyJhbGciOiJIUzI1NiIsImtpZCI6InRva2VuSUQifQ..QAvK9DAjF0hSyASEkH1MOTB5rJMmbWEY9j-z1NSYILE")),
|
||||||
|
}
|
||||||
|
|
||||||
|
verifyActions(t, expected, cl.Actions())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNoSignNeeded(t *testing.T) {
|
||||||
|
signer, cl := newBootstrapSigner()
|
||||||
|
|
||||||
|
cm := newConfigMap("tokenID", "eyJhbGciOiJIUzI1NiIsImtpZCI6InRva2VuSUQifQ..QAvK9DAjF0hSyASEkH1MOTB5rJMmbWEY9j-z1NSYILE")
|
||||||
|
signer.configMaps.Add(cm)
|
||||||
|
|
||||||
|
secret := newTokenSecret("tokenID", "tokenSecret")
|
||||||
|
addSecretSigningUsage(secret, "true")
|
||||||
|
signer.secrets.Add(secret)
|
||||||
|
|
||||||
|
signer.signConfigMap()
|
||||||
|
|
||||||
|
verifyActions(t, []core.Action{}, cl.Actions())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateSignature(t *testing.T) {
|
||||||
|
signer, cl := newBootstrapSigner()
|
||||||
|
|
||||||
|
cm := newConfigMap("tokenID", "old signature")
|
||||||
|
signer.configMaps.Add(cm)
|
||||||
|
|
||||||
|
secret := newTokenSecret("tokenID", "tokenSecret")
|
||||||
|
addSecretSigningUsage(secret, "true")
|
||||||
|
signer.secrets.Add(secret)
|
||||||
|
|
||||||
|
signer.signConfigMap()
|
||||||
|
|
||||||
|
expected := []core.Action{
|
||||||
|
core.NewUpdateAction(schema.GroupVersionResource{Version: "v1", Resource: "configmaps"},
|
||||||
|
api.NamespacePublic,
|
||||||
|
newConfigMap("tokenID", "eyJhbGciOiJIUzI1NiIsImtpZCI6InRva2VuSUQifQ..QAvK9DAjF0hSyASEkH1MOTB5rJMmbWEY9j-z1NSYILE")),
|
||||||
|
}
|
||||||
|
|
||||||
|
verifyActions(t, expected, cl.Actions())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRemoveSignature(t *testing.T) {
|
||||||
|
signer, cl := newBootstrapSigner()
|
||||||
|
|
||||||
|
cm := newConfigMap("tokenID", "old signature")
|
||||||
|
signer.configMaps.Add(cm)
|
||||||
|
|
||||||
|
signer.signConfigMap()
|
||||||
|
|
||||||
|
expected := []core.Action{
|
||||||
|
core.NewUpdateAction(schema.GroupVersionResource{Version: "v1", Resource: "configmaps"},
|
||||||
|
api.NamespacePublic,
|
||||||
|
newConfigMap("", "")),
|
||||||
|
}
|
||||||
|
|
||||||
|
verifyActions(t, expected, cl.Actions())
|
||||||
|
}
|
74
pkg/controller/bootstrap/common_test.go
Normal file
74
pkg/controller/bootstrap/common_test.go
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2016 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 bootstrap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/davecgh/go-spew/spew"
|
||||||
|
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/client-go/pkg/api"
|
||||||
|
"k8s.io/client-go/pkg/api/v1"
|
||||||
|
core "k8s.io/client-go/testing"
|
||||||
|
bootstrapapi "k8s.io/kubernetes/pkg/bootstrap/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newTokenSecret(tokenID, tokenSecret string) *v1.Secret {
|
||||||
|
return &v1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Namespace: metav1.NamespaceSystem,
|
||||||
|
Name: "secretName",
|
||||||
|
ResourceVersion: "1",
|
||||||
|
},
|
||||||
|
Type: bootstrapapi.SecretTypeBootstrapToken,
|
||||||
|
Data: map[string][]byte{
|
||||||
|
bootstrapapi.BootstrapTokenIDKey: []byte(tokenID),
|
||||||
|
bootstrapapi.BootstrapTokenSecretKey: []byte(tokenSecret),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func addSecretExpiration(s *v1.Secret, expiration string) {
|
||||||
|
s.Data[bootstrapapi.BootstrapTokenExpirationKey] = []byte(expiration)
|
||||||
|
}
|
||||||
|
|
||||||
|
func addSecretSigningUsage(s *v1.Secret, value string) {
|
||||||
|
s.Data[bootstrapapi.BootstrapTokenUsageSigningKey] = []byte(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func verifyActions(t *testing.T, expected, actual []core.Action) {
|
||||||
|
for i, a := range actual {
|
||||||
|
if len(expected) < i+1 {
|
||||||
|
t.Errorf("%d unexpected actions: %s", len(actual)-len(expected), spew.Sdump(actual[i:]))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
e := expected[i]
|
||||||
|
if !api.Semantic.DeepEqual(e, a) {
|
||||||
|
t.Errorf("Expected\n\t%s\ngot\n\t%s", spew.Sdump(e), spew.Sdump(a))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(expected) > len(actual) {
|
||||||
|
t.Errorf("%d additional expected actions", len(expected)-len(actual))
|
||||||
|
for _, a := range expected[len(actual):] {
|
||||||
|
t.Logf(" %s", spew.Sdump(a))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
20
pkg/controller/bootstrap/doc.go
Normal file
20
pkg/controller/bootstrap/doc.go
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2016 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 bootstrap provides automatic processes necessary for bootstraping.
|
||||||
|
// This includes managing and expiring tokens along with signing well known
|
||||||
|
// configmaps with those tokens.
|
||||||
|
package bootstrap // import "k8s.io/kubernetes/pkg/controller/bootstrap"
|
64
pkg/controller/bootstrap/jws.go
Normal file
64
pkg/controller/bootstrap/jws.go
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2016 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 bootstrap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
jose "github.com/square/go-jose"
|
||||||
|
)
|
||||||
|
|
||||||
|
// computeDetachedSig takes content and token details and computes a detached
|
||||||
|
// JWS signature. This is described in Appendix F of RFC 7515. Basically, this
|
||||||
|
// is a regular JWS with the content part of the signature elided.
|
||||||
|
func computeDetachedSig(content, tokenID, tokenSecret string) (string, error) {
|
||||||
|
jwk := &jose.JsonWebKey{
|
||||||
|
Key: []byte(tokenSecret),
|
||||||
|
KeyID: tokenID,
|
||||||
|
}
|
||||||
|
|
||||||
|
signer, err := jose.NewSigner(jose.HS256, jwk)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
jws, err := signer.Sign([]byte(content))
|
||||||
|
if err != nil {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
fullSig, err := jws.CompactSerialize()
|
||||||
|
if err != nil {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
return stripContent(fullSig)
|
||||||
|
}
|
||||||
|
|
||||||
|
// stripContent will remove the content part of a compact JWS
|
||||||
|
//
|
||||||
|
// The `go-jose` library doesn't support generating signatures with "detatched"
|
||||||
|
// content. To make up for this we take the full compact signature, break it
|
||||||
|
// apart and put it back together without the content section.
|
||||||
|
func stripContent(fullSig string) (string, error) {
|
||||||
|
parts := strings.Split(fullSig, ".")
|
||||||
|
if len(parts) != 3 {
|
||||||
|
return "", fmt.Errorf("Compact JWS format must have three parts")
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts[0] + ".." + parts[2], nil
|
||||||
|
}
|
53
pkg/controller/bootstrap/jws_test.go
Normal file
53
pkg/controller/bootstrap/jws_test.go
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2016 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 bootstrap
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
const (
|
||||||
|
content = "Hello from the other side. I must have called a thousand times."
|
||||||
|
secret = "my voice is my passcode"
|
||||||
|
id = "joshua"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestComputeDetachedSig(t *testing.T) {
|
||||||
|
sig, err := computeDetachedSig(content, id, secret)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error when computing signature: %v", err)
|
||||||
|
}
|
||||||
|
if sig != "eyJhbGciOiJIUzI1NiIsImtpZCI6Impvc2h1YSJ9..VShe2taLd-YTrmWuRkcL_8QTNDHYxQIEBsAYYiIj1_8" {
|
||||||
|
t.Errorf("Wrong signature. Got: %v", sig)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try with null content
|
||||||
|
sig, err = computeDetachedSig("", id, secret)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error when computing signature: %v", err)
|
||||||
|
}
|
||||||
|
if sig != "eyJhbGciOiJIUzI1NiIsImtpZCI6Impvc2h1YSJ9..7Ui1ALizW4jXphVUB7xUqC9vLYLL9RZeOFfVLoB7Tgk" {
|
||||||
|
t.Errorf("Wrong signature. Got: %v", sig)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try with no secret
|
||||||
|
sig, err = computeDetachedSig(content, id, "")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error when computing signature: %v", err)
|
||||||
|
}
|
||||||
|
if sig != "eyJhbGciOiJIUzI1NiIsImtpZCI6Impvc2h1YSJ9..UfkqvDGiIFxrMnFseDj9LYJOLNrvjW8aHhF71mvvAs8" {
|
||||||
|
t.Errorf("Wrong signature. Got: %v", sig)
|
||||||
|
}
|
||||||
|
}
|
118
pkg/controller/bootstrap/tokencleaner.go
Normal file
118
pkg/controller/bootstrap/tokencleaner.go
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2016 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 bootstrap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/golang/glog"
|
||||||
|
|
||||||
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/fields"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/watch"
|
||||||
|
clientset "k8s.io/client-go/kubernetes"
|
||||||
|
"k8s.io/client-go/pkg/api"
|
||||||
|
"k8s.io/client-go/pkg/api/v1"
|
||||||
|
"k8s.io/client-go/tools/cache"
|
||||||
|
bootstrapapi "k8s.io/kubernetes/pkg/bootstrap/api"
|
||||||
|
"k8s.io/kubernetes/pkg/util/metrics"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TokenCleanerOptions contains options for the TokenCleaner
|
||||||
|
type TokenCleanerOptions struct {
|
||||||
|
// TokenSecretNamespace string is the namespace for token Secrets.
|
||||||
|
TokenSecretNamespace string
|
||||||
|
|
||||||
|
// SecretResync is the time.Duration at which to fully re-list secrets.
|
||||||
|
// If zero, re-list will be delayed as long as possible
|
||||||
|
SecretResync time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultTokenCleanerOptions returns a set of default options for creating a
|
||||||
|
// TokenCleaner
|
||||||
|
func DefaultTokenCleanerOptions() TokenCleanerOptions {
|
||||||
|
return TokenCleanerOptions{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TokenCleaner is a controller that deletes expired tokens
|
||||||
|
type TokenCleaner struct {
|
||||||
|
tokenSecretNamespace string
|
||||||
|
|
||||||
|
client clientset.Interface
|
||||||
|
|
||||||
|
secrets cache.Store
|
||||||
|
secretsController cache.Controller
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTokenCleaner returns a new *NewTokenCleaner.
|
||||||
|
//
|
||||||
|
// TODO: Switch to shared informers
|
||||||
|
func NewTokenCleaner(cl clientset.Interface, options TokenCleanerOptions) *TokenCleaner {
|
||||||
|
e := &TokenCleaner{
|
||||||
|
client: cl,
|
||||||
|
tokenSecretNamespace: options.TokenSecretNamespace,
|
||||||
|
}
|
||||||
|
if cl.Core().RESTClient().GetRateLimiter() != nil {
|
||||||
|
metrics.RegisterMetricAndTrackRateLimiterUsage("token_cleaner", cl.Core().RESTClient().GetRateLimiter())
|
||||||
|
}
|
||||||
|
|
||||||
|
secretSelector := fields.SelectorFromSet(map[string]string{api.SecretTypeField: string(bootstrapapi.SecretTypeBootstrapToken)})
|
||||||
|
e.secrets, e.secretsController = cache.NewInformer(
|
||||||
|
&cache.ListWatch{
|
||||||
|
ListFunc: func(lo metav1.ListOptions) (runtime.Object, error) {
|
||||||
|
lo.FieldSelector = secretSelector.String()
|
||||||
|
return e.client.Core().Secrets(e.tokenSecretNamespace).List(lo)
|
||||||
|
},
|
||||||
|
WatchFunc: func(lo metav1.ListOptions) (watch.Interface, error) {
|
||||||
|
lo.FieldSelector = secretSelector.String()
|
||||||
|
return e.client.Core().Secrets(e.tokenSecretNamespace).Watch(lo)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&v1.Secret{},
|
||||||
|
options.SecretResync,
|
||||||
|
cache.ResourceEventHandlerFuncs{
|
||||||
|
AddFunc: e.evalSecret,
|
||||||
|
UpdateFunc: func(oldSecret, newSecret interface{}) { e.evalSecret(newSecret) },
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run runs controller loops and returns when they are done
|
||||||
|
func (tc *TokenCleaner) Run(stopCh <-chan struct{}) {
|
||||||
|
go tc.secretsController.Run(stopCh)
|
||||||
|
<-stopCh
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tc *TokenCleaner) evalSecret(o interface{}) {
|
||||||
|
secret := o.(*v1.Secret)
|
||||||
|
if isSecretExpired(secret) {
|
||||||
|
glog.V(3).Infof("Deleting expired secret %s/%s", secret.Namespace, secret.Name)
|
||||||
|
var options *metav1.DeleteOptions
|
||||||
|
if len(secret.UID) > 0 {
|
||||||
|
options = &metav1.DeleteOptions{Preconditions: &metav1.Preconditions{UID: &secret.UID}}
|
||||||
|
}
|
||||||
|
err := tc.client.Core().Secrets(secret.Namespace).Delete(secret.Name, options)
|
||||||
|
// NotFound isn't a real error (it's already been deleted)
|
||||||
|
// Conflict isn't a real error (the UID precondition failed)
|
||||||
|
if err != nil && !apierrors.IsConflict(err) && !apierrors.IsNotFound(err) {
|
||||||
|
glog.V(3).Infof("Error deleting Secret: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
85
pkg/controller/bootstrap/tokencleaner_test.go
Normal file
85
pkg/controller/bootstrap/tokencleaner_test.go
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2016 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 bootstrap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/davecgh/go-spew/spew"
|
||||||
|
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
"k8s.io/client-go/kubernetes/fake"
|
||||||
|
"k8s.io/client-go/pkg/api"
|
||||||
|
core "k8s.io/client-go/testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
spew.Config.DisableMethods = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTokenCleaner() (*TokenCleaner, *fake.Clientset) {
|
||||||
|
options := DefaultTokenCleanerOptions()
|
||||||
|
cl := fake.NewSimpleClientset()
|
||||||
|
return NewTokenCleaner(cl, options), cl
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCleanerNoExpiration(t *testing.T) {
|
||||||
|
cleaner, cl := newTokenCleaner()
|
||||||
|
|
||||||
|
secret := newTokenSecret("tokenID", "tokenSecret")
|
||||||
|
cleaner.secrets.Add(secret)
|
||||||
|
|
||||||
|
cleaner.evalSecret(secret)
|
||||||
|
|
||||||
|
expected := []core.Action{}
|
||||||
|
|
||||||
|
verifyActions(t, expected, cl.Actions())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCleanerExpired(t *testing.T) {
|
||||||
|
cleaner, cl := newTokenCleaner()
|
||||||
|
|
||||||
|
secret := newTokenSecret("tokenID", "tokenSecret")
|
||||||
|
addSecretExpiration(secret, timeString(-time.Hour))
|
||||||
|
cleaner.secrets.Add(secret)
|
||||||
|
|
||||||
|
cleaner.evalSecret(secret)
|
||||||
|
|
||||||
|
expected := []core.Action{
|
||||||
|
core.NewDeleteAction(
|
||||||
|
schema.GroupVersionResource{Version: "v1", Resource: "secrets"},
|
||||||
|
api.NamespaceSystem,
|
||||||
|
secret.ObjectMeta.Name),
|
||||||
|
}
|
||||||
|
|
||||||
|
verifyActions(t, expected, cl.Actions())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCleanerNotExpired(t *testing.T) {
|
||||||
|
cleaner, cl := newTokenCleaner()
|
||||||
|
|
||||||
|
secret := newTokenSecret("tokenID", "tokenSecret")
|
||||||
|
addSecretExpiration(secret, timeString(time.Hour))
|
||||||
|
cleaner.secrets.Add(secret)
|
||||||
|
|
||||||
|
cleaner.evalSecret(secret)
|
||||||
|
|
||||||
|
expected := []core.Action{}
|
||||||
|
|
||||||
|
verifyActions(t, expected, cl.Actions())
|
||||||
|
}
|
85
pkg/controller/bootstrap/util.go
Normal file
85
pkg/controller/bootstrap/util.go
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2016 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 bootstrap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/golang/glog"
|
||||||
|
|
||||||
|
"k8s.io/client-go/pkg/api/v1"
|
||||||
|
bootstrapapi "k8s.io/kubernetes/pkg/bootstrap/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
// getSecretString gets a string value from a secret. If there is an error or
|
||||||
|
// if the key doesn't exist, an empty string is returned.
|
||||||
|
func getSecretString(secret *v1.Secret, key string) string {
|
||||||
|
data, ok := secret.Data[key]
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateSecretForSigning(secret *v1.Secret) (tokenID, tokenSecret string, ok bool) {
|
||||||
|
tokenID = getSecretString(secret, bootstrapapi.BootstrapTokenIDKey)
|
||||||
|
if len(tokenID) == 0 {
|
||||||
|
glog.V(3).Infof("No %s key in %s/%s Secret", bootstrapapi.BootstrapTokenIDKey, secret.Namespace, secret.Name)
|
||||||
|
return "", "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenSecret = getSecretString(secret, bootstrapapi.BootstrapTokenSecretKey)
|
||||||
|
if len(tokenSecret) == 0 {
|
||||||
|
glog.V(3).Infof("No %s key in %s/%s Secret", bootstrapapi.BootstrapTokenSecretKey, secret.Namespace, secret.Name)
|
||||||
|
return "", "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure this secret hasn't expired. The TokenCleaner should remove this
|
||||||
|
// but if that isn't working or it hasn't gotten there yet we should check
|
||||||
|
// here.
|
||||||
|
if isSecretExpired(secret) {
|
||||||
|
return "", "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure this secret can be used for signing
|
||||||
|
okToSign := getSecretString(secret, bootstrapapi.BootstrapTokenUsageSigningKey)
|
||||||
|
if okToSign != "true" {
|
||||||
|
return "", "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokenID, tokenSecret, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// isSecretExpired returns true if the Secret is expired.
|
||||||
|
func isSecretExpired(secret *v1.Secret) bool {
|
||||||
|
expiration := getSecretString(secret, bootstrapapi.BootstrapTokenExpirationKey)
|
||||||
|
if len(expiration) > 0 {
|
||||||
|
expTime, err2 := time.Parse(time.RFC3339, expiration)
|
||||||
|
if err2 != nil {
|
||||||
|
glog.V(3).Infof("Unparseable expiration time (%s) in %s/%s Secret: %v. Treating as expired.",
|
||||||
|
expiration, secret.Namespace, secret.Name, err2)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if time.Now().After(expTime) {
|
||||||
|
glog.V(3).Infof("Expired bootstrap token in %s/%s Secret: %v",
|
||||||
|
secret.Namespace, secret.Name, expiration)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
137
pkg/controller/bootstrap/util_test.go
Normal file
137
pkg/controller/bootstrap/util_test.go
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2016 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 bootstrap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/client-go/pkg/api/v1"
|
||||||
|
bootstrapapi "k8s.io/kubernetes/pkg/bootstrap/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
givenTokenID = "tokenID"
|
||||||
|
givenTokenSecret = "tokenSecret"
|
||||||
|
)
|
||||||
|
|
||||||
|
func timeString(delta time.Duration) string {
|
||||||
|
return time.Now().Add(delta).Format(time.RFC3339)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateSecretForSigning(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
description string
|
||||||
|
tokenID string
|
||||||
|
tokenSecret string
|
||||||
|
okToSign string
|
||||||
|
expiration string
|
||||||
|
valid bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"Signing token with no exp",
|
||||||
|
givenTokenID, givenTokenSecret, "true", "", true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Signing token with valid exp",
|
||||||
|
givenTokenID, givenTokenSecret, "true", timeString(time.Hour), true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Expired signing token",
|
||||||
|
givenTokenID, givenTokenSecret, "true", timeString(-time.Hour), false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Signing token with bad exp",
|
||||||
|
givenTokenID, givenTokenSecret, "true", "garbage", false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Signing token without signing bit",
|
||||||
|
givenTokenID, givenTokenSecret, "", "garbage", false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Signing token with bad signing bit",
|
||||||
|
givenTokenID, givenTokenSecret, "", "", false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Signing token with no ID",
|
||||||
|
"", givenTokenSecret, "true", "", false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Signing token with no secret",
|
||||||
|
givenTokenID, "", "true", "", false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range cases {
|
||||||
|
secret := &v1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Namespace: metav1.NamespaceSystem,
|
||||||
|
Name: "secretName",
|
||||||
|
ResourceVersion: "1",
|
||||||
|
},
|
||||||
|
Type: bootstrapapi.SecretTypeBootstrapToken,
|
||||||
|
Data: map[string][]byte{
|
||||||
|
bootstrapapi.BootstrapTokenIDKey: []byte(tc.tokenID),
|
||||||
|
bootstrapapi.BootstrapTokenSecretKey: []byte(tc.tokenSecret),
|
||||||
|
bootstrapapi.BootstrapTokenUsageSigningKey: []byte(tc.okToSign),
|
||||||
|
bootstrapapi.BootstrapTokenExpirationKey: []byte(tc.expiration),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenID, tokenSecret, ok := validateSecretForSigning(secret)
|
||||||
|
if ok != tc.valid {
|
||||||
|
t.Errorf("%s: Unexpected validation failure. Expected %v, got %v", tc.description, tc.valid, ok)
|
||||||
|
}
|
||||||
|
if ok {
|
||||||
|
if tokenID != tc.tokenID {
|
||||||
|
t.Errorf("%s: Unexpected Token ID. Expected %q, got %q", tc.description, givenTokenID, tokenID)
|
||||||
|
}
|
||||||
|
if tokenSecret != tc.tokenSecret {
|
||||||
|
t.Errorf("%s: Unexpected Token Secret. Expected %q, got %q", tc.description, givenTokenSecret, tokenSecret)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateSecret(t *testing.T) {
|
||||||
|
secret := &v1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Namespace: metav1.NamespaceSystem,
|
||||||
|
Name: "secretName",
|
||||||
|
ResourceVersion: "1",
|
||||||
|
},
|
||||||
|
Type: bootstrapapi.SecretTypeBootstrapToken,
|
||||||
|
Data: map[string][]byte{
|
||||||
|
bootstrapapi.BootstrapTokenIDKey: []byte(givenTokenID),
|
||||||
|
bootstrapapi.BootstrapTokenSecretKey: []byte(givenTokenSecret),
|
||||||
|
bootstrapapi.BootstrapTokenUsageSigningKey: []byte("true"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenID, tokenSecret, ok := validateSecretForSigning(secret)
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("Unexpected validation failure.")
|
||||||
|
}
|
||||||
|
if tokenID != givenTokenID {
|
||||||
|
t.Errorf("Unexpected Token ID. Expected %q, got %q", givenTokenID, tokenID)
|
||||||
|
}
|
||||||
|
if tokenSecret != givenTokenSecret {
|
||||||
|
t.Errorf("Unexpected Token Secret. Expected %q, got %q", givenTokenSecret, tokenSecret)
|
||||||
|
}
|
||||||
|
}
|
@ -41,9 +41,10 @@ import (
|
|||||||
|
|
||||||
const kubernetesServiceName = "kubernetes"
|
const kubernetesServiceName = "kubernetes"
|
||||||
|
|
||||||
// Controller is the controller manager for the core bootstrap Kubernetes controller
|
// Controller is the controller manager for the core bootstrap Kubernetes
|
||||||
// loops, which manage creating the "kubernetes" service, the "default" and "kube-system"
|
// controller loops, which manage creating the "kubernetes" service, the
|
||||||
// namespace, and provide the IP repair check on service IPs
|
// "default", "kube-system" and "kube-public" namespaces, and provide the IP
|
||||||
|
// repair check on service IPs
|
||||||
type Controller struct {
|
type Controller struct {
|
||||||
ServiceClient coreclient.ServicesGetter
|
ServiceClient coreclient.ServicesGetter
|
||||||
NamespaceClient coreclient.NamespacesGetter
|
NamespaceClient coreclient.NamespacesGetter
|
||||||
@ -84,7 +85,7 @@ func (c *Config) NewBootstrapController(legacyRESTStorage corerest.LegacyRESTSto
|
|||||||
EndpointReconciler: c.EndpointReconcilerConfig.Reconciler,
|
EndpointReconciler: c.EndpointReconcilerConfig.Reconciler,
|
||||||
EndpointInterval: c.EndpointReconcilerConfig.Interval,
|
EndpointInterval: c.EndpointReconcilerConfig.Interval,
|
||||||
|
|
||||||
SystemNamespaces: []string{metav1.NamespaceSystem},
|
SystemNamespaces: []string{metav1.NamespaceSystem, metav1.NamespacePublic},
|
||||||
SystemNamespacesInterval: 1 * time.Minute,
|
SystemNamespacesInterval: 1 * time.Minute,
|
||||||
|
|
||||||
ServiceClusterIPRegistry: legacyRESTStorage.ServiceClusterIPAllocator,
|
ServiceClusterIPRegistry: legacyRESTStorage.ServiceClusterIPAllocator,
|
||||||
|
@ -51,7 +51,7 @@ const (
|
|||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
admission.RegisterPlugin(PluginName, func(config io.Reader) (admission.Interface, error) {
|
admission.RegisterPlugin(PluginName, func(config io.Reader) (admission.Interface, error) {
|
||||||
return NewLifecycle(sets.NewString(metav1.NamespaceDefault, metav1.NamespaceSystem))
|
return NewLifecycle(sets.NewString(metav1.NamespaceDefault, metav1.NamespaceSystem, metav1.NamespacePublic))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -73,7 +73,7 @@ type ListMeta struct {
|
|||||||
|
|
||||||
// These are internal finalizer values for Kubernetes-like APIs, must be qualified name unless defined here
|
// These are internal finalizer values for Kubernetes-like APIs, must be qualified name unless defined here
|
||||||
const (
|
const (
|
||||||
FinalizerOrphan string = "orphan"
|
FinalizerOrphan string = "orphan"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ObjectMeta is metadata that all persisted resources must have, which includes all objects
|
// ObjectMeta is metadata that all persisted resources must have, which includes all objects
|
||||||
@ -233,6 +233,8 @@ const (
|
|||||||
NamespaceNone string = ""
|
NamespaceNone string = ""
|
||||||
// NamespaceSystem is the system namespace where we place system components.
|
// NamespaceSystem is the system namespace where we place system components.
|
||||||
NamespaceSystem string = "kube-system"
|
NamespaceSystem string = "kube-system"
|
||||||
|
// NamespacePublic is the namespace where we place public info (ConfigMaps)
|
||||||
|
NamespacePublic string = "kube-public"
|
||||||
)
|
)
|
||||||
|
|
||||||
// OwnerReference contains enough information to let you identify an owning
|
// OwnerReference contains enough information to let you identify an owning
|
||||||
|
@ -228,6 +228,8 @@ const (
|
|||||||
NamespaceNone string = ""
|
NamespaceNone string = ""
|
||||||
// NamespaceSystem is the system namespace where we place system components.
|
// NamespaceSystem is the system namespace where we place system components.
|
||||||
NamespaceSystem string = "kube-system"
|
NamespaceSystem string = "kube-system"
|
||||||
|
// NamespacePublic is the namespace where we place public info (ConfigMaps)
|
||||||
|
NamespacePublic string = "kube-public"
|
||||||
)
|
)
|
||||||
|
|
||||||
// OwnerReference contains enough information to let you identify an owning
|
// OwnerReference contains enough information to let you identify an owning
|
||||||
|
@ -189,6 +189,8 @@ const (
|
|||||||
NamespaceNone string = ""
|
NamespaceNone string = ""
|
||||||
// NamespaceSystem is the system namespace where we place system components.
|
// NamespaceSystem is the system namespace where we place system components.
|
||||||
NamespaceSystem string = "kube-system"
|
NamespaceSystem string = "kube-system"
|
||||||
|
// NamespacePublic is the namespace where we place public info (ConfigMaps)
|
||||||
|
NamespacePublic string = "kube-public"
|
||||||
// TerminationMessagePathDefault means the default path to capture the application termination message running in a container
|
// TerminationMessagePathDefault means the default path to capture the application termination message running in a container
|
||||||
TerminationMessagePathDefault string = "/dev/termination-log"
|
TerminationMessagePathDefault string = "/dev/termination-log"
|
||||||
)
|
)
|
||||||
@ -3344,9 +3346,6 @@ const (
|
|||||||
// - Secret.Data["token"] - a token that identifies the service account to the API
|
// - Secret.Data["token"] - a token that identifies the service account to the API
|
||||||
SecretTypeServiceAccountToken SecretType = "kubernetes.io/service-account-token"
|
SecretTypeServiceAccountToken SecretType = "kubernetes.io/service-account-token"
|
||||||
|
|
||||||
// SecretTypeBootstrapToken is the key for tokens used by kubeadm to validate cluster info during discovery.
|
|
||||||
SecretTypeBootstrapToken = "bootstrap.kubernetes.io/token"
|
|
||||||
|
|
||||||
// ServiceAccountNameKey is the key of the required annotation for SecretTypeServiceAccountToken secrets
|
// ServiceAccountNameKey is the key of the required annotation for SecretTypeServiceAccountToken secrets
|
||||||
ServiceAccountNameKey = "kubernetes.io/service-account.name"
|
ServiceAccountNameKey = "kubernetes.io/service-account.name"
|
||||||
// ServiceAccountUIDKey is the key of the required annotation for SecretTypeServiceAccountToken secrets
|
// ServiceAccountUIDKey is the key of the required annotation for SecretTypeServiceAccountToken secrets
|
||||||
|
Loading…
Reference in New Issue
Block a user