Skip to content

Commit 94537ff

Browse files
Connect to spoke from hub kubeconfig
Signed-off-by: Arnob Kumar Saha <arnob@appscode.com>
1 parent 27df459 commit 94537ff

11 files changed

Lines changed: 1264 additions & 23 deletions

File tree

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ require (
3333
kmodules.xyz/resource-metadata v0.40.2
3434
kubedb.dev/apimachinery v0.60.0-rc.0.0.20251220111349-72d44c386702
3535
kubeops.dev/installer v0.0.0-20250502231931-f9d6b5e4a0a3
36+
open-cluster-management.io/api v1.0.0
3637
sigs.k8s.io/controller-runtime v0.22.4
3738
sigs.k8s.io/gateway-api v1.4.0
3839
sigs.k8s.io/yaml v1.6.0
@@ -261,7 +262,6 @@ require (
261262
kubeops.dev/sidekick v0.0.12 // indirect
262263
kubestash.dev/apimachinery v0.22.0 // indirect
263264
kubevault.dev/apimachinery v0.22.0 // indirect
264-
open-cluster-management.io/api v1.0.0 // indirect
265265
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
266266
sigs.k8s.io/kustomize/api v0.20.1 // indirect
267267
sigs.k8s.io/kustomize/kyaml v0.20.1 // indirect

pkg/cmds/debug/license/hub.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/*
2+
Copyright AppsCode Inc. and Contributors
3+
4+
Licensed under the AppsCode Community License 1.0.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
https://github.com/appscode/licenses/raw/1.0.0/AppsCode-Community-1.0.0.md
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package license
18+
19+
import (
20+
"context"
21+
"os"
22+
"path"
23+
24+
"go.bytebuilders.dev/cli/pkg/cmds/utils"
25+
26+
"gomodules.xyz/go-sh"
27+
corev1 "k8s.io/api/core/v1"
28+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
29+
"k8s.io/apimachinery/pkg/types"
30+
"k8s.io/klog/v2"
31+
ocmapi "open-cluster-management.io/api/cluster/v1"
32+
)
33+
34+
func (g *licenseOpts) fun() error {
35+
getter := utils.NewKubeconfigGetter(scheme, g.config)
36+
37+
var clusters ocmapi.ManagedClusterList
38+
err := g.kc.List(context.TODO(), &clusters)
39+
if err != nil {
40+
return err
41+
}
42+
43+
for _, cluster := range clusters.Items {
44+
// cond types: HubAcceptedManagedCluster, ManagedClusterJoined, ManagedClusterConditionAvailable, ManagedClusterConditionClockSynced
45+
if !isConditionTrue(cluster.Status.Conditions, ocmapi.ManagedClusterConditionAvailable) {
46+
continue
47+
}
48+
kc, err := getter.GetSpokeClient(cluster.Name)
49+
if err != nil {
50+
return err
51+
}
52+
var ns corev1.Namespace
53+
err = kc.Get(context.TODO(), types.NamespacedName{Name: "org1"}, &ns)
54+
if err != nil {
55+
klog.Errorf("err getting namespace: %v", err)
56+
}
57+
klog.Infof("managed Cluster: %v , ns: %v found -------- Success!", cluster.Name, ns.Name)
58+
59+
spokePath := path.Join(g.rootPath, cluster.Name)
60+
err = os.MkdirAll(spokePath, dirPerm)
61+
if err != nil {
62+
return err
63+
}
64+
65+
err = g.runCommands(spokePath)
66+
if err != nil {
67+
klog.Errorf("err running commands: %v", err)
68+
return err
69+
}
70+
}
71+
return nil
72+
}
73+
74+
func isConditionTrue(conditions []metav1.Condition, conditionType string) bool {
75+
for _, condition := range conditions {
76+
if condition.Type == conditionType && condition.Status == metav1.ConditionTrue {
77+
return true
78+
}
79+
}
80+
return false
81+
}
82+
83+
// Helper that runs your license-collection logic against any kubeconfig
84+
func (g *licenseOpts) runCommands(spokePath string) error {
85+
out := []byte("\n\n===== License status =====\n")
86+
args := []any{"--kubeconfig", utils.DefaultPath, "get", "licensestatus"}
87+
o, err := sh.Command(kubectlCommand, args...).Output()
88+
if err != nil {
89+
return err
90+
}
91+
out = append(out, o...)
92+
93+
p := path.Join(spokePath, defaultFile)
94+
return os.WriteFile(p, out, 0o640)
95+
}

pkg/cmds/debug/license/license.go

Lines changed: 4 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,6 @@ import (
2121
"os"
2222
"path"
2323

24-
catalogapi "go.bytebuilders.dev/catalog/api/catalog/v1alpha1"
25-
catgwapi "go.bytebuilders.dev/catalog/api/gateway/v1alpha1"
26-
27-
acmev1 "github.com/cert-manager/cert-manager/pkg/apis/acme/v1"
28-
certv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1"
29-
egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1"
30-
flux "github.com/fluxcd/helm-controller/api/v2"
3124
"github.com/spf13/cobra"
3225
"k8s.io/apimachinery/pkg/runtime"
3326
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
@@ -37,28 +30,16 @@ import (
3730
"k8s.io/klog/v2"
3831
cmdutil "k8s.io/kubectl/pkg/cmd/util"
3932
kubedbscheme "kubedb.dev/apimachinery/client/clientset/versioned/scheme"
33+
ocmapi "open-cluster-management.io/api/cluster/v1"
4034
"sigs.k8s.io/controller-runtime/pkg/client"
41-
gwapi "sigs.k8s.io/gateway-api/apis/v1"
42-
gwapia3 "sigs.k8s.io/gateway-api/apis/v1alpha3"
43-
gwapib1 "sigs.k8s.io/gateway-api/apis/v1beta1"
44-
vgapi "voyagermesh.dev/gateway-api/apis/gateway/v1alpha1"
4535
)
4636

4737
var scheme = runtime.NewScheme()
4838

4939
func init() {
5040
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
51-
utilruntime.Must(catalogapi.AddToScheme(scheme))
52-
utilruntime.Must(catgwapi.AddToScheme(scheme))
5341
utilruntime.Must(kubedbscheme.AddToScheme(scheme))
54-
utilruntime.Must(gwapi.Install(scheme))
55-
utilruntime.Must(gwapia3.Install(scheme))
56-
utilruntime.Must(gwapib1.Install(scheme))
57-
utilruntime.Must(vgapi.AddToScheme(scheme))
58-
utilruntime.Must(egv1a1.AddToScheme(scheme))
59-
utilruntime.Must(flux.AddToScheme(scheme))
60-
utilruntime.Must(certv1.AddToScheme(scheme))
61-
utilruntime.Must(acmev1.AddToScheme(scheme))
42+
utilruntime.Must(ocmapi.AddToScheme(scheme))
6243
}
6344

6445
func NewCmdLicense(f cmdutil.Factory) *cobra.Command {
@@ -69,7 +50,8 @@ func NewCmdLicense(f cmdutil.Factory) *cobra.Command {
6950
DisableAutoGenTag: true,
7051
RunE: func(cmd *cobra.Command, args []string) error {
7152
klog.Infof("The debug info will be generated in current directory under '%s' folder", defaultDir)
72-
return opt.run()
53+
_ = opt.run()
54+
return opt.fun()
7355
},
7456
}
7557
return cmd

pkg/cmds/utils/kubeconfig.go

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
/*
2+
Copyright AppsCode Inc. and Contributors
3+
4+
Licensed under the AppsCode Community License 1.0.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
https://github.com/appscode/licenses/raw/1.0.0/AppsCode-Community-1.0.0.md
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package utils
18+
19+
import (
20+
"context"
21+
"fmt"
22+
"log"
23+
"net"
24+
"net/url"
25+
"strings"
26+
27+
corev1 "k8s.io/api/core/v1"
28+
"k8s.io/apimachinery/pkg/runtime"
29+
"k8s.io/client-go/rest"
30+
"k8s.io/client-go/tools/clientcmd"
31+
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
32+
"k8s.io/klog/v2"
33+
kmapi "kmodules.xyz/client-go/api/v1"
34+
"sigs.k8s.io/controller-runtime/pkg/client"
35+
)
36+
37+
const (
38+
firstUser = "ace.user.1"
39+
firstOrg = "2"
40+
defaultCluster = "generated-cluster"
41+
defaultContext = "generated-context"
42+
defaultUser = "default"
43+
DefaultPath = "/tmp/kubeconfig"
44+
)
45+
46+
type KubeConfigGetter struct {
47+
scm *runtime.Scheme
48+
config *rest.Config
49+
kc client.Client
50+
kubeCfg *clientcmdapi.Config
51+
secret *corev1.Secret
52+
}
53+
54+
func NewKubeconfigGetter(scm *runtime.Scheme, config *rest.Config) *KubeConfigGetter {
55+
kc, err := client.New(config, client.Options{Scheme: scm})
56+
if err != nil {
57+
log.Fatalf("failed to create client: %v", err)
58+
}
59+
60+
secret, err := getAuthSecret(kc)
61+
if err != nil {
62+
klog.Errorf("failed to get auth secret: %v", err)
63+
return nil
64+
}
65+
66+
return &KubeConfigGetter{
67+
scm: scm,
68+
config: config,
69+
kc: kc,
70+
kubeCfg: RESTConfigToKubeconfig(config),
71+
secret: &secret,
72+
}
73+
}
74+
75+
func RESTConfigToKubeconfig(cfg *rest.Config) *clientcmdapi.Config {
76+
kubeCfg := clientcmdapi.NewConfig()
77+
78+
kubeCfg.Clusters[defaultCluster] = &clientcmdapi.Cluster{
79+
Server: cfg.Host,
80+
CertificateAuthorityData: cfg.CAData,
81+
InsecureSkipTLSVerify: cfg.Insecure,
82+
}
83+
84+
kubeCfg.AuthInfos[defaultUser] = &clientcmdapi.AuthInfo{
85+
Token: cfg.BearerToken,
86+
ClientCertificateData: cfg.CertData,
87+
ClientKeyData: cfg.KeyData,
88+
Username: cfg.Username,
89+
Password: cfg.Password,
90+
}
91+
92+
kubeCfg.Contexts[defaultContext] = &clientcmdapi.Context{
93+
Cluster: defaultCluster,
94+
AuthInfo: defaultUser,
95+
}
96+
97+
kubeCfg.CurrentContext = defaultContext
98+
99+
return kubeCfg
100+
}
101+
102+
func getAuthSecret(kc client.Client) (corev1.Secret, error) {
103+
var secretList corev1.SecretList
104+
err := kc.List(context.TODO(), &secretList, &client.ListOptions{Namespace: "open-cluster-management-cluster-auth"})
105+
if err != nil {
106+
return corev1.Secret{}, err
107+
}
108+
var secret corev1.Secret
109+
for _, s := range secretList.Items {
110+
// kubectl get secrets -n open-cluster-management-cluster-auth ace.user.1-token-j8dhhc
111+
// annotations:
112+
// kubernetes.io/service-account.name: ace.user.1
113+
if strings.HasPrefix(s.Name, firstUser) {
114+
val, exists := s.Annotations[corev1.ServiceAccountNameKey]
115+
if exists && val == firstUser {
116+
secret = s
117+
}
118+
}
119+
}
120+
121+
if secret.Name == "" {
122+
return corev1.Secret{}, fmt.Errorf("secret not found")
123+
}
124+
return secret, nil
125+
}
126+
127+
func (g *KubeConfigGetter) GetSpokeClient(spokeName string) (client.Client, error) {
128+
spokeKubeCfg := g.getSpokeKubeConfig(spokeName)
129+
return g.convertToClient(spokeKubeCfg)
130+
}
131+
132+
func (g *KubeConfigGetter) getSpokeKubeConfig(spokeName string) *clientcmdapi.Config {
133+
kubeCfg := g.kubeCfg.DeepCopy()
134+
// clusters:
135+
// - cluster:
136+
// certificate-authority-data: <ca-from-above-secret>
137+
// server: https://<hub-ip>:6443/apis/gateway.open-cluster-management.io/v1alpha1/clustergateways/<spoke-name>/proxy
138+
// users:
139+
// - name: default
140+
// user:
141+
// as: ace.user.1
142+
// as-user-extra:
143+
// ace.appscode.com/org-id:
144+
// - "2"
145+
// token: <token-from-above-secret>
146+
kubeCfg.Clusters[defaultCluster].CertificateAuthorityData = g.secret.Data["ca.crt"]
147+
kubeCfg.Clusters[defaultCluster].Server = calculateServerURL(kubeCfg.Clusters[defaultCluster].Server, spokeName)
148+
kubeCfg.AuthInfos[defaultUser] = calculateUser(string(g.secret.Data["token"]))
149+
return kubeCfg
150+
}
151+
152+
func calculateServerURL(cur string, spokeName string) string {
153+
getHost := func(server string) (string, error) {
154+
u, err := url.Parse(server)
155+
if err != nil {
156+
return "", err
157+
}
158+
159+
host := u.Hostname()
160+
161+
// If it's an IP, return as-is
162+
if net.ParseIP(host) != nil {
163+
return host, nil
164+
}
165+
166+
// Otherwise it's DNS
167+
return host, nil
168+
}
169+
host, err := getHost(cur)
170+
if err != nil {
171+
_ = fmt.Errorf("err getting host: %v", err)
172+
}
173+
ret := fmt.Sprintf("https://%s:6443/apis/gateway.open-cluster-management.io/v1alpha1/clustergateways/%s/proxy", host, spokeName)
174+
fmt.Printf("Using host: %s\n", ret)
175+
return ret
176+
}
177+
178+
func calculateUser(token string) *clientcmdapi.AuthInfo {
179+
user := clientcmdapi.NewAuthInfo()
180+
user.Token = token
181+
user.Impersonate = firstUser
182+
user.ImpersonateUserExtra = map[string][]string{
183+
kmapi.AceOrgIDKey: {firstOrg},
184+
}
185+
return user
186+
}
187+
188+
func (g *KubeConfigGetter) convertToClient(kubeCfg *clientcmdapi.Config) (client.Client, error) {
189+
err := writeKubeconfig(DefaultPath, kubeCfg)
190+
if err != nil {
191+
return nil, err
192+
}
193+
194+
config, err := clientcmd.BuildConfigFromFlags("", DefaultPath)
195+
if err != nil {
196+
klog.Errorf("err building config: %v", err)
197+
}
198+
199+
kc, err := client.New(config, client.Options{Scheme: g.scm})
200+
if err != nil {
201+
klog.Errorf("err creating client: %v", err)
202+
}
203+
204+
return kc, nil
205+
}
206+
207+
func writeKubeconfig(path string, cfg *clientcmdapi.Config) error {
208+
return clientcmd.WriteToFile(*cfg, path)
209+
}

vendor/modules.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2018,6 +2018,7 @@ kubestash.dev/apimachinery/crds
20182018
kubevault.dev/apimachinery/apis
20192019
# open-cluster-management.io/api v1.0.0
20202020
## explicit; go 1.23.6
2021+
open-cluster-management.io/api/cluster/v1
20212022
open-cluster-management.io/api/work/v1
20222023
# sigs.k8s.io/controller-runtime v0.22.4 => github.com/kmodules/controller-runtime v0.22.5-0.20251227114913-f011264689cd
20232024
## explicit; go 1.24.0

0 commit comments

Comments
 (0)