refactor bootstrap token utils

This commit is contained in:
Di Xu 2019-04-29 20:52:48 +08:00
parent 00e13dbc12
commit af9ae4c11a
11 changed files with 352 additions and 296 deletions

View File

@ -27,6 +27,7 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
bootstrapapi "k8s.io/cluster-bootstrap/token/api"
bootstraputil "k8s.io/cluster-bootstrap/token/util"
bootstrapsecretutil "k8s.io/cluster-bootstrap/util/secrets"
)
// ToSecret converts the given BootstrapToken object to its Secret representation that
@ -55,7 +56,7 @@ func encodeTokenSecretData(token *BootstrapToken, now time.Time) map[string][]by
}
// If for some strange reason both token.TTL and token.Expires would be set
// (they are mutually exlusive in validation so this shouldn't be the case),
// (they are mutually exclusive in validation so this shouldn't be the case),
// token.Expires has higher priority, as can be seen in the logic here.
if token.Expires != nil {
// Format the expiration date accordingly
@ -83,7 +84,7 @@ func encodeTokenSecretData(token *BootstrapToken, now time.Time) map[string][]by
// BootstrapTokenFromSecret returns a BootstrapToken object from the given Secret
func BootstrapTokenFromSecret(secret *v1.Secret) (*BootstrapToken, error) {
// Get the Token ID field from the Secret data
tokenID := getSecretString(secret, bootstrapapi.BootstrapTokenIDKey)
tokenID := bootstrapsecretutil.GetData(secret, bootstrapapi.BootstrapTokenIDKey)
if len(tokenID) == 0 {
return nil, errors.Errorf("bootstrap Token Secret has no token-id data: %s", secret.Name)
}
@ -94,7 +95,7 @@ func BootstrapTokenFromSecret(secret *v1.Secret) (*BootstrapToken, error) {
bootstrapapi.BootstrapTokenSecretPrefix, secret.Name, bootstraputil.BootstrapTokenSecretName(tokenID))
}
tokenSecret := getSecretString(secret, bootstrapapi.BootstrapTokenSecretKey)
tokenSecret := bootstrapsecretutil.GetData(secret, bootstrapapi.BootstrapTokenSecretKey)
if len(tokenSecret) == 0 {
return nil, errors.Errorf("bootstrap Token Secret has no token-secret data: %s", secret.Name)
}
@ -106,11 +107,11 @@ func BootstrapTokenFromSecret(secret *v1.Secret) (*BootstrapToken, error) {
}
// Get the description (if any) from the Secret
description := getSecretString(secret, bootstrapapi.BootstrapTokenDescriptionKey)
description := bootstrapsecretutil.GetData(secret, bootstrapapi.BootstrapTokenDescriptionKey)
// Expiration time is optional, if not specified this implies the token
// never expires.
secretExpiration := getSecretString(secret, bootstrapapi.BootstrapTokenExpirationKey)
secretExpiration := bootstrapsecretutil.GetData(secret, bootstrapapi.BootstrapTokenExpirationKey)
var expires *metav1.Time
if len(secretExpiration) > 0 {
expTime, err := time.Parse(time.RFC3339, secretExpiration)
@ -142,7 +143,7 @@ func BootstrapTokenFromSecret(secret *v1.Secret) (*BootstrapToken, error) {
// It's done this way to make .Groups be nil in case there is no items, rather than an
// empty slice or an empty slice with a "" string only
var groups []string
groupsString := getSecretString(secret, bootstrapapi.BootstrapTokenExtraGroupsKey)
groupsString := bootstrapsecretutil.GetData(secret, bootstrapapi.BootstrapTokenExtraGroupsKey)
g := strings.Split(groupsString, ",")
if len(g) > 0 && len(g[0]) > 0 {
groups = g
@ -156,14 +157,3 @@ func BootstrapTokenFromSecret(secret *v1.Secret) (*BootstrapToken, error) {
Groups: groups,
}, nil
}
// getSecretString returns the string value for the given key in the specified Secret
func getSecretString(secret *v1.Secret, key string) string {
if secret.Data == nil {
return ""
}
if val, ok := secret.Data[key]; ok {
return string(val)
}
return ""
}

View File

@ -454,55 +454,3 @@ func jsonMarshal(bt *BootstrapToken) string {
b, _ := json.Marshal(*bt)
return string(b)
}
func TestGetSecretString(t *testing.T) {
var tests = []struct {
name string
secret *v1.Secret
key string
expectedVal string
}{
{
name: "existing key",
secret: &v1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: "foo"},
Data: map[string][]byte{
"foo": []byte("bar"),
},
},
key: "foo",
expectedVal: "bar",
},
{
name: "non-existing key",
secret: &v1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: "foo"},
Data: map[string][]byte{
"foo": []byte("bar"),
},
},
key: "baz",
expectedVal: "",
},
{
name: "no data",
secret: &v1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: "foo"},
},
key: "foo",
expectedVal: "",
},
}
for _, rt := range tests {
t.Run(rt.name, func(t *testing.T) {
actual := getSecretString(rt.secret, rt.key)
if actual != rt.expectedVal {
t.Errorf(
"failed getSecretString:\n\texpected: %s\n\t actual: %s",
rt.expectedVal,
actual,
)
}
})
}
}

