diff --git a/go.mod b/go.mod index 902ce03b626..6491764e5e7 100644 --- a/go.mod +++ b/go.mod @@ -37,7 +37,7 @@ require ( k8s.io/client-go v0.19.2 k8s.io/cluster-bootstrap v0.0.0 k8s.io/component-base v0.18.8 - k8s.io/klog/v2 v2.9.0 + k8s.io/klog/v2 v2.30.0 k8s.io/kube-controller-manager v0.0.0 k8s.io/kubectl v0.0.0 k8s.io/kubelet v0.0.0 diff --git a/go.sum b/go.sum index 904d54e06ad..0c379a2495e 100644 --- a/go.sum +++ b/go.sum @@ -164,8 +164,8 @@ github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2 github.com/go-lintpack/lintpack v0.5.2/go.mod h1:NwZuYi2nUHho8XEIZ6SIxihrnPoqBTDqfpXvXAN0sXM= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= -github.com/go-logr/logr v0.4.0 h1:K7/B1jt6fIBQVd4Owv2MqGQClcgf0R266+7C/QjRcLc= -github.com/go-logr/logr v0.4.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= +github.com/go-logr/logr v1.2.0 h1:QK40JKJyMdUDz+h+xvCsru/bJhvG0UxvePV0ufL/AcE= +github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/zapr v0.1.0/go.mod h1:tabnROwaDl0UNxkVeFRbY8bwB37GwRv0P8lg6aAiEnk= github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8= github.com/go-openapi/analysis v0.0.0-20180825180245-b006789cd277/go.mod h1:k70tL6pCuVxPJOHXQ+wIac1FUrvNkHolPie/cLEU6hI= @@ -573,7 +573,6 @@ github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/y github.com/storageos/go-api v0.0.0-20180912212459-343b3eff91fc/go.mod h1:ZrLn+e0ZuF3Y65PNF6dIwbJPZqfmtCXxFm9ckv0agOY= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= @@ -860,16 +859,13 @@ k8s.io/cli-runtime v0.18.8 h1:ycmbN3hs7CfkJIYxJAOB10iW7BVPmXGXkfEyiV9NJ+k= k8s.io/cli-runtime v0.18.8/go.mod h1:7EzWiDbS9PFd0hamHHVoCY4GrokSTPSL32MA4rzIu0M= k8s.io/client-go v0.18.8 h1:SdbLpIxk5j5YbFr1b7fq8S7mDgDjYmUxSbszyoesoDM= k8s.io/client-go v0.18.8/go.mod h1:HqFqMllQ5NnQJNwjro9k5zMyfhZlOwpuTLVrxjkYSxU= -k8s.io/cloud-provider v0.18.8 h1:XNCJIzKFtoXhn6cyyXe7JWde0KjK6o8vo2Dtat7hb6Q= k8s.io/cloud-provider v0.18.8/go.mod h1:cn9AlzMPVIXA4HHLVbgGUigaQlZyHSZ7WAwDEFNrQSs= k8s.io/cluster-bootstrap v0.18.8 h1:+gkx/sfGBtokxvRbVA5nVA8bPy1YvpDYRiGRqyEtSXc= k8s.io/cluster-bootstrap v0.18.8/go.mod h1:guq0Uc+QwazHgpS1yAw5Z7yUlBCtGppbgWQkbN3lxIY= k8s.io/code-generator v0.18.18-rc.0/go.mod h1:TgNEVx9hCyPGpdtCWA34olQYLkh3ok9ar7XfSsr8b6c= k8s.io/component-base v0.18.8 h1:BW5CORobxb6q5mb+YvdwQlyXXS6NVH5fDXWbU7tf2L8= k8s.io/component-base v0.18.8/go.mod h1:00frPRDas29rx58pPCxNkhUfPbwajlyyvu8ruNgSErU= -k8s.io/cri-api v0.18.18-rc.0 h1:/ON2X6LsejWId5HntijA0AQ2NewlT+4bAtDzck8CSG4= k8s.io/cri-api v0.18.18-rc.0/go.mod h1:OJtpjDvfsKoLGhvcc0qfygved0S0dGX56IJzPbqTG1s= -k8s.io/csi-translation-lib v0.18.8 h1:HdyTgN4+O0zPDsF3rDGVYNwuhsG16HLQvC7lKuIxBq4= k8s.io/csi-translation-lib v0.18.8/go.mod h1:6cA6Btlzxy9s3QrS4BCZzQqclIWnTLr6Jx3H2ctAzY4= k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/gengo v0.0.0-20200114144118-36b2048a9120/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= @@ -879,8 +875,8 @@ k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8= k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= -k8s.io/klog/v2 v2.9.0 h1:D7HV+n1V57XeZ0m6tdRkfknthUaM06VFbWldOFh8kzM= -k8s.io/klog/v2 v2.9.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec= +k8s.io/klog/v2 v2.30.0 h1:bUO6drIvCIsvZ/XFgfxoGFQU/a4Qkh0iAlvUR7vlHJw= +k8s.io/klog/v2 v2.30.0/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= k8s.io/kube-aggregator v0.18.8/go.mod h1:CyLoGZB+io8eEwnn+6RbV7QWJQhj8a3TBH8ZM8sLbhI= k8s.io/kube-controller-manager v0.18.8 h1:9bFm6s+eyWTKB0cI475xFJZazpnb0ld1CHPT3U/g/fY= k8s.io/kube-controller-manager v0.18.8/go.mod h1:IYZteddXJFD1TVgAw8eRP3c9OOA2WtHdXdE8aH6gXnc= @@ -888,7 +884,6 @@ k8s.io/kube-openapi v0.0.0-20200410145947-61e04a5be9a6 h1:Oh3Mzx5pJ+yIumsAD0MOEC k8s.io/kube-openapi v0.0.0-20200410145947-61e04a5be9a6/go.mod h1:GRQhZsXIAJ1xR0C9bd8UpWHZ5plfAS9fzPjJuQ6JL3E= k8s.io/kube-proxy v0.18.8 h1:b7+JbH0WbMh9Y41qqxEULby2RVVuZ6dp2oYHijGWv9Y= k8s.io/kube-proxy v0.18.8/go.mod h1:u4E8OsUpUzfZ9CEFf9rdLsbYiusZr8utbtF4WQrX+qs= -k8s.io/kube-scheduler v0.18.8 h1:t08qdlU0UzBoKaOmNu0ripIHawR4emSJUTFdRZMg6Zk= k8s.io/kube-scheduler v0.18.8/go.mod h1:OeliYiILv1XkSq0nmQjRewgt5NimKsTidZFEhfL5fqA= k8s.io/kubectl v0.18.8 h1:qTkHCz21YmK0+S0oE6TtjtxmjeDP42gJcZJyRKsIenA= k8s.io/kubectl v0.18.8/go.mod h1:PlEgIAjOMua4hDFTEkVf+W5M0asHUKfE4y7VDZkpLHM= diff --git a/pkg/util/kubeadmapi/kubeadmapi.go b/pkg/util/kubeadmapi/kubeadmapi.go new file mode 100644 index 00000000000..23fffe7d954 --- /dev/null +++ b/pkg/util/kubeadmapi/kubeadmapi.go @@ -0,0 +1,423 @@ +/* +Copyright 2014 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 kubeadmapi + +import ( + "context" + "sort" + "strings" + "time" + + "github.com/pkg/errors" + v1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/apiserver/pkg/authentication/user" + clientset "k8s.io/client-go/kubernetes" + "k8s.io/client-go/tools/clientcmd" + clientcmdapi "k8s.io/client-go/tools/clientcmd/api" + bootstrapapi "k8s.io/cluster-bootstrap/token/api" + bootstraputil "k8s.io/cluster-bootstrap/token/util" + bootstrapsecretutil "k8s.io/cluster-bootstrap/util/secrets" + "k8s.io/klog/v2" +) + +const ( + BootstrapSignerClusterRoleName = "kubeadm:bootstrap-signer-clusterinfo" + // NodeBootstrapTokenAuthGroup specifies which group a Node Bootstrap Token should be authenticated in + NodeBootstrapTokenAuthGroup = "system:bootstrappers:kubeadm:default-node-token" +) + +var ( + // DefaultTokenUsages specifies the default functions a token will get + DefaultTokenUsages = bootstrapapi.KnownTokenUsages + // DefaultTokenGroups specifies the default groups that this token will authenticate as when used for authentication + DefaultTokenGroups = []string{NodeBootstrapTokenAuthGroup} +) + +// BootstrapTokenString is a token of the format abcdef.abcdef0123456789 that is used +// for both validation of the practically of the API server from a joining node's point +// of view and as an authentication method for the node in the bootstrap phase of +// "kubeadm join". This token is and should be short-lived +type BootstrapTokenString struct { + ID string + Secret string +} + +// BootstrapToken describes one bootstrap token, stored as a Secret in the cluster +// TODO: The BootstrapToken object should move out to either k8s.io/client-go or k8s.io/api in the future +// (probably as part of Bootstrap Tokens going GA). It should not be staged under the kubeadm API as it is now. +type BootstrapToken struct { + // Token is used for establishing bidirectional trust between nodes and control-planes. + // Used for joining nodes in the cluster. + Token *BootstrapTokenString + // Description sets a human-friendly message why this token exists and what it's used + // for, so other administrators can know its purpose. + Description string + // TTL defines the time to live for this token. Defaults to 24h. + // Expires and TTL are mutually exclusive. + TTL *metav1.Duration + // Expires specifies the timestamp when this token expires. Defaults to being set + // dynamically at runtime based on the TTL. Expires and TTL are mutually exclusive. + Expires *metav1.Time + // Usages describes the ways in which this token can be used. Can by default be used + // for establishing bidirectional trust, but that can be changed here. + Usages []string + // Groups specifies the extra groups that this token will authenticate as when/if + // used for authentication + Groups []string +} + +// String returns the string representation of the BootstrapTokenString +func (bts BootstrapTokenString) String() string { + if len(bts.ID) > 0 && len(bts.Secret) > 0 { + return bootstraputil.TokenFromIDAndSecret(bts.ID, bts.Secret) + } + return "" +} + +// ToSecret converts the given BootstrapToken object to its Secret representation that +// may be submitted to the API Server in order to be stored. +func (bt *BootstrapToken) ToSecret() *v1.Secret { + return &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: bootstraputil.BootstrapTokenSecretName(bt.Token.ID), + Namespace: metav1.NamespaceSystem, + }, + Type: v1.SecretType(bootstrapapi.SecretTypeBootstrapToken), + Data: encodeTokenSecretData(bt, time.Now()), + } +} + +// encodeTokenSecretData takes the token discovery object and an optional duration and returns the .Data for the Secret +// now is passed in order to be able to used in unit testing +func encodeTokenSecretData(token *BootstrapToken, now time.Time) map[string][]byte { + data := map[string][]byte{ + bootstrapapi.BootstrapTokenIDKey: []byte(token.Token.ID), + bootstrapapi.BootstrapTokenSecretKey: []byte(token.Token.Secret), + } + + if len(token.Description) > 0 { + data[bootstrapapi.BootstrapTokenDescriptionKey] = []byte(token.Description) + } + + // If for some strange reason both token.TTL and token.Expires would be set + // (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 + // TODO: This maybe should be a helper function in bootstraputil? + expirationString := token.Expires.Time.Format(time.RFC3339) + data[bootstrapapi.BootstrapTokenExpirationKey] = []byte(expirationString) + + } else if token.TTL != nil && token.TTL.Duration > 0 { + // Only if .Expires is unset, TTL might have an effect + // Get the current time, add the specified duration, and format it accordingly + expirationString := now.Add(token.TTL.Duration).Format(time.RFC3339) + data[bootstrapapi.BootstrapTokenExpirationKey] = []byte(expirationString) + } + + for _, usage := range token.Usages { + data[bootstrapapi.BootstrapTokenUsagePrefix+usage] = []byte("true") + } + + if len(token.Groups) > 0 { + data[bootstrapapi.BootstrapTokenExtraGroupsKey] = []byte(strings.Join(token.Groups, ",")) + } + return data +} + +// 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 := bootstrapsecretutil.GetData(secret, bootstrapapi.BootstrapTokenIDKey) + if len(tokenID) == 0 { + return nil, errors.Errorf("bootstrap Token Secret has no token-id data: %s", secret.Name) + } + + // Enforce the right naming convention + if secret.Name != bootstraputil.BootstrapTokenSecretName(tokenID) { + return nil, errors.Errorf("bootstrap token name is not of the form '%s(token-id)'. Actual: %q. Expected: %q", + bootstrapapi.BootstrapTokenSecretPrefix, secret.Name, bootstraputil.BootstrapTokenSecretName(tokenID)) + } + + 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) + } + + // Create the BootstrapTokenString object based on the ID and Secret + bts, err := NewBootstrapTokenStringFromIDAndSecret(tokenID, tokenSecret) + if err != nil { + return nil, errors.Wrap(err, "bootstrap Token Secret is invalid and couldn't be parsed") + } + + // Get the description (if any) from the Secret + description := bootstrapsecretutil.GetData(secret, bootstrapapi.BootstrapTokenDescriptionKey) + + // Expiration time is optional, if not specified this implies the token + // never expires. + secretExpiration := bootstrapsecretutil.GetData(secret, bootstrapapi.BootstrapTokenExpirationKey) + var expires *metav1.Time + if len(secretExpiration) > 0 { + expTime, err := time.Parse(time.RFC3339, secretExpiration) + if err != nil { + return nil, errors.Wrapf(err, "can't parse expiration time of bootstrap token %q", secret.Name) + } + expires = &metav1.Time{Time: expTime} + } + + // Build an usages string slice from the Secret data + var usages []string + for k, v := range secret.Data { + // Skip all fields that don't include this prefix + if !strings.HasPrefix(k, bootstrapapi.BootstrapTokenUsagePrefix) { + continue + } + // Skip those that don't have this usage set to true + if string(v) != "true" { + continue + } + usages = append(usages, strings.TrimPrefix(k, bootstrapapi.BootstrapTokenUsagePrefix)) + } + // Only sort the slice if defined + if usages != nil { + sort.Strings(usages) + } + + // Get the extra groups information from the Secret + // 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 := bootstrapsecretutil.GetData(secret, bootstrapapi.BootstrapTokenExtraGroupsKey) + g := strings.Split(groupsString, ",") + if len(g) > 0 && len(g[0]) > 0 { + groups = g + } + + return &BootstrapToken{ + Token: bts, + Description: description, + Expires: expires, + Usages: usages, + Groups: groups, + }, nil +} + +// NewBootstrapTokenString converts the given Bootstrap Token as a string +// to the BootstrapTokenString object used for serialization/deserialization +// and internal usage. It also automatically validates that the given token +// is of the right format +func NewBootstrapTokenString(token string) (*BootstrapTokenString, error) { + substrs := bootstraputil.BootstrapTokenRegexp.FindStringSubmatch(token) + // TODO: Add a constant for the 3 value here, and explain better why it's needed (other than because how the regexp parsin works) + if len(substrs) != 3 { + return nil, errors.Errorf("the bootstrap token %q was not of the form %q", token, bootstrapapi.BootstrapTokenPattern) + } + + return &BootstrapTokenString{ID: substrs[1], Secret: substrs[2]}, nil +} + +// NewBootstrapTokenStringFromIDAndSecret is a wrapper around NewBootstrapTokenString +// that allows the caller to specify the ID and Secret separately +func NewBootstrapTokenStringFromIDAndSecret(id, secret string) (*BootstrapTokenString, error) { + return NewBootstrapTokenString(bootstraputil.TokenFromIDAndSecret(id, secret)) +} + +// CreateNewTokens tries to create a token and fails if one with the same ID already exists +func CreateNewTokens(client clientset.Interface, tokens []BootstrapToken) error { + return UpdateOrCreateTokens(client, true, tokens) +} + +// UpdateOrCreateTokens attempts to update a token with the given ID, or create if it does not already exist. +func UpdateOrCreateTokens(client clientset.Interface, failIfExists bool, tokens []BootstrapToken) error { + + for _, token := range tokens { + + secretName := bootstraputil.BootstrapTokenSecretName(token.Token.ID) + secret, err := client.CoreV1().Secrets(metav1.NamespaceSystem).Get(context.TODO(), secretName, metav1.GetOptions{}) + if secret != nil && err == nil && failIfExists { + return errors.Errorf("a token with id %q already exists", token.Token.ID) + } + + updatedOrNewSecret := token.ToSecret() + // Try to create or update the token with an exponential backoff + err = TryRunCommand(func() error { + if err := CreateOrUpdateSecret(client, updatedOrNewSecret); err != nil { + return errors.Wrapf(err, "failed to create or update bootstrap token with name %s", secretName) + } + return nil + }, 5) + if err != nil { + return err + } + } + return nil +} + +// TryRunCommand runs a function a maximum of failureThreshold times, and retries on error. If failureThreshold is hit; the last error is returned +func TryRunCommand(f func() error, failureThreshold int) error { + backoff := wait.Backoff{ + Duration: 5 * time.Second, + Factor: 2, // double the timeout for every failure + Steps: failureThreshold, + } + return wait.ExponentialBackoff(backoff, func() (bool, error) { + err := f() + if err != nil { + // Retry until the timeout + return false, nil + } + // The last f() call was a success, return cleanly + return true, nil + }) +} + +// CreateOrUpdateSecret creates a Secret if the target resource doesn't exist. If the resource exists already, this function will update the resource instead. +func CreateOrUpdateSecret(client clientset.Interface, secret *v1.Secret) error { + if _, err := client.CoreV1().Secrets(secret.ObjectMeta.Namespace).Create(context.TODO(), secret, metav1.CreateOptions{}); err != nil { + if !apierrors.IsAlreadyExists(err) { + return errors.Wrap(err, "unable to create secret") + } + + if _, err := client.CoreV1().Secrets(secret.ObjectMeta.Namespace).Update(context.TODO(), secret, metav1.UpdateOptions{}); err != nil { + return errors.Wrap(err, "unable to update secret") + } + } + return nil +} + +// CreateOrUpdateConfigMap creates a ConfigMap if the target resource doesn't exist. If the resource exists already, this function will update the resource instead. +func CreateOrUpdateConfigMap(client clientset.Interface, cm *v1.ConfigMap) error { + if _, err := client.CoreV1().ConfigMaps(cm.ObjectMeta.Namespace).Create(context.TODO(), cm, metav1.CreateOptions{}); err != nil { + if !apierrors.IsAlreadyExists(err) { + return errors.Wrap(err, "unable to create ConfigMap") + } + + if _, err := client.CoreV1().ConfigMaps(cm.ObjectMeta.Namespace).Update(context.TODO(), cm, metav1.UpdateOptions{}); err != nil { + return errors.Wrap(err, "unable to update ConfigMap") + } + } + return nil +} + +// CreateOrUpdateRole creates a Role if the target resource doesn't exist. If the resource exists already, this function will update the resource instead. +func CreateOrUpdateRole(client clientset.Interface, role *rbacv1.Role) error { + if _, err := client.RbacV1().Roles(role.ObjectMeta.Namespace).Create(context.TODO(), role, metav1.CreateOptions{}); err != nil { + if !apierrors.IsAlreadyExists(err) { + return errors.Wrap(err, "unable to create RBAC role") + } + + if _, err := client.RbacV1().Roles(role.ObjectMeta.Namespace).Update(context.TODO(), role, metav1.UpdateOptions{}); err != nil { + return errors.Wrap(err, "unable to update RBAC role") + } + } + return nil +} + +// CreateOrUpdateRoleBinding creates a RoleBinding if the target resource doesn't exist. If the resource exists already, this function will update the resource instead. +func CreateOrUpdateRoleBinding(client clientset.Interface, roleBinding *rbacv1.RoleBinding) error { + if _, err := client.RbacV1().RoleBindings(roleBinding.ObjectMeta.Namespace).Create(context.TODO(), roleBinding, metav1.CreateOptions{}); err != nil { + if !apierrors.IsAlreadyExists(err) { + return errors.Wrap(err, "unable to create RBAC rolebinding") + } + + if _, err := client.RbacV1().RoleBindings(roleBinding.ObjectMeta.Namespace).Update(context.TODO(), roleBinding, metav1.UpdateOptions{}); err != nil { + return errors.Wrap(err, "unable to update RBAC rolebinding") + } + } + return nil +} + +// CreateBootstrapConfigMapIfNotExists creates the kube-public ConfigMap if it doesn't exist already +func CreateBootstrapConfigMapIfNotExists(client clientset.Interface, file string) error { + + klog.V(1).Infof("[bootstrap-token] Creating the %q ConfigMap in the %q namespace", bootstrapapi.ConfigMapClusterInfo, metav1.NamespacePublic) + + klog.V(1).Infoln("[bootstrap-token] loading admin kubeconfig") + adminConfig, err := clientcmd.LoadFromFile(file) + if err != nil { + return errors.Wrap(err, "failed to load admin kubeconfig") + } + + adminCluster := adminConfig.Contexts[adminConfig.CurrentContext].Cluster + // Copy the cluster from admin.conf to the bootstrap kubeconfig, contains the CA cert and the server URL + klog.V(1).Infoln("[bootstrap-token] copying the cluster from admin.conf to the bootstrap kubeconfig") + bootstrapConfig := &clientcmdapi.Config{ + Clusters: map[string]*clientcmdapi.Cluster{ + "": adminConfig.Clusters[adminCluster], + }, + } + bootstrapBytes, err := clientcmd.Write(*bootstrapConfig) + if err != nil { + return err + } + + // Create or update the ConfigMap in the kube-public namespace + klog.V(1).Infoln("[bootstrap-token] creating/updating ConfigMap in kube-public namespace") + return CreateOrUpdateConfigMap(client, &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: bootstrapapi.ConfigMapClusterInfo, + Namespace: metav1.NamespacePublic, + }, + Data: map[string]string{ + bootstrapapi.KubeConfigKey: string(bootstrapBytes), + }, + }) +} + +// CreateClusterInfoRBACRules creates the RBAC rules for exposing the cluster-info ConfigMap in the kube-public namespace to unauthenticated users +func CreateClusterInfoRBACRules(client clientset.Interface) error { + klog.V(1).Infoln("creating the RBAC rules for exposing the cluster-info ConfigMap in the kube-public namespace") + err := CreateOrUpdateRole(client, &rbacv1.Role{ + ObjectMeta: metav1.ObjectMeta{ + Name: BootstrapSignerClusterRoleName, + Namespace: metav1.NamespacePublic, + }, + Rules: []rbacv1.PolicyRule{ + { + Verbs: []string{"get"}, + APIGroups: []string{""}, + Resources: []string{"configmaps"}, + ResourceNames: []string{bootstrapapi.ConfigMapClusterInfo}, + }, + }, + }) + if err != nil { + return err + } + + return CreateOrUpdateRoleBinding(client, &rbacv1.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: BootstrapSignerClusterRoleName, + Namespace: metav1.NamespacePublic, + }, + RoleRef: rbacv1.RoleRef{ + APIGroup: rbacv1.GroupName, + Kind: "Role", + Name: BootstrapSignerClusterRoleName, + }, + Subjects: []rbacv1.Subject{ + { + Kind: rbacv1.UserKind, + Name: user.Anonymous, + }, + }, + }) +} diff --git a/pkg/yurtctl/cmd/convert/convert.go b/pkg/yurtctl/cmd/convert/convert.go index 72e0caa16ec..e8c240abef3 100644 --- a/pkg/yurtctl/cmd/convert/convert.go +++ b/pkg/yurtctl/cmd/convert/convert.go @@ -35,11 +35,11 @@ import ( "k8s.io/client-go/kubernetes" bootstrapapi "k8s.io/cluster-bootstrap/token/api" "k8s.io/klog/v2" - clusterinfophase "k8s.io/kubernetes/cmd/kubeadm/app/phases/bootstraptoken/clusterinfo" nodeutil "github.com/openyurtio/openyurt/pkg/controller/util/node" nodeservant "github.com/openyurtio/openyurt/pkg/node-servant" "github.com/openyurtio/openyurt/pkg/projectinfo" + "github.com/openyurtio/openyurt/pkg/util/kubeadmapi" "github.com/openyurtio/openyurt/pkg/yurtctl/constants" "github.com/openyurtio/openyurt/pkg/yurtctl/lock" enutil "github.com/openyurtio/openyurt/pkg/yurtctl/util/edgenode" @@ -510,10 +510,10 @@ func prepareClusterInfoConfigMap(client *kubernetes.Clientset, file string) erro info, err := client.CoreV1().ConfigMaps(metav1.NamespacePublic).Get(context.Background(), bootstrapapi.ConfigMapClusterInfo, metav1.GetOptions{}) if err != nil && apierrors.IsNotFound(err) { // Create the cluster-info ConfigMap with the associated RBAC rules - if err := clusterinfophase.CreateBootstrapConfigMapIfNotExists(client, file); err != nil { + if err := kubeadmapi.CreateBootstrapConfigMapIfNotExists(client, file); err != nil { return fmt.Errorf("error creating bootstrap ConfigMap, %v", err) } - if err := clusterinfophase.CreateClusterInfoRBACRules(client); err != nil { + if err := kubeadmapi.CreateClusterInfoRBACRules(client); err != nil { return fmt.Errorf("error creating clusterinfo RBAC rules, %v", err) } } else if err != nil || info == nil { diff --git a/pkg/yurtctl/util/kubernetes/util.go b/pkg/yurtctl/util/kubernetes/util.go index 499aeda213d..087f97217a8 100644 --- a/pkg/yurtctl/util/kubernetes/util.go +++ b/pkg/yurtctl/util/kubernetes/util.go @@ -57,10 +57,8 @@ import ( bootstrapapi "k8s.io/cluster-bootstrap/token/api" bootstraputil "k8s.io/cluster-bootstrap/token/util" "k8s.io/klog/v2" - kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm" - kubeadmcontants "k8s.io/kubernetes/cmd/kubeadm/app/constants" - tokenphase "k8s.io/kubernetes/cmd/kubeadm/app/phases/bootstraptoken/node" + "github.com/openyurtio/openyurt/pkg/util/kubeadmapi" "github.com/openyurtio/openyurt/pkg/yurtctl/constants" "github.com/openyurtio/openyurt/pkg/yurtctl/util" "github.com/openyurtio/openyurt/pkg/yurtctl/util/edgenode" @@ -664,11 +662,11 @@ func GetOrCreateJoinTokenString(cliSet *kubernetes.Clientset) (string, error) { } klog.V(1).Infoln("[token] creating token") - if err := tokenphase.CreateNewTokens(cliSet, + if err := kubeadmapi.CreateNewTokens(cliSet, []kubeadmapi.BootstrapToken{{ Token: token, - Usages: kubeadmcontants.DefaultTokenUsages, - Groups: kubeadmcontants.DefaultTokenGroups, + Usages: kubeadmapi.DefaultTokenUsages, + Groups: kubeadmapi.DefaultTokenGroups, }}); err != nil { return "", err } @@ -691,7 +689,7 @@ func usagesAndGroupsAreValid(token *kubeadmapi.BootstrapToken) bool { return true } - return sliceEqual(token.Usages, kubeadmcontants.DefaultTokenUsages) && sliceEqual(token.Groups, kubeadmcontants.DefaultTokenGroups) + return sliceEqual(token.Usages, kubeadmapi.DefaultTokenUsages) && sliceEqual(token.Groups, kubeadmapi.DefaultTokenGroups) } // find kube-controller-manager deployed through static file