Skip to content

Commit 844bb58

Browse files
authored
feat(sync): add flags for central cluster context, namespace & remote cluster name (#5)
1 parent 3c7ab3b commit 844bb58

File tree

4 files changed

+127
-85
lines changed

4 files changed

+127
-85
lines changed

cmd/root.go

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,15 @@
44
package cmd
55

66
import (
7-
"fmt"
8-
97
"github.com/spf13/cobra"
108
)
119

1210
var rootCmd = &cobra.Command{
1311
Use: "cloudctl",
14-
Short: "A CLI tool to manage clusters, including OIDC login and kubeconfig sync.",
12+
Short: "A CLI tool to access Greenhouse clusters",
1513
Long: `cloudctl is a command line interface that helps:
1614
17-
1) Fetch and merge Kubeconfigs from a special cluster CRD`,
15+
1) Fetch and merge kubeconfigs from central Greenhouse cluster`,
1816
}
1917

2018
func Execute() error {
@@ -25,8 +23,3 @@ func init() {
2523
// Add subcommands here
2624
rootCmd.AddCommand(syncCmd)
2725
}
28-
29-
// A utility function that might be used across multiple commands
30-
func printDebugMessage(msg string) {
31-
fmt.Println("DEBUG:", msg)
32-
}

cmd/sync.go

Lines changed: 81 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -12,69 +12,94 @@ import (
1212
"maps"
1313
"strings"
1414

15-
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
16-
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
17-
1815
"github.com/cloudoperators/greenhouse/pkg/apis/greenhouse/v1alpha1"
1916
"github.com/spf13/cobra"
2017
"k8s.io/apimachinery/pkg/runtime"
21-
"k8s.io/apimachinery/pkg/runtime/schema"
22-
"k8s.io/client-go/dynamic"
18+
"k8s.io/client-go/rest"
2319
clientcmd "k8s.io/client-go/tools/clientcmd"
2420
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
21+
"sigs.k8s.io/controller-runtime/pkg/client"
2522
)
2623

2724
var (
28-
greenhouseCentralClusterKubeconfig string
29-
greenhouseRemoteClusterKubeconfig string
30-
prefix string
31-
mergeIdenticalUsers bool
25+
greenhouseClusterKubeconfig string
26+
greenhouseClusterContext string
27+
greenhouseClusterNamespace string
28+
remoteClusterKubeconfig string
29+
remoteClusterName string
30+
prefix string
31+
mergeIdenticalUsers bool
3232
)
3333

3434
func init() {
35-
syncCmd.Flags().StringVar(&greenhouseCentralClusterKubeconfig, "central-cluster-kubeconfig", clientcmd.RecommendedHomeFile, "kubeconfig for central Greenhouse cluster")
36-
syncCmd.Flags().StringVar(&greenhouseRemoteClusterKubeconfig, "remote-cluster-kubeconfig", clientcmd.RecommendedHomeFile, "kubeconfig for remote Greenhouse clusters")
37-
syncCmd.Flags().StringVar(&prefix, "prefix", "cloudctl", "prefix for kubeconfig entries. It is used to separate and manage the entries of this tool only")
35+
syncCmd.Flags().StringVarP(&greenhouseClusterKubeconfig, "greenhouse-cluster-kubeconfig", "k", clientcmd.RecommendedHomeFile, "kubeconfig file path for Greenhouse cluster")
36+
syncCmd.Flags().StringVarP(&greenhouseClusterContext, "greenhouse-cluster-context", "c", "", "context in greenhouse-cluster-kubeconfig, the context in the file is used if this flag is not set")
37+
syncCmd.Flags().StringVarP(&greenhouseClusterNamespace, "greenhouse-cluster-namespace", "n", "", "namespace for greenhouse-cluster-kubeconfig, it is the same value as Greenhouse organization")
38+
syncCmd.MarkFlagRequired("greenhouse-cluster-namespace")
39+
syncCmd.Flags().StringVarP(&remoteClusterKubeconfig, "remote-cluster-kubeconfig", "r", clientcmd.RecommendedHomeFile, "kubeconfig file path for remote clusters")
40+
syncCmd.Flags().StringVar(&remoteClusterName, "remote-cluster-name", "", "name of the remote cluster, if not set all clusters are retrieved")
41+
syncCmd.Flags().StringVar(&prefix, "prefix", "cloudctl", "prefix for kubeconfig entries. it is used to separate and manage the entries of this tool only")
3842
syncCmd.Flags().BoolVar(&mergeIdenticalUsers, "merge-identical-users", true, "merge identical user information in kubeconfig file so that you only login once for the clusters that share the same auth info")
3943
}
4044

4145
var (
4246
syncCmd = &cobra.Command{
4347
Use: "sync",
44-
Short: "Fetches remote kubeconfigs from Greenhouse cluster and merges them into your local config",
48+
Short: "Fetches kubeconfigs of remote clusters from Greenhouse cluster and merges them into your local config",
4549
RunE: runSync,
4650
}
4751
)
4852

4953
func runSync(cmd *cobra.Command, args []string) error {
5054

51-
centralConfig, err := clientcmd.BuildConfigFromFlags("", greenhouseCentralClusterKubeconfig)
55+
centralConfig, err := clientcmd.BuildConfigFromFlags("", greenhouseClusterKubeconfig)
5256
if err != nil {
53-
return fmt.Errorf("failed to build central kubeconfig: %w", err)
57+
return fmt.Errorf("failed to build greenhouse kubeconfig: %w", err)
5458
}
5559

56-
dynamicClient, err := dynamic.NewForConfig(centralConfig)
57-
if err != nil {
58-
return fmt.Errorf("failed to create dynamic client: %w", err)
60+
if greenhouseClusterContext != "" {
61+
centralConfig, err = configWithContext(greenhouseClusterContext, greenhouseClusterKubeconfig)
62+
if err != nil {
63+
return fmt.Errorf("failed to build greenhouse kubeconfig with context %s: %w", greenhouseClusterContext, err)
64+
}
5965
}
6066

61-
gvr := schema.GroupVersionResource{
62-
Group: "greenhouse.sap",
63-
Version: "v1alpha1",
64-
Resource: "clusterkubeconfigs",
67+
// Create a scheme and register Greenhouse types.
68+
scheme := runtime.NewScheme()
69+
if err := v1alpha1.AddToScheme(scheme); err != nil {
70+
return fmt.Errorf("failed to add greenhouse scheme: %w", err)
6571
}
6672

67-
unstructuredList, err := dynamicClient.Resource(gvr).Namespace("ccloud").List(cmd.Context(), metav1.ListOptions{})
73+
// Create a typed client.
74+
c, err := client.New(centralConfig, client.Options{Scheme: scheme})
6875
if err != nil {
69-
return fmt.Errorf("failed to list ClusterKubeconfigs: %w", err)
76+
return fmt.Errorf("failed to create client: %w", err)
7077
}
7178

72-
if len(unstructuredList.Items) == 0 {
79+
ctx := cmd.Context()
80+
var clusterKubeconfigs []v1alpha1.ClusterKubeconfig
81+
82+
// If a specific remote cluster name is provided, fetch that single resource;
83+
// otherwise, list all ClusterKubeconfigs in the given namespace.
84+
if remoteClusterName != "" {
85+
var ckc v1alpha1.ClusterKubeconfig
86+
if err := c.Get(ctx, client.ObjectKey{Namespace: greenhouseClusterNamespace, Name: remoteClusterName}, &ckc); err != nil {
87+
return fmt.Errorf("failed to get ClusterKubeconfig %q: %w", remoteClusterName, err)
88+
}
89+
clusterKubeconfigs = append(clusterKubeconfigs, ckc)
90+
} else {
91+
var list v1alpha1.ClusterKubeconfigList
92+
if err := c.List(ctx, &list, client.InNamespace(greenhouseClusterNamespace)); err != nil {
93+
return fmt.Errorf("failed to list ClusterKubeconfigs: %w", err)
94+
}
95+
clusterKubeconfigs = list.Items
96+
}
97+
if len(clusterKubeconfigs) == 0 {
7398
log.Println("No ClusterKubeconfigs found to sync.")
7499
return nil
75100
}
76101

77-
localConfig, err := clientcmd.LoadFromFile(greenhouseRemoteClusterKubeconfig)
102+
localConfig, err := clientcmd.LoadFromFile(remoteClusterKubeconfig)
78103
if err != nil {
79104
return fmt.Errorf("failed to load local kubeconfig: %w", err)
80105
}
@@ -83,7 +108,7 @@ func runSync(cmd *cobra.Command, args []string) error {
83108
localConfig = clientcmdapi.NewConfig()
84109
}
85110

86-
serverConfig, err := buildIncomingKubeconfig(unstructuredList.Items)
111+
serverConfig, err := buildIncomingKubeconfig(clusterKubeconfigs)
87112
if err != nil {
88113
return fmt.Errorf("failed to create server config: %w", err)
89114
}
@@ -93,52 +118,47 @@ func runSync(cmd *cobra.Command, args []string) error {
93118
return fmt.Errorf("failed to merge ClusterKubeconfig: %w", err)
94119
}
95120

96-
err = writeConfig(localConfig, greenhouseRemoteClusterKubeconfig)
121+
err = writeConfig(localConfig, remoteClusterKubeconfig)
97122
if err != nil {
98123
return fmt.Errorf("failed to write merged kubeconfig: %w", err)
99124
}
100125

101-
log.Println("Successfully synced and merged the new cluster kubeconfig with your local config.")
126+
log.Println("Successfully synced and merged into your local config.")
102127
return nil
103128
}
104129

105-
func buildIncomingKubeconfig(items []unstructured.Unstructured) (*clientcmdapi.Config, error) {
130+
// buildIncomingKubeconfig converts the list of typed ClusterKubeconfig objects
131+
// into a clientcmdapi.Config.
132+
func buildIncomingKubeconfig(items []v1alpha1.ClusterKubeconfig) (*clientcmdapi.Config, error) {
106133
kubeconfig := clientcmdapi.NewConfig()
107134

108-
for _, unstructuredItem := range items {
109-
var ckc v1alpha1.ClusterKubeconfig
110-
err := runtime.DefaultUnstructuredConverter.FromUnstructured(unstructuredItem.Object, &ckc)
111-
if err != nil {
112-
return nil, fmt.Errorf("failed to convert unstructured to ClusterKubeconfig: %w", err)
113-
}
114-
115-
// Assuming each ClusterKubeconfig has exactly one context, authInfo, and cluster
135+
for _, ckc := range items {
136+
// Assuming each ClusterKubeconfig has exactly one context, authInfo, and cluster.
116137
if len(ckc.Spec.Kubeconfig.Contexts) > 0 {
117-
ctx := ckc.Spec.Kubeconfig.Contexts[0]
118-
kubeconfig.Contexts[ctx.Name] = &clientcmdapi.Context{
119-
Cluster: ctx.Context.Cluster,
120-
AuthInfo: ctx.Context.AuthInfo,
121-
Namespace: ctx.Context.Namespace,
138+
ctxItem := ckc.Spec.Kubeconfig.Contexts[0]
139+
kubeconfig.Contexts[ctxItem.Name] = &clientcmdapi.Context{
140+
Cluster: ctxItem.Context.Cluster,
141+
AuthInfo: ctxItem.Context.AuthInfo,
142+
Namespace: ctxItem.Context.Namespace,
122143
}
123144
}
124145

125146
if len(ckc.Spec.Kubeconfig.AuthInfo) > 0 {
126-
auth := ckc.Spec.Kubeconfig.AuthInfo[0].AuthInfo
127-
kubeconfig.AuthInfos[ckc.Spec.Kubeconfig.AuthInfo[0].Name] = &clientcmdapi.AuthInfo{
128-
ClientCertificateData: auth.ClientCertificateData,
129-
ClientKeyData: auth.ClientKeyData,
130-
AuthProvider: &auth.AuthProvider,
147+
authItem := ckc.Spec.Kubeconfig.AuthInfo[0]
148+
kubeconfig.AuthInfos[authItem.Name] = &clientcmdapi.AuthInfo{
149+
ClientCertificateData: authItem.AuthInfo.ClientCertificateData,
150+
ClientKeyData: authItem.AuthInfo.ClientKeyData,
151+
AuthProvider: &authItem.AuthInfo.AuthProvider,
131152
}
132153
}
133154

134155
if len(ckc.Spec.Kubeconfig.Clusters) > 0 {
135-
cluster := ckc.Spec.Kubeconfig.Clusters[0].Cluster
136-
kubeconfig.Clusters[ckc.Spec.Kubeconfig.Clusters[0].Name] = &clientcmdapi.Cluster{
137-
Server: cluster.Server,
138-
CertificateAuthorityData: cluster.CertificateAuthorityData,
156+
clusterItem := ckc.Spec.Kubeconfig.Clusters[0]
157+
kubeconfig.Clusters[clusterItem.Name] = &clientcmdapi.Cluster{
158+
Server: clusterItem.Cluster.Server,
159+
CertificateAuthorityData: clusterItem.Cluster.CertificateAuthorityData,
139160
}
140161
}
141-
142162
}
143163

144164
return kubeconfig, nil
@@ -447,3 +467,11 @@ func mergeAuthInfo(serverAuth, localAuth *clientcmdapi.AuthInfo) *clientcmdapi.A
447467

448468
return mergedAuth
449469
}
470+
471+
func configWithContext(context, kubeconfigPath string) (*rest.Config, error) {
472+
return clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
473+
&clientcmd.ClientConfigLoadingRules{ExplicitPath: kubeconfigPath},
474+
&clientcmd.ConfigOverrides{
475+
CurrentContext: context,
476+
}).ClientConfig()
477+
}

go.mod

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,35 @@ go 1.23.4
44

55
require (
66
github.com/cloudoperators/greenhouse v0.0.1-alpha.1
7-
github.com/spf13/cobra v1.8.1
7+
github.com/spf13/cobra v1.9.1
88
github.com/spf13/viper v1.19.0
9-
k8s.io/apimachinery v0.32.1
10-
k8s.io/client-go v0.32.1
9+
k8s.io/apimachinery v0.32.2
10+
k8s.io/client-go v0.32.2
11+
sigs.k8s.io/controller-runtime v0.20.2
1112
)
1213

1314
require (
1415
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
16+
github.com/emicklei/go-restful/v3 v3.11.2 // indirect
17+
github.com/evanphx/json-patch/v5 v5.9.11 // indirect
1518
github.com/fsnotify/fsnotify v1.7.0 // indirect
1619
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
1720
github.com/go-logr/logr v1.4.2 // indirect
21+
github.com/go-openapi/jsonpointer v0.21.0 // indirect
22+
github.com/go-openapi/jsonreference v0.20.4 // indirect
23+
github.com/go-openapi/swag v0.23.0 // indirect
1824
github.com/gogo/protobuf v1.3.2 // indirect
25+
github.com/golang/protobuf v1.5.4 // indirect
26+
github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49 // indirect
1927
github.com/google/go-cmp v0.6.0 // indirect
2028
github.com/google/gofuzz v1.2.0 // indirect
29+
github.com/google/uuid v1.6.0 // indirect
2130
github.com/hashicorp/hcl v1.0.0 // indirect
2231
github.com/inconshreveable/mousetrap v1.1.0 // indirect
32+
github.com/josharian/intern v1.0.0 // indirect
2333
github.com/json-iterator/go v1.1.12 // indirect
2434
github.com/magiconair/properties v1.8.7 // indirect
35+
github.com/mailru/easyjson v0.7.7 // indirect
2536
github.com/mitchellh/mapstructure v1.5.0 // indirect
2637
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
2738
github.com/modern-go/reflect2 v1.0.2 // indirect
@@ -32,7 +43,7 @@ require (
3243
github.com/sourcegraph/conc v0.3.0 // indirect
3344
github.com/spf13/afero v1.11.0 // indirect
3445
github.com/spf13/cast v1.6.0 // indirect
35-
github.com/spf13/pflag v1.0.5 // indirect
46+
github.com/spf13/pflag v1.0.6 // indirect
3647
github.com/subosito/gotenv v1.6.0 // indirect
3748
github.com/x448/float16 v0.8.4 // indirect
3849
go.uber.org/multierr v1.11.0 // indirect
@@ -43,12 +54,14 @@ require (
4354
golang.org/x/term v0.27.0 // indirect
4455
golang.org/x/text v0.21.0 // indirect
4556
golang.org/x/time v0.7.0 // indirect
57+
google.golang.org/protobuf v1.35.1 // indirect
4658
gopkg.in/inf.v0 v0.9.1 // indirect
4759
gopkg.in/ini.v1 v1.67.0 // indirect
4860
gopkg.in/yaml.v3 v3.0.1 // indirect
49-
k8s.io/api v0.32.1 // indirect
50-
k8s.io/apiextensions-apiserver v0.30.4 // indirect
61+
k8s.io/api v0.32.2 // indirect
62+
k8s.io/apiextensions-apiserver v0.32.1 // indirect
5163
k8s.io/klog/v2 v2.130.1 // indirect
64+
k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect
5265
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect
5366
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
5467
sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect

0 commit comments

Comments
 (0)