Skip to content

Commit 830894c

Browse files
committed
feat: support kubeconfig from string
These changes enable kubeconfig to be passed as a string to initialize the helm Configuration
1 parent 4e16800 commit 830894c

File tree

5 files changed

+1303
-50
lines changed

5 files changed

+1303
-50
lines changed

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ RUN mkdir -p /build/helm_sdkpy/_lib/linux-amd64 && \
4242
cd shim && \
4343
go build -buildmode=c-shared \
4444
-o /build/helm_sdkpy/_lib/linux-amd64/libhelm_sdkpy.so \
45-
main.go
45+
.
4646

4747
# Verify the shared library was built
4848
RUN ls -lh /build/helm_sdkpy/_lib/linux-amd64/libhelm_sdkpy.so && \

go/shim/kubeconfig_string.go

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
// Copyright 2025 Vantage Compute
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package main
16+
17+
import (
18+
"strings"
19+
20+
"k8s.io/apimachinery/pkg/api/meta"
21+
"k8s.io/client-go/discovery"
22+
"k8s.io/client-go/discovery/cached/memory"
23+
"k8s.io/client-go/rest"
24+
"k8s.io/client-go/restmapper"
25+
"k8s.io/client-go/tools/clientcmd"
26+
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
27+
)
28+
29+
// kubeconfigStringGetter implements genericclioptions.RESTClientGetter
30+
// for loading kubeconfig from a string instead of a file.
31+
type kubeconfigStringGetter struct {
32+
kubeconfigContent string
33+
namespace string
34+
context string
35+
cachedDiscovery discovery.CachedDiscoveryInterface
36+
}
37+
38+
// NewKubeconfigStringGetter creates a RESTClientGetter that loads
39+
// kubeconfig from a YAML string instead of a file path.
40+
func NewKubeconfigStringGetter(kubeconfigContent, namespace, context string) *kubeconfigStringGetter {
41+
return &kubeconfigStringGetter{
42+
kubeconfigContent: kubeconfigContent,
43+
namespace: namespace,
44+
context: context,
45+
}
46+
}
47+
48+
// ToRESTConfig returns a REST config from the kubeconfig string content.
49+
func (k *kubeconfigStringGetter) ToRESTConfig() (*rest.Config, error) {
50+
config, err := clientcmd.RESTConfigFromKubeConfig([]byte(k.kubeconfigContent))
51+
if err != nil {
52+
return nil, err
53+
}
54+
return config, nil
55+
}
56+
57+
// ToDiscoveryClient returns a discovery client.
58+
func (k *kubeconfigStringGetter) ToDiscoveryClient() (discovery.CachedDiscoveryInterface, error) {
59+
if k.cachedDiscovery != nil {
60+
return k.cachedDiscovery, nil
61+
}
62+
63+
config, err := k.ToRESTConfig()
64+
if err != nil {
65+
return nil, err
66+
}
67+
68+
discoveryClient, err := discovery.NewDiscoveryClientForConfig(config)
69+
if err != nil {
70+
return nil, err
71+
}
72+
73+
k.cachedDiscovery = memory.NewMemCacheClient(discoveryClient)
74+
return k.cachedDiscovery, nil
75+
}
76+
77+
// ToRESTMapper returns a RESTMapper.
78+
func (k *kubeconfigStringGetter) ToRESTMapper() (meta.RESTMapper, error) {
79+
discoveryClient, err := k.ToDiscoveryClient()
80+
if err != nil {
81+
return nil, err
82+
}
83+
84+
mapper := restmapper.NewDeferredDiscoveryRESTMapper(discoveryClient)
85+
return mapper, nil
86+
}
87+
88+
// ToRawKubeConfigLoader returns a clientcmd.ClientConfig.
89+
func (k *kubeconfigStringGetter) ToRawKubeConfigLoader() clientcmd.ClientConfig {
90+
config, err := clientcmd.NewClientConfigFromBytes([]byte(k.kubeconfigContent))
91+
if err != nil {
92+
// Return a default config on error
93+
return clientcmd.NewDefaultClientConfig(clientcmdapi.Config{}, &clientcmd.ConfigOverrides{})
94+
}
95+
return config
96+
}
97+
98+
// isKubeconfigYAMLContent checks if the string is YAML content rather than a file path.
99+
// It looks for typical kubeconfig YAML markers.
100+
func isKubeconfigYAMLContent(s string) bool {
101+
trimmed := strings.TrimSpace(s)
102+
return strings.HasPrefix(trimmed, "apiVersion:") ||
103+
strings.HasPrefix(trimmed, "kind:") ||
104+
strings.Contains(trimmed, "\nclusters:") ||
105+
strings.Contains(trimmed, "\ncontexts:")
106+
}

go/shim/main.go

Lines changed: 63 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import (
4040
"helm.sh/helm/v4/pkg/kube"
4141
"helm.sh/helm/v4/pkg/registry"
4242
"helm.sh/helm/v4/pkg/repo/v1"
43+
"k8s.io/cli-runtime/pkg/genericclioptions"
4344
)
4445