View File

@ -31,6 +31,7 @@ import (
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/util/workqueue"
bootstrapapi "k8s.io/cluster-bootstrap/token/api"
bootstrapsecretutil "k8s.io/cluster-bootstrap/util/secrets"
"k8s.io/klog"
api "k8s.io/kubernetes/pkg/apis/core"
"k8s.io/kubernetes/pkg/controller"
@ -187,7 +188,7 @@ func (tc *TokenCleaner) syncFunc(key string) error {
func (tc *TokenCleaner) evalSecret(o interface{}) {
secret := o.(*v1.Secret)
if isSecretExpired(secret) {
if bootstrapsecretutil.HasExpired(secret, time.Now()) {
klog.V(3).Infof("Deleting expired secret %s/%s", secret.Namespace, secret.Name)
var options *metav1.DeleteOptions
if len(secret.UID) > 0 {

View File

@ -17,46 +17,23 @@ limitations under the License.
package bootstrap
import (
"regexp"
"time"
"k8s.io/klog"
"k8s.io/api/core/v1"
bootstrapapi "k8s.io/cluster-bootstrap/token/api"
bootstrapsecretutil "k8s.io/cluster-bootstrap/util/secrets"
)
var namePattern = `^` + regexp.QuoteMeta(bootstrapapi.BootstrapTokenSecretPrefix) + `([a-z0-9]{6})$`
var nameRegExp = regexp.MustCompile(namePattern)
// 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)
}
// parseSecretName parses the name of the secret to extract the secret ID.
func parseSecretName(name string) (secretID string, ok bool) {
r := nameRegExp.FindStringSubmatch(name)
if r == nil {
return "", false
}
return r[1], true
}
func validateSecretForSigning(secret *v1.Secret) (tokenID, tokenSecret string, ok bool) {
nameTokenID, ok := parseSecretName(secret.Name)
nameTokenID, ok := bootstrapsecretutil.ParseName(secret.Name)
if !ok {
klog.V(3).Infof("Invalid secret name: %s. Must be of form %s<secret-id>.", secret.Name, bootstrapapi.BootstrapTokenSecretPrefix)
return "", "", false
}
tokenID = getSecretString(secret, bootstrapapi.BootstrapTokenIDKey)
tokenID = bootstrapsecretutil.GetData(secret, bootstrapapi.BootstrapTokenIDKey)
if len(tokenID) == 0 {
klog.V(3).Infof("No %s key in %s/%s Secret", bootstrapapi.BootstrapTokenIDKey, secret.Namespace, secret.Name)
return "", "", false
@ -67,7 +44,7 @@ func validateSecretForSigning(secret *v1.Secret) (tokenID, tokenSecret string, o
return "", "", false
}
tokenSecret = getSecretString(secret, bootstrapapi.BootstrapTokenSecretKey)
tokenSecret = bootstrapsecretutil.GetData(secret, bootstrapapi.BootstrapTokenSecretKey)
if len(tokenSecret) == 0 {
klog.V(3).Infof("No %s key in %s/%s Secret", bootstrapapi.BootstrapTokenSecretKey, secret.Namespace, secret.Name)
return "", "", false
@ -76,34 +53,15 @@ func validateSecretForSigning(secret *v1.Secret) (tokenID, tokenSecret string, o
// 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) {
if bootstrapsecretutil.HasExpired(secret, time.Now()) {
return "", "", false
}
// Make sure this secret can be used for signing
okToSign := getSecretString(secret, bootstrapapi.BootstrapTokenUsageSigningKey)
okToSign := bootstrapsecretutil.GetData(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 {
klog.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) {
klog.V(3).Infof("Expired bootstrap token in %s/%s Secret: %v",
secret.Namespace, secret.Name, expiration)
return true
}
}
return false
}

View File

@ -20,8 +20,6 @@ import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
bootstrapapi "k8s.io/cluster-bootstrap/token/api"
@ -180,27 +178,3 @@ func TestMismatchSecretName(t *testing.T) {
t.Errorf("Token validation should fail with mismatched name")
}
}
func TestParseSecretName(t *testing.T) {
tokenID, ok := parseSecretName("bootstrap-token-abc123")
assert.True(t, ok, "parseSecretName should accept valid name")
assert.Equal(t, "abc123", tokenID, "parseSecretName should return token ID")
_, ok = parseSecretName("")
assert.False(t, ok, "parseSecretName should reject blank name")
_, ok = parseSecretName("abc123")
assert.False(t, ok, "parseSecretName should reject with no prefix")
_, ok = parseSecretName("bootstrap-token-")
assert.False(t, ok, "parseSecretName should reject no token ID")
_, ok = parseSecretName("bootstrap-token-abc")
assert.False(t, ok, "parseSecretName should reject short token ID")
_, ok = parseSecretName("bootstrap-token-abc123ghi")
assert.False(t, ok, "parseSecretName should reject long token ID")
_, ok = parseSecretName("bootstrap-token-ABC123")
assert.False(t, ok, "parseSecretName should reject invalid token ID")
}

View File

@ -23,20 +23,18 @@ import (
"context"
"crypto/subtle"
"fmt"
"regexp"
"strings"
"time"
"k8s.io/klog"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apiserver/pkg/authentication/authenticator"
"k8s.io/apiserver/pkg/authentication/user"
corev1listers "k8s.io/client-go/listers/core/v1"
bootstrapapi "k8s.io/cluster-bootstrap/token/api"
bootstraputil "k8s.io/cluster-bootstrap/token/util"
bootstrapsecretutil "k8s.io/cluster-bootstrap/util/secrets"
bootstraptokenutil "k8s.io/cluster-bootstrap/util/tokens"
)
// TODO: A few methods in this package is copied from other sources. Either
@ -92,7 +90,7 @@ func tokenErrorf(s *corev1.Secret, format string, i ...interface{}) {
// ( token-id ).( token-secret )
//
func (t *TokenAuthenticator) AuthenticateToken(ctx context.Context, token string) (*authenticator.Response, bool, error) {
tokenID, tokenSecret, err := parseToken(token)
tokenID, tokenSecret, err := bootstraptokenutil.ParseToken(token)
if err != nil {
// Token isn't of the correct form, ignore it.
return nil, false, nil
@ -118,29 +116,29 @@ func (t *TokenAuthenticator) AuthenticateToken(ctx context.Context, token string
return nil, false, nil
}
ts := getSecretString(secret, bootstrapapi.BootstrapTokenSecretKey)
ts := bootstrapsecretutil.GetData(secret, bootstrapapi.BootstrapTokenSecretKey)
if subtle.ConstantTimeCompare([]byte(ts), []byte(tokenSecret)) != 1 {
tokenErrorf(secret, "has invalid value for key %s, expected %s.", bootstrapapi.BootstrapTokenSecretKey, tokenSecret)
return nil, false, nil
}
id := getSecretString(secret, bootstrapapi.BootstrapTokenIDKey)
id := bootstrapsecretutil.GetData(secret, bootstrapapi.BootstrapTokenIDKey)
if id != tokenID {
tokenErrorf(secret, "has invalid value for key %s, expected %s.", bootstrapapi.BootstrapTokenIDKey, tokenID)
return nil, false, nil
}
if isSecretExpired(secret) {
if bootstrapsecretutil.HasExpired(secret, time.Now()) {
// logging done in isSecretExpired method.
return nil, false, nil
}
if getSecretString(secret, bootstrapapi.BootstrapTokenUsageAuthentication) != "true" {
if bootstrapsecretutil.GetData(secret, bootstrapapi.BootstrapTokenUsageAuthentication) != "true" {
tokenErrorf(secret, "not marked %s=true.", bootstrapapi.BootstrapTokenUsageAuthentication)
return nil, false, nil
}
groups, err := getGroups(secret)
groups, err := bootstrapsecretutil.GetGroups(secret)
if err != nil {
tokenErrorf(secret, "has invalid value for key %s: %v.", bootstrapapi.BootstrapTokenExtraGroupsKey, err)
return nil, false, nil
@ -153,76 +151,3 @@ func (t *TokenAuthenticator) AuthenticateToken(ctx context.Context, token string
},
}, true, nil
}
// Copied from k8s.io/cluster-bootstrap/token/api
func getSecretString(secret *corev1.Secret, key string) string {
data, ok := secret.Data[key]
if !ok {
return ""
}
return string(data)
}
// Copied from k8s.io/cluster-bootstrap/token/api
func isSecretExpired(secret *corev1.Secret) bool {
expiration := getSecretString(secret, bootstrapapi.BootstrapTokenExpirationKey)
if len(expiration) > 0 {
expTime, err2 := time.Parse(time.RFC3339, expiration)
if err2 != nil {
klog.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) {
klog.V(3).Infof("Expired bootstrap token in %s/%s Secret: %v",
secret.Namespace, secret.Name, expiration)
return true
}
}
return false
}
// Copied from kubernetes/cmd/kubeadm/app/util/token
var (
// tokenRegexpString defines id.secret regular expression pattern
tokenRegexpString = "^([a-z0-9]{6})\\.([a-z0-9]{16})$"
// tokenRegexp is a compiled regular expression of TokenRegexpString
tokenRegexp = regexp.MustCompile(tokenRegexpString)
)
// parseToken tries and parse a valid token from a string.
// A token ID and token secret are returned in case of success, an error otherwise.
func parseToken(s string) (string, string, error) {
split := tokenRegexp.FindStringSubmatch(s)
if len(split) != 3 {
return "", "", fmt.Errorf("token [%q] was not of form [%q]", s, tokenRegexpString)
}
return split[1], split[2], nil
}
// getGroups loads and validates the bootstrapapi.BootstrapTokenExtraGroupsKey
// key from the bootstrap token secret, returning a list of group names or an
// error if any of the group names are invalid.
func getGroups(secret *corev1.Secret) ([]string, error) {
// always include the default group
groups := sets.NewString(bootstrapapi.BootstrapDefaultGroup)
// grab any extra groups and if there are none, return just the default
extraGroupsString := getSecretString(secret, bootstrapapi.BootstrapTokenExtraGroupsKey)
if extraGroupsString == "" {
return groups.List(), nil
}
// validate the names of the extra groups
for _, group := range strings.Split(extraGroupsString, ",") {
if err := bootstraputil.ValidateBootstrapGroupName(group); err != nil {
return nil, err
}
groups.Insert(group)
}
// return the result as a deduplicated, sorted list
return groups.List(), nil
}

View File

@ -288,72 +288,3 @@ func TestTokenAuthenticator(t *testing.T) {
}()
}
}
func TestGetGroups(t *testing.T) {
tests := []struct {
name string
secret *corev1.Secret
expectResult []string
expectError bool
}{
{
name: "not set",
secret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: "test"},
Data: map[string][]byte{},
},
expectResult: []string{"system:bootstrappers"},
},
{
name: "set to empty value",
secret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: "test"},
Data: map[string][]byte{
bootstrapapi.BootstrapTokenExtraGroupsKey: []byte(""),
},
},
expectResult: []string{"system:bootstrappers"},
},
{
name: "invalid prefix",
secret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: "test"},
Data: map[string][]byte{
bootstrapapi.BootstrapTokenExtraGroupsKey: []byte("foo"),
},
},
expectError: true,
},
{
name: "valid",
secret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: "test"},
Data: map[string][]byte{
bootstrapapi.BootstrapTokenExtraGroupsKey: []byte("system:bootstrappers:foo,system:bootstrappers:bar,system:bootstrappers:bar"),
},
},
// expect the results in deduplicated, sorted order
expectResult: []string{
"system:bootstrappers",
"system:bootstrappers:bar",
"system:bootstrappers:foo",
},
},
}
for _, test := range tests {
result, err := getGroups(test.secret)
if test.expectError {
if err == nil {
t.Errorf("test %q expected an error, but didn't get one (result: %#v)", test.name, result)
}
continue
}
if err != nil {
t.Errorf("test %q return an unexpected error: %v", test.name, err)
continue
}
if !reflect.DeepEqual(result, test.expectResult) {
t.Errorf("test %q expected %#v, got %#v", test.name, test.expectResult, result)
}
}
}

View File

@ -27,6 +27,8 @@ import (
"k8s.io/cluster-bootstrap/token/api"
)
// TODO(dixudx): refactor this to util/secrets and util/tokens
// validBootstrapTokenChars defines the characters a bootstrap token can consist of
const validBootstrapTokenChars = "0123456789abcdefghijklmnopqrstuvwxyz"
@ -110,6 +112,7 @@ func BootstrapTokenSecretName(tokenID string) string {
// ValidateBootstrapGroupName checks if the provided group name is a valid
// bootstrap group name. Returns nil if valid or a validation error if invalid.
// TODO(dixudx): should be moved to util/secrets
func ValidateBootstrapGroupName(name string) error {
if BootstrapGroupRegexp.Match([]byte(name)) {
return nil

View File

@ -0,0 +1,100 @@
/*
Copyright 2019 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 secrets
import (
"regexp"
"strings"
"time"
"k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/cluster-bootstrap/token/api"
legacyutil "k8s.io/cluster-bootstrap/token/util"
"k8s.io/klog"
)
// GetData returns the string value for the given key in the specified Secret
// If there is an error or if the key doesn't exist, an empty string is returned.
func GetData(secret *v1.Secret, key string) string {
if secret.Data == nil {
return ""
}
if val, ok := secret.Data[key]; ok {
return string(val)
}
return ""
}
// HasExpired will identify whether the secret expires
func HasExpired(secret *v1.Secret, currentTime time.Time) bool {
expiration := GetData(secret, api.BootstrapTokenExpirationKey)
if len(expiration) > 0 {
expTime, err2 := time.Parse(time.RFC3339, expiration)
if err2 != nil {
klog.V(3).Infof("Unparseable expiration time (%s) in %s/%s Secret: %v. Treating as expired.",
expiration, secret.Namespace, secret.Name, err2)
return true
}
if currentTime.After(expTime) {
klog.V(3).Infof("Expired bootstrap token in %s/%s Secret: %v",
secret.Namespace, secret.Name, expiration)
return true
}
}
return false
}
// ParseName parses the name of the secret to extract the secret ID.
func ParseName(name string) (secretID string, ok bool) {
namePattern := `^` + regexp.QuoteMeta(api.BootstrapTokenSecretPrefix) + `([a-z0-9]{6})$`
nameRegExp, err := regexp.Compile(namePattern)
if err != nil {
klog.Errorf("error compiling bootstrap regex %q: %v", namePattern, err)
return "", false
}
r := nameRegExp.FindStringSubmatch(name)
if r == nil {
return "", false
}
return r[1], true
}
// GetGroups loads and validates the bootstrapapi.BootstrapTokenExtraGroupsKey
// key from the bootstrap token secret, returning a list of group names or an
// error if any of the group names are invalid.
func GetGroups(secret *v1.Secret) ([]string, error) {
// always include the default group
groups := sets.NewString(api.BootstrapDefaultGroup)
// grab any extra groups and if there are none, return just the default
extraGroupsString := GetData(secret, api.BootstrapTokenExtraGroupsKey)
if extraGroupsString == "" {
return groups.List(), nil
}
// validate the names of the extra groups
for _, group := range strings.Split(extraGroupsString, ",") {
if err := legacyutil.ValidateBootstrapGroupName(group); err != nil {
return nil, err
}
groups.Insert(group)
}
// return the result as a deduplicated, sorted list
return groups.List(), nil
}

View File

@ -0,0 +1,187 @@
/*
Copyright 2019 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 secrets
import (
"reflect"
"testing"
"k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
bootstrapapi "k8s.io/cluster-bootstrap/token/api"
)
func TestGetSecretString(t *testing.T) {
var tests = []struct {
name string
secret *v1.Secret
key string
expectedVal string
}{
{
name: "existing key",
secret: &v1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: "foo"},
Data: map[string][]byte{
"foo": []byte("bar"),
},
},
key: "foo",
expectedVal: "bar",
},
{
name: "non-existing key",
secret: &v1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: "foo"},
Data: map[string][]byte{
"foo": []byte("bar"),
},
},
key: "baz",
expectedVal: "",
},
{
name: "no data",
secret: &v1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: "foo"},
},
key: "foo",
expectedVal: "",
},
}
for _, rt := range tests {
t.Run(rt.name, func(t *testing.T) {
actual := GetData(rt.secret, rt.key)
if actual != rt.expectedVal {
t.Errorf(
"failed getSecretString:\n\texpected: %s\n\t actual: %s",
rt.expectedVal,
actual,
)
}
})
}
}
func TestParseSecretName(t *testing.T) {
tokenID, ok := ParseName("bootstrap-token-abc123")
if !ok {
t.Error("ParseName should accept valid name")
}
if tokenID != "abc123" {
t.Error("ParseName should return token ID")
}
_, ok = ParseName("")
if ok {
t.Error("ParseName should reject blank name")
}
_, ok = ParseName("abc123")
if ok {
t.Error("ParseName should reject with no prefix")
}
_, ok = ParseName("bootstrap-token-")
if ok {
t.Error("ParseName should reject no token ID")
}
_, ok = ParseName("bootstrap-token-abc")
if ok {
t.Error("ParseName should reject short token ID")
}
_, ok = ParseName("bootstrap-token-abc123ghi")
if ok {
t.Error("ParseName should reject long token ID")
}
_, ok = ParseName("bootstrap-token-ABC123")
if ok {
t.Error("ParseName should reject invalid token ID")
}
}
func TestGetGroups(t *testing.T) {
tests := []struct {
name string
secret *v1.Secret
expectResult []string
expectError bool
}{
{
name: "not set",
secret: &v1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: "test"},
Data: map[string][]byte{},
},
expectResult: []string{"system:bootstrappers"},
},
{
name: "set to empty value",
secret: &v1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: "test"},
Data: map[string][]byte{
bootstrapapi.BootstrapTokenExtraGroupsKey: []byte(""),
},
},
expectResult: []string{"system:bootstrappers"},
},
{
name: "invalid prefix",
secret: &v1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: "test"},
Data: map[string][]byte{
bootstrapapi.BootstrapTokenExtraGroupsKey: []byte("foo"),
},
},
expectError: true,
},
{
name: "valid",
secret: &v1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: "test"},
Data: map[string][]byte{
bootstrapapi.BootstrapTokenExtraGroupsKey: []byte("system:bootstrappers:foo,system:bootstrappers:bar,system:bootstrappers:bar"),
},
},
// expect the results in deduplicated, sorted order
expectResult: []string{
"system:bootstrappers",
"system:bootstrappers:bar",
"system:bootstrappers:foo",
},
},
}
for _, test := range tests {
result, err := GetGroups(test.secret)
if test.expectError {
if err == nil {
t.Errorf("test %q expected an error, but didn't get one (result: %#v)", test.name, result)
}
continue
}
if err != nil {
t.Errorf("test %q return an unexpected error: %v", test.name, err)
continue
}
if !reflect.DeepEqual(result, test.expectResult) {
t.Errorf("test %q expected %#v, got %#v", test.name, test.expectResult, result)
}
}
}

View File

@ -0,0 +1,39 @@
/*
Copyright 2019 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 tokens
import (
"fmt"
"regexp"
"k8s.io/cluster-bootstrap/token/api"
)
// ParseToken tries and parse a valid token from a string.
// A token ID and token secret are returned in case of success, an error otherwise.
func ParseToken(s string) (tokenID, tokenSecret string, err error) {
bootstrapTokenRegexp, err := regexp.Compile(api.BootstrapTokenPattern)
if err != nil {
return "", "", fmt.Errorf("error compiling bootstrap regex %q: %v", api.BootstrapTokenPattern, err)
}
split := bootstrapTokenRegexp.FindStringSubmatch(s)
if len(split) != 3 {
return "", "", fmt.Errorf("token [%q] was not of form [%q]", s, api.BootstrapTokenPattern)
}
return split[1], split[2], nil
}