Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 2 additions & 9 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,15 @@
package cmd

import (
"fmt"

"github.com/spf13/cobra"
)

var rootCmd = &cobra.Command{
Use: "cloudctl",
Short: "A CLI tool to manage clusters, including OIDC login and kubeconfig sync.",
Short: "A CLI tool to access Greenhouse clusters",
Long: `cloudctl is a command line interface that helps:

1) Fetch and merge Kubeconfigs from a special cluster CRD`,
1) Fetch and merge kubeconfigs from central Greenhouse cluster`,
}

func Execute() error {
Expand All @@ -25,8 +23,3 @@ func init() {
// Add subcommands here
rootCmd.AddCommand(syncCmd)
}

// A utility function that might be used across multiple commands
func printDebugMessage(msg string) {
fmt.Println("DEBUG:", msg)
}
134 changes: 81 additions & 53 deletions cmd/sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,69 +12,94 @@ import (
"maps"
"strings"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"

"github.com/cloudoperators/greenhouse/pkg/apis/greenhouse/v1alpha1"
"github.com/spf13/cobra"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/rest"
clientcmd "k8s.io/client-go/tools/clientcmd"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
"sigs.k8s.io/controller-runtime/pkg/client"
)

var (
greenhouseCentralClusterKubeconfig string
greenhouseRemoteClusterKubeconfig string
prefix string
mergeIdenticalUsers bool
greenhouseClusterKubeconfig string
greenhouseClusterContext string
greenhouseClusterNamespace string
remoteClusterKubeconfig string
remoteClusterName string
prefix string
mergeIdenticalUsers bool
)

func init() {
syncCmd.Flags().StringVar(&greenhouseCentralClusterKubeconfig, "central-cluster-kubeconfig", clientcmd.RecommendedHomeFile, "kubeconfig for central Greenhouse cluster")
syncCmd.Flags().StringVar(&greenhouseRemoteClusterKubeconfig, "remote-cluster-kubeconfig", clientcmd.RecommendedHomeFile, "kubeconfig for remote Greenhouse clusters")
syncCmd.Flags().StringVar(&prefix, "prefix", "cloudctl", "prefix for kubeconfig entries. It is used to separate and manage the entries of this tool only")
syncCmd.Flags().StringVarP(&greenhouseClusterKubeconfig, "greenhouse-cluster-kubeconfig", "k", clientcmd.RecommendedHomeFile, "kubeconfig file path for Greenhouse cluster")
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")
syncCmd.Flags().StringVarP(&greenhouseClusterNamespace, "greenhouse-cluster-namespace", "n", "", "namespace for greenhouse-cluster-kubeconfig, it is the same value as Greenhouse organization")
syncCmd.MarkFlagRequired("greenhouse-cluster-namespace")
syncCmd.Flags().StringVarP(&remoteClusterKubeconfig, "remote-cluster-kubeconfig", "r", clientcmd.RecommendedHomeFile, "kubeconfig file path for remote clusters")
syncCmd.Flags().StringVar(&remoteClusterName, "remote-cluster-name", "", "name of the remote cluster, if not set all clusters are retrieved")
syncCmd.Flags().StringVar(&prefix, "prefix", "cloudctl", "prefix for kubeconfig entries. it is used to separate and manage the entries of this tool only")
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")
}

var (
syncCmd = &cobra.Command{
Use: "sync",
Short: "Fetches remote kubeconfigs from Greenhouse cluster and merges them into your local config",
Short: "Fetches kubeconfigs of remote clusters from Greenhouse cluster and merges them into your local config",
RunE: runSync,
}
)

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

centralConfig, err := clientcmd.BuildConfigFromFlags("", greenhouseCentralClusterKubeconfig)
centralConfig, err := clientcmd.BuildConfigFromFlags("", greenhouseClusterKubeconfig)
if err != nil {
return fmt.Errorf("failed to build central kubeconfig: %w", err)
return fmt.Errorf("failed to build greenhouse kubeconfig: %w", err)
}

dynamicClient, err := dynamic.NewForConfig(centralConfig)
if err != nil {
return fmt.Errorf("failed to create dynamic client: %w", err)
if greenhouseClusterContext != "" {
centralConfig, err = configWithContext(greenhouseClusterContext, greenhouseClusterKubeconfig)
if err != nil {
return fmt.Errorf("failed to build greenhouse kubeconfig with context %s: %w", greenhouseClusterContext, err)
}
}

gvr := schema.GroupVersionResource{
Group: "greenhouse.sap",
Version: "v1alpha1",
Resource: "clusterkubeconfigs",
// Create a scheme and register Greenhouse types.
scheme := runtime.NewScheme()
if err := v1alpha1.AddToScheme(scheme); err != nil {
return fmt.Errorf("failed to add greenhouse scheme: %w", err)
}

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

if len(unstructuredList.Items) == 0 {
ctx := cmd.Context()
var clusterKubeconfigs []v1alpha1.ClusterKubeconfig

// If a specific remote cluster name is provided, fetch that single resource;
// otherwise, list all ClusterKubeconfigs in the given namespace.
if remoteClusterName != "" {
var ckc v1alpha1.ClusterKubeconfig
if err := c.Get(ctx, client.ObjectKey{Namespace: greenhouseClusterNamespace, Name: remoteClusterName}, &ckc); err != nil {
return fmt.Errorf("failed to get ClusterKubeconfig %q: %w", remoteClusterName, err)
}
clusterKubeconfigs = append(clusterKubeconfigs, ckc)
} else {
var list v1alpha1.ClusterKubeconfigList
if err := c.List(ctx, &list, client.InNamespace(greenhouseClusterNamespace)); err != nil {
return fmt.Errorf("failed to list ClusterKubeconfigs: %w", err)
}
clusterKubeconfigs = list.Items
}
if len(clusterKubeconfigs) == 0 {
log.Println("No ClusterKubeconfigs found to sync.")
return nil
}

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

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

err = writeConfig(localConfig, greenhouseRemoteClusterKubeconfig)
err = writeConfig(localConfig, remoteClusterKubeconfig)
if err != nil {
return fmt.Errorf("failed to write merged kubeconfig: %w", err)
}

log.Println("Successfully synced and merged the new cluster kubeconfig with your local config.")
log.Println("Successfully synced and merged into your local config.")
return nil
}

func buildIncomingKubeconfig(items []unstructured.Unstructured) (*clientcmdapi.Config, error) {
// buildIncomingKubeconfig converts the list of typed ClusterKubeconfig objects
// into a clientcmdapi.Config.
func buildIncomingKubeconfig(items []v1alpha1.ClusterKubeconfig) (*clientcmdapi.Config, error) {
kubeconfig := clientcmdapi.NewConfig()

for _, unstructuredItem := range items {
var ckc v1alpha1.ClusterKubeconfig
err := runtime.DefaultUnstructuredConverter.FromUnstructured(unstructuredItem.Object, &ckc)
if err != nil {
return nil, fmt.Errorf("failed to convert unstructured to ClusterKubeconfig: %w", err)
}

// Assuming each ClusterKubeconfig has exactly one context, authInfo, and cluster
for _, ckc := range items {
// Assuming each ClusterKubeconfig has exactly one context, authInfo, and cluster.
if len(ckc.Spec.Kubeconfig.Contexts) > 0 {
ctx := ckc.Spec.Kubeconfig.Contexts[0]
kubeconfig.Contexts[ctx.Name] = &clientcmdapi.Context{
Cluster: ctx.Context.Cluster,
AuthInfo: ctx.Context.AuthInfo,
Namespace: ctx.Context.Namespace,
ctxItem := ckc.Spec.Kubeconfig.Contexts[0]
kubeconfig.Contexts[ctxItem.Name] = &clientcmdapi.Context{
Cluster: ctxItem.Context.Cluster,
AuthInfo: ctxItem.Context.AuthInfo,
Namespace: ctxItem.Context.Namespace,
}
}

if len(ckc.Spec.Kubeconfig.AuthInfo) > 0 {
auth := ckc.Spec.Kubeconfig.AuthInfo[0].AuthInfo
kubeconfig.AuthInfos[ckc.Spec.Kubeconfig.AuthInfo[0].Name] = &clientcmdapi.AuthInfo{
ClientCertificateData: auth.ClientCertificateData,
ClientKeyData: auth.ClientKeyData,
AuthProvider: &auth.AuthProvider,
authItem := ckc.Spec.Kubeconfig.AuthInfo[0]
kubeconfig.AuthInfos[authItem.Name] = &clientcmdapi.AuthInfo{
ClientCertificateData: authItem.AuthInfo.ClientCertificateData,
ClientKeyData: authItem.AuthInfo.ClientKeyData,
AuthProvider: &authItem.AuthInfo.AuthProvider,
}
}

if len(ckc.Spec.Kubeconfig.Clusters) > 0 {
cluster := ckc.Spec.Kubeconfig.Clusters[0].Cluster
kubeconfig.Clusters[ckc.Spec.Kubeconfig.Clusters[0].Name] = &clientcmdapi.Cluster{
Server: cluster.Server,
CertificateAuthorityData: cluster.CertificateAuthorityData,
clusterItem := ckc.Spec.Kubeconfig.Clusters[0]
kubeconfig.Clusters[clusterItem.Name] = &clientcmdapi.Cluster{
Server: clusterItem.Cluster.Server,
CertificateAuthorityData: clusterItem.Cluster.CertificateAuthorityData,
}
}

}

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

return mergedAuth
}

func configWithContext(context, kubeconfigPath string) (*rest.Config, error) {
return clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
&clientcmd.ClientConfigLoadingRules{ExplicitPath: kubeconfigPath},
&clientcmd.ConfigOverrides{
CurrentContext: context,
}).ClientConfig()
}
25 changes: 19 additions & 6 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,35 @@ go 1.23.4

require (
github.com/cloudoperators/greenhouse v0.0.1-alpha.1
github.com/spf13/cobra v1.8.1
github.com/spf13/cobra v1.9.1
github.com/spf13/viper v1.19.0
k8s.io/apimachinery v0.32.1
k8s.io/client-go v0.32.1
k8s.io/apimachinery v0.32.2
k8s.io/client-go v0.32.2
sigs.k8s.io/controller-runtime v0.20.2
)

require (
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/emicklei/go-restful/v3 v3.11.2 // indirect
github.com/evanphx/json-patch/v5 v5.9.11 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference v0.20.4 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
Expand All @@ -32,7 +43,7 @@ require (
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/spf13/pflag v1.0.6 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/x448/float16 v0.8.4 // indirect
go.uber.org/multierr v1.11.0 // indirect
Expand All @@ -43,12 +54,14 @@ require (
golang.org/x/term v0.27.0 // indirect
golang.org/x/text v0.21.0 // indirect
golang.org/x/time v0.7.0 // indirect
google.golang.org/protobuf v1.35.1 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/api v0.32.1 // indirect
k8s.io/apiextensions-apiserver v0.30.4 // indirect
k8s.io/api v0.32.2 // indirect
k8s.io/apiextensions-apiserver v0.32.1 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect
Expand Down
Loading
Loading