diff --git a/cmd/kops/BUILD.bazel b/cmd/kops/BUILD.bazel index d8ed70e52f..77403c4989 100644 --- a/cmd/kops/BUILD.bazel +++ b/cmd/kops/BUILD.bazel @@ -69,6 +69,7 @@ go_library( "//pkg/cloudinstances:go_default_library", "//pkg/clusteraddons:go_default_library", "//pkg/commands:go_default_library", + "//pkg/commands/commandutils:go_default_library", "//pkg/dump:go_default_library", "//pkg/edit:go_default_library", "//pkg/featureflag:go_default_library", diff --git a/cmd/kops/delete_cluster.go b/cmd/kops/delete_cluster.go index 1cdd175ac3..0bf76d7518 100644 --- a/cmd/kops/delete_cluster.go +++ b/cmd/kops/delete_cluster.go @@ -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 - err = b.DeleteKubeConfig() + err = b.DeleteKubeConfig(clientcmd.NewDefaultPathOptions()) if err != nil { klog.Warningf("error removing kube config: %v", err) } diff --git a/cmd/kops/export_kubecfg.go b/cmd/kops/export_kubecfg.go index 8fe6c18240..d672095052 100644 --- a/cmd/kops/export_kubecfg.go +++ b/cmd/kops/export_kubecfg.go @@ -60,6 +60,9 @@ type ExportKubecfgOptions struct { admin time.Duration user string 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 { @@ -85,6 +88,7 @@ func NewCmdExportKubecfg(f *util.Factory, out io.Writer) *cobra.Command { 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().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 } @@ -135,12 +139,21 @@ func RunExportKubecfg(ctx context.Context, f *util.Factory, out io.Writer, optio 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 { return err } - if err := conf.WriteKubecfg(); err != nil { + if err := conf.WriteKubecfg(buildPathOptions(options)); err != nil { return err } } diff --git a/cmd/kops/main.go b/cmd/kops/main.go index 4c9d1fa0f5..7fd515999e 100644 --- a/cmd/kops/main.go +++ b/cmd/kops/main.go @@ -17,8 +17,7 @@ limitations under the License. package main // import "k8s.io/kops/cmd/kops" import ( - "fmt" - "os" + "k8s.io/kops/pkg/commands/commandutils" ) func main() { @@ -28,6 +27,5 @@ func main() { // 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) + commandutils.ExitWithError(err) } diff --git a/cmd/kops/root.go b/cmd/kops/root.go index 5a9e53a1dd..0ca32f0f2e 100644 --- a/cmd/kops/root.go +++ b/cmd/kops/root.go @@ -35,6 +35,7 @@ import ( "k8s.io/kops/cmd/kops/util" kopsapi "k8s.io/kops/pkg/apis/kops" "k8s.io/kops/pkg/client/simple" + "k8s.io/kops/pkg/commands" "k8s.io/kubectl/pkg/util/i18n" "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(NewCmdExport(f, out)) cmd.AddCommand(NewCmdGet(f, out)) + cmd.AddCommand(commands.NewCmdHelpers(f, out)) cmd.AddCommand(NewCmdUpdate(f, out)) cmd.AddCommand(NewCmdReplace(f, out)) cmd.AddCommand(NewCmdRollingUpdate(f, out)) diff --git a/cmd/kops/update_cluster.go b/cmd/kops/update_cluster.go index ab07c3cb11..1049c6278a 100644 --- a/cmd/kops/update_cluster.go +++ b/cmd/kops/update_cluster.go @@ -306,12 +306,23 @@ func RunUpdateCluster(ctx context.Context, f *util.Factory, clusterName string, firstRun = !hasKubecfg 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 { return nil, err } - err = conf.WriteKubecfg() + err = conf.WriteKubecfg(clientcmd.NewDefaultPathOptions()) if err != nil { return nil, err } diff --git a/cmd/kops/util/factory.go b/cmd/kops/util/factory.go index b027328bf5..0fa1a1961f 100644 --- a/cmd/kops/util/factory.go +++ b/cmd/kops/util/factory.go @@ -124,3 +124,8 @@ func (f *Factory) Clientset() (simple.Clientset, error) { return f.clientset, nil } + +// KopsStateStore returns the configured KOPS_STATE_STORE in use +func (f *Factory) KopsStateStore() string { + return f.options.RegistryPath +} diff --git a/docs/cli/kops_export_kubecfg.md b/docs/cli/kops_export_kubecfg.md index 94b738d702..47e507e7f2 100644 --- a/docs/cli/kops_export_kubecfg.md +++ b/docs/cli/kops_export_kubecfg.md @@ -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 --all export all clusters from the kops state store + --auth-plugin use the kops authentication plugin -h, --help help for kubecfg --internal use the cluster's internal DNS name --kubeconfig string the location of the kubeconfig file to create. diff --git a/go.mod b/go.mod index b178215155..87e173fe52 100644 --- a/go.mod +++ b/go.mod @@ -73,6 +73,7 @@ require ( github.com/go-ini/ini v1.51.0 github.com/go-logr/logr v0.2.1-0.20200730175230-ee2de8da5be6 github.com/gogo/protobuf v1.3.1 + github.com/google/go-cmp v0.4.0 github.com/google/uuid v1.1.1 github.com/gophercloud/gophercloud v0.11.1-0.20200518183226-7aec46f32c19 github.com/gorilla/mux v1.7.3 diff --git a/hack/.packages b/hack/.packages index a5d598f443..336c414f2c 100644 --- a/hack/.packages +++ b/hack/.packages @@ -84,6 +84,8 @@ k8s.io/kops/pkg/client/simple/vfsclientset k8s.io/kops/pkg/cloudinstances k8s.io/kops/pkg/clusteraddons 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/diff k8s.io/kops/pkg/dns diff --git a/pkg/commands/BUILD.bazel b/pkg/commands/BUILD.bazel index 7638c4e251..b90a7de0ef 100644 --- a/pkg/commands/BUILD.bazel +++ b/pkg/commands/BUILD.bazel @@ -3,6 +3,7 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") go_library( name = "go_default_library", srcs = [ + "helpers.go", "helpers_readwrite.go", "set_cluster.go", "status_discovery.go", @@ -17,6 +18,7 @@ go_library( "//pkg/apis/kops/validation:go_default_library", "//pkg/assets:go_default_library", "//pkg/client/simple:go_default_library", + "//pkg/commands/helpers:go_default_library", "//pkg/featureflag:go_default_library", "//pkg/resources/digitalocean: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/k8s.io/apimachinery/pkg/apis/meta/v1: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", ], ) diff --git a/pkg/commands/commandutils/BUILD.bazel b/pkg/commands/commandutils/BUILD.bazel new file mode 100644 index 0000000000..1b96ee4145 --- /dev/null +++ b/pkg/commands/commandutils/BUILD.bazel @@ -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"], +) diff --git a/pkg/commands/commandutils/exit.go b/pkg/commands/commandutils/exit.go new file mode 100644 index 0000000000..9ca61eae86 --- /dev/null +++ b/pkg/commands/commandutils/exit.go @@ -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) +} diff --git a/pkg/commands/helpers.go b/pkg/commands/helpers.go new file mode 100644 index 0000000000..276289f257 --- /dev/null +++ b/pkg/commands/helpers.go @@ -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 +} diff --git a/pkg/commands/helpers/BUILD.bazel b/pkg/commands/helpers/BUILD.bazel new file mode 100644 index 0000000000..89d5a53400 --- /dev/null +++ b/pkg/commands/helpers/BUILD.bazel @@ -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", + ], +) diff --git a/pkg/commands/helpers/kubectl_auth.go b/pkg/commands/helpers/kubectl_auth.go new file mode 100644 index 0000000000..25a0f98174 --- /dev/null +++ b/pkg/commands/helpers/kubectl_auth.go @@ -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 +} diff --git a/pkg/kubeconfig/BUILD.bazel b/pkg/kubeconfig/BUILD.bazel index ef854141b9..b2bbd6a489 100644 --- a/pkg/kubeconfig/BUILD.bazel +++ b/pkg/kubeconfig/BUILD.bazel @@ -32,6 +32,6 @@ go_test( "//pkg/pki:go_default_library", "//upup/pkg/fi: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", ], ) diff --git a/pkg/kubeconfig/create_kubecfg.go b/pkg/kubeconfig/create_kubecfg.go index 9db8fd52c5..4e129a075f 100644 --- a/pkg/kubeconfig/create_kubecfg.go +++ b/pkg/kubeconfig/create_kubecfg.go @@ -23,7 +23,6 @@ import ( "sort" "time" - "k8s.io/client-go/tools/clientcmd" "k8s.io/klog/v2" "k8s.io/kops/pkg/apis/kops" "k8s.io/kops/pkg/apis/kops/util" @@ -35,7 +34,7 @@ import ( 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 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.Server = server @@ -141,7 +140,6 @@ func BuildKubecfg(cluster *kops.Cluster, keyStore fi.Keystore, secretStore fi.Se if err != nil { return nil, err } - b.ClientCert, err = cert.AsBytes() if err != nil { 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 k8sVersion, err := util.ParseKubernetesVersion(cluster.Spec.KubernetesVersion) diff --git a/pkg/kubeconfig/create_kubecfg_test.go b/pkg/kubeconfig/create_kubecfg_test.go index f88109be63..59839452db 100644 --- a/pkg/kubeconfig/create_kubecfg_test.go +++ b/pkg/kubeconfig/create_kubecfg_test.go @@ -18,11 +18,10 @@ package kubeconfig import ( "fmt" - "reflect" "testing" "time" - "k8s.io/client-go/tools/clientcmd" + "github.com/google/go-cmp/cmp" "k8s.io/kops/pkg/apis/kops" "k8s.io/kops/pkg/pki" "k8s.io/kops/upup/pkg/fi" @@ -123,14 +122,13 @@ func TestBuildKubecfg(t *testing.T) { }() type args struct { - cluster *kops.Cluster - keyStore fakeKeyStore - secretStore fi.SecretStore - status fakeStatusStore - configAccess clientcmd.ConfigAccess - admin time.Duration - user string - internal bool + cluster *kops.Cluster + secretStore fi.SecretStore + status fakeStatusStore + admin time.Duration + user string + internal bool + useKopsAuthenticationPlugin bool } publiccluster := buildMinimalCluster("testcluster", "testcluster.test.com") @@ -138,106 +136,65 @@ func TestBuildKubecfg(t *testing.T) { gossipCluster := buildMinimalCluster("testgossipcluster.k8s.local", "") tests := []struct { - name string - args args - want *KubeconfigBuilder - wantErr bool + name string + args args + want *KubeconfigBuilder + wantErr bool + wantClientCert bool }{ { - "Test Kube Config Data For Public DNS with admin", - args{ - publiccluster, - fakeKeyStore{ - FindKeypairFn: func(name string) (*pki.Certificate, *pki.PrivateKey, bool, error) { - return fakeCertificate(), - fakePrivateKey(), - true, - nil - }, - }, - nil, - fakeStatusStore{}, - nil, - DefaultKubecfgAdminLifetime, - "", - false, + name: "Test Kube Config Data For Public DNS with admin", + args: args{ + cluster: publiccluster, + status: fakeStatusStore{}, + admin: DefaultKubecfgAdminLifetime, + user: "", }, - &KubeconfigBuilder{ + want: &KubeconfigBuilder{ Context: "testcluster", Server: "https://testcluster.test.com", CACert: []byte(certData), User: "testcluster", }, - false, + wantClientCert: true, }, { - "Test Kube Config Data For Public DNS without admin", - args{ - publiccluster, - fakeKeyStore{ - FindKeypairFn: func(name string) (*pki.Certificate, *pki.PrivateKey, bool, error) { - return fakeCertificate(), - fakePrivateKey(), - true, - nil - }, - }, - nil, - fakeStatusStore{}, - nil, - 0, - "myuser", - false, + name: "Test Kube Config Data For Public DNS without admin", + args: args{ + cluster: publiccluster, + status: fakeStatusStore{}, + admin: 0, + user: "myuser", }, - &KubeconfigBuilder{ + want: &KubeconfigBuilder{ Context: "testcluster", Server: "https://testcluster.test.com", CACert: []byte(certData), User: "myuser", }, - false, + wantClientCert: false, }, { - "Test Kube Config Data For Public DNS with Empty Master Name", - args{ - emptyMasterPublicNameCluster, - fakeKeyStore{ - FindKeypairFn: func(name string) (*pki.Certificate, *pki.PrivateKey, bool, error) { - return fakeCertificate(), - fakePrivateKey(), - true, - nil - }, - }, - nil, - fakeStatusStore{}, - nil, - 0, - "", - false, + name: "Test Kube Config Data For Public DNS with Empty Master Name", + args: args{ + cluster: emptyMasterPublicNameCluster, + status: fakeStatusStore{}, + admin: 0, + user: "", }, - &KubeconfigBuilder{ + want: &KubeconfigBuilder{ Context: "emptyMasterPublicNameCluster", Server: "https://api.emptyMasterPublicNameCluster", CACert: []byte(certData), User: "emptyMasterPublicNameCluster", }, - false, + wantClientCert: false, }, { - "Test Kube Config Data For Gossip cluster", - args{ - gossipCluster, - fakeKeyStore{ - FindKeypairFn: func(name string) (*pki.Certificate, *pki.PrivateKey, bool, error) { - return fakeCertificate(), - fakePrivateKey(), - true, - nil - }, - }, - nil, - fakeStatusStore{ + name: "Test Kube Config Data For Gossip cluster", + args: args{ + cluster: gossipCluster, + status: fakeStatusStore{ GetApiIngressStatusFn: func(cluster *kops.Cluster) ([]kops.ApiIngressStatus, error) { return []kops.ApiIngressStatus{ { @@ -246,57 +203,74 @@ func TestBuildKubecfg(t *testing.T) { }, nil }, }, - nil, - 0, - "", - false, }, - &KubeconfigBuilder{ + want: &KubeconfigBuilder{ Context: "testgossipcluster.k8s.local", Server: "https://elbHostName", CACert: []byte(certData), User: "testgossipcluster.k8s.local", }, - false, + wantClientCert: false, }, { - "Test Kube Config Data For internal DNS name with admin", - args{ - publiccluster, - fakeKeyStore{ - FindKeypairFn: func(name string) (*pki.Certificate, *pki.PrivateKey, bool, error) { - return fakeCertificate(), - fakePrivateKey(), - true, - nil - }, + name: "Public DNS with kops auth plugin", + args: args{ + cluster: publiccluster, + status: fakeStatusStore{}, + admin: 0, + useKopsAuthenticationPlugin: true, + }, + want: &KubeconfigBuilder{ + 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{ - Context: "testcluster", - Server: "https://internal.testcluster.test.com", - CACert: []byte(certData), - ClientCert: []byte(certData), - ClientKey: []byte(privatekeyData), - User: "testcluster", + wantClientCert: false, + }, + { + name: "Test Kube Config Data For internal DNS name with admin", + args: args{ + cluster: publiccluster, + 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 { 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 { t.Errorf("BuildKubecfg() error = %v, wantErr %v", err, tt.wantErr) return } - if tt.args.admin != 0 { + if tt.wantClientCert { if got.ClientCert == nil { t.Errorf("Expected ClientCert, got nil") } @@ -306,8 +280,8 @@ func TestBuildKubecfg(t *testing.T) { tt.want.ClientCert = got.ClientCert tt.want.ClientKey = got.ClientKey } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("BuildKubecfg() = %+v, want %+v", got, tt.want) + if diff := cmp.Diff(got, tt.want); diff != "" { + t.Errorf("BuildKubecfg() diff (+got, -want): %s", diff) } }) } diff --git a/pkg/kubeconfig/kubecfg_builder.go b/pkg/kubeconfig/kubecfg_builder.go index e5f26ca18a..0946bcc55a 100644 --- a/pkg/kubeconfig/kubecfg_builder.go +++ b/pkg/kubeconfig/kubecfg_builder.go @@ -42,18 +42,16 @@ type KubeconfigBuilder struct { ClientCert []byte ClientKey []byte - configAccess clientcmd.ConfigAccess + AuthenticationExec []string } // Create new KubeconfigBuilder -func NewKubeconfigBuilder(configAccess clientcmd.ConfigAccess) *KubeconfigBuilder { - c := &KubeconfigBuilder{} - c.configAccess = configAccess - return c +func NewKubeconfigBuilder() *KubeconfigBuilder { + return &KubeconfigBuilder{} } -func (b *KubeconfigBuilder) DeleteKubeConfig() error { - config, err := b.configAccess.GetStartingConfig() +func (b *KubeconfigBuilder) DeleteKubeConfig(configAccess clientcmd.ConfigAccess) error { + config, err := configAccess.GetStartingConfig() if err != nil { return fmt.Errorf("error loading kubeconfig: %v", err) } @@ -72,7 +70,7 @@ func (b *KubeconfigBuilder) DeleteKubeConfig() error { 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) } @@ -101,8 +99,8 @@ func (c *KubeconfigBuilder) BuildRestConfig() (*rest.Config, error) { } // Write out a new kubeconfig -func (b *KubeconfigBuilder) WriteKubecfg() error { - config, err := b.configAccess.GetStartingConfig() +func (b *KubeconfigBuilder) WriteKubecfg(configAccess clientcmd.ConfigAccess) error { + config, err := configAccess.GetStartingConfig() if err != nil { return fmt.Errorf("error reading kubeconfig: %v", err) } @@ -146,6 +144,14 @@ func (b *KubeconfigBuilder) WriteKubecfg() error { 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 { config.AuthInfos = make(map[string]*clientcmdapi.AuthInfo) } @@ -198,7 +204,7 @@ func (b *KubeconfigBuilder) WriteKubecfg() error { 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 } diff --git a/vendor/modules.txt b/vendor/modules.txt index daae35051e..13eec25626 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -279,6 +279,7 @@ github.com/golang/snappy # github.com/google/btree v1.0.0 github.com/google/btree # github.com/google/go-cmp v0.4.0 +## explicit github.com/google/go-cmp/cmp github.com/google/go-cmp/cmp/internal/diff github.com/google/go-cmp/cmp/internal/flags