4546
// Configuration state
@@ -110,61 +111,75 @@ func helm_sdkpy_version_number() C.int {
110111

111112
//export helm_sdkpy_config_create
112113
func helm_sdkpy_config_create(namespace *C.char, kubeconfig *C.char, kubecontext *C.char, handle_out *C.helm_sdkpy_handle) C.int {
113-
ns := C.GoString(namespace)
114-
kc := C.GoString(kubeconfig)
115-
kctx := C.GoString(kubecontext)
116-
117-
// Create environment settings
118-
envs := cli.New()
119-
if ns != "" {
120-
envs.SetNamespace(ns)
121-
}
122-
if kc != "" {
123-
envs.KubeConfig = kc
124-
}
125-
if kctx != "" {
126-
envs.KubeContext = kctx
127-
}
114+
ns := C.GoString(namespace)
115+
kc := C.GoString(kubeconfig)
116+
kctx := C.GoString(kubecontext)
117+
118+
var restClientGetter genericclioptions.RESTClientGetter
119+
var envs *cli.EnvSettings
120+
121+
// Check if kubeconfig is YAML content or a file path
122+
if kc != "" && isKubeconfigYAMLContent(kc) {
123+
// Use custom RESTClientGetter for in-memory kubeconfig
124+
restClientGetter = NewKubeconfigStringGetter(kc, ns, kctx)
125+
envs = cli.New()
126+
if ns != "" {
127+
envs.SetNamespace(ns)
128+
}
129+
} else {
130+
// Standard file-based kubeconfig
131+
envs = cli.New()
132+
if ns != "" {
133+
envs.SetNamespace(ns)
134+
}
135+
if kc != "" {
136+
envs.KubeConfig = kc
137+
}
138+
if kctx != "" {
139+
envs.KubeContext = kctx
140+
}
141+
restClientGetter = envs.RESTClientGetter()
142+
}
128143

129-
// Create action configuration
130-
cfg := new(action.Configuration)
144+
// Create action configuration
145+
cfg := new(action.Configuration)
131146

132-
// Initialize the configuration with Kubernetes settings
133-
err := cfg.Init(envs.RESTClientGetter(), envs.Namespace(), os.Getenv("HELM_DRIVER"))
134-
if err != nil {
135-
return setError(fmt.Errorf("failed to initialize helm config: %w", err))
136-
}
147+
// Initialize the configuration with Kubernetes settings
148+
err := cfg.Init(restClientGetter, envs.Namespace(), os.Getenv("HELM_DRIVER"))
149+
if err != nil {
150+
return setError(fmt.Errorf("failed to initialize helm config: %w", err))
151+
}
137152

138-
// Configure the Kubernetes client to use Ignore field validation
139-
// This allows charts with managedFields in templates (like rook-ceph v1.18.x)
140-
// to install successfully without strict Kubernetes API validation errors
141-
if cfg.KubeClient != nil {
142-
// Note: In Helm v4, field validation is handled via client options during Create/Update
143-
// We'll configure this in the Install action instead
144-
}
153+
// Configure the Kubernetes client to use Ignore field validation
154+
// This allows charts with managedFields in templates (like rook-ceph v1.18.x)
155+
// to install successfully without strict Kubernetes API validation errors
156+
if cfg.KubeClient != nil {
157+
// Note: In Helm v4, field validation is handled via client options during Create/Update
158+
// We'll configure this in the Install action instead
159+
}
145160

146-
// Initialize registry client for OCI operations
147-
registryClient, err := registry.NewClient(
148-
registry.ClientOptDebug(false),
149-
registry.ClientOptEnableCache(true),
150-
registry.ClientOptWriter(os.Stdout),
151-
registry.ClientOptCredentialsFile(envs.RegistryConfig),
152-
)
153-
if err != nil {
154-
return setError(fmt.Errorf("failed to initialize registry client: %w", err))
155-
}
156-
cfg.RegistryClient = registryClient
161+
// Initialize registry client for OCI operations
162+
registryClient, err := registry.NewClient(
163+
registry.ClientOptDebug(false),
164+
registry.ClientOptEnableCache(true),
165+
registry.ClientOptWriter(os.Stdout),
166+
registry.ClientOptCredentialsFile(envs.RegistryConfig),
167+
)
168+
if err != nil {
169+
return setError(fmt.Errorf("failed to initialize registry client: %w", err))
170+
}
171+
cfg.RegistryClient = registryClient
157172

158-
state := &configState{
159-
cfg: cfg,
160-
envs: envs,
161-
}
173+
state := &configState{
174+
cfg: cfg,
175+
envs: envs,
176+
}
162177

163-
handle := nextHandle()
164-
configs.Store(handle, state)
165-
*handle_out = handle
178+
handle := nextHandle()
179+
configs.Store(handle, state)
180+
*handle_out = handle
166181

167-
return 0
182+
return 0
168183
}
169184

170185
//export helm_sdkpy_config_destroy

0 commit comments

Comments
 (0)