Skip to content

Commit 53208fa

Browse files
committed
chore: generate kubeconfig on the fly
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.
1 parent af5bd10 commit 53208fa

File tree

4 files changed

+92
-36
lines changed

4 files changed

+92
-36
lines changed

cmd/kops/util/factory.go

Lines changed: 64 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,15 @@ limitations under the License.
1717
package util
1818

1919
import (
20+
"context"
2021
"fmt"
2122
"net/http"
2223
"net/url"
2324
"strings"
2425
"sync"
26+
"time"
2527

2628
"k8s.io/apimachinery/pkg/util/validation/field"
27-
"k8s.io/cli-runtime/pkg/genericclioptions"
2829
"k8s.io/client-go/dynamic"
2930
"k8s.io/client-go/rest"
3031
"k8s.io/client-go/tools/clientcmd"
@@ -36,6 +37,8 @@ import (
3637
"k8s.io/kops/pkg/client/simple"
3738
"k8s.io/kops/pkg/client/simple/api"
3839
"k8s.io/kops/pkg/client/simple/vfsclientset"
40+
"k8s.io/kops/pkg/kubeconfig"
41+
"k8s.io/kops/upup/pkg/fi/cloudup"
3942
"k8s.io/kops/util/pkg/vfs"
4043
)
4144

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

5861
// clusterInfo holds REST connection configuration for connecting to a cluster
5962
type clusterInfo struct {
60-
clusterName string
63+
factory *Factory
64+
cluster *kops.Cluster
6165

6266
cachedHTTPClient *http.Client
6367
cachedRESTConfig *rest.Config
@@ -155,48 +159,47 @@ func (f *Factory) KopsStateStore() string {
155159
return f.options.RegistryPath
156160
}
157161

158-
func (f *Factory) getClusterInfo(clusterName string) *clusterInfo {
162+
func (f *Factory) getClusterInfo(cluster *kops.Cluster) *clusterInfo {
159163
f.mutex.Lock()
160164
defer f.mutex.Unlock()
161165

162-
if clusterInfo, ok := f.clusters[clusterName]; ok {
166+
key := cluster.ObjectMeta.Name
167+
if clusterInfo, ok := f.clusters[key]; ok {
163168
return clusterInfo
164169
}
165-
clusterInfo := &clusterInfo{}
166-
f.clusters[clusterName] = clusterInfo
170+
clusterInfo := &clusterInfo{
171+
factory: f,
172+
cluster: cluster,
173+
}
174+
f.clusters[key] = clusterInfo
167175
return clusterInfo
168176
}
169177

170178
func (f *Factory) RESTConfig(cluster *kops.Cluster) (*rest.Config, error) {
171-
clusterInfo := f.getClusterInfo(cluster.ObjectMeta.Name)
179+
clusterInfo := f.getClusterInfo(cluster)
172180
return clusterInfo.RESTConfig()
173181
}
174182

175183
func (f *clusterInfo) RESTConfig() (*rest.Config, error) {
176-
if f.cachedRESTConfig == nil {
177-
// Get the kubeconfig from the context
178-
179-
clientGetter := genericclioptions.NewConfigFlags(true)
180-
if f.clusterName != "" {
181-
contextName := f.clusterName
182-
clientGetter.Context = &contextName
183-
}
184+
ctx := context.Background()
184185

185-
restConfig, err := clientGetter.ToRESTConfig()
186+
if f.cachedRESTConfig == nil {
187+
restConfig, err := f.factory.buildRESTConfig(ctx, f.cluster)
186188
if err != nil {
187-
return nil, fmt.Errorf("loading kubecfg settings for %q: %w", f.clusterName, err)
189+
return nil, err
188190
}
189191

190192
restConfig.UserAgent = "kops"
191193
restConfig.Burst = 50
192194
restConfig.QPS = 20
195+
193196
f.cachedRESTConfig = restConfig
194197
}
195198
return f.cachedRESTConfig, nil
196199
}
197200

198201
func (f *Factory) HTTPClient(cluster *kops.Cluster) (*http.Client, error) {
199-
clusterInfo := f.getClusterInfo(cluster.ObjectMeta.Name)
202+
clusterInfo := f.getClusterInfo(cluster)
200203
return clusterInfo.HTTPClient()
201204
}
202205

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

218221
// DynamicClient returns a dynamic client
219-
func (f *Factory) DynamicClient(clusterName string) (dynamic.Interface, error) {
220-
clusterInfo := f.getClusterInfo(clusterName)
222+
func (f *Factory) DynamicClient(cluster *kops.Cluster) (dynamic.Interface, error) {
223+
clusterInfo := f.getClusterInfo(cluster)
221224
return clusterInfo.DynamicClient()
222225
}
223226

@@ -249,3 +252,44 @@ func (f *Factory) VFSContext() *vfs.VFSContext {
249252
}
250253
return f.vfsContext
251254
}
255+
256+
func (f *Factory) buildRESTConfig(ctx context.Context, cluster *kops.Cluster) (*rest.Config, error) {
257+
clientset, err := f.KopsClient()
258+
if err != nil {
259+
return nil, err
260+
}
261+
262+
keyStore, err := clientset.KeyStore(cluster)
263+
if err != nil {
264+
return nil, err
265+
}
266+
267+
secretStore, err := clientset.SecretStore(cluster)
268+
if err != nil {
269+
return nil, err
270+
}
271+
272+
cloud, err := cloudup.BuildCloud(cluster)
273+
if err != nil {
274+
return nil, err
275+
}
276+
277+
// Generate a relatively short-lived certificate / kubeconfig
278+
createKubecfgOptions := kubeconfig.CreateKubecfgOptions{
279+
Admin: 1 * time.Hour,
280+
}
281+
282+
conf, err := kubeconfig.BuildKubecfg(
283+
ctx,
284+
cluster,
285+
keyStore,
286+
secretStore,
287+
cloud,
288+
createKubecfgOptions,
289+
f.KopsStateStore())
290+
if err != nil {
291+
return nil, err
292+
}
293+
294+
return conf.ToRESTConfig()
295+
}

cmd/kops/validate_cluster.go

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -147,19 +147,6 @@ func RunValidateCluster(ctx context.Context, f *util.Factory, out io.Writer, opt
147147
return nil, fmt.Errorf("no InstanceGroup objects found")
148148
}
149149

150-
// // TODO: Refactor into util.Factory
151-
// contextName := cluster.ObjectMeta.Name
152-
// configLoadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
153-
// if options.kubeconfig != "" {
154-
// configLoadingRules.ExplicitPath = options.kubeconfig
155-
// }
156-
// config, err := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
157-
// configLoadingRules,
158-
// &clientcmd.ConfigOverrides{CurrentContext: contextName}).ClientConfig()
159-
// if err != nil {
160-
// return nil, fmt.Errorf("cannot load kubecfg settings for %q: %v", contextName, err)
161-
// }
162-
163150
restConfig, err := f.RESTConfig(cluster)
164151
if err != nil {
165152
return nil, fmt.Errorf("getting rest config: %w", err)

pkg/kubeconfig/create_kubecfg.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,15 @@ const DefaultKubecfgAdminLifetime = 18 * time.Hour
3535

3636
type CreateKubecfgOptions struct {
3737
CreateKubecfg bool
38-
Admin time.Duration
39-
User string
40-
Internal bool
38+
39+
// Admin is the lifetime of the admin certificate
40+
Admin time.Duration
41+
42+
// User is the user to use in the kubeconfig
43+
User string
44+
45+
// Internal is whether to use the internal API endpoint
46+
Internal bool
4147

4248
// UseKopsAuthenticationPlugin controls whether we should use the kOps auth helper instead of a static credential
4349
UseKopsAuthenticationPlugin bool

pkg/kubeconfig/kubecfg_builder.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package kubeconfig
1919
import (
2020
"fmt"
2121

22+
"k8s.io/client-go/rest"
2223
"k8s.io/client-go/tools/clientcmd"
2324
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
2425
"k8s.io/klog/v2"
@@ -208,3 +209,21 @@ func (b *KubeconfigBuilder) WriteKubecfg(configAccess clientcmd.ConfigAccess) er
208209
fmt.Printf("kOps has set your kubectl context to %s\n", b.Context)
209210
return nil
210211
}
212+
213+
func (b *KubeconfigBuilder) ToRESTConfig() (*rest.Config, error) {
214+
restConfig := &rest.Config{}
215+
216+
restConfig.Host = b.Server
217+
restConfig.TLSClientConfig.CAData = b.CACerts
218+
restConfig.TLSClientConfig.ServerName = b.TLSServerName
219+
220+
usingAuthPlugin := len(b.AuthenticationExec) != 0
221+
if usingAuthPlugin {
222+
return nil, fmt.Errorf("auth plugin not yet supported by ToRESTConfig")
223+
}
224+
225+
restConfig.CertData = b.ClientCert
226+
restConfig.KeyData = b.ClientKey
227+
228+
return restConfig, nil
229+
}

0 commit comments

Comments
 (0)