Merge pull request #9667 from justinsb/kubectl_auth_helper

Support authentication helper for kubectl
This commit is contained in:
Kubernetes Prow Robot 2020-08-30 21:46:21 -07:00 committed by GitHub
commit 5d09a9a95b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 557 additions and 144 deletions

View File

@ -69,6 +69,7 @@ go_library(
"//pkg/cloudinstances:go_default_library", "//pkg/cloudinstances:go_default_library",
"//pkg/clusteraddons:go_default_library", "//pkg/clusteraddons:go_default_library",
"//pkg/commands:go_default_library", "//pkg/commands:go_default_library",
"//pkg/commands/commandutils:go_default_library",
"//pkg/dump:go_default_library", "//pkg/dump:go_default_library",
"//pkg/edit:go_default_library", "//pkg/edit:go_default_library",
"//pkg/featureflag:go_default_library", "//pkg/featureflag:go_default_library",

View File

@ -204,9 +204,9 @@ func RunDeleteCluster(ctx context.Context, f *util.Factory, out io.Writer, optio
} }
} }
b := kubeconfig.NewKubeconfigBuilder(clientcmd.NewDefaultPathOptions()) b := kubeconfig.NewKubeconfigBuilder()
b.Context = clusterName b.Context = clusterName
err = b.DeleteKubeConfig() err = b.DeleteKubeConfig(clientcmd.NewDefaultPathOptions())
if err != nil { if err != nil {
klog.Warningf("error removing kube config: %v", err) klog.Warningf("error removing kube config: %v", err)
} }

View File

@ -60,6 +60,9 @@ type ExportKubecfgOptions struct {
admin time.Duration admin time.Duration
user string user string
internal bool internal bool
// UseKopsAuthenticationPlugin controls whether we should use the kops auth helper instead of a static credential
UseKopsAuthenticationPlugin bool
} }
func NewCmdExportKubecfg(f *util.Factory, out io.Writer) *cobra.Command { func NewCmdExportKubecfg(f *util.Factory, out io.Writer) *cobra.Command {
@ -85,6 +88,7 @@ func NewCmdExportKubecfg(f *util.Factory, out io.Writer) *cobra.Command {
cmd.Flags().Lookup("admin").NoOptDefVal = kubeconfig.DefaultKubecfgAdminLifetime.String() cmd.Flags().Lookup("admin").NoOptDefVal = kubeconfig.DefaultKubecfgAdminLifetime.String()
cmd.Flags().StringVar(&options.user, "user", options.user, "add an existing user to the cluster context") cmd.Flags().StringVar(&options.user, "user", options.user, "add an existing user to the cluster context")
cmd.Flags().BoolVar(&options.internal, "internal", options.internal, "use the cluster's internal DNS name") cmd.Flags().BoolVar(&options.internal, "internal", options.internal, "use the cluster's internal DNS name")
cmd.Flags().BoolVar(&options.UseKopsAuthenticationPlugin, "auth-plugin", options.UseKopsAuthenticationPlugin, "use the kops authentication plugin")
return cmd return cmd
} }
@ -135,12 +139,21 @@ func RunExportKubecfg(ctx context.Context, f *util.Factory, out io.Writer, optio
return err return err
} }
conf, err := kubeconfig.BuildKubecfg(cluster, keyStore, secretStore, &commands.CloudDiscoveryStatusStore{}, buildPathOptions(options), options.admin, options.user, options.internal) conf, err := kubeconfig.BuildKubecfg(
cluster,
keyStore,
secretStore,
&commands.CloudDiscoveryStatusStore{},
options.admin,
options.user,
options.internal,
f.KopsStateStore(),
options.UseKopsAuthenticationPlugin)
if err != nil { if err != nil {
return err return err
} }
if err := conf.WriteKubecfg(); err != nil { if err := conf.WriteKubecfg(buildPathOptions(options)); err != nil {
return err return err
} }
} }

View File

