From b441e7700e7d9585afe3704916299342be932102 Mon Sep 17 00:00:00 2001 From: jamesbeedy Date: Tue, 13 Jan 2026 20:31:07 +0000 Subject: [PATCH] feat: support kubeconfig from string These changes enable kubeconfig to be passed as a string to initialize the helm Configuration --- Dockerfile | 2 +- README.md | 44 ++ examples/kubeconfig_usage.py | 297 +++++++++ go/shim/kubeconfig_string.go | 106 ++++ go/shim/main.go | 111 ++-- go/shim/main.go.bak | 1132 ++++++++++++++++++++++++++++++++++ helm_sdkpy/actions.py | 20 +- uv.lock | 2 +- 8 files changed, 1663 insertions(+), 51 deletions(-) create mode 100644 examples/kubeconfig_usage.py create mode 100644 go/shim/kubeconfig_string.go create mode 100644 go/shim/main.go.bak diff --git a/Dockerfile b/Dockerfile index 7946781..f06b4c5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -42,7 +42,7 @@ RUN mkdir -p /build/helm_sdkpy/_lib/linux-amd64 && \ cd shim && \ go build -buildmode=c-shared \ -o /build/helm_sdkpy/_lib/linux-amd64/libhelm_sdkpy.so \ - main.go + . # Verify the shared library was built RUN ls -lh /build/helm_sdkpy/_lib/linux-amd64/libhelm_sdkpy.so && \ diff --git a/README.md b/README.md index 6b24000..a0e7fbe 100644 --- a/README.md +++ b/README.md @@ -138,6 +138,50 @@ chart_path="https://charts.bitnami.com/bitnami/nginx-15.0.0.tgz" chart_path="https://example.com/charts/myapp-1.2.3.tgz" ``` +## 🔧 Kubeconfig Configuration + +helm-sdkpy provides flexible options for configuring Kubernetes cluster access: + +### Default Kubeconfig +```python +# Uses $KUBECONFIG env var or ~/.kube/config +config = helm_sdkpy.Configuration(namespace="default") +``` + +### File Path +```python +# Explicit path to kubeconfig file +config = helm_sdkpy.Configuration( + namespace="default", + kubeconfig="/path/to/kubeconfig.yaml" +) +``` + +### YAML String +Pass kubeconfig content directly as a string - useful for dynamic configurations, secrets, or CI/CD environments: +```python +# Kubeconfig from environment variable +kubeconfig_content = os.environ.get("KUBECONFIG_CONTENT") + +# Or read from a secret, API response, etc. +config = helm_sdkpy.Configuration( + namespace="default", + kubeconfig=kubeconfig_content # YAML string auto-detected +) +``` + +### Specific Context +```python +# Use a specific context from multi-cluster kubeconfig +config = helm_sdkpy.Configuration( + namespace="production", + kubeconfig="/path/to/kubeconfig.yaml", + kubecontext="production-cluster" +) +``` + +See [examples/kubeconfig_usage.py](examples/kubeconfig_usage.py) for more detailed examples. + ## 📖 API Overview ### Core Actions (All Async) diff --git a/examples/kubeconfig_usage.py b/examples/kubeconfig_usage.py new file mode 100644 index 0000000..df17f29 --- /dev/null +++ b/examples/kubeconfig_usage.py @@ -0,0 +1,297 @@ +#!/usr/bin/env python3 +# Copyright 2025 Vantage Compute +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Kubeconfig Usage Example + +This example demonstrates different ways to configure kubeconfig in helm-sdkpy: +1. Using the default kubeconfig (~/.kube/config or $KUBECONFIG) +2. Using an explicit file path +3. Using a kubeconfig YAML string directly + +The kubeconfig string approach is particularly useful when: +- Working with dynamic or programmatically generated configurations +- Retrieving kubeconfig from Kubernetes secrets, environment variables, or APIs +- Working in containerized environments where file access may be limited +""" + +import asyncio +import os +from pathlib import Path + +import helm_sdkpy + + +async def example_default_kubeconfig(): + """Example 1: Use default kubeconfig location. + + This uses $KUBECONFIG environment variable if set, + otherwise falls back to ~/.kube/config. + """ + print("=" * 60) + print("Example 1: Default Kubeconfig") + print("=" * 60) + + # When kubeconfig is None, uses default locations + config = helm_sdkpy.Configuration( + namespace="default", + # kubeconfig=None uses default: $KUBECONFIG or ~/.kube/config + ) + + # List releases to verify connection + list_action = helm_sdkpy.List(config) + releases = await list_action.run() + print(f"Found {len(releases)} releases in default namespace") + + return config + + +async def example_kubeconfig_filepath(): + """Example 2: Use explicit kubeconfig file path. + + Useful when you have multiple kubeconfig files for different clusters. + """ + print("\n" + "=" * 60) + print("Example 2: Kubeconfig from File Path") + print("=" * 60) + + # Specify an explicit path to a kubeconfig file + kubeconfig_path = os.path.expanduser("~/.kube/config") + + # Or use a different file for a specific cluster + # kubeconfig_path = "/path/to/production-cluster.yaml" + # kubeconfig_path = "/etc/rancher/k3s/k3s.yaml" + + config = helm_sdkpy.Configuration( + namespace="kube-system", + kubeconfig=kubeconfig_path, + ) + + # List releases in kube-system namespace + list_action = helm_sdkpy.List(config) + releases = await list_action.run() + print(f"Found {len(releases)} releases in kube-system namespace") + + return config + + +async def example_kubeconfig_string(): + """Example 3: Use kubeconfig YAML content as a string. + + This is useful when: + - Retrieving kubeconfig from environment variables + - Loading from Kubernetes secrets + - Programmatically generating configurations + - Working in serverless or containerized environments + """ + print("\n" + "=" * 60) + print("Example 3: Kubeconfig from String") + print("=" * 60) + + # Example: Read kubeconfig content from a file into a string + # In practice, this could come from an environment variable, + # API response, Kubernetes secret, etc. + + kubeconfig_path = os.path.expanduser("~/.kube/config") + if Path(kubeconfig_path).exists(): + with open(kubeconfig_path, "r") as f: + kubeconfig_content = f.read() + else: + # Example kubeconfig YAML structure (won't work without real cluster) + kubeconfig_content = """ +apiVersion: v1 +kind: Config +clusters: +- cluster: + server: https://kubernetes.example.com:6443 + certificate-authority-data: + name: my-cluster +contexts: +- context: + cluster: my-cluster + user: my-user + namespace: default + name: my-context +current-context: my-context +users: +- name: my-user + user: + token: +""" + + # Pass the YAML content directly as the kubeconfig parameter + # helm-sdkpy automatically detects if it's a file path or YAML content + config = helm_sdkpy.Configuration( + namespace="default", + kubeconfig=kubeconfig_content, # YAML string, not a file path! + ) + + # List releases to verify connection + list_action = helm_sdkpy.List(config) + releases = await list_action.run() + print(f"Found {len(releases)} releases using kubeconfig string") + + return config + + +async def example_kubeconfig_from_env(): + """Example 4: Load kubeconfig from environment variable. + + Many CI/CD systems and container orchestrators provide kubeconfig + as an environment variable. + """ + print("\n" + "=" * 60) + print("Example 4: Kubeconfig from Environment Variable") + print("=" * 60) + + # Common patterns for kubeconfig in environment variables: + # - KUBECONFIG_CONTENT: Full YAML content + # - KUBECONFIG_BASE64: Base64-encoded YAML content + + import base64 + + kubeconfig_content = None + + # Try getting YAML content directly + if "KUBECONFIG_CONTENT" in os.environ: + kubeconfig_content = os.environ["KUBECONFIG_CONTENT"] + print("Loaded kubeconfig from KUBECONFIG_CONTENT env var") + + # Try getting base64-encoded content (common in CI/CD) + elif "KUBECONFIG_BASE64" in os.environ: + kubeconfig_content = base64.b64decode( + os.environ["KUBECONFIG_BASE64"] + ).decode("utf-8") + print("Loaded kubeconfig from KUBECONFIG_BASE64 env var") + + # Fall back to file path from KUBECONFIG env var + elif "KUBECONFIG" in os.environ: + kubeconfig_path = os.environ["KUBECONFIG"] + print(f"Using kubeconfig file from KUBECONFIG env var: {kubeconfig_path}") + config = helm_sdkpy.Configuration( + namespace="default", + kubeconfig=kubeconfig_path, + ) + list_action = helm_sdkpy.List(config) + releases = await list_action.run() + print(f"Found {len(releases)} releases") + return config + + else: + print("No kubeconfig environment variable found") + print("Set KUBECONFIG_CONTENT, KUBECONFIG_BASE64, or KUBECONFIG") + return None + + # Use the content string directly + config = helm_sdkpy.Configuration( + namespace="default", + kubeconfig=kubeconfig_content, + ) + + list_action = helm_sdkpy.List(config) + releases = await list_action.run() + print(f"Found {len(releases)} releases") + + return config + + +async def example_kubeconfig_with_context(): + """Example 5: Use kubeconfig with specific context. + + When your kubeconfig has multiple contexts (clusters), you can + specify which one to use. + """ + print("\n" + "=" * 60) + print("Example 5: Kubeconfig with Specific Context") + print("=" * 60) + + kubeconfig_path = os.path.expanduser("~/.kube/config") + + # Specify both the kubeconfig file and the context to use + config = helm_sdkpy.Configuration( + namespace="default", + kubeconfig=kubeconfig_path, + kubecontext="my-cluster-context", # Use a specific context + ) + + print(f"Using kubeconfig: {kubeconfig_path}") + print(f"Using context: my-cluster-context") + + # This would fail if the context doesn't exist + # list_action = helm_sdkpy.List(config) + # releases = await list_action.run() + + return config + + +async def main(): + """Run all kubeconfig usage examples.""" + print("helm-sdkpy Kubeconfig Usage Examples") + print("=" * 60) + print(f"helm-sdkpy version: {helm_sdkpy.__version__}") + + try: + print(f"Library version: {helm_sdkpy.get_version()}") + except helm_sdkpy.HelmLibraryNotFound: + print("\nLibrary not found - please build the library first with 'just build-lib'") + return + + # Run examples that work with available kubeconfig + try: + await example_default_kubeconfig() + except Exception as e: + print(f"Example 1 failed (expected if no cluster): {e}") + + try: + await example_kubeconfig_filepath() + except Exception as e: + print(f"Example 2 failed (expected if no cluster): {e}") + + try: + await example_kubeconfig_string() + except Exception as e: + print(f"Example 3 failed (expected if no cluster): {e}") + + try: + await example_kubeconfig_from_env() + except Exception as e: + print(f"Example 4 failed (expected if no env vars set): {e}") + + print("\n" + "=" * 60) + print("Summary: Kubeconfig Configuration Options") + print("=" * 60) + print(""" +Configuration(namespace, kubeconfig, kubecontext) parameters: + +1. kubeconfig=None (default) + - Uses $KUBECONFIG environment variable if set + - Falls back to ~/.kube/config + +2. kubeconfig="/path/to/config.yaml" + - Uses explicit file path to kubeconfig + +3. kubeconfig="apiVersion: v1\\nkind: Config\\n..." + - YAML content passed directly as a string + - Auto-detected by looking for apiVersion:, kind:, or clusters: markers + - Useful for dynamic configurations, secrets, or environment variables + +4. kubecontext="my-context" + - Select a specific context from the kubeconfig + - Works with both file paths and string content +""") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/go/shim/kubeconfig_string.go b/go/shim/kubeconfig_string.go new file mode 100644 index 0000000..0e77350 --- /dev/null +++ b/go/shim/kubeconfig_string.go @@ -0,0 +1,106 @@ +// Copyright 2025 Vantage Compute +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( +"strings" + +"k8s.io/apimachinery/pkg/api/meta" +"k8s.io/client-go/discovery" +"k8s.io/client-go/discovery/cached/memory" +"k8s.io/client-go/rest" +"k8s.io/client-go/restmapper" +"k8s.io/client-go/tools/clientcmd" +clientcmdapi "k8s.io/client-go/tools/clientcmd/api" +) + +// kubeconfigStringGetter implements genericclioptions.RESTClientGetter +// for loading kubeconfig from a string instead of a file. +type kubeconfigStringGetter struct { +kubeconfigContent string +namespace string +context string +cachedDiscovery discovery.CachedDiscoveryInterface +} + +// NewKubeconfigStringGetter creates a RESTClientGetter that loads +// kubeconfig from a YAML string instead of a file path. +func NewKubeconfigStringGetter(kubeconfigContent, namespace, context string) *kubeconfigStringGetter { +return &kubeconfigStringGetter{ +kubeconfigContent: kubeconfigContent, +namespace: namespace, +context: context, +} +} + +// ToRESTConfig returns a REST config from the kubeconfig string content. +func (k *kubeconfigStringGetter) ToRESTConfig() (*rest.Config, error) { +config, err := clientcmd.RESTConfigFromKubeConfig([]byte(k.kubeconfigContent)) +if err != nil { +return nil, err +} +return config, nil +} + +// ToDiscoveryClient returns a discovery client. +func (k *kubeconfigStringGetter) ToDiscoveryClient() (discovery.CachedDiscoveryInterface, error) { +if k.cachedDiscovery != nil { +return k.cachedDiscovery, nil +} + +config, err := k.ToRESTConfig() +if err != nil { +return nil, err +} + +discoveryClient, err := discovery.NewDiscoveryClientForConfig(config) +if err != nil { +return nil, err +} + +k.cachedDiscovery = memory.NewMemCacheClient(discoveryClient) +return k.cachedDiscovery, nil +} + +// ToRESTMapper returns a RESTMapper. +func (k *kubeconfigStringGetter) ToRESTMapper() (meta.RESTMapper, error) { +discoveryClient, err := k.ToDiscoveryClient() +if err != nil { +return nil, err +} + +mapper := restmapper.NewDeferredDiscoveryRESTMapper(discoveryClient) +return mapper, nil +} + +// ToRawKubeConfigLoader returns a clientcmd.ClientConfig. +func (k *kubeconfigStringGetter) ToRawKubeConfigLoader() clientcmd.ClientConfig { +config, err := clientcmd.NewClientConfigFromBytes([]byte(k.kubeconfigContent)) +if err != nil { +// Return a default config on error +return clientcmd.NewDefaultClientConfig(clientcmdapi.Config{}, &clientcmd.ConfigOverrides{}) +} +return config +} + +// isKubeconfigYAMLContent checks if the string is YAML content rather than a file path. +// It looks for typical kubeconfig YAML markers. +func isKubeconfigYAMLContent(s string) bool { +trimmed := strings.TrimSpace(s) +return strings.HasPrefix(trimmed, "apiVersion:") || +strings.HasPrefix(trimmed, "kind:") || +strings.Contains(trimmed, "\nclusters:") || +strings.Contains(trimmed, "\ncontexts:") +} diff --git a/go/shim/main.go b/go/shim/main.go index 4835d7c..2619ede 100644 --- a/go/shim/main.go +++ b/go/shim/main.go @@ -40,6 +40,7 @@ import ( "helm.sh/helm/v4/pkg/kube" "helm.sh/helm/v4/pkg/registry" "helm.sh/helm/v4/pkg/repo/v1" + "k8s.io/cli-runtime/pkg/genericclioptions" ) // Configuration state @@ -110,61 +111,75 @@ func helm_sdkpy_version_number() C.int { //export helm_sdkpy_config_create func helm_sdkpy_config_create(namespace *C.char, kubeconfig *C.char, kubecontext *C.char, handle_out *C.helm_sdkpy_handle) C.int { - ns := C.GoString(namespace) - kc := C.GoString(kubeconfig) - kctx := C.GoString(kubecontext) - - // Create environment settings - envs := cli.New() - if ns != "" { - envs.SetNamespace(ns) - } - if kc != "" { - envs.KubeConfig = kc - } - if kctx != "" { - envs.KubeContext = kctx - } +ns := C.GoString(namespace) +kc := C.GoString(kubeconfig) +kctx := C.GoString(kubecontext) + +var restClientGetter genericclioptions.RESTClientGetter +var envs *cli.EnvSettings + +// Check if kubeconfig is YAML content or a file path +if kc != "" && isKubeconfigYAMLContent(kc) { +// Use custom RESTClientGetter for in-memory kubeconfig +restClientGetter = NewKubeconfigStringGetter(kc, ns, kctx) +envs = cli.New() +if ns != "" { +envs.SetNamespace(ns) +} +} else { +// Standard file-based kubeconfig +envs = cli.New() +if ns != "" { +envs.SetNamespace(ns) +} +if kc != "" { +envs.KubeConfig = kc +} +if kctx != "" { +envs.KubeContext = kctx +} +restClientGetter = envs.RESTClientGetter() +} - // Create action configuration - cfg := new(action.Configuration) +// Create action configuration +cfg := new(action.Configuration) - // Initialize the configuration with Kubernetes settings - err := cfg.Init(envs.RESTClientGetter(), envs.Namespace(), os.Getenv("HELM_DRIVER")) - if err != nil { - return setError(fmt.Errorf("failed to initialize helm config: %w", err)) - } +// Initialize the configuration with Kubernetes settings +err := cfg.Init(restClientGetter, envs.Namespace(), os.Getenv("HELM_DRIVER")) +if err != nil { +return setError(fmt.Errorf("failed to initialize helm config: %w", err)) +} - // Configure the Kubernetes client to use Ignore field validation - // This allows charts with managedFields in templates (like rook-ceph v1.18.x) - // to install successfully without strict Kubernetes API validation errors - if cfg.KubeClient != nil { - // Note: In Helm v4, field validation is handled via client options during Create/Update - // We'll configure this in the Install action instead - } +// Configure the Kubernetes client to use Ignore field validation +// This allows charts with managedFields in templates (like rook-ceph v1.18.x) +// to install successfully without strict Kubernetes API validation errors +if cfg.KubeClient != nil { +// Note: In Helm v4, field validation is handled via client options during Create/Update +// We'll configure this in the Install action instead +} - // Initialize registry client for OCI operations - registryClient, err := registry.NewClient( - registry.ClientOptDebug(false), - registry.ClientOptEnableCache(true), - registry.ClientOptWriter(os.Stdout), - registry.ClientOptCredentialsFile(envs.RegistryConfig), - ) - if err != nil { - return setError(fmt.Errorf("failed to initialize registry client: %w", err)) - } - cfg.RegistryClient = registryClient +// Initialize registry client for OCI operations +registryClient, err := registry.NewClient( +registry.ClientOptDebug(false), +registry.ClientOptEnableCache(true), +registry.ClientOptWriter(os.Stdout), +registry.ClientOptCredentialsFile(envs.RegistryConfig), +) +if err != nil { +return setError(fmt.Errorf("failed to initialize registry client: %w", err)) +} +cfg.RegistryClient = registryClient - state := &configState{ - cfg: cfg, - envs: envs, - } +state := &configState{ +cfg: cfg, +envs: envs, +} - handle := nextHandle() - configs.Store(handle, state) - *handle_out = handle +handle := nextHandle() +configs.Store(handle, state) +*handle_out = handle - return 0 +return 0 } //export helm_sdkpy_config_destroy diff --git a/go/shim/main.go.bak b/go/shim/main.go.bak new file mode 100644 index 0000000..4835d7c --- /dev/null +++ b/go/shim/main.go.bak @@ -0,0 +1,1132 @@ +// Copyright 2025 Vantage Compute +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +/* +#include +#include +#include + +typedef unsigned long long helm_sdkpy_handle; +*/ +import "C" + +import ( + "encoding/json" + "fmt" + "os" + "sync" + "sync/atomic" + "unsafe" + + "time" + + "helm.sh/helm/v4/pkg/action" + "helm.sh/helm/v4/pkg/chart/v2/loader" + "helm.sh/helm/v4/pkg/cli" + "helm.sh/helm/v4/pkg/getter" + "helm.sh/helm/v4/pkg/kube" + "helm.sh/helm/v4/pkg/registry" + "helm.sh/helm/v4/pkg/repo/v1" +) + +// Configuration state +type configState struct { + cfg *action.Configuration + envs *cli.EnvSettings + mu sync.Mutex +} + +var ( + handleSeq atomic.Uint64 + configs sync.Map + + errMu sync.Mutex + lastErr string + versionCString *C.char +) + +func init() { + versionCString = C.CString("helm-sdkpy-v0.0.1") +} + +func setError(err error) C.int { + errMu.Lock() + defer errMu.Unlock() + if err != nil { + lastErr = err.Error() + return -1 + } + lastErr = "" + return 0 +} + +func recordErrorf(format string, args ...any) C.int { + return setError(fmt.Errorf(format, args...)) +} + +func nextHandle() C.helm_sdkpy_handle { + return C.helm_sdkpy_handle(handleSeq.Add(1)) +} + +//export helm_sdkpy_last_error +func helm_sdkpy_last_error() *C.char { + errMu.Lock() + defer errMu.Unlock() + if lastErr == "" { + return nil + } + return C.CString(lastErr) +} + +//export helm_sdkpy_free +func helm_sdkpy_free(ptr unsafe.Pointer) { + C.free(ptr) +} + +//export helm_sdkpy_version_string +func helm_sdkpy_version_string() *C.char { + return versionCString +} + +//export helm_sdkpy_version_number +func helm_sdkpy_version_number() C.int { + return 1 // Version 0.0.1 +} + +// Configuration management + +//export helm_sdkpy_config_create +func helm_sdkpy_config_create(namespace *C.char, kubeconfig *C.char, kubecontext *C.char, handle_out *C.helm_sdkpy_handle) C.int { + ns := C.GoString(namespace) + kc := C.GoString(kubeconfig) + kctx := C.GoString(kubecontext) + + // Create environment settings + envs := cli.New() + if ns != "" { + envs.SetNamespace(ns) + } + if kc != "" { + envs.KubeConfig = kc + } + if kctx != "" { + envs.KubeContext = kctx + } + + // Create action configuration + cfg := new(action.Configuration) + + // Initialize the configuration with Kubernetes settings + err := cfg.Init(envs.RESTClientGetter(), envs.Namespace(), os.Getenv("HELM_DRIVER")) + if err != nil { + return setError(fmt.Errorf("failed to initialize helm config: %w", err)) + } + + // Configure the Kubernetes client to use Ignore field validation + // This allows charts with managedFields in templates (like rook-ceph v1.18.x) + // to install successfully without strict Kubernetes API validation errors + if cfg.KubeClient != nil { + // Note: In Helm v4, field validation is handled via client options during Create/Update + // We'll configure this in the Install action instead + } + + // Initialize registry client for OCI operations + registryClient, err := registry.NewClient( + registry.ClientOptDebug(false), + registry.ClientOptEnableCache(true), + registry.ClientOptWriter(os.Stdout), + registry.ClientOptCredentialsFile(envs.RegistryConfig), + ) + if err != nil { + return setError(fmt.Errorf("failed to initialize registry client: %w", err)) + } + cfg.RegistryClient = registryClient + + state := &configState{ + cfg: cfg, + envs: envs, + } + + handle := nextHandle() + configs.Store(handle, state) + *handle_out = handle + + return 0 +} + +//export helm_sdkpy_config_destroy +func helm_sdkpy_config_destroy(handle C.helm_sdkpy_handle) { + configs.Delete(handle) +} + +func getConfig(handle C.helm_sdkpy_handle) (*configState, error) { + val, ok := configs.Load(handle) + if !ok { + return nil, fmt.Errorf("invalid configuration handle") + } + return val.(*configState), nil +} + +// Install action + +//export helm_sdkpy_install +func helm_sdkpy_install(handle C.helm_sdkpy_handle, release_name *C.char, chart_path *C.char, values_json *C.char, version *C.char, create_namespace C.int, wait C.int, timeout_seconds C.int, result_json **C.char) C.int { + state, err := getConfig(handle) + if err != nil { + return setError(err) + } + + state.mu.Lock() + defer state.mu.Unlock() + + releaseName := C.GoString(release_name) + chartPath := C.GoString(chart_path) + valuesJSON := C.GoString(values_json) + chartVersion := C.GoString(version) + + // Create install action + client := action.NewInstall(state.cfg) + client.ReleaseName = releaseName + client.Namespace = state.envs.Namespace() + client.CreateNamespace = create_namespace != 0 + + // Use client-side apply instead of server-side to avoid strict field validation + // Server-side apply (default in Helm v4) enforces strict field validation which + // rejects charts with managedFields in templates (like rook-ceph v1.18.x) + client.ServerSideApply = false + + // Disable OpenAPI validation as well + client.DisableOpenAPIValidation = true + + // Set version if provided + if chartVersion != "" { + client.Version = chartVersion + } + + // Configure wait behavior + if wait != 0 { + client.WaitStrategy = kube.StatusWatcherStrategy // Use the status watcher strategy + if timeout_seconds > 0 { + client.Timeout = time.Duration(timeout_seconds) * time.Second + } else { + client.Timeout = 5 * time.Minute // Default timeout + } + } else { + client.WaitStrategy = kube.HookOnlyStrategy // Only wait for hooks by default + } + + // Locate and load the chart (supports local, OCI, and HTTP) + cp, err := client.ChartPathOptions.LocateChart(chartPath, state.envs) + if err != nil { + return setError(fmt.Errorf("failed to locate chart: %w", err)) + } + + // Load the chart from the located path + chart, err := loader.Load(cp) + if err != nil { + return setError(fmt.Errorf("failed to load chart: %w", err)) + } + + // Parse values + var values map[string]interface{} + if valuesJSON != "" { + if err := json.Unmarshal([]byte(valuesJSON), &values); err != nil { + return setError(fmt.Errorf("failed to parse values JSON: %w", err)) + } + } + + // Run the install + rel, err := client.Run(chart, values) + if err != nil { + return setError(fmt.Errorf("install failed: %w", err)) + } + + // Serialize result + resultData, err := json.Marshal(rel) + if err != nil { + return setError(fmt.Errorf("failed to serialize result: %w", err)) + } + + *result_json = C.CString(string(resultData)) + return 0 +} + +// Upgrade action + +//export helm_sdkpy_upgrade +func helm_sdkpy_upgrade(handle C.helm_sdkpy_handle, release_name *C.char, chart_path *C.char, values_json *C.char, version *C.char, result_json **C.char) C.int { + state, err := getConfig(handle) + if err != nil { + return setError(err) + } + + state.mu.Lock() + defer state.mu.Unlock() + + releaseName := C.GoString(release_name) + chartPath := C.GoString(chart_path) + valuesJSON := C.GoString(values_json) + chartVersion := C.GoString(version) + + // Create upgrade action + client := action.NewUpgrade(state.cfg) + client.Namespace = state.envs.Namespace() + + // Set wait strategy + client.WaitStrategy = kube.HookOnlyStrategy + + // Set version if provided + if chartVersion != "" { + client.Version = chartVersion + } + + // Locate and load the chart (supports local, OCI, and HTTP) + cp, err := client.ChartPathOptions.LocateChart(chartPath, state.envs) + if err != nil { + return setError(fmt.Errorf("failed to locate chart: %w", err)) + } + + // Load the chart from the located path + chart, err := loader.Load(cp) + if err != nil { + return setError(fmt.Errorf("failed to load chart: %w", err)) + } + + // Parse values + var values map[string]interface{} + if valuesJSON != "" { + if err := json.Unmarshal([]byte(valuesJSON), &values); err != nil { + return setError(fmt.Errorf("failed to parse values JSON: %w", err)) + } + } + + // Run the upgrade + rel, err := client.Run(releaseName, chart, values) + if err != nil { + return setError(fmt.Errorf("upgrade failed: %w", err)) + } + + // Serialize result + resultData, err := json.Marshal(rel) + if err != nil { + return setError(fmt.Errorf("failed to serialize result: %w", err)) + } + + *result_json = C.CString(string(resultData)) + return 0 +} + +// Uninstall action + +//export helm_sdkpy_uninstall +func helm_sdkpy_uninstall(handle C.helm_sdkpy_handle, release_name *C.char, wait C.int, timeout_seconds C.int, result_json **C.char) C.int { + state, err := getConfig(handle) + if err != nil { + return setError(err) + } + + state.mu.Lock() + defer state.mu.Unlock() + + releaseName := C.GoString(release_name) + + // Create uninstall action + client := action.NewUninstall(state.cfg) + + // Configure wait behavior + if wait != 0 { + client.WaitStrategy = kube.StatusWatcherStrategy // Use the status watcher strategy + if timeout_seconds > 0 { + client.Timeout = time.Duration(timeout_seconds) * time.Second + } else { + client.Timeout = 5 * time.Minute // Default timeout + } + } else { + client.WaitStrategy = kube.HookOnlyStrategy // Only wait for hooks by default + } + + // Run the uninstall + resp, err := client.Run(releaseName) + if err != nil { + return setError(fmt.Errorf("uninstall failed: %w", err)) + } + + // Serialize result + resultData, err := json.Marshal(resp) + if err != nil { + return setError(fmt.Errorf("failed to serialize result: %w", err)) + } + + *result_json = C.CString(string(resultData)) + return 0 +} + +// List action + +//export helm_sdkpy_list +func helm_sdkpy_list(handle C.helm_sdkpy_handle, all C.int, result_json **C.char) C.int { + state, err := getConfig(handle) + if err != nil { + return setError(err) + } + + state.mu.Lock() + defer state.mu.Unlock() + + // Create list action + client := action.NewList(state.cfg) + if all != 0 { + client.All = true + } + + // Run the list + releases, err := client.Run() + if err != nil { + return setError(fmt.Errorf("list failed: %w", err)) + } + + // Serialize result + resultData, err := json.Marshal(releases) + if err != nil { + return setError(fmt.Errorf("failed to serialize result: %w", err)) + } + + *result_json = C.CString(string(resultData)) + return 0 +} + +// Get/Status action + +//export helm_sdkpy_status +func helm_sdkpy_status(handle C.helm_sdkpy_handle, release_name *C.char, result_json **C.char) C.int { + state, err := getConfig(handle) + if err != nil { + return setError(err) + } + + state.mu.Lock() + defer state.mu.Unlock() + + releaseName := C.GoString(release_name) + + // Create status action + client := action.NewStatus(state.cfg) + + // Run the status + rel, err := client.Run(releaseName) + if err != nil { + return setError(fmt.Errorf("status failed: %w", err)) + } + + // Serialize result + resultData, err := json.Marshal(rel) + if err != nil { + return setError(fmt.Errorf("failed to serialize result: %w", err)) + } + + *result_json = C.CString(string(resultData)) + return 0 +} + +// Rollback action + +//export helm_sdkpy_rollback +func helm_sdkpy_rollback(handle C.helm_sdkpy_handle, release_name *C.char, revision C.int, result_json **C.char) C.int { + state, err := getConfig(handle) + if err != nil { + return setError(err) + } + + state.mu.Lock() + defer state.mu.Unlock() + + releaseName := C.GoString(release_name) + + // Create rollback action + client := action.NewRollback(state.cfg) + client.Version = int(revision) + + // Run the rollback + err = client.Run(releaseName) + if err != nil { + return setError(fmt.Errorf("rollback failed: %w", err)) + } + + // Return success message + result := map[string]string{"status": "success"} + resultData, err := json.Marshal(result) + if err != nil { + return setError(fmt.Errorf("failed to serialize result: %w", err)) + } + + *result_json = C.CString(string(resultData)) + return 0 +} + +// Get values action + +//export helm_sdkpy_get_values +func helm_sdkpy_get_values(handle C.helm_sdkpy_handle, release_name *C.char, all_values C.int, result_json **C.char) C.int { + state, err := getConfig(handle) + if err != nil { + return setError(err) + } + + state.mu.Lock() + defer state.mu.Unlock() + + releaseName := C.GoString(release_name) + + // Create get values action + client := action.NewGetValues(state.cfg) + if all_values != 0 { + client.AllValues = true + } + + // Run the get values + values, err := client.Run(releaseName) + if err != nil { + return setError(fmt.Errorf("get values failed: %w", err)) + } + + // Serialize result + resultData, err := json.Marshal(values) + if err != nil { + return setError(fmt.Errorf("failed to serialize result: %w", err)) + } + + *result_json = C.CString(string(resultData)) + return 0 +} + +// History action + +//export helm_sdkpy_history +func helm_sdkpy_history(handle C.helm_sdkpy_handle, release_name *C.char, result_json **C.char) C.int { + state, err := getConfig(handle) + if err != nil { + return setError(err) + } + + state.mu.Lock() + defer state.mu.Unlock() + + releaseName := C.GoString(release_name) + + // Create history action + client := action.NewHistory(state.cfg) + + // Run the history + releases, err := client.Run(releaseName) + if err != nil { + return setError(fmt.Errorf("history failed: %w", err)) + } + + // Serialize result + resultData, err := json.Marshal(releases) + if err != nil { + return setError(fmt.Errorf("failed to serialize result: %w", err)) + } + + *result_json = C.CString(string(resultData)) + return 0 +} + +// Pull action + +//export helm_sdkpy_pull +func helm_sdkpy_pull(handle C.helm_sdkpy_handle, chart_ref *C.char, dest_dir *C.char, version *C.char) C.int { + state, err := getConfig(handle) + if err != nil { + return setError(err) + } + + state.mu.Lock() + defer state.mu.Unlock() + + chartRef := C.GoString(chart_ref) + destDir := C.GoString(dest_dir) + chartVersion := C.GoString(version) + + // Create pull action + client := action.NewPull() + client.Settings = state.envs + if destDir != "" { + client.DestDir = destDir + } + // Set version if provided + if chartVersion != "" { + client.Version = chartVersion + } + + // Run the pull + _, err = client.Run(chartRef) + if err != nil { + return setError(fmt.Errorf("pull failed: %w", err)) + } + + return 0 +} + +// Show chart action + +//export helm_sdkpy_show_chart +func helm_sdkpy_show_chart(handle C.helm_sdkpy_handle, chart_path *C.char, result_json **C.char) C.int { + state, err := getConfig(handle) + if err != nil { + return setError(err) + } + + state.mu.Lock() + defer state.mu.Unlock() + + chartPath := C.GoString(chart_path) + + // Create show action + client := action.NewShow(action.ShowChart, state.cfg) + + // Locate the chart (supports local, OCI, and HTTP) + cp, err := client.ChartPathOptions.LocateChart(chartPath, state.envs) + if err != nil { + return setError(fmt.Errorf("failed to locate chart: %w", err)) + } + + // Run the show + output, err := client.Run(cp) + if err != nil { + return setError(fmt.Errorf("show chart failed: %w", err)) + } + + *result_json = C.CString(output) + return 0 +} + +// Show values action + +//export helm_sdkpy_show_values +func helm_sdkpy_show_values(handle C.helm_sdkpy_handle, chart_path *C.char, result_json **C.char) C.int { + state, err := getConfig(handle) + if err != nil { + return setError(err) + } + + state.mu.Lock() + defer state.mu.Unlock() + + chartPath := C.GoString(chart_path) + + // Create show action + client := action.NewShow(action.ShowValues, state.cfg) + + // Locate the chart (supports local, OCI, and HTTP) + cp, err := client.ChartPathOptions.LocateChart(chartPath, state.envs) + if err != nil { + return setError(fmt.Errorf("failed to locate chart: %w", err)) + } + + // Run the show + output, err := client.Run(cp) + if err != nil { + return setError(fmt.Errorf("show values failed: %w", err)) + } + + *result_json = C.CString(output) + return 0 +} + +// Test action + +//export helm_sdkpy_test +func helm_sdkpy_test(handle C.helm_sdkpy_handle, release_name *C.char, result_json **C.char) C.int { + state, err := getConfig(handle) + if err != nil { + return setError(err) + } + + state.mu.Lock() + defer state.mu.Unlock() + + releaseName := C.GoString(release_name) + + // Create test action + client := action.NewReleaseTesting(state.cfg) + + // Run the test + rel, err := client.Run(releaseName) + if err != nil { + return setError(fmt.Errorf("test failed: %w", err)) + } + + // Serialize result + resultData, err := json.Marshal(rel) + if err != nil { + return setError(fmt.Errorf("failed to serialize result: %w", err)) + } + + *result_json = C.CString(string(resultData)) + return 0 +} + +// Lint action + +//export helm_sdkpy_lint +func helm_sdkpy_lint(handle C.helm_sdkpy_handle, chart_path *C.char, result_json **C.char) C.int { + state, err := getConfig(handle) + if err != nil { + return setError(err) + } + + state.mu.Lock() + defer state.mu.Unlock() + + chartPath := C.GoString(chart_path) + + // For remote charts (OCI/HTTP), we need to locate them first + // For local charts, LocateChart will just return the path as-is + var pathOpts action.ChartPathOptions + + cp, err := pathOpts.LocateChart(chartPath, state.envs) + if err != nil { + return setError(fmt.Errorf("failed to locate chart: %w", err)) + } + + // Create lint action + client := action.NewLint() + + // Run the lint + result := client.Run([]string{cp}, map[string]interface{}{}) + + // Serialize result + resultData, err := json.Marshal(result) + if err != nil { + return setError(fmt.Errorf("failed to serialize result: %w", err)) + } + + *result_json = C.CString(string(resultData)) + return 0 +} + +// Package action + +//export helm_sdkpy_package +func helm_sdkpy_package(handle C.helm_sdkpy_handle, chart_path *C.char, dest_dir *C.char, result_path **C.char) C.int { + state, err := getConfig(handle) + if err != nil { + return setError(err) + } + + state.mu.Lock() + defer state.mu.Unlock() + + chartPath := C.GoString(chart_path) + destDir := C.GoString(dest_dir) + + // Create package action + client := action.NewPackage() + if destDir != "" { + client.Destination = destDir + } + + // Run the package + path, err := client.Run(chartPath, nil) + if err != nil { + return setError(fmt.Errorf("package failed: %w", err)) + } + + *result_path = C.CString(path) + return 0 +} + +// Repository management actions + +//export helm_sdkpy_repo_add +func helm_sdkpy_repo_add(handle C.helm_sdkpy_handle, name *C.char, url *C.char, username *C.char, password *C.char, options_json *C.char) C.int { + state, err := getConfig(handle) + if err != nil { + return setError(err) + } + + state.mu.Lock() + defer state.mu.Unlock() + + repoName := C.GoString(name) + repoURL := C.GoString(url) + userName := C.GoString(username) + passWord := C.GoString(password) + optionsJSON := C.GoString(options_json) + + // Parse options if provided + var options map[string]interface{} + if optionsJSON != "" { + if err := json.Unmarshal([]byte(optionsJSON), &options); err != nil { + return setError(fmt.Errorf("failed to parse options JSON: %w", err)) + } + } + + // Create repo entry + entry := &repo.Entry{ + Name: repoName, + URL: repoURL, + Username: userName, + Password: passWord, + } + + // Apply additional options + if options != nil { + if v, ok := options["insecure_skip_tls_verify"].(bool); ok { + entry.InsecureSkipTLSverify = v + } + if v, ok := options["pass_credentials_all"].(bool); ok { + entry.PassCredentialsAll = v + } + if v, ok := options["cert_file"].(string); ok { + entry.CertFile = v + } + if v, ok := options["key_file"].(string); ok { + entry.KeyFile = v + } + if v, ok := options["ca_file"].(string); ok { + entry.CAFile = v + } + } + + // Get repository file path + repoFile := state.envs.RepositoryConfig + + // Load existing repos + f, err := repo.LoadFile(repoFile) + if err != nil && !os.IsNotExist(err) { + return setError(fmt.Errorf("failed to load repository file: %w", err)) + } + if f == nil { + f = repo.NewFile() + } + + // Check if repo already exists + if f.Has(repoName) { + return setError(fmt.Errorf("repository %s already exists", repoName)) + } + + // Create chart repository and download index + r, err := repo.NewChartRepository(entry, getter.All(state.envs)) + if err != nil { + return setError(fmt.Errorf("failed to create chart repository: %w", err)) + } + + // Set cache path if available + if state.envs.RepositoryCache != "" { + r.CachePath = state.envs.RepositoryCache + } + + // Download the index file + _, err = r.DownloadIndexFile() + if err != nil { + return setError(fmt.Errorf("failed to download index file: %w", err)) + } + + // Add to repo file + f.Update(entry) + + // Write the repo file + if err := f.WriteFile(repoFile, 0644); err != nil { + return setError(fmt.Errorf("failed to write repository file: %w", err)) + } + + return 0 +} + +//export helm_sdkpy_repo_remove +func helm_sdkpy_repo_remove(handle C.helm_sdkpy_handle, name *C.char) C.int { + state, err := getConfig(handle) + if err != nil { + return setError(err) + } + + state.mu.Lock() + defer state.mu.Unlock() + + repoName := C.GoString(name) + + // Get repository file path + repoFile := state.envs.RepositoryConfig + + // Load existing repos + f, err := repo.LoadFile(repoFile) + if err != nil { + return setError(fmt.Errorf("failed to load repository file: %w", err)) + } + + // Remove the repo + if !f.Remove(repoName) { + return setError(fmt.Errorf("no repository named %q found", repoName)) + } + + // Write the repo file + if err := f.WriteFile(repoFile, 0644); err != nil { + return setError(fmt.Errorf("failed to write repository file: %w", err)) + } + + return 0 +} + +//export helm_sdkpy_repo_list +func helm_sdkpy_repo_list(handle C.helm_sdkpy_handle, result_json **C.char) C.int { + state, err := getConfig(handle) + if err != nil { + return setError(err) + } + + state.mu.Lock() + defer state.mu.Unlock() + + // Get repository file path + repoFile := state.envs.RepositoryConfig + + // Load existing repos + f, err := repo.LoadFile(repoFile) + if err != nil && !os.IsNotExist(err) { + return setError(fmt.Errorf("failed to load repository file: %w", err)) + } + if f == nil || len(f.Repositories) == 0 { + *result_json = C.CString("[]") + return 0 + } + + // Serialize result + resultData, err := json.Marshal(f.Repositories) + if err != nil { + return setError(fmt.Errorf("failed to serialize result: %w", err)) + } + + *result_json = C.CString(string(resultData)) + return 0 +} + +//export helm_sdkpy_repo_update +func helm_sdkpy_repo_update(handle C.helm_sdkpy_handle, name *C.char) C.int { + state, err := getConfig(handle) + if err != nil { + return setError(err) + } + + state.mu.Lock() + defer state.mu.Unlock() + + repoName := C.GoString(name) + + // Get repository file path + repoFile := state.envs.RepositoryConfig + + // Load existing repos + f, err := repo.LoadFile(repoFile) + if err != nil { + return setError(fmt.Errorf("failed to load repository file: %w", err)) + } + + // Find the repo to update + var entry *repo.Entry + if repoName != "" { + entry = f.Get(repoName) + if entry == nil { + return setError(fmt.Errorf("no repository named %q found", repoName)) + } + } + + // Update the specified repo or all repos + if repoName != "" { + // Update single repo + r, err := repo.NewChartRepository(entry, getter.All(state.envs)) + if err != nil { + return setError(fmt.Errorf("failed to create chart repository: %w", err)) + } + + if state.envs.RepositoryCache != "" { + r.CachePath = state.envs.RepositoryCache + } + + if _, err := r.DownloadIndexFile(); err != nil { + return setError(fmt.Errorf("failed to update repository %q: %w", repoName, err)) + } + } else { + // Update all repos + for _, entry := range f.Repositories { + r, err := repo.NewChartRepository(entry, getter.All(state.envs)) + if err != nil { + return setError(fmt.Errorf("failed to create chart repository %q: %w", entry.Name, err)) + } + + if state.envs.RepositoryCache != "" { + r.CachePath = state.envs.RepositoryCache + } + + if _, err := r.DownloadIndexFile(); err != nil { + return setError(fmt.Errorf("failed to update repository %q: %w", entry.Name, err)) + } + } + } + + return 0 +} + +// Registry login action + +//export helm_sdkpy_registry_login +func helm_sdkpy_registry_login(handle C.helm_sdkpy_handle, hostname *C.char, username *C.char, password *C.char, options_json *C.char) C.int { + state, err := getConfig(handle) + if err != nil { + return setError(err) + } + + state.mu.Lock() + defer state.mu.Unlock() + + host := C.GoString(hostname) + user := C.GoString(username) + pass := C.GoString(password) + + // Parse options + var options struct { + CertFile string `json:"cert_file"` + KeyFile string `json:"key_file"` + CAFile string `json:"ca_file"` + Insecure bool `json:"insecure"` + PlainHTTP bool `json:"plain_http"` + } + + optionsStr := C.GoString(options_json) + if optionsStr != "" { + if err := json.Unmarshal([]byte(optionsStr), &options); err != nil { + return setError(fmt.Errorf("failed to parse options: %w", err)) + } + } + + // Create registry login action + client := action.NewRegistryLogin(state.cfg) + + // Build options slice + var loginOpts []action.RegistryLoginOpt + + if options.CertFile != "" { + loginOpts = append(loginOpts, action.WithCertFile(options.CertFile)) + } + if options.KeyFile != "" { + loginOpts = append(loginOpts, action.WithKeyFile(options.KeyFile)) + } + if options.CAFile != "" { + loginOpts = append(loginOpts, action.WithCAFile(options.CAFile)) + } + if options.Insecure { + loginOpts = append(loginOpts, action.WithInsecure(options.Insecure)) + } + if options.PlainHTTP { + loginOpts = append(loginOpts, action.WithPlainHTTPLogin(options.PlainHTTP)) + } + + // Run the login + err = client.Run(os.Stdout, host, user, pass, loginOpts...) + if err != nil { + return setError(fmt.Errorf("registry login failed: %w", err)) + } + + return 0 +} + +// Registry logout action + +//export helm_sdkpy_registry_logout +func helm_sdkpy_registry_logout(handle C.helm_sdkpy_handle, hostname *C.char) C.int { + state, err := getConfig(handle) + if err != nil { + return setError(err) + } + + state.mu.Lock() + defer state.mu.Unlock() + + host := C.GoString(hostname) + + // Create registry logout action + client := action.NewRegistryLogout(state.cfg) + + // Run the logout + err = client.Run(os.Stdout, host) + if err != nil { + return setError(fmt.Errorf("registry logout failed: %w", err)) + } + + return 0 +} + +// Push action (for pushing charts to OCI registries) + +//export helm_sdkpy_push +func helm_sdkpy_push(handle C.helm_sdkpy_handle, chart_ref *C.char, remote *C.char, options_json *C.char) C.int { + state, err := getConfig(handle) + if err != nil { + return setError(err) + } + + state.mu.Lock() + defer state.mu.Unlock() + + chartRef := C.GoString(chart_ref) + remoteRef := C.GoString(remote) + + // Parse options + var options struct { + CertFile string `json:"cert_file"` + KeyFile string `json:"key_file"` + CAFile string `json:"ca_file"` + InsecureSkipTLSVerify bool `json:"insecure_skip_tls_verify"` + PlainHTTP bool `json:"plain_http"` + } + + optionsStr := C.GoString(options_json) + if optionsStr != "" { + if err := json.Unmarshal([]byte(optionsStr), &options); err != nil { + return setError(fmt.Errorf("failed to parse options: %w", err)) + } + } + + // Create push action + var pushOpts []action.PushOpt + pushOpts = append(pushOpts, action.WithPushConfig(state.cfg)) + + if options.CertFile != "" || options.KeyFile != "" || options.CAFile != "" { + pushOpts = append(pushOpts, action.WithTLSClientConfig(options.CertFile, options.KeyFile, options.CAFile)) + } + if options.InsecureSkipTLSVerify { + pushOpts = append(pushOpts, action.WithInsecureSkipTLSVerify(options.InsecureSkipTLSVerify)) + } + if options.PlainHTTP { + pushOpts = append(pushOpts, action.WithPlainHTTP(options.PlainHTTP)) + } + + client := action.NewPushWithOpts(pushOpts...) + client.Settings = state.envs + + // Run the push + _, err = client.Run(chartRef, remoteRef) + if err != nil { + return setError(fmt.Errorf("push failed: %w", err)) + } + + return 0 +} + +func main() { + // Required for CGO shared library +} diff --git a/helm_sdkpy/actions.py b/helm_sdkpy/actions.py index f681f3d..f6ac5b2 100644 --- a/helm_sdkpy/actions.py +++ b/helm_sdkpy/actions.py @@ -38,12 +38,30 @@ class Configuration: Args: namespace: Kubernetes namespace to operate in (default: "default") - kubeconfig: Path to kubeconfig file (default: uses $KUBECONFIG or ~/.kube/config) + kubeconfig: Kubeconfig source. Can be: + - None: Uses $KUBECONFIG env var or ~/.kube/config (default) + - File path: Path to a kubeconfig file (e.g., "/path/to/config.yaml") + - YAML string: Kubeconfig content as a YAML string (auto-detected) kubecontext: Kubernetes context to use (default: current context) Example: >>> import asyncio + >>> # Using default kubeconfig >>> config = Configuration(namespace="my-namespace") + >>> + >>> # Using explicit file path + >>> config = Configuration( + ... namespace="default", + ... kubeconfig="/path/to/kubeconfig.yaml" + ... ) + >>> + >>> # Using kubeconfig YAML string (useful for secrets/env vars) + >>> kubeconfig_content = os.environ.get("KUBECONFIG_CONTENT") + >>> config = Configuration( + ... namespace="default", + ... kubeconfig=kubeconfig_content + ... ) + >>> >>> install = Install(config) >>> result = asyncio.run(install.run("my-release", "/path/to/chart")) """ diff --git a/uv.lock b/uv.lock index c0cdcdf..3cac865 100644 --- a/uv.lock +++ b/uv.lock @@ -153,7 +153,7 @@ wheels = [ [[package]] name = "helm-sdkpy" -version = "0.0.12" +version = "0.0.13" source = { editable = "." } dependencies = [ { name = "cffi" },