@@ -24,23 +24,29 @@ typedef unsigned long long helm_sdkpy_handle;
2424import "C"
2525
2626import (
27+ "bytes"
2728 "encoding/json"
2829 "fmt"
30+ "net/url"
2931 "os"
32+ "path/filepath"
33+ "strings"
3034 "sync"
3135 "sync/atomic"
3236 "unsafe"
3337
3438 "time"
3539
3640 "helm.sh/helm/v4/pkg/action"
37- "helm.sh/helm/v4/pkg/chart/v2/loader"
41+ "helm.sh/helm/v4/pkg/chart"
42+ "helm.sh/helm/v4/pkg/chart/loader"
3843 "helm.sh/helm/v4/pkg/cli"
3944 "helm.sh/helm/v4/pkg/getter"
4045 "helm.sh/helm/v4/pkg/kube"
4146 "helm.sh/helm/v4/pkg/registry"
4247 "helm.sh/helm/v4/pkg/repo/v1"
4348 "k8s.io/cli-runtime/pkg/genericclioptions"
49+ "k8s.io/client-go/util/homedir"
4450)
4551
4652// Configuration state
@@ -107,6 +113,211 @@ func helm_sdkpy_version_number() C.int {
107113 return 1 // Version 0.0.1
108114}
109115
116+ // ensureWritableHelmPaths checks if Helm cache paths are set and writable.
117+ // If not, it attempts to configure them to use a writable temp directory.
118+ // This is critical for running in read-only container environments.
119+ func ensureWritableHelmPaths () error {
120+ // Check if HELM_CACHE_HOME is already set
121+ if os .Getenv ("HELM_CACHE_HOME" ) == "" {
122+ // Try default path first
123+ homeDir := homedir .HomeDir ()
124+ defaultCache := filepath .Join (homeDir , ".cache" , "helm" )
125+
126+ // Test if we can write to the default location
127+ if ! isPathWritable (defaultCache ) {
128+ // Fall back to a temp directory
129+ tmpDir := filepath .Join (os .TempDir (), "helm-sdkpy-cache" )
130+ if err := os .MkdirAll (tmpDir , 0755 ); err != nil {
131+ return fmt .Errorf ("cannot create writable cache directory: %w (hint: set HELM_CACHE_HOME to a writable path)" , err )
132+ }
133+ os .Setenv ("HELM_CACHE_HOME" , tmpDir )
134+ }
135+ }
136+
137+ // Check if HELM_CONFIG_HOME is already set
138+ if os .Getenv ("HELM_CONFIG_HOME" ) == "" {
139+ homeDir := homedir .HomeDir ()
140+ defaultConfig := filepath .Join (homeDir , ".config" , "helm" )
141+
142+ if ! isPathWritable (defaultConfig ) {
143+ tmpDir := filepath .Join (os .TempDir (), "helm-sdkpy-config" )
144+ if err := os .MkdirAll (tmpDir , 0755 ); err != nil {
145+ return fmt .Errorf ("cannot create writable config directory: %w (hint: set HELM_CONFIG_HOME to a writable path)" , err )
146+ }
147+ os .Setenv ("HELM_CONFIG_HOME" , tmpDir )
148+ }
149+ }
150+
151+ // Check if HELM_DATA_HOME is already set
152+ if os .Getenv ("HELM_DATA_HOME" ) == "" {
153+ homeDir := homedir .HomeDir ()
154+ defaultData := filepath .Join (homeDir , ".local" , "share" , "helm" )
155+
156+ if ! isPathWritable (defaultData ) {
157+ tmpDir := filepath .Join (os .TempDir (), "helm-sdkpy-data" )
158+ if err := os .MkdirAll (tmpDir , 0755 ); err != nil {
159+ return fmt .Errorf ("cannot create writable data directory: %w (hint: set HELM_DATA_HOME to a writable path)" , err )
160+ }
161+ os .Setenv ("HELM_DATA_HOME" , tmpDir )
162+ }
163+ }
164+
165+ return nil
166+ }
167+
168+ // isPathWritable checks if a path is writable by attempting to create it
169+ // and a test file within it.
170+ func isPathWritable (path string ) bool {
171+ // Try to create the directory
172+ if err := os .MkdirAll (path , 0755 ); err != nil {
173+ return false
174+ }
175+
176+ // Try to create a test file
177+ testFile := filepath .Join (path , ".helm-sdkpy-write-test" )
178+ f , err := os .Create (testFile )
179+ if err != nil {
180+ return false
181+ }
182+ f .Close ()
183+ os .Remove (testFile )
184+
185+ return true
186+ }
187+
188+ // loadChartDiskless loads a chart without requiring filesystem writes.
189+ // For OCI and HTTP charts, it downloads directly to memory and loads from there.
190+ // For local paths, it uses the standard loader.
191+ // This enables helm-sdkpy to work in read-only filesystem environments.
192+ func loadChartDiskless (chartRef string , version string , registryClient * registry.Client , envs * cli.EnvSettings ) (chart.Charter , error ) {
193+ // Check if it's an OCI reference
194+ if registry .IsOCI (chartRef ) {
195+ return loadChartFromOCI (chartRef , version , registryClient )
196+ }
197+
198+ // Check if it's an HTTP/HTTPS URL
199+ if strings .HasPrefix (chartRef , "http://" ) || strings .HasPrefix (chartRef , "https://" ) {
200+ return loadChartFromHTTP (chartRef , envs )
201+ }
202+
203+ // Check if it's a local file or directory
204+ if fi , err := os .Stat (chartRef ); err == nil {
205+ if fi .IsDir () {
206+ return loader .LoadDir (chartRef )
207+ }
208+ return loader .LoadFile (chartRef )
209+ }
210+
211+ // It might be a repo/chart reference (e.g., "bitnami/nginx")
212+ // For these, we need to resolve via repository and then download
213+ return loadChartFromRepo (chartRef , version , envs )
214+ }
215+
216+ // loadChartFromOCI loads a chart directly from an OCI registry into memory.
217+ // No disk writes required - the chart bytes are loaded directly into a chart.Chart object.
218+ func loadChartFromOCI (chartRef string , version string , registryClient * registry.Client ) (chart.Charter , error ) {
219+ if registryClient == nil {
220+ return nil , fmt .Errorf ("registry client is required for OCI charts" )
221+ }
222+
223+ // Build the full reference with version tag
224+ ref := strings .TrimPrefix (chartRef , fmt .Sprintf ("%s://" , registry .OCIScheme ))
225+ if version != "" && ! strings .Contains (ref , ":" ) {
226+ ref = fmt .Sprintf ("%s:%s" , ref , version )
227+ }
228+
229+ // Pull the chart - this downloads to memory, not disk!
230+ result , err := registryClient .Pull (ref )
231+ if err != nil {
232+ return nil , fmt .Errorf ("failed to pull OCI chart: %w" , err )
233+ }
234+
235+ // Load directly from the in-memory bytes
236+ return loader .LoadArchive (bytes .NewReader (result .Chart .Data ))
237+ }
238+
239+ // loadChartFromHTTP loads a chart directly from an HTTP/HTTPS URL into memory.
240+ func loadChartFromHTTP (chartURL string , envs * cli.EnvSettings ) (chart.Charter , error ) {
241+ // Parse the URL to get the scheme
242+ u , err := url .Parse (chartURL )
243+ if err != nil {
244+ return nil , fmt .Errorf ("invalid chart URL: %w" , err )
245+ }
246+
247+ // Get the appropriate getter for HTTP/HTTPS
248+ getters := getter .All (envs )
249+ g , err := getters .ByScheme (u .Scheme )
250+ if err != nil {
251+ return nil , fmt .Errorf ("no getter for scheme %s: %w" , u .Scheme , err )
252+ }
253+
254+ // Download directly to memory
255+ data , err := g .Get (chartURL , getter .WithURL (chartURL ))
256+ if err != nil {
257+ return nil , fmt .Errorf ("failed to download chart: %w" , err )
258+ }
259+
260+ // Load directly from the in-memory bytes
261+ return loader .LoadArchive (data )
262+ }
263+
264+ // loadChartFromRepo resolves a repo/chart reference and loads it.
265+ // This path may still require disk access for repository index caching.
266+ func loadChartFromRepo (chartRef string , version string , envs * cli.EnvSettings ) (chart.Charter , error ) {
267+ // Split the reference into repo and chart name
268+ parts := strings .SplitN (chartRef , "/" , 2 )
269+ if len (parts ) != 2 {
270+ return nil , fmt .Errorf ("invalid chart reference %q: expected format 'repo/chart'" , chartRef )
271+ }
272+
273+ repoName := parts [0 ]
274+ chartName := parts [1 ]
275+
276+ // Load the repository file
277+ repoFile , err := repo .LoadFile (envs .RepositoryConfig )
278+ if err != nil {
279+ return nil , fmt .Errorf ("failed to load repository file: %w" , err )
280+ }
281+
282+ // Find the repository entry
283+ repoEntry := repoFile .Get (repoName )
284+ if repoEntry == nil {
285+ return nil , fmt .Errorf ("repository %q not found" , repoName )
286+ }
287+
288+ // Create a chart repository client
289+ chartRepo , err := repo .NewChartRepository (repoEntry , getter .All (envs ))
290+ if err != nil {
291+ return nil , fmt .Errorf ("failed to create chart repository: %w" , err )
292+ }
293+ chartRepo .CachePath = envs .RepositoryCache
294+
295+ // Find the chart URL in the repo index
296+ indexFile , err := repo .LoadIndexFile (filepath .Join (envs .RepositoryCache , fmt .Sprintf ("%s-index.yaml" , repoName )))
297+ if err != nil {
298+ return nil , fmt .Errorf ("failed to load index file (try running 'helm repo update'): %w" , err )
299+ }
300+
301+ // Get the chart version
302+ cv , err := indexFile .Get (chartName , version )
303+ if err != nil {
304+ return nil , fmt .Errorf ("chart %q version %q not found in repository %q: %w" , chartName , version , repoName , err )
305+ }
306+
307+ if len (cv .URLs ) == 0 {
308+ return nil , fmt .Errorf ("chart %q has no download URLs" , chartName )
309+ }
310+
311+ // Resolve the URL (it might be relative)
312+ chartURL , err := repo .ResolveReferenceURL (repoEntry .URL , cv .URLs [0 ])
313+ if err != nil {
314+ return nil , fmt .Errorf ("failed to resolve chart URL: %w" , err )
315+ }
316+
317+ // Now download via HTTP
318+ return loadChartFromHTTP (chartURL , envs )
319+ }
320+
110321// Configuration management
111322
112323//export helm_sdkpy_config_create
@@ -118,6 +329,12 @@ func helm_sdkpy_config_create(namespace *C.char, kubeconfig *C.char, kubecontext
118329 var restClientGetter genericclioptions.RESTClientGetter
119330 var envs * cli.EnvSettings
120331
332+ // Ensure Helm has writable paths before initializing
333+ // This auto-configures temp directories for read-only filesystem environments
334+ if err := ensureWritableHelmPaths (); err != nil {
335+ return setError (err )
336+ }
337+
121338 // Initialize env settings (needed for all paths)
122339 envs = cli .New ()
123340 if ns != "" {
@@ -250,14 +467,9 @@ func helm_sdkpy_install(handle C.helm_sdkpy_handle, release_name *C.char, chart_
250467 client .WaitStrategy = kube .HookOnlyStrategy // Only wait for hooks by default
251468 }
252469
253- // Locate and load the chart (supports local, OCI, and HTTP)
254- cp , err := client .ChartPathOptions .LocateChart (chartPath , state .envs )
255- if err != nil {
256- return setError (fmt .Errorf ("failed to locate chart: %w" , err ))
257- }
258-
259- // Load the chart from the located path
260- chart , err := loader .Load (cp )
470+ // Load chart using diskless approach (works in read-only filesystem environments)
471+ // For OCI and HTTP charts, this downloads directly to memory - no disk writes needed
472+ loadedChart , err := loadChartDiskless (chartPath , chartVersion , state .cfg .RegistryClient , state .envs )
261473 if err != nil {
262474 return setError (fmt .Errorf ("failed to load chart: %w" , err ))
263475 }
@@ -271,7 +483,7 @@ func helm_sdkpy_install(handle C.helm_sdkpy_handle, release_name *C.char, chart_
271483 }
272484
273485 // Run the install
274- rel , err := client .Run (chart , values )
486+ rel , err := client .Run (loadedChart , values )
275487 if err != nil {
276488 return setError (fmt .Errorf ("install failed: %w" , err ))
277489 }
@@ -315,14 +527,9 @@ func helm_sdkpy_upgrade(handle C.helm_sdkpy_handle, release_name *C.char, chart_
315527 client .Version = chartVersion
316528 }
317529
318- // Locate and load the chart (supports local, OCI, and HTTP)
319- cp , err := client .ChartPathOptions .LocateChart (chartPath , state .envs )
320- if err != nil {
321- return setError (fmt .Errorf ("failed to locate chart: %w" , err ))
322- }
323-
324- // Load the chart from the located path
325- chart , err := loader .Load (cp )
530+ // Load chart using diskless approach (works in read-only filesystem environments)
531+ // For OCI and HTTP charts, this downloads directly to memory - no disk writes needed
532+ loadedChart , err := loadChartDiskless (chartPath , chartVersion , state .cfg .RegistryClient , state .envs )
326533 if err != nil {
327534 return setError (fmt .Errorf ("failed to load chart: %w" , err ))
328535 }
@@ -336,7 +543,7 @@ func helm_sdkpy_upgrade(handle C.helm_sdkpy_handle, release_name *C.char, chart_
336543 }
337544
338545 // Run the upgrade
339- rel , err := client .Run (releaseName , chart , values )
546+ rel , err := client .Run (releaseName , loadedChart , values )
340547 if err != nil {
341548 return setError (fmt .Errorf ("upgrade failed: %w" , err ))
342549 }
0 commit comments