Merge pull request #77211 from dixudx/bootstrap_token_refactor

Bootstrap token refactor
This commit is contained in:
Kubernetes Prow Robot 2019-06-24 13:36:36 -07:00 committed by GitHub
commit 6f0f62b2c4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 424 additions and 298 deletions

View File

@ -26,6 +26,7 @@ go_library(
"//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
"//staging/src/k8s.io/cluster-bootstrap/token/api:go_default_library", "//staging/src/k8s.io/cluster-bootstrap/token/api:go_default_library",
"//staging/src/k8s.io/cluster-bootstrap/token/util:go_default_library", "//staging/src/k8s.io/cluster-bootstrap/token/util:go_default_library",
"//staging/src/k8s.io/cluster-bootstrap/util/secrets:go_default_library",
"//vendor/github.com/pkg/errors:go_default_library", "//vendor/github.com/pkg/errors:go_default_library",
], ],
) )

View File

@ -27,6 +27,7 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
bootstrapapi "k8s.io/cluster-bootstrap/token/api" bootstrapapi "k8s.io/cluster-bootstrap/token/api"
bootstraputil "k8s.io/cluster-bootstrap/token/util" 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 // 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 // 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. // token.Expires has higher priority, as can be seen in the logic here.
if token.Expires != nil { if token.Expires != nil {
// Format the expiration date accordingly // 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 // BootstrapTokenFromSecret returns a BootstrapToken object from the given Secret
func BootstrapTokenFromSecret(secret *v1.Secret) (*BootstrapToken, error) { func BootstrapTokenFromSecret(secret *v1.Secret) (*BootstrapToken, error) {
// Get the Token ID field from the Secret data // Get the Token ID field from the Secret data
tokenID := getSecretString(secret, bootstrapapi.BootstrapTokenIDKey) tokenID := bootstrapsecretutil.GetData(secret, bootstrapapi.BootstrapTokenIDKey)
if len(tokenID) == 0 { if len(tokenID) == 0 {
return nil, errors.Errorf("bootstrap Token Secret has no token-id data: %s", secret.Name) 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)) bootstrapapi.BootstrapTokenSecretPrefix, secret.Name, bootstraputil.BootstrapTokenSecretName(tokenID))
} }
tokenSecret := getSecretString(secret, bootstrapapi.BootstrapTokenSecretKey) tokenSecret := bootstrapsecretutil.GetData(secret, bootstrapapi.BootstrapTokenSecretKey)
if len(tokenSecret) == 0 { if len(tokenSecret) == 0 {
return nil, errors.Errorf("bootstrap Token Secret has no token-secret data: %s", secret.Name) 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 // 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 // Expiration time is optional, if not specified this implies the token
// never expires. // never expires.
secretExpiration := getSecretString(secret, bootstrapapi.BootstrapTokenExpirationKey) secretExpiration := bootstrapsecretutil.GetData(secret, bootstrapapi.BootstrapTokenExpirationKey)
var expires *metav1.Time var expires *metav1.Time
if len(secretExpiration) > 0 { if len(secretExpiration) > 0 {
expTime, err := time.Parse(time.RFC3339, secretExpiration) 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 // 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 // empty slice or an empty slice with a "" string only
var groups []string var groups []string
groupsString := getSecretString(secret, bootstrapapi.BootstrapTokenExtraGroupsKey) groupsString := bootstrapsecretutil.GetData(secret, bootstrapapi.BootstrapTokenExtraGroupsKey)
g := strings.Split(groupsString, ",") g := strings.Split(groupsString, ",")
if len(g) > 0 && len(g[0]) > 0 { if len(g) > 0 && len(g[0]) > 0 {
groups = g groups = g
@ -156,14 +157,3 @@ func BootstrapTokenFromSecret(secret *v1.Secret) (*BootstrapToken, error) {
Groups: groups, Groups: groups,
}, nil }, 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) b, _ := json.Marshal(*bt)
return string(b) 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

@ -59,6 +59,7 @@ go_library(
"//staging/src/k8s.io/client-go/tools/cache:go_default_library", "//staging/src/k8s.io/client-go/tools/cache:go_default_library",
"//staging/src/k8s.io/client-go/util/workqueue:go_default_library", "//staging/src/k8s.io/client-go/util/workqueue:go_default_library",
"//staging/src/k8s.io/cluster-bootstrap/token/api:go_default_library", "//staging/src/k8s.io/cluster-bootstrap/token/api:go_default_library",
"//staging/src/k8s.io/cluster-bootstrap/util/secrets:go_default_library",
"//vendor/gopkg.in/square/go-jose.v2:go_default_library", "//vendor/gopkg.in/square/go-jose.v2:go_default_library",
"//vendor/k8s.io/klog:go_default_library", "//vendor/k8s.io/klog:go_default_library",
], ],

View File

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

View File

@ -17,46 +17,23 @@ limitations under the License.
package bootstrap package bootstrap
import ( import (
"regexp"
"time" "time"
"k8s.io/klog" "k8s.io/klog"
"k8s.io/api/core/v1" "k8s.io/api/core/v1"
bootstrapapi "k8s.io/cluster-bootstrap/token/api" 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) { func validateSecretForSigning(secret *v1.Secret) (tokenID, tokenSecret string, ok bool) {
nameTokenID, ok := parseSecretName(secret.Name) nameTokenID, ok := bootstrapsecretutil.ParseName(secret.Name)
if !ok { if !ok {
klog.V(3).Infof("Invalid secret name: %s. Must be of form %s<secret-id>.", secret.Name, bootstrapapi.BootstrapTokenSecretPrefix) klog.V(3).Infof("Invalid secret name: %s. Must be of form %s<secret-id>.", secret.Name, bootstrapapi.BootstrapTokenSecretPrefix)
return "", "", false return "", "", false
} }
tokenID = getSecretString(secret, bootstrapapi.BootstrapTokenIDKey) tokenID = bootstrapsecretutil.GetData(secret, bootstrapapi.BootstrapTokenIDKey)
if len(tokenID) == 0 { if len(tokenID) == 0 {
klog.V(3).Infof("No %s key in %s/%s Secret", bootstrapapi.BootstrapTokenIDKey, secret.Namespace, secret.Name) klog.V(3).Infof("No %s key in %s/%s Secret", bootstrapapi.BootstrapTokenIDKey, secret.Namespace, secret.Name)
return "", "", false return "", "", false
@ -67,7 +44,7 @@ func validateSecretForSigning(secret *v1.Secret) (tokenID, tokenSecret string, o
return "", "", false return "", "", false
} }
tokenSecret = getSecretString(secret, bootstrapapi.BootstrapTokenSecretKey) tokenSecret = bootstrapsecretutil.GetData(secret, bootstrapapi.BootstrapTokenSecretKey)
if len(tokenSecret) == 0 { if len(tokenSecret) == 0 {
klog.V(3).Infof("No %s key in %s/%s Secret", bootstrapapi.BootstrapTokenSecretKey, secret.Namespace, secret.Name) klog.V(3).Infof("No %s key in %s/%s Secret", bootstrapapi.BootstrapTokenSecretKey, secret.Namespace, secret.Name)
return "", "", false 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 // 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 // but if that isn't working or it hasn't gotten there yet we should check
// here. // here.
if isSecretExpired(secret) { if bootstrapsecretutil.HasExpired(secret, time.Now()) {
return "", "", false return "", "", false
} }
// Make sure this secret can be used for signing // Make sure this secret can be used for signing
okToSign := getSecretString(secret, bootstrapapi.BootstrapTokenUsageSigningKey) okToSign := bootstrapsecretutil.GetData(secret, bootstrapapi.BootstrapTokenUsageSigningKey)
if okToSign != "true" { if okToSign != "true" {
return "", "", false return "", "", false
} }
return tokenID, tokenSecret, true 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" "testing"
"time" "time"
"github.com/stretchr/testify/assert"
"k8s.io/api/core/v1" "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
bootstrapapi "k8s.io/cluster-bootstrap/token/api" 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") 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

@ -28,12 +28,12 @@ go_library(
deps = [ deps = [
"//staging/src/k8s.io/api/core/v1:go_default_library", "//staging/src/k8s.io/api/core/v1:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/api/errors:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/api/errors:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/util/sets:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/authentication/authenticator:go_default_library", "//staging/src/k8s.io/apiserver/pkg/authentication/authenticator:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/authentication/user:go_default_library", "//staging/src/k8s.io/apiserver/pkg/authentication/user:go_default_library",
"//staging/src/k8s.io/client-go/listers/core/v1:go_default_library", "//staging/src/k8s.io/client-go/listers/core/v1:go_default_library",
"//staging/src/k8s.io/cluster-bootstrap/token/api:go_default_library", "//staging/src/k8s.io/cluster-bootstrap/token/api:go_default_library",
"//staging/src/k8s.io/cluster-bootstrap/token/util:go_default_library", "//staging/src/k8s.io/cluster-bootstrap/util/secrets:go_default_library",
"//staging/src/k8s.io/cluster-bootstrap/util/tokens:go_default_library",
"//vendor/k8s.io/klog:go_default_library", "//vendor/k8s.io/klog:go_default_library",
], ],
) )

View File

@ -23,20 +23,18 @@ import (
"context" "context"
"crypto/subtle" "crypto/subtle"
"fmt" "fmt"
"regexp"
"strings"
"time" "time"
"k8s.io/klog" "k8s.io/klog"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apiserver/pkg/authentication/authenticator" "k8s.io/apiserver/pkg/authentication/authenticator"
"k8s.io/apiserver/pkg/authentication/user" "k8s.io/apiserver/pkg/authentication/user"
corev1listers "k8s.io/client-go/listers/core/v1" corev1listers "k8s.io/client-go/listers/core/v1"
bootstrapapi "k8s.io/cluster-bootstrap/token/api" 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 // 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 ) // ( token-id ).( token-secret )
// //
func (t *TokenAuthenticator) AuthenticateToken(ctx context.Context, token string) (*authenticator.Response, bool, error) { 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 { if err != nil {
// Token isn't of the correct form, ignore it. // Token isn't of the correct form, ignore it.
return nil, false, nil return nil, false, nil
@ -118,29 +116,29 @@ func (t *TokenAuthenticator) AuthenticateToken(ctx context.Context, token string
return nil, false, nil return nil, false, nil
} }
ts := getSecretString(secret, bootstrapapi.BootstrapTokenSecretKey) ts := bootstrapsecretutil.GetData(secret, bootstrapapi.BootstrapTokenSecretKey)
if subtle.ConstantTimeCompare([]byte(ts), []byte(tokenSecret)) != 1 { if subtle.ConstantTimeCompare([]byte(ts), []byte(tokenSecret)) != 1 {
tokenErrorf(secret, "has invalid value for key %s, expected %s.", bootstrapapi.BootstrapTokenSecretKey, tokenSecret) tokenErrorf(secret, "has invalid value for key %s, expected %s.", bootstrapapi.BootstrapTokenSecretKey, tokenSecret)
return nil, false, nil return nil, false, nil
} }
id := getSecretString(secret, bootstrapapi.BootstrapTokenIDKey) id := bootstrapsecretutil.GetData(secret, bootstrapapi.BootstrapTokenIDKey)
if id != tokenID { if id != tokenID {
tokenErrorf(secret, "has invalid value for key %s, expected %s.", bootstrapapi.BootstrapTokenIDKey, tokenID) tokenErrorf(secret, "has invalid value for key %s, expected %s.", bootstrapapi.BootstrapTokenIDKey, tokenID)
return nil, false, nil return nil, false, nil
} }
if isSecretExpired(secret) { if bootstrapsecretutil.HasExpired(secret, time.Now()) {
// logging done in isSecretExpired method. // logging done in isSecretExpired method.
return nil, false, nil 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) tokenErrorf(secret, "not marked %s=true.", bootstrapapi.BootstrapTokenUsageAuthentication)
return nil, false, nil return nil, false, nil
} }
groups, err := getGroups(secret) groups, err := bootstrapsecretutil.GetGroups(secret)
if err != nil { if err != nil {
tokenErrorf(secret, "has invalid value for key %s: %v.", bootstrapapi.BootstrapTokenExtraGroupsKey, err) tokenErrorf(secret, "has invalid value for key %s: %v.", bootstrapapi.BootstrapTokenExtraGroupsKey, err)
return nil, false, nil return nil, false, nil
@ -153,76 +151,3 @@ func (t *TokenAuthenticator) AuthenticateToken(ctx context.Context, token string
}, },
}, true, nil }, 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

@ -201,6 +201,7 @@
- k8s.io/api - k8s.io/api
- k8s.io/apimachinery - k8s.io/apimachinery
- k8s.io/cluster-bootstrap - k8s.io/cluster-bootstrap
- k8s.io/klog
- baseImportPath: "./vendor/k8s.io/cloud-provider/" - baseImportPath: "./vendor/k8s.io/cloud-provider/"
allowedImports: allowedImports:

View File

@ -11,6 +11,8 @@ filegroup(
":package-srcs", ":package-srcs",
"//staging/src/k8s.io/cluster-bootstrap/token/api:all-srcs", "//staging/src/k8s.io/cluster-bootstrap/token/api:all-srcs",
"//staging/src/k8s.io/cluster-bootstrap/token/util:all-srcs", "//staging/src/k8s.io/cluster-bootstrap/token/util:all-srcs",
"//staging/src/k8s.io/cluster-bootstrap/util/secrets:all-srcs",
"//staging/src/k8s.io/cluster-bootstrap/util/tokens:all-srcs",
], ],
tags = ["automanaged"], tags = ["automanaged"],
visibility = ["//visibility:public"], visibility = ["//visibility:public"],

View File

@ -7,6 +7,7 @@ go 1.12
require ( require (
k8s.io/api v0.0.0 k8s.io/api v0.0.0
k8s.io/apimachinery v0.0.0 k8s.io/apimachinery v0.0.0
k8s.io/klog v0.3.1
) )
replace ( replace (

View File

@ -27,6 +27,8 @@ import (
"k8s.io/cluster-bootstrap/token/api" "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 // validBootstrapTokenChars defines the characters a bootstrap token can consist of
const validBootstrapTokenChars = "0123456789abcdefghijklmnopqrstuvwxyz" const validBootstrapTokenChars = "0123456789abcdefghijklmnopqrstuvwxyz"
@ -110,6 +112,7 @@ func BootstrapTokenSecretName(tokenID string) string {
// ValidateBootstrapGroupName checks if the provided group name is a valid // ValidateBootstrapGroupName checks if the provided group name is a valid
// bootstrap group name. Returns nil if valid or a validation error if invalid. // 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 { func ValidateBootstrapGroupName(name string) error {
if BootstrapGroupRegexp.Match([]byte(name)) { if BootstrapGroupRegexp.Match([]byte(name)) {
return nil return nil

View File

@ -0,0 +1,41 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "go_default_library",
srcs = ["secrets.go"],
importmap = "k8s.io/kubernetes/vendor/k8s.io/cluster-bootstrap/util/secrets",
importpath = "k8s.io/cluster-bootstrap/util/secrets",
visibility = ["//visibility:public"],
deps = [
"//staging/src/k8s.io/api/core/v1:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/util/sets:go_default_library",
"//staging/src/k8s.io/cluster-bootstrap/token/api:go_default_library",
"//staging/src/k8s.io/cluster-bootstrap/token/util:go_default_library",
"//vendor/k8s.io/klog:go_default_library",
],
)
go_test(
name = "go_default_test",
srcs = ["secrets_test.go"],
embed = [":go_default_library"],
deps = [
"//staging/src/k8s.io/api/core/v1:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//staging/src/k8s.io/cluster-bootstrap/token/api:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@ -0,0 +1,98 @@
/*
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"
)
var (
secretNameRe = regexp.MustCompile(`^` + regexp.QuoteMeta(api.BootstrapTokenSecretPrefix) + `([a-z0-9]{6})$`)
)
// 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) {
r := secretNameRe.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,24 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library")
go_library(
name = "go_default_library",
srcs = ["tokens.go"],
importmap = "k8s.io/kubernetes/vendor/k8s.io/cluster-bootstrap/util/tokens",
importpath = "k8s.io/cluster-bootstrap/util/tokens",
visibility = ["//visibility:public"],
deps = ["//staging/src/k8s.io/cluster-bootstrap/token/api:go_default_library"],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@ -0,0 +1,38 @@
/*
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"
)
var (
bootstrapTokenRe = regexp.MustCompile(api.BootstrapTokenPattern)
)
// 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) {
split := bootstrapTokenRe.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
}

2
vendor/modules.txt vendored
View File

@ -1544,6 +1544,8 @@ k8s.io/cloud-provider/volume/helpers
# k8s.io/cluster-bootstrap v0.0.0 => ./staging/src/k8s.io/cluster-bootstrap # k8s.io/cluster-bootstrap v0.0.0 => ./staging/src/k8s.io/cluster-bootstrap
k8s.io/cluster-bootstrap/token/api k8s.io/cluster-bootstrap/token/api
k8s.io/cluster-bootstrap/token/util k8s.io/cluster-bootstrap/token/util
k8s.io/cluster-bootstrap/util/secrets
k8s.io/cluster-bootstrap/util/tokens
# k8s.io/code-generator v0.0.0 => ./staging/src/k8s.io/code-generator # k8s.io/code-generator v0.0.0 => ./staging/src/k8s.io/code-generator
k8s.io/code-generator/cmd/go-to-protobuf k8s.io/code-generator/cmd/go-to-protobuf
k8s.io/code-generator/cmd/go-to-protobuf/protobuf k8s.io/code-generator/cmd/go-to-protobuf/protobuf