@ -17,8 +17,7 @@ limitations under the License.
package main // import "k8s.io/kops/cmd/kops" package main // import "k8s.io/kops/cmd/kops"
import ( import (
"fmt" "k8s.io/kops/pkg/commands/commandutils"
"os"
) )
func main() { func main() {
@ -28,6 +27,5 @@ func main() {
// exitWithError will terminate execution with an error result // exitWithError will terminate execution with an error result
// It prints the error to stderr and exits with a non-zero exit code // It prints the error to stderr and exits with a non-zero exit code
func exitWithError(err error) { func exitWithError(err error) {
fmt.Fprintf(os.Stderr, "\n%v\n", err) commandutils.ExitWithError(err)
os.Exit(1)
} }

View File

@ -35,6 +35,7 @@ import (
"k8s.io/kops/cmd/kops/util" "k8s.io/kops/cmd/kops/util"
kopsapi "k8s.io/kops/pkg/apis/kops" kopsapi "k8s.io/kops/pkg/apis/kops"
"k8s.io/kops/pkg/client/simple" "k8s.io/kops/pkg/client/simple"
"k8s.io/kops/pkg/commands"
"k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/i18n"
"k8s.io/kubectl/pkg/util/templates" "k8s.io/kubectl/pkg/util/templates"
) )
@ -143,6 +144,7 @@ func NewCmdRoot(f *util.Factory, out io.Writer) *cobra.Command {
cmd.AddCommand(NewCmdEdit(f, out)) cmd.AddCommand(NewCmdEdit(f, out))
cmd.AddCommand(NewCmdExport(f, out)) cmd.AddCommand(NewCmdExport(f, out))
cmd.AddCommand(NewCmdGet(f, out)) cmd.AddCommand(NewCmdGet(f, out))
cmd.AddCommand(commands.NewCmdHelpers(f, out))
cmd.AddCommand(NewCmdUpdate(f, out)) cmd.AddCommand(NewCmdUpdate(f, out))
cmd.AddCommand(NewCmdReplace(f, out)) cmd.AddCommand(NewCmdReplace(f, out))
cmd.AddCommand(NewCmdRollingUpdate(f, out)) cmd.AddCommand(NewCmdRollingUpdate(f, out))

View File

@ -306,12 +306,23 @@ func RunUpdateCluster(ctx context.Context, f *util.Factory, clusterName string,
firstRun = !hasKubecfg firstRun = !hasKubecfg
klog.Infof("Exporting kubecfg for cluster") klog.Infof("Exporting kubecfg for cluster")
conf, err := kubeconfig.BuildKubecfg(cluster, keyStore, secretStore, &commands.CloudDiscoveryStatusStore{}, clientcmd.NewDefaultPathOptions(), c.admin, c.user, c.internal) // TODO: Another flag?
useKopsAuthenticationPlugin := false
conf, err := kubeconfig.BuildKubecfg(
cluster,
keyStore,
secretStore,
&commands.CloudDiscoveryStatusStore{},
c.admin,
c.user,
c.internal,
f.KopsStateStore(),
useKopsAuthenticationPlugin)
if err != nil { if err != nil {
return nil, err return nil, err
} }
err = conf.WriteKubecfg() err = conf.WriteKubecfg(clientcmd.NewDefaultPathOptions())
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -124,3 +124,8 @@ func (f *Factory) Clientset() (simple.Clientset, error) {
return f.clientset, nil return f.clientset, nil
} }
// KopsStateStore returns the configured KOPS_STATE_STORE in use
func (f *Factory) KopsStateStore() string {
return f.options.RegistryPath
}

View File

@ -31,6 +31,7 @@ kops export kubecfg CLUSTERNAME [flags]
``` ```
--admin duration[=18h0m0s] export a cluster admin user credential with the given lifetime and add it to the cluster context --admin duration[=18h0m0s] export a cluster admin user credential with the given lifetime and add it to the cluster context
--all export all clusters from the kops state store --all export all clusters from the kops state store
--auth-plugin use the kops authentication plugin
-h, --help help for kubecfg -h, --help help for kubecfg
--internal use the cluster's internal DNS name --internal use the cluster's internal DNS name
--kubeconfig string the location of the kubeconfig file to create. --kubeconfig string the location of the kubeconfig file to create.

1
go.mod
View File

@ -73,6 +73,7 @@ require (
github.com/go-ini/ini v1.51.0 github.com/go-ini/ini v1.51.0
github.com/go-logr/logr v0.2.1-0.20200730175230-ee2de8da5be6 github.com/go-logr/logr v0.2.1-0.20200730175230-ee2de8da5be6
github.com/gogo/protobuf v1.3.1 github.com/gogo/protobuf v1.3.1
github.com/google/go-cmp v0.4.0
github.com/google/uuid v1.1.1 github.com/google/uuid v1.1.1
github.com/gophercloud/gophercloud v0.11.1-0.20200518183226-7aec46f32c19 github.com/gophercloud/gophercloud v0.11.1-0.20200518183226-7aec46f32c19
github.com/gorilla/mux v1.7.3 github.com/gorilla/mux v1.7.3

View File

@ -84,6 +84,8 @@ k8s.io/kops/pkg/client/simple/vfsclientset
k8s.io/kops/pkg/cloudinstances k8s.io/kops/pkg/cloudinstances
k8s.io/kops/pkg/clusteraddons k8s.io/kops/pkg/clusteraddons
k8s.io/kops/pkg/commands k8s.io/kops/pkg/commands
k8s.io/kops/pkg/commands/commandutils
k8s.io/kops/pkg/commands/helpers
k8s.io/kops/pkg/configbuilder k8s.io/kops/pkg/configbuilder
k8s.io/kops/pkg/diff k8s.io/kops/pkg/diff
k8s.io/kops/pkg/dns k8s.io/kops/pkg/dns

View File

@ -3,6 +3,7 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
go_library( go_library(
name = "go_default_library", name = "go_default_library",
srcs = [ srcs = [
"helpers.go",
"helpers_readwrite.go", "helpers_readwrite.go",
"set_cluster.go", "set_cluster.go",
"status_discovery.go", "status_discovery.go",
@ -17,6 +18,7 @@ go_library(
"//pkg/apis/kops/validation:go_default_library", "//pkg/apis/kops/validation:go_default_library",
"//pkg/assets:go_default_library", "//pkg/assets:go_default_library",
"//pkg/client/simple:go_default_library", "//pkg/client/simple:go_default_library",
"//pkg/commands/helpers:go_default_library",
"//pkg/featureflag:go_default_library", "//pkg/featureflag:go_default_library",
"//pkg/resources/digitalocean:go_default_library", "//pkg/resources/digitalocean:go_default_library",
"//upup/pkg/fi/cloudup:go_default_library", "//upup/pkg/fi/cloudup:go_default_library",
@ -30,6 +32,8 @@ go_library(
"//vendor/github.com/spf13/cobra:go_default_library", "//vendor/github.com/spf13/cobra:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/validation/field:go_default_library", "//vendor/k8s.io/apimachinery/pkg/util/validation/field:go_default_library",
"//vendor/k8s.io/kubectl/pkg/util/i18n:go_default_library",
"//vendor/k8s.io/kubectl/pkg/util/templates:go_default_library",
], ],
) )

View File

@ -0,0 +1,8 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library")
go_library(
name = "go_default_library",
srcs = ["exit.go"],
importpath = "k8s.io/kops/pkg/commands/commandutils",
visibility = ["//visibility:public"],
)

View File

@ -0,0 +1,29 @@
/*
Copyright 2020 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 commandutils
import (
"fmt"
"os"
)
// ExitWithError will terminate execution with an error result
// It prints the error to stderr and exits with a non-zero exit code
func ExitWithError(err error) {
fmt.Fprintf(os.Stderr, "\n%v\n", err)
os.Exit(1)
}

50
pkg/commands/helpers.go Normal file
View File

@ -0,0 +1,50 @@
/*
Copyright 2020 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 commands
import (
"io"
"github.com/spf13/cobra"
"k8s.io/kops/cmd/kops/util"
"k8s.io/kops/pkg/commands/helpers"
"k8s.io/kubectl/pkg/util/i18n"
"k8s.io/kubectl/pkg/util/templates"
)
var (
helpersLong = templates.LongDesc(i18n.T(`
Commands intended for integration with other systems.`))
helpersShort = i18n.T(`Commands for use with other systems.`)
)
// NewCmdHelpers builds the cobra command tree for the `helpers` subcommand
func NewCmdHelpers(f *util.Factory, out io.Writer) *cobra.Command {
cmd := &cobra.Command{
Use: "helpers",
Short: helpersShort,
Long: helpersLong,
// We hide the command, as it is intended for internal usage
Hidden: true,
}
cmd.AddCommand(helpers.NewCmdHelperKubectlAuth(f, out))
return cmd
}

View File

@ -0,0 +1,19 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library")
go_library(
name = "go_default_library",
srcs = ["kubectl_auth.go"],
importpath = "k8s.io/kops/pkg/commands/helpers",
visibility = ["//visibility:public"],
deps = [
"//cmd/kops/util:go_default_library",
"//pkg/commands/commandutils:go_default_library",
"//pkg/pki:go_default_library",
"//pkg/rbac:go_default_library",
"//upup/pkg/fi:go_default_library",
"//vendor/github.com/spf13/cobra:go_default_library",
"//vendor/k8s.io/client-go/util/homedir:go_default_library",
"//vendor/k8s.io/klog/v2:go_default_library",
"//vendor/k8s.io/kubectl/pkg/util/i18n:go_default_library",
],
)

View File

@ -0,0 +1,280 @@
/*
Copyright 2020 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 helpers
import (
"bytes"
"context"
"crypto/sha256"
"crypto/x509/pkix"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"os"
"os/user"
"path/filepath"
"strings"
"time"
"github.com/spf13/cobra"
"k8s.io/client-go/util/homedir"
"k8s.io/klog/v2"
"k8s.io/kops/cmd/kops/util"
"k8s.io/kops/pkg/commands/commandutils"
"k8s.io/kops/pkg/pki"
"k8s.io/kops/pkg/rbac"
"k8s.io/kops/upup/pkg/fi"
"k8s.io/kubectl/pkg/util/i18n"
)
var (
kubectlAuthShort = i18n.T(`kubectl authentication plugin`)
)
// HelperKubectlAuthOptions holds the options for generating an authentication token
type HelperKubectlAuthOptions struct {
// ClusterName is the name of the cluster we are targeting
ClusterName string
// Lifetime specifies the desired duration of the credential
Lifetime time.Duration
// APIVersion specifies the version of the client.authentication.k8s.io schema in use
APIVersion string
}
// InitDefaults populates the default values of options
func (o *HelperKubectlAuthOptions) InitDefaults() {
o.Lifetime = 1 * time.Hour
o.APIVersion = "v1beta1"
}
// NewCmdHelperKubectlAuth builds a cobra command for the kubectl-auth command
func NewCmdHelperKubectlAuth(f *util.Factory, out io.Writer) *cobra.Command {
options := &HelperKubectlAuthOptions{}
options.InitDefaults()
cmd := &cobra.Command{
Use: "kubectl-auth",
Short: kubectlAuthShort,
Run: func(cmd *cobra.Command, args []string) {
ctx := context.TODO()
err := RunKubectlAuthHelper(ctx, f, out, options)
if err != nil {
commandutils.ExitWithError(err)
}
},
}
cmd.Flags().StringVar(&options.APIVersion, "api-version", options.APIVersion, "version of client.authentication.k8s.io schema in use")
cmd.Flags().StringVar(&options.ClusterName, "cluster", options.ClusterName, "cluster to target")
cmd.Flags().DurationVar(&options.Lifetime, "lifetime", options.Lifetime, "lifetime of the credential to issue")
return cmd
}
// RunKubectlAuthHelper implements the kubectl auth helper, which creates an authentication token
func RunKubectlAuthHelper(ctx context.Context, f *util.Factory, out io.Writer, options *HelperKubectlAuthOptions) error {
if options.ClusterName == "" {
return fmt.Errorf("ClusterName is required")
}
execCredential := &ExecCredential{
Kind: "ExecCredential",
}
switch options.APIVersion {
case "":
return fmt.Errorf("api-version must be specified")
case "v1alpha1":
execCredential.APIVersion = "client.authentication.k8s.io/v1alpha1"
case "v1beta1":
execCredential.APIVersion = "client.authentication.k8s.io/v1beta1"
default:
return fmt.Errorf("api-version %q is not supported", options.APIVersion)
}
cacheFilePath := cacheFilePath(f.KopsStateStore(), options.ClusterName)
cached, err := loadCachedExecCredential(cacheFilePath)
if err != nil {
klog.Infof("cached credential %q was not valid: %v", cacheFilePath, err)
cached = nil
}
if cached != nil && cached.APIVersion != execCredential.APIVersion {
klog.Infof("cached credential had wrong api version")
cached = nil
}
isCached := false
if cached != nil {
execCredential = cached
isCached = true
} else {
status, err := buildCredentials(ctx, f, options)
if err != nil {
return err
}
execCredential.Status = *status
}
b, err := json.MarshalIndent(execCredential, "", " ")
if err != nil {
return fmt.Errorf("error marshaling json: %v", err)
}
_, err = out.Write(b)
if err != nil {
return fmt.Errorf("error writing to stdout: %v", err)
}
if !isCached {
if err := os.MkdirAll(filepath.Dir(cacheFilePath), 0755); err != nil {
klog.Warningf("failed to make cache directory for %q: %v", cacheFilePath, err)
}
if err := ioutil.WriteFile(cacheFilePath, b, 0600); err != nil {
klog.Warningf("failed to write cache file %q: %v", cacheFilePath, err)
}
}
return nil
}
// ExecCredential specifies the client.authentication.k8s.io ExecCredential object
type ExecCredential struct {
APIVersion string `json:"apiVersion,omitempty"`
Kind string `json:"kind,omitempty"`
Status ExecCredentialStatus `json:"status"`
}
// ExecCredentialStatus specifies the status of the client.authentication.k8s.io ExecCredential object
type ExecCredentialStatus struct {
ClientCertificateData string `json:"clientCertificateData,omitempty"`
ClientKeyData string `json:"clientKeyData,omitempty"`
ExpirationTimestamp time.Time `json:"expirationTimestamp,omitempty"`
}
func cacheFilePath(kopsStateStore string, clusterName string) string {
var b bytes.Buffer
b.WriteString(kopsStateStore)
b.WriteByte(0)
b.WriteString(clusterName)
b.WriteByte(0)
hash := fmt.Sprintf("%x", sha256.New().Sum(b.Bytes()))
sanitizedName := strings.Map(func(r rune) rune {
switch {
case r >= 'a' && r <= 'z':
return r
case r >= 'A' && r <= 'Z':
return r
case r >= '0' && r <= '9':
return r
default:
return '_'
}
}, clusterName)
return filepath.Join(homedir.HomeDir(), ".kube", "cache", "kops-authentication", sanitizedName+"_"+hash)
}
func loadCachedExecCredential(cacheFilePath string) (*ExecCredential, error) {
b, err := ioutil.ReadFile(cacheFilePath)
if err != nil {
if os.IsNotExist(err) {
// expected - a cache miss
return nil, nil
} else {
return nil, err
}
}
execCredential := &ExecCredential{}
if err := json.Unmarshal(b, execCredential); err != nil {
return nil, fmt.Errorf("error parsing: %v", err)
}
if execCredential.Status.ExpirationTimestamp.Before(time.Now()) {
return nil, nil
}
if execCredential.Status.ClientCertificateData == "" || execCredential.Status.ClientKeyData == "" {
return nil, fmt.Errorf("no credentials in cached file")
}
return execCredential, nil
}
func buildCredentials(ctx context.Context, f *util.Factory, options *HelperKubectlAuthOptions) (*ExecCredentialStatus, error) {
clientset, err := f.Clientset()
if err != nil {
return nil, err
}
cluster, err := clientset.GetCluster(ctx, options.ClusterName)
if err != nil {
return nil, err
}
if cluster == nil {
return nil, fmt.Errorf("cluster not found %q", options.ClusterName)
}
keyStore, err := clientset.KeyStore(cluster)
if err != nil {
return nil, fmt.Errorf("unable to get cluster keystore: %v", err)
}
cn := "kubecfg"
user, err := user.Current()
if err != nil || user == nil {
klog.Infof("unable to get user: %v", err)
} else {
cn += "-" + user.Name
}
req := pki.IssueCertRequest{
Signer: fi.CertificateIDCA,
Type: "client",
Subject: pkix.Name{
CommonName: cn,
Organization: []string{rbac.SystemPrivilegedGroup},
},
Validity: options.Lifetime,
}
cert, privateKey, _, err := pki.IssueCert(&req, keyStore)
if err != nil {
return nil, fmt.Errorf("unable to issue certificate: %v", err)
}
status := &ExecCredentialStatus{}
status.ClientCertificateData, err = cert.AsString()
if err != nil {
return nil, err
}
status.ClientKeyData, err = privateKey.AsString()
if err != nil {
return nil, err
}
// Subtract a few minutes from the validity for clock skew
status.ExpirationTimestamp = cert.Certificate.NotAfter.Add(-5 * time.Minute)
return status, nil
}

View File

@ -32,6 +32,6 @@ go_test(
"//pkg/pki:go_default_library", "//pkg/pki:go_default_library",
"//upup/pkg/fi:go_default_library", "//upup/pkg/fi:go_default_library",
"//util/pkg/vfs:go_default_library", "//util/pkg/vfs:go_default_library",
"//vendor/k8s.io/client-go/tools/clientcmd:go_default_library", "//vendor/github.com/google/go-cmp/cmp:go_default_library",
], ],
) )

View File

@ -23,7 +23,6 @@ import (
"sort" "sort"
"time" "time"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/klog/v2" "k8s.io/klog/v2"
"k8s.io/kops/pkg/apis/kops" "k8s.io/kops/pkg/apis/kops"
"k8s.io/kops/pkg/apis/kops/util" "k8s.io/kops/pkg/apis/kops/util"
@ -35,7 +34,7 @@ import (
const DefaultKubecfgAdminLifetime = 18 * time.Hour const DefaultKubecfgAdminLifetime = 18 * time.Hour
func BuildKubecfg(cluster *kops.Cluster, keyStore fi.Keystore, secretStore fi.SecretStore, status kops.StatusStore, configAccess clientcmd.ConfigAccess, admin time.Duration, configUser string, internal bool) (*KubeconfigBuilder, error) { func BuildKubecfg(cluster *kops.Cluster, keyStore fi.Keystore, secretStore fi.SecretStore, status kops.StatusStore, admin time.Duration, configUser string, internal bool, kopsStateStore string, useKopsAuthenticationPlugin bool) (*KubeconfigBuilder, error) {
clusterName := cluster.ObjectMeta.Name clusterName := cluster.ObjectMeta.Name
var master string var master string
@ -98,7 +97,7 @@ func BuildKubecfg(cluster *kops.Cluster, keyStore fi.Keystore, secretStore fi.Se
} }
} }
b := NewKubeconfigBuilder(configAccess) b := NewKubeconfigBuilder()
b.Context = clusterName b.Context = clusterName
b.Server = server b.Server = server
@ -141,7 +140,6 @@ func BuildKubecfg(cluster *kops.Cluster, keyStore fi.Keystore, secretStore fi.Se
if err != nil { if err != nil {
return nil, err return nil, err
} }
b.ClientCert, err = cert.AsBytes() b.ClientCert, err = cert.AsBytes()
if err != nil { if err != nil {
return nil, err return nil, err
@ -152,6 +150,16 @@ func BuildKubecfg(cluster *kops.Cluster, keyStore fi.Keystore, secretStore fi.Se
} }
} }
if useKopsAuthenticationPlugin {
b.AuthenticationExec = []string{
"kops",
"helpers",
"kubectl-auth",
"--cluster=" + clusterName,
"--state=" + kopsStateStore,
}
}
b.Server = server b.Server = server
k8sVersion, err := util.ParseKubernetesVersion(cluster.Spec.KubernetesVersion) k8sVersion, err := util.ParseKubernetesVersion(cluster.Spec.KubernetesVersion)

View File

@ -18,11 +18,10 @@ package kubeconfig
import ( import (
"fmt" "fmt"
"reflect"
"testing" "testing"
"time" "time"
"k8s.io/client-go/tools/clientcmd" "github.com/google/go-cmp/cmp"
"k8s.io/kops/pkg/apis/kops" "k8s.io/kops/pkg/apis/kops"
"k8s.io/kops/pkg/pki" "k8s.io/kops/pkg/pki"
"k8s.io/kops/upup/pkg/fi" "k8s.io/kops/upup/pkg/fi"
@ -123,14 +122,13 @@ func TestBuildKubecfg(t *testing.T) {
}() }()
type args struct { type args struct {
cluster *kops.Cluster cluster *kops.Cluster
keyStore fakeKeyStore secretStore fi.SecretStore
secretStore fi.SecretStore status fakeStatusStore
status fakeStatusStore admin time.Duration
configAccess clientcmd.ConfigAccess user string
admin time.Duration internal bool
user string useKopsAuthenticationPlugin bool
internal bool
} }
publiccluster := buildMinimalCluster("testcluster", "testcluster.test.com") publiccluster := buildMinimalCluster("testcluster", "testcluster.test.com")
@ -138,106 +136,65 @@ func TestBuildKubecfg(t *testing.T) {
gossipCluster := buildMinimalCluster("testgossipcluster.k8s.local", "") gossipCluster := buildMinimalCluster("testgossipcluster.k8s.local", "")
tests := []struct { tests := []struct {
name string name string
args args args args
want *KubeconfigBuilder want *KubeconfigBuilder
wantErr bool wantErr bool
wantClientCert bool
}{ }{
{ {
"Test Kube Config Data For Public DNS with admin", name: "Test Kube Config Data For Public DNS with admin",
args{ args: args{
publiccluster, cluster: publiccluster,
fakeKeyStore{ status: fakeStatusStore{},
FindKeypairFn: func(name string) (*pki.Certificate, *pki.PrivateKey, bool, error) { admin: DefaultKubecfgAdminLifetime,
return fakeCertificate(), user: "",
fakePrivateKey(),
true,
nil
},
},
nil,
fakeStatusStore{},
nil,
DefaultKubecfgAdminLifetime,
"",
false,
}, },
&KubeconfigBuilder{ want: &KubeconfigBuilder{
Context: "testcluster", Context: "testcluster",
Server: "https://testcluster.test.com", Server: "https://testcluster.test.com",
CACert: []byte(certData), CACert: []byte(certData),
User: "testcluster", User: "testcluster",
}, },
false, wantClientCert: true,
}, },
{ {
"Test Kube Config Data For Public DNS without admin", name: "Test Kube Config Data For Public DNS without admin",
args{ args: args{
publiccluster, cluster: publiccluster,
fakeKeyStore{ status: fakeStatusStore{},
FindKeypairFn: func(name string) (*pki.Certificate, *pki.PrivateKey, bool, error) { admin: 0,
return fakeCertificate(), user: "myuser",
fakePrivateKey(),
true,
nil
},
},
nil,
fakeStatusStore{},
nil,
0,
"myuser",
false,
}, },
&KubeconfigBuilder{ want: &KubeconfigBuilder{
Context: "testcluster", Context: "testcluster",
Server: "https://testcluster.test.com", Server: "https://testcluster.test.com",
CACert: []byte(certData), CACert: []byte(certData),
User: "myuser", User: "myuser",
}, },
false, wantClientCert: false,
}, },
{ {
"Test Kube Config Data For Public DNS with Empty Master Name", name: "Test Kube Config Data For Public DNS with Empty Master Name",
args{ args: args{
emptyMasterPublicNameCluster, cluster: emptyMasterPublicNameCluster,
fakeKeyStore{ status: fakeStatusStore{},
FindKeypairFn: func(name string) (*pki.Certificate, *pki.PrivateKey, bool, error) { admin: 0,
return fakeCertificate(), user: "",
fakePrivateKey(),
true,
nil
},
},
nil,
fakeStatusStore{},
nil,
0,
"",
false,
}, },
&KubeconfigBuilder{ want: &KubeconfigBuilder{
Context: "emptyMasterPublicNameCluster", Context: "emptyMasterPublicNameCluster",
Server: "https://api.emptyMasterPublicNameCluster", Server: "https://api.emptyMasterPublicNameCluster",
CACert: []byte(certData), CACert: []byte(certData),
User: "emptyMasterPublicNameCluster", User: "emptyMasterPublicNameCluster",
}, },
false, wantClientCert: false,
}, },
{ {
"Test Kube Config Data For Gossip cluster", name: "Test Kube Config Data For Gossip cluster",
args{ args: args{
gossipCluster, cluster: gossipCluster,
fakeKeyStore{ status: fakeStatusStore{
FindKeypairFn: func(name string) (*pki.Certificate, *pki.PrivateKey, bool, error) {
return fakeCertificate(),
fakePrivateKey(),
true,
nil
},
},
nil,
fakeStatusStore{
GetApiIngressStatusFn: func(cluster *kops.Cluster) ([]kops.ApiIngressStatus, error) { GetApiIngressStatusFn: func(cluster *kops.Cluster) ([]kops.ApiIngressStatus, error) {
return []kops.ApiIngressStatus{ return []kops.ApiIngressStatus{
{ {
@ -246,57 +203,74 @@ func TestBuildKubecfg(t *testing.T) {
}, nil }, nil
}, },
}, },
nil,
0,
"",
false,
}, },
&KubeconfigBuilder{ want: &KubeconfigBuilder{
Context: "testgossipcluster.k8s.local", Context: "testgossipcluster.k8s.local",
Server: "https://elbHostName", Server: "https://elbHostName",
CACert: []byte(certData), CACert: []byte(certData),
User: "testgossipcluster.k8s.local", User: "testgossipcluster.k8s.local",
}, },
false, wantClientCert: false,
}, },
{ {
"Test Kube Config Data For internal DNS name with admin", name: "Public DNS with kops auth plugin",
args{ args: args{
publiccluster, cluster: publiccluster,
fakeKeyStore{ status: fakeStatusStore{},
FindKeypairFn: func(name string) (*pki.Certificate, *pki.PrivateKey, bool, error) { admin: 0,
return fakeCertificate(), useKopsAuthenticationPlugin: true,
fakePrivateKey(), },
true, want: &KubeconfigBuilder{
nil Context: "testcluster",
}, Server: "https://testcluster.test.com",
CACert: []byte(certData),
User: "testcluster",
AuthenticationExec: []string{
"kops",
"helpers",
"kubectl-auth",
"--cluster=testcluster",
"--state=memfs://example-state-store",
}, },
nil,
fakeStatusStore{},
nil,
DefaultKubecfgAdminLifetime,
"",
true,
}, },
&KubeconfigBuilder{ wantClientCert: false,
Context: "testcluster", },
Server: "https://internal.testcluster.test.com", {
CACert: []byte(certData), name: "Test Kube Config Data For internal DNS name with admin",
ClientCert: []byte(certData), args: args{
ClientKey: []byte(privatekeyData), cluster: publiccluster,
User: "testcluster", status: fakeStatusStore{},
admin: DefaultKubecfgAdminLifetime,
internal: true,
}, },
false, want: &KubeconfigBuilder{
Context: "testcluster",
Server: "https://internal.testcluster.test.com",
CACert: []byte(certData),
User: "testcluster",
},
wantClientCert: true,
}, },
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
got, err := BuildKubecfg(tt.args.cluster, tt.args.keyStore, tt.args.secretStore, tt.args.status, tt.args.configAccess, tt.args.admin, tt.args.user, tt.args.internal) kopsStateStore := "memfs://example-state-store"
keyStore := fakeKeyStore{
FindKeypairFn: func(name string) (*pki.Certificate, *pki.PrivateKey, bool, error) {
return fakeCertificate(),
fakePrivateKey(),
true,
nil
},
}
got, err := BuildKubecfg(tt.args.cluster, keyStore, tt.args.secretStore, tt.args.status, tt.args.admin, tt.args.user, tt.args.internal, kopsStateStore, tt.args.useKopsAuthenticationPlugin)
if (err != nil) != tt.wantErr { if (err != nil) != tt.wantErr {
t.Errorf("BuildKubecfg() error = %v, wantErr %v", err, tt.wantErr) t.Errorf("BuildKubecfg() error = %v, wantErr %v", err, tt.wantErr)
return return
} }
if tt.args.admin != 0 { if tt.wantClientCert {
if got.ClientCert == nil { if got.ClientCert == nil {
t.Errorf("Expected ClientCert, got nil") t.Errorf("Expected ClientCert, got nil")
} }
@ -306,8 +280,8 @@ func TestBuildKubecfg(t *testing.T) {
tt.want.ClientCert = got.ClientCert tt.want.ClientCert = got.ClientCert
tt.want.ClientKey = got.ClientKey tt.want.ClientKey = got.ClientKey
} }
if !reflect.DeepEqual(got, tt.want) { if diff := cmp.Diff(got, tt.want); diff != "" {
t.Errorf("BuildKubecfg() = %+v, want %+v", got, tt.want) t.Errorf("BuildKubecfg() diff (+got, -want): %s", diff)
} }
}) })
} }

View File

@ -42,18 +42,16 @@ type KubeconfigBuilder struct {
ClientCert []byte ClientCert []byte
ClientKey []byte ClientKey []byte
configAccess clientcmd.ConfigAccess AuthenticationExec []string
} }
// Create new KubeconfigBuilder // Create new KubeconfigBuilder
func NewKubeconfigBuilder(configAccess clientcmd.ConfigAccess) *KubeconfigBuilder { func NewKubeconfigBuilder() *KubeconfigBuilder {
c := &KubeconfigBuilder{} return &KubeconfigBuilder{}
c.configAccess = configAccess
return c
} }
func (b *KubeconfigBuilder) DeleteKubeConfig() error { func (b *KubeconfigBuilder) DeleteKubeConfig(configAccess clientcmd.ConfigAccess) error {
config, err := b.configAccess.GetStartingConfig() config, err := configAccess.GetStartingConfig()
if err != nil { if err != nil {
return fmt.Errorf("error loading kubeconfig: %v", err) return fmt.Errorf("error loading kubeconfig: %v", err)
} }
@ -72,7 +70,7 @@ func (b *KubeconfigBuilder) DeleteKubeConfig() error {
config.CurrentContext = "" config.CurrentContext = ""
} }
if err := clientcmd.ModifyConfig(b.configAccess, *config, false); err != nil { if err := clientcmd.ModifyConfig(configAccess, *config, false); err != nil {
return fmt.Errorf("error writing kubeconfig: %v", err) return fmt.Errorf("error writing kubeconfig: %v", err)
} }
@ -101,8 +99,8 @@ func (c *KubeconfigBuilder) BuildRestConfig() (*rest.Config, error) {
} }
// Write out a new kubeconfig // Write out a new kubeconfig
func (b *KubeconfigBuilder) WriteKubecfg() error { func (b *KubeconfigBuilder) WriteKubecfg(configAccess clientcmd.ConfigAccess) error {
config, err := b.configAccess.GetStartingConfig() config, err := configAccess.GetStartingConfig()
if err != nil { if err != nil {
return fmt.Errorf("error reading kubeconfig: %v", err) return fmt.Errorf("error reading kubeconfig: %v", err)
} }
@ -146,6 +144,14 @@ func (b *KubeconfigBuilder) WriteKubecfg() error {
authInfo.ClientKeyData = b.ClientKey authInfo.ClientKeyData = b.ClientKey
} }
if len(b.AuthenticationExec) != 0 {
authInfo.Exec = &clientcmdapi.ExecConfig{
APIVersion: "client.authentication.k8s.io/v1beta1",
Command: b.AuthenticationExec[0],
Args: b.AuthenticationExec[1:],
}
}
if config.AuthInfos == nil { if config.AuthInfos == nil {
config.AuthInfos = make(map[string]*clientcmdapi.AuthInfo) config.AuthInfos = make(map[string]*clientcmdapi.AuthInfo)
} }
@ -198,7 +204,7 @@ func (b *KubeconfigBuilder) WriteKubecfg() error {
config.CurrentContext = b.Context config.CurrentContext = b.Context
if err := clientcmd.ModifyConfig(b.configAccess, *config, true); err != nil { if err := clientcmd.ModifyConfig(configAccess, *config, true); err != nil {
return err return err
} }

1
vendor/modules.txt vendored
View File

@ -279,6 +279,7 @@ github.com/golang/snappy
# github.com/google/btree v1.0.0 # github.com/google/btree v1.0.0
github.com/google/btree github.com/google/btree
# github.com/google/go-cmp v0.4.0 # github.com/google/go-cmp v0.4.0
## explicit
github.com/google/go-cmp/cmp github.com/google/go-cmp/cmp
github.com/google/go-cmp/cmp/internal/diff github.com/google/go-cmp/cmp/internal/diff
github.com/google/go-cmp/cmp/internal/flags github.com/google/go-cmp/cmp/internal/flags