Skip to content

Commit

Permalink
chore: generate kubeconfig on the fly
Browse files Browse the repository at this point in the history
Some kOps actions require connecting to the cluster, but
we don't always have a kubeconfig available.

This commit adds a function to generate a client config on the fly
(including a certificate) when needed.
  • Loading branch information
justinsb committed Dec 27, 2024
1 parent 859a9fd commit 324117c
Show file tree
Hide file tree
Showing 4 changed files with 92 additions and 36 deletions.
84 changes: 64 additions & 20 deletions cmd/kops/util/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,15 @@ limitations under the License.
package util

import (
"context"
"fmt"
"net/http"
"net/url"
"strings"
"sync"
"time"

"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/cli-runtime/pkg/genericclioptions"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
Expand All @@ -36,6 +37,8 @@ import (
"k8s.io/kops/pkg/client/simple"
"k8s.io/kops/pkg/client/simple/api"
"k8s.io/kops/pkg/client/simple/vfsclientset"
"k8s.io/kops/pkg/kubeconfig"
"k8s.io/kops/upup/pkg/fi/cloudup"
"k8s.io/kops/util/pkg/vfs"
)

Expand All @@ -57,7 +60,8 @@ type Factory struct {

// clusterInfo holds REST connection configuration for connecting to a cluster
type clusterInfo struct {
clusterName string
factory *Factory
cluster *kops.Cluster

cachedHTTPClient *http.Client
cachedRESTConfig *rest.Config
Expand Down Expand Up @@ -155,48 +159,47 @@ func (f *Factory) KopsStateStore() string {
return f.options.RegistryPath
}

func (f *Factory) getClusterInfo(clusterName string) *clusterInfo {
func (f *Factory) getClusterInfo(cluster *kops.Cluster) *clusterInfo {
f.mutex.Lock()
defer f.mutex.Unlock()

if clusterInfo, ok := f.clusters[clusterName]; ok {
key := cluster.ObjectMeta.Name
if clusterInfo, ok := f.clusters[key]; ok {
return clusterInfo
}
clusterInfo := &clusterInfo{}
f.clusters[clusterName] = clusterInfo
clusterInfo := &clusterInfo{
factory: f,
cluster: cluster,
}
f.clusters[key] = clusterInfo
return clusterInfo
}

func (f *Factory) RESTConfig(cluster *kops.Cluster) (*rest.Config, error) {
clusterInfo := f.getClusterInfo(cluster.ObjectMeta.Name)
clusterInfo := f.getClusterInfo(cluster)
return clusterInfo.RESTConfig()
}

func (f *clusterInfo) RESTConfig() (*rest.Config, error) {
if f.cachedRESTConfig == nil {
// Get the kubeconfig from the context

clientGetter := genericclioptions.NewConfigFlags(true)
if f.clusterName != "" {
contextName := f.clusterName
clientGetter.Context = &contextName
}
ctx := context.Background()

restConfig, err := clientGetter.ToRESTConfig()
if f.cachedRESTConfig == nil {
restConfig, err := f.factory.buildRESTConfig(ctx, f.cluster)
if err != nil {
return nil, fmt.Errorf("loading kubecfg settings for %q: %w", f.clusterName, err)
return nil, err
}

restConfig.UserAgent = "kops"
restConfig.Burst = 50
restConfig.QPS = 20

f.cachedRESTConfig = restConfig
}
return f.cachedRESTConfig, nil
}

func (f *Factory) HTTPClient(cluster *kops.Cluster) (*http.Client, error) {
clusterInfo := f.getClusterInfo(cluster.ObjectMeta.Name)
clusterInfo := f.getClusterInfo(cluster)
return clusterInfo.HTTPClient()
}

Expand All @@ -216,8 +219,8 @@ func (f *clusterInfo) HTTPClient() (*http.Client, error) {
}

// DynamicClient returns a dynamic client
func (f *Factory) DynamicClient(clusterName string) (dynamic.Interface, error) {
clusterInfo := f.getClusterInfo(clusterName)
func (f *Factory) DynamicClient(cluster *kops.Cluster) (dynamic.Interface, error) {
clusterInfo := f.getClusterInfo(cluster)
return clusterInfo.DynamicClient()
}

Expand Down Expand Up @@ -249,3 +252,44 @@ func (f *Factory) VFSContext() *vfs.VFSContext {
}
return f.vfsContext
}

func (f *Factory) buildRESTConfig(ctx context.Context, cluster *kops.Cluster) (*rest.Config, error) {
clientset, err := f.KopsClient()
if err != nil {
return nil, err
}

keyStore, err := clientset.KeyStore(cluster)
if err != nil {
return nil, err
}

secretStore, err := clientset.SecretStore(cluster)
if err != nil {
return nil, err
}

cloud, err := cloudup.BuildCloud(cluster)
if err != nil {
return nil, err
}

// Generate a relatively short-lived certificate / kubeconfig
createKubecfgOptions := kubeconfig.CreateKubecfgOptions{
Admin: 1 * time.Hour,
}

conf, err := kubeconfig.BuildKubecfg(
ctx,
cluster,
keyStore,
secretStore,
cloud,
createKubecfgOptions,
f.KopsStateStore())
if err != nil {
return nil, err
}

return conf.ToRESTConfig()
}
13 changes: 0 additions & 13 deletions cmd/kops/validate_cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,19 +147,6 @@ func RunValidateCluster(ctx context.Context, f *util.Factory, out io.Writer, opt
return nil, fmt.Errorf("no InstanceGroup objects found")
}

// // TODO: Refactor into util.Factory
// contextName := cluster.ObjectMeta.Name
// configLoadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
// if options.kubeconfig != "" {
// configLoadingRules.ExplicitPath = options.kubeconfig
// }
// config, err := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
// configLoadingRules,
// &clientcmd.ConfigOverrides{CurrentContext: contextName}).ClientConfig()
// if err != nil {
// return nil, fmt.Errorf("cannot load kubecfg settings for %q: %v", contextName, err)
// }

restConfig, err := f.RESTConfig(cluster)
if err != nil {
return nil, fmt.Errorf("getting rest config: %w", err)
Expand Down
12 changes: 9 additions & 3 deletions pkg/kubeconfig/create_kubecfg.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,15 @@ const DefaultKubecfgAdminLifetime = 18 * time.Hour

type CreateKubecfgOptions struct {
CreateKubecfg bool
Admin time.Duration
User string
Internal bool

// Admin is the lifetime of the admin certificate
Admin time.Duration

// User is the user to use in the kubeconfig
User string

// Internal is whether to use the internal API endpoint
Internal bool

// UseKopsAuthenticationPlugin controls whether we should use the kOps auth helper instead of a static credential
UseKopsAuthenticationPlugin bool
Expand Down
19 changes: 19 additions & 0 deletions pkg/kubeconfig/kubecfg_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package kubeconfig
import (
"fmt"

"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
"k8s.io/klog/v2"
Expand Down Expand Up @@ -208,3 +209,21 @@ func (b *KubeconfigBuilder) WriteKubecfg(configAccess clientcmd.ConfigAccess) er
fmt.Printf("kOps has set your kubectl context to %s\n", b.Context)
return nil
}

func (b *KubeconfigBuilder) ToRESTConfig() (*rest.Config, error) {
restConfig := &rest.Config{}

restConfig.Host = b.Server
restConfig.TLSClientConfig.CAData = b.CACerts
restConfig.TLSClientConfig.ServerName = b.TLSServerName

usingAuthPlugin := len(b.AuthenticationExec) != 0
if usingAuthPlugin {
return nil, fmt.Errorf("auth plugin not yet supported by ToRESTConfig")
}

restConfig.CertData = b.ClientCert
restConfig.KeyData = b.ClientKey

return restConfig, nil
}

0 comments on commit 324117c

Please sign in to comment.