mirror of https://github.com/k3s-io/k3s.git
Add support for `k3s token` command
Signed-off-by: Brad Davidson <brad.davidson@rancher.com>
This commit is contained in:
parent
7d49202721
commit
373df1c8b0
|
@ -37,6 +37,7 @@ func main() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tokenCommand := internalCLIAction(version.Program+"-"+cmds.TokenCommand, dataDir, os.Args)
|
||||||
etcdsnapshotCommand := internalCLIAction(version.Program+"-"+cmds.EtcdSnapshotCommand, dataDir, os.Args)
|
etcdsnapshotCommand := internalCLIAction(version.Program+"-"+cmds.EtcdSnapshotCommand, dataDir, os.Args)
|
||||||
secretsencryptCommand := internalCLIAction(version.Program+"-"+cmds.SecretsEncryptCommand, dataDir, os.Args)
|
secretsencryptCommand := internalCLIAction(version.Program+"-"+cmds.SecretsEncryptCommand, dataDir, os.Args)
|
||||||
certCommand := internalCLIAction(version.Program+"-"+cmds.CertCommand, dataDir, os.Args)
|
certCommand := internalCLIAction(version.Program+"-"+cmds.CertCommand, dataDir, os.Args)
|
||||||
|
@ -51,6 +52,12 @@ func main() {
|
||||||
cmds.NewCRICTL(externalCLIAction("crictl", dataDir)),
|
cmds.NewCRICTL(externalCLIAction("crictl", dataDir)),
|
||||||
cmds.NewCtrCommand(externalCLIAction("ctr", dataDir)),
|
cmds.NewCtrCommand(externalCLIAction("ctr", dataDir)),
|
||||||
cmds.NewCheckConfigCommand(externalCLIAction("check-config", dataDir)),
|
cmds.NewCheckConfigCommand(externalCLIAction("check-config", dataDir)),
|
||||||
|
cmds.NewTokenCommands(
|
||||||
|
tokenCommand,
|
||||||
|
tokenCommand,
|
||||||
|
tokenCommand,
|
||||||
|
tokenCommand,
|
||||||
|
),
|
||||||
cmds.NewEtcdSnapshotCommands(
|
cmds.NewEtcdSnapshotCommands(
|
||||||
etcdsnapshotCommand,
|
etcdsnapshotCommand,
|
||||||
etcdsnapshotCommand,
|
etcdsnapshotCommand,
|
||||||
|
|
|
@ -17,6 +17,7 @@ import (
|
||||||
"github.com/k3s-io/k3s/pkg/cli/kubectl"
|
"github.com/k3s-io/k3s/pkg/cli/kubectl"
|
||||||
"github.com/k3s-io/k3s/pkg/cli/secretsencrypt"
|
"github.com/k3s-io/k3s/pkg/cli/secretsencrypt"
|
||||||
"github.com/k3s-io/k3s/pkg/cli/server"
|
"github.com/k3s-io/k3s/pkg/cli/server"
|
||||||
|
"github.com/k3s-io/k3s/pkg/cli/token"
|
||||||
"github.com/k3s-io/k3s/pkg/configfilearg"
|
"github.com/k3s-io/k3s/pkg/configfilearg"
|
||||||
"github.com/k3s-io/k3s/pkg/containerd"
|
"github.com/k3s-io/k3s/pkg/containerd"
|
||||||
ctr2 "github.com/k3s-io/k3s/pkg/ctr"
|
ctr2 "github.com/k3s-io/k3s/pkg/ctr"
|
||||||
|
@ -48,6 +49,12 @@ func main() {
|
||||||
cmds.NewKubectlCommand(kubectl.Run),
|
cmds.NewKubectlCommand(kubectl.Run),
|
||||||
cmds.NewCRICTL(crictl.Run),
|
cmds.NewCRICTL(crictl.Run),
|
||||||
cmds.NewCtrCommand(ctr.Run),
|
cmds.NewCtrCommand(ctr.Run),
|
||||||
|
cmds.NewTokenCommands(
|
||||||
|
token.Create,
|
||||||
|
token.Delete,
|
||||||
|
token.Generate,
|
||||||
|
token.List,
|
||||||
|
),
|
||||||
cmds.NewEtcdSnapshotCommands(
|
cmds.NewEtcdSnapshotCommands(
|
||||||
etcdsnapshot.Run,
|
etcdsnapshot.Run,
|
||||||
etcdsnapshot.Delete,
|
etcdsnapshot.Delete,
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/k3s-io/k3s/pkg/cli/cmds"
|
||||||
|
"github.com/k3s-io/k3s/pkg/cli/token"
|
||||||
|
"github.com/k3s-io/k3s/pkg/configfilearg"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"github.com/urfave/cli"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
app := cmds.NewApp()
|
||||||
|
app.Commands = []cli.Command{
|
||||||
|
cmds.NewTokenCommands(
|
||||||
|
token.Create,
|
||||||
|
token.Delete,
|
||||||
|
token.Generate,
|
||||||
|
token.List,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := app.Run(configfilearg.MustParse(os.Args)); err != nil && !errors.Is(err, context.Canceled) {
|
||||||
|
logrus.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
2
go.mod
2
go.mod
|
@ -152,6 +152,7 @@ require (
|
||||||
k8s.io/apiserver v0.26.1
|
k8s.io/apiserver v0.26.1
|
||||||
k8s.io/client-go v11.0.1-0.20190409021438-1a26190bd76a+incompatible
|
k8s.io/client-go v11.0.1-0.20190409021438-1a26190bd76a+incompatible
|
||||||
k8s.io/cloud-provider v0.26.1
|
k8s.io/cloud-provider v0.26.1
|
||||||
|
k8s.io/cluster-bootstrap v0.0.0
|
||||||
k8s.io/component-base v0.26.1
|
k8s.io/component-base v0.26.1
|
||||||
k8s.io/component-helpers v0.26.1
|
k8s.io/component-helpers v0.26.1
|
||||||
k8s.io/cri-api v0.26.1
|
k8s.io/cri-api v0.26.1
|
||||||
|
@ -396,7 +397,6 @@ require (
|
||||||
honnef.co/go/tools v0.2.2 // indirect
|
honnef.co/go/tools v0.2.2 // indirect
|
||||||
k8s.io/apiextensions-apiserver v0.25.4 // indirect
|
k8s.io/apiextensions-apiserver v0.25.4 // indirect
|
||||||
k8s.io/cli-runtime v0.22.2 // indirect
|
k8s.io/cli-runtime v0.22.2 // indirect
|
||||||
k8s.io/cluster-bootstrap v0.0.0 // indirect
|
|
||||||
k8s.io/code-generator v0.25.4 // indirect
|
k8s.io/code-generator v0.25.4 // indirect
|
||||||
k8s.io/controller-manager v0.25.4 // indirect
|
k8s.io/controller-manager v0.25.4 // indirect
|
||||||
k8s.io/csi-translation-lib v0.0.0 // indirect
|
k8s.io/csi-translation-lib v0.0.0 // indirect
|
||||||
|
|
|
@ -0,0 +1,97 @@
|
||||||
|
package cmds
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/urfave/cli"
|
||||||
|
)
|
||||||
|
|
||||||
|
const TokenCommand = "token"
|
||||||
|
|
||||||
|
// Config holds CLI values for the token subcommands
|
||||||
|
type Token struct {
|
||||||
|
Description string
|
||||||
|
Kubeconfig string
|
||||||
|
Token string
|
||||||
|
Output string
|
||||||
|
Groups cli.StringSlice
|
||||||
|
Usages cli.StringSlice
|
||||||
|
TTL time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
TokenConfig = Token{}
|
||||||
|
TokenFlags = []cli.Flag{
|
||||||
|
DataDirFlag,
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "kubeconfig",
|
||||||
|
Usage: "(cluster) Server to connect to",
|
||||||
|
EnvVar: "KUBECONFIG",
|
||||||
|
Destination: &TokenConfig.Kubeconfig,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewTokenCommands(create, delete, generate, list func(ctx *cli.Context) error) cli.Command {
|
||||||
|
return cli.Command{
|
||||||
|
Name: TokenCommand,
|
||||||
|
Usage: "Manage bootstrap tokens",
|
||||||
|
SkipFlagParsing: false,
|
||||||
|
SkipArgReorder: true,
|
||||||
|
Subcommands: []cli.Command{
|
||||||
|
{
|
||||||
|
Name: "create",
|
||||||
|
Usage: "Create bootstrap tokens on the server",
|
||||||
|
Flags: append(TokenFlags, &cli.StringFlag{
|
||||||
|
Name: "description",
|
||||||
|
Usage: "A human friendly description of how this token is used",
|
||||||
|
Destination: &TokenConfig.Description,
|
||||||
|
}, &cli.StringSliceFlag{
|
||||||
|
Name: "groups",
|
||||||
|
Usage: "Extra groups that this token will authenticate as when used for authentication",
|
||||||
|
Value: &TokenConfig.Groups,
|
||||||
|
}, &cli.DurationFlag{
|
||||||
|
Name: "ttl",
|
||||||
|
Usage: "The duration before the token is automatically deleted (e.g. 1s, 2m, 3h). If set to '0', the token will never expire",
|
||||||
|
Value: time.Hour * 24,
|
||||||
|
Destination: &TokenConfig.TTL,
|
||||||
|
}, &cli.StringSliceFlag{
|
||||||
|
Name: "usages",
|
||||||
|
Usage: "Describes the ways in which this token can be used.",
|
||||||
|
Value: &TokenConfig.Usages,
|
||||||
|
}),
|
||||||
|
SkipFlagParsing: false,
|
||||||
|
SkipArgReorder: true,
|
||||||
|
Action: create,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "delete",
|
||||||
|
Usage: "Delete bootstrap tokens on the server",
|
||||||
|
Flags: TokenFlags,
|
||||||
|
SkipFlagParsing: false,
|
||||||
|
SkipArgReorder: true,
|
||||||
|
Action: delete,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "generate",
|
||||||
|
Usage: "Generate and print a bootstrap token, but do not create it on the server",
|
||||||
|
Flags: TokenFlags,
|
||||||
|
SkipFlagParsing: false,
|
||||||
|
SkipArgReorder: true,
|
||||||
|
Action: generate,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "list",
|
||||||
|
Usage: "List bootstrap tokens on the server",
|
||||||
|
Flags: append(TokenFlags, &cli.StringFlag{
|
||||||
|
Name: "output,o",
|
||||||
|
Value: "text",
|
||||||
|
Destination: &TokenConfig.Output,
|
||||||
|
}),
|
||||||
|
SkipFlagParsing: false,
|
||||||
|
SkipArgReorder: true,
|
||||||
|
Action: list,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,224 @@
|
||||||
|
package token
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"text/tabwriter"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/k3s-io/k3s/pkg/cli/cmds"
|
||||||
|
"github.com/k3s-io/k3s/pkg/clientaccess"
|
||||||
|
"github.com/k3s-io/k3s/pkg/kubeadm"
|
||||||
|
"github.com/k3s-io/k3s/pkg/util"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/urfave/cli"
|
||||||
|
"gopkg.in/yaml.v2"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/fields"
|
||||||
|
"k8s.io/apimachinery/pkg/util/duration"
|
||||||
|
"k8s.io/client-go/tools/clientcmd"
|
||||||
|
bootstrapapi "k8s.io/cluster-bootstrap/token/api"
|
||||||
|
bootstraputil "k8s.io/cluster-bootstrap/token/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Create(app *cli.Context) error {
|
||||||
|
if err := cmds.InitLogging(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return create(app, &cmds.TokenConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
func create(app *cli.Context, cfg *cmds.Token) error {
|
||||||
|
if err := kubeadm.SetDefaults(app, cfg); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.Kubeconfig = util.GetKubeConfigPath(cfg.Kubeconfig)
|
||||||
|
client, err := util.GetClientSet(cfg.Kubeconfig)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
restConfig, err := clientcmd.BuildConfigFromFlags("", cfg.Kubeconfig)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(restConfig.TLSClientConfig.CAData) == 0 && restConfig.TLSClientConfig.CAFile != "" {
|
||||||
|
restConfig.TLSClientConfig.CAData, err = os.ReadFile(restConfig.TLSClientConfig.CAFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bts, err := kubeadm.NewBootstrapTokenString(cfg.Token)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
bt := kubeadm.BootstrapToken{
|
||||||
|
Token: bts,
|
||||||
|
Description: cfg.Description,
|
||||||
|
TTL: &metav1.Duration{Duration: cfg.TTL},
|
||||||
|
Usages: cfg.Usages,
|
||||||
|
Groups: cfg.Groups,
|
||||||
|
}
|
||||||
|
|
||||||
|
secretName := bootstraputil.BootstrapTokenSecretName(bt.Token.ID)
|
||||||
|
if secret, err := client.CoreV1().Secrets(metav1.NamespaceSystem).Get(context.TODO(), secretName, metav1.GetOptions{}); secret != nil && err == nil {
|
||||||
|
return fmt.Errorf("a token with id %q already exists", bt.Token.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
secret := kubeadm.BootstrapTokenToSecret(&bt)
|
||||||
|
if _, err := client.CoreV1().Secrets(metav1.NamespaceSystem).Create(context.TODO(), secret, metav1.CreateOptions{}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := clientaccess.FormatTokenBytes(bt.Token.String(), restConfig.TLSClientConfig.CAData)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(token)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Delete(app *cli.Context) error {
|
||||||
|
if err := cmds.InitLogging(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return delete(app, &cmds.TokenConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
func delete(app *cli.Context, cfg *cmds.Token) error {
|
||||||
|
args := app.Args()
|
||||||
|
if len(args) < 1 {
|
||||||
|
return errors.New("missing argument; 'token delete' is missing token")
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.Kubeconfig = util.GetKubeConfigPath(cfg.Kubeconfig)
|
||||||
|
client, err := util.GetClientSet(cfg.Kubeconfig)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, token := range args {
|
||||||
|
if !bootstraputil.IsValidBootstrapTokenID(token) {
|
||||||
|
bts, err := kubeadm.NewBootstrapTokenString(cfg.Token)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("given token didn't match pattern %q or %q", bootstrapapi.BootstrapTokenIDPattern, bootstrapapi.BootstrapTokenIDPattern)
|
||||||
|
}
|
||||||
|
token = bts.ID
|
||||||
|
}
|
||||||
|
secretName := bootstraputil.BootstrapTokenSecretName(token)
|
||||||
|
if err := client.CoreV1().Secrets(metav1.NamespaceSystem).Delete(context.TODO(), secretName, metav1.DeleteOptions{}); err != nil {
|
||||||
|
return errors.Wrapf(err, "failed to delete bootstrap token %q", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("bootstrap token %q deleted\n", token)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Generate(app *cli.Context) error {
|
||||||
|
if err := cmds.InitLogging(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return generate(app, &cmds.TokenConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
func generate(app *cli.Context, cfg *cmds.Token) error {
|
||||||
|
token, err := bootstraputil.GenerateBootstrapToken()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Println(token)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func List(app *cli.Context) error {
|
||||||
|
if err := cmds.InitLogging(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return list(app, &cmds.TokenConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
func list(app *cli.Context, cfg *cmds.Token) error {
|
||||||
|
if err := kubeadm.SetDefaults(app, cfg); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.Kubeconfig = util.GetKubeConfigPath(cfg.Kubeconfig)
|
||||||
|
client, err := util.GetClientSet(cfg.Kubeconfig)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenSelector := fields.SelectorFromSet(
|
||||||
|
map[string]string{
|
||||||
|
"type": string(bootstrapapi.SecretTypeBootstrapToken),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
listOptions := metav1.ListOptions{
|
||||||
|
FieldSelector: tokenSelector.String(),
|
||||||
|
}
|
||||||
|
|
||||||
|
secrets, err := client.CoreV1().Secrets(metav1.NamespaceSystem).List(context.TODO(), listOptions)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "failed to list bootstrap tokens")
|
||||||
|
}
|
||||||
|
|
||||||
|
tokens := make([]*kubeadm.BootstrapToken, len(secrets.Items))
|
||||||
|
for i, secret := range secrets.Items {
|
||||||
|
token, err := kubeadm.BootstrapTokenFromSecret(&secret)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("%v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
tokens[i] = token
|
||||||
|
}
|
||||||
|
|
||||||
|
switch cfg.Output {
|
||||||
|
case "json":
|
||||||
|
if err := json.NewEncoder(os.Stdout).Encode(tokens); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
case "yaml":
|
||||||
|
if err := yaml.NewEncoder(os.Stdout).Encode(tokens); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
format := "%s\t%s\t%s\t%s\t%s\t%s\n"
|
||||||
|
w := tabwriter.NewWriter(os.Stdout, 10, 4, 3, ' ', 0)
|
||||||
|
defer w.Flush()
|
||||||
|
|
||||||
|
fmt.Fprintf(w, format, "TOKEN", "TTL", "EXPIRES", "USAGES", "DESCRIPTION", "EXTRA GROUPS")
|
||||||
|
for _, token := range tokens {
|
||||||
|
ttl := "<forever>"
|
||||||
|
expires := "<never>"
|
||||||
|
if token.Expires != nil {
|
||||||
|
ttl = duration.ShortHumanDuration(token.Expires.Sub(time.Now()))
|
||||||
|
expires = token.Expires.Format(time.RFC3339)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintf(w, format, token.Token.ID, ttl, expires, joinOrNone(token.Usages...), joinOrNone(token.Description), joinOrNone(token.Groups...))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// joinOrNone joins strings with a comma. If the resulting output is an empty string,
|
||||||
|
// it instead returns the replacement string "<none>"
|
||||||
|
func joinOrNone(s ...string) string {
|
||||||
|
j := strings.Join(s, ",")
|
||||||
|
if j == "" {
|
||||||
|
return "<none>"
|
||||||
|
}
|
||||||
|
return j
|
||||||
|
}
|
|
@ -369,7 +369,7 @@ func put(u string, body []byte, client *http.Client, username, password string)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// FormatToken takes a username:password string, and a path to a certificate bundle, and
|
// FormatToken takes a username:password string or join token, and a path to a certificate bundle, and
|
||||||
// returns a string containing the full K10 format token string. If the credentials are
|
// returns a string containing the full K10 format token string. If the credentials are
|
||||||
// empty, an empty token is returned. If the certificate bundle does not exist or does not
|
// empty, an empty token is returned. If the certificate bundle does not exist or does not
|
||||||
// contain a valid bundle, an error is returned.
|
// contain a valid bundle, an error is returned.
|
||||||
|
@ -382,6 +382,15 @@ func FormatToken(creds, certFile string) (string, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
return FormatTokenBytes(creds, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FormatTokenBytes has the same interface as FormatToken, but accepts a byte slice instead
|
||||||
|
// of file path.
|
||||||
|
func FormatTokenBytes(creds string, b []byte) (string, error) {
|
||||||
|
if len(creds) == 0 {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
digest, err := hashCA(b)
|
digest, err := hashCA(b)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -90,6 +90,7 @@ func Server(ctx context.Context, cfg *config.Control) error {
|
||||||
func controllerManager(ctx context.Context, cfg *config.Control) error {
|
func controllerManager(ctx context.Context, cfg *config.Control) error {
|
||||||
runtime := cfg.Runtime
|
runtime := cfg.Runtime
|
||||||
argsMap := map[string]string{
|
argsMap := map[string]string{
|
||||||
|
"controllers": "*,tokencleaner",
|
||||||
"feature-gates": "JobTrackingWithFinalizers=true",
|
"feature-gates": "JobTrackingWithFinalizers=true",
|
||||||
"kubeconfig": runtime.KubeConfigController,
|
"kubeconfig": runtime.KubeConfigController,
|
||||||
"authorization-kubeconfig": runtime.KubeConfigController,
|
"authorization-kubeconfig": runtime.KubeConfigController,
|
||||||
|
@ -117,7 +118,7 @@ func controllerManager(ctx context.Context, cfg *config.Control) error {
|
||||||
}
|
}
|
||||||
if !cfg.DisableCCM {
|
if !cfg.DisableCCM {
|
||||||
argsMap["configure-cloud-routes"] = "false"
|
argsMap["configure-cloud-routes"] = "false"
|
||||||
argsMap["controllers"] = "*,-service,-route,-cloud-node-lifecycle"
|
argsMap["controllers"] = argsMap["controllers"] + ",-service,-route,-cloud-node-lifecycle"
|
||||||
}
|
}
|
||||||
|
|
||||||
args := config.GetArgs(argsMap, cfg.ExtraControllerArgs)
|
args := config.GetArgs(argsMap, cfg.ExtraControllerArgs)
|
||||||
|
@ -158,6 +159,7 @@ func apiServer(ctx context.Context, cfg *config.Control) error {
|
||||||
|
|
||||||
argsMap["cert-dir"] = certDir
|
argsMap["cert-dir"] = certDir
|
||||||
argsMap["allow-privileged"] = "true"
|
argsMap["allow-privileged"] = "true"
|
||||||
|
argsMap["enable-bootstrap-token-auth"] = "true"
|
||||||
argsMap["authorization-mode"] = strings.Join([]string{modes.ModeNode, modes.ModeRBAC}, ",")
|
argsMap["authorization-mode"] = strings.Join([]string{modes.ModeNode, modes.ModeRBAC}, ",")
|
||||||
argsMap["service-account-signing-key-file"] = runtime.ServiceCurrentKey
|
argsMap["service-account-signing-key-file"] = runtime.ServiceCurrentKey
|
||||||
argsMap["service-cluster-ip-range"] = util.JoinIPNets(cfg.ServiceIPRanges)
|
argsMap["service-cluster-ip-range"] = util.JoinIPNets(cfg.ServiceIPRanges)
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
package kubeadm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/k3s-io/k3s/pkg/cli/cmds"
|
||||||
|
"github.com/k3s-io/k3s/pkg/version"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/urfave/cli"
|
||||||
|
bootstrapapi "k8s.io/cluster-bootstrap/token/api"
|
||||||
|
bootstraputil "k8s.io/cluster-bootstrap/token/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
NodeBootstrapTokenAuthGroup = "system:bootstrappers:" + version.Program + ":default-node-token"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SetDefaults ensures that the default values are set on the token configuration.
|
||||||
|
// These are set here, rather than in the default Token struct, to avoid
|
||||||
|
// importing the cluster-bootstrap packages into the CLI.
|
||||||
|
func SetDefaults(clx *cli.Context, cfg *cmds.Token) error {
|
||||||
|
if !clx.IsSet("groups") {
|
||||||
|
cfg.Groups = []string{NodeBootstrapTokenAuthGroup}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !clx.IsSet("usages") {
|
||||||
|
cfg.Usages = bootstrapapi.KnownTokenUsages
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.Output == "" {
|
||||||
|
cfg.Output = "text"
|
||||||
|
} else {
|
||||||
|
switch cfg.Output {
|
||||||
|
case "text", "json", "yaml":
|
||||||
|
default:
|
||||||
|
return errors.New("invalid output format: " + cfg.Output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
args := clx.Args()
|
||||||
|
if len(args) > 0 {
|
||||||
|
cfg.Token = args[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.Token == "" {
|
||||||
|
var err error
|
||||||
|
cfg.Token, err = bootstraputil.GenerateBootstrapToken()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
package kubeadm
|
||||||
|
|
||||||
|
import (
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// kubeadm bootstrap token types cribbed from:
|
||||||
|
// https://github.com/kubernetes/kubernetes/blob/v1.25.4/cmd/kubeadm/app/apis/bootstraptoken/v1/types.go
|
||||||
|
// Copying these instead of importing from kubeadm saves about 4mb of binary size.
|
||||||
|
|
||||||
|
// BootstrapToken describes one bootstrap token, stored as a Secret in the cluster
|
||||||
|
type BootstrapToken struct {
|
||||||
|
// Token is used for establishing bidirectional trust between nodes and control-planes.
|
||||||
|
// Used for joining nodes in the cluster.
|
||||||
|
Token *BootstrapTokenString `json:"token" datapolicy:"token"`
|
||||||
|
// Description sets a human-friendly message why this token exists and what it's used
|
||||||
|
// for, so other administrators can know its purpose.
|
||||||
|
// +optional
|
||||||
|
Description string `json:"description,omitempty"`
|
||||||
|
// TTL defines the time to live for this token. Defaults to 24h.
|
||||||
|
// Expires and TTL are mutually exclusive.
|
||||||
|
// +optional
|
||||||
|
TTL *metav1.Duration `json:"ttl,omitempty"`
|
||||||
|
// 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.
|
||||||
|
// +optional
|
||||||
|
Expires *metav1.Time `json:"expires,omitempty"`
|
||||||
|
// 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.
|
||||||
|
// +optional
|
||||||
|
Usages []string `json:"usages,omitempty"`
|
||||||
|
// Groups specifies the extra groups that this token will authenticate as when/if
|
||||||
|
// used for authentication
|
||||||
|
// +optional
|
||||||
|
Groups []string `json:"groups,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// BootstrapTokenString is a token of the format abcdef.abcdef0123456789 that is used
|
||||||
|
// for both validation of the identity of the API server from a joining node's point
|
||||||
|
// of view and as an authentication method for the node. This token is and should be
|
||||||
|
// short-lived.
|
||||||
|
type BootstrapTokenString struct {
|
||||||
|
ID string `json:"-"`
|
||||||
|
Secret string `json:"-" datapolicy:"token"`
|
||||||
|
}
|
|
@ -0,0 +1,173 @@
|
||||||
|
package kubeadm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
v1 "k8s.io/api/core/v1"
|
||||||
|
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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// kubeadm bootstrap token utilities cribbed from:
|
||||||
|
// https://github.com/kubernetes/kubernetes/blob/v1.25.4/cmd/kubeadm/app/apis/bootstraptoken/v1/utils.go
|
||||||
|
// Copying these instead of importing from kubeadm saves about 4mb of binary size.
|
||||||
|
|
||||||
|
// 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 ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
|
||||||
|
// BootstrapTokenToSecret converts the given BootstrapToken object to its Secret representation that
|
||||||
|
// may be submitted to the API Server in order to be stored.
|
||||||
|
func BootstrapTokenToSecret(bt *BootstrapToken) *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.UTC().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).UTC().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
|
||||||
|
}
|
|
@ -32,6 +32,7 @@ import (
|
||||||
"k8s.io/apimachinery/pkg/util/json"
|
"k8s.io/apimachinery/pkg/util/json"
|
||||||
"k8s.io/apiserver/pkg/authentication/user"
|
"k8s.io/apiserver/pkg/authentication/user"
|
||||||
"k8s.io/apiserver/pkg/endpoints/handlers/responsewriters"
|
"k8s.io/apiserver/pkg/endpoints/handlers/responsewriters"
|
||||||
|
bootstrapapi "k8s.io/cluster-bootstrap/token/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -44,7 +45,7 @@ func router(ctx context.Context, config *Config, cfg *cmds.Server) http.Handler
|
||||||
|
|
||||||
prefix := "/v1-" + version.Program
|
prefix := "/v1-" + version.Program
|
||||||
authed := mux.NewRouter().SkipClean(true)
|
authed := mux.NewRouter().SkipClean(true)
|
||||||
authed.Use(authMiddleware(serverConfig, version.Program+":agent"))
|
authed.Use(authMiddleware(serverConfig, version.Program+":agent", user.NodesGroup, bootstrapapi.BootstrapDefaultGroup))
|
||||||
authed.Path(prefix + "/serving-kubelet.crt").Handler(servingKubeletCert(serverConfig, serverConfig.Runtime.ServingKubeletKey, nodeAuth))
|
authed.Path(prefix + "/serving-kubelet.crt").Handler(servingKubeletCert(serverConfig, serverConfig.Runtime.ServingKubeletKey, nodeAuth))
|
||||||
authed.Path(prefix + "/client-kubelet.crt").Handler(clientKubeletCert(serverConfig, serverConfig.Runtime.ClientKubeletKey, nodeAuth))
|
authed.Path(prefix + "/client-kubelet.crt").Handler(clientKubeletCert(serverConfig, serverConfig.Runtime.ClientKubeletKey, nodeAuth))
|
||||||
authed.Path(prefix + "/client-kube-proxy.crt").Handler(fileHandler(serverConfig.Runtime.ClientKubeProxyCert, serverConfig.Runtime.ClientKubeProxyKey))
|
authed.Path(prefix + "/client-kube-proxy.crt").Handler(fileHandler(serverConfig.Runtime.ClientKubeProxyCert, serverConfig.Runtime.ClientKubeProxyKey))
|
||||||
|
|
|
@ -92,6 +92,7 @@ fi
|
||||||
rm -f \
|
rm -f \
|
||||||
bin/k3s-agent \
|
bin/k3s-agent \
|
||||||
bin/k3s-server \
|
bin/k3s-server \
|
||||||
|
bin/k3s-token \
|
||||||
bin/k3s-etcd-snapshot \
|
bin/k3s-etcd-snapshot \
|
||||||
bin/k3s-secrets-encrypt \
|
bin/k3s-secrets-encrypt \
|
||||||
bin/k3s-certificate \
|
bin/k3s-certificate \
|
||||||
|
@ -127,6 +128,7 @@ echo Building k3s
|
||||||
CGO_ENABLED=1 "${GO}" build -tags "$TAGS" -gcflags="all=${GCFLAGS}" -ldflags "$VERSIONFLAGS $LDFLAGS $STATIC" -o bin/k3s ./cmd/server/main.go
|
CGO_ENABLED=1 "${GO}" build -tags "$TAGS" -gcflags="all=${GCFLAGS}" -ldflags "$VERSIONFLAGS $LDFLAGS $STATIC" -o bin/k3s ./cmd/server/main.go
|
||||||
ln -s k3s ./bin/k3s-agent
|
ln -s k3s ./bin/k3s-agent
|
||||||
ln -s k3s ./bin/k3s-server
|
ln -s k3s ./bin/k3s-server
|
||||||
|
ln -s k3s ./bin/k3s-token
|
||||||
ln -s k3s ./bin/k3s-etcd-snapshot
|
ln -s k3s ./bin/k3s-etcd-snapshot
|
||||||
ln -s k3s ./bin/k3s-secrets-encrypt
|
ln -s k3s ./bin/k3s-secrets-encrypt
|
||||||
ln -s k3s ./bin/k3s-certificate
|
ln -s k3s ./bin/k3s-certificate
|
||||||
|
|
|
@ -7,7 +7,7 @@ cd $(dirname $0)/..
|
||||||
|
|
||||||
GO=${GO-go}
|
GO=${GO-go}
|
||||||
|
|
||||||
for i in crictl kubectl k3s-agent k3s-server k3s-etcd-snapshot k3s-secrets-encrypt k3s-certificate k3s-completion; do
|
for i in crictl kubectl k3s-agent k3s-server k3s-token k3s-etcd-snapshot k3s-secrets-encrypt k3s-certificate k3s-completion; do
|
||||||
rm -f bin/$i
|
rm -f bin/$i
|
||||||
ln -s k3s bin/$i
|
ln -s k3s bin/$i
|
||||||
done
|
done
|
||||||
|
|
Loading…
Reference in New Issue