Skip to content

Commit 552eb5e

Browse files
committed
fix: override fs dependency
Override the filesystem dependency.
1 parent 730c33c commit 552eb5e

File tree

1 file changed

+226
-19
lines changed

1 file changed

+226
-19
lines changed

go/shim/main.go

Lines changed: 226 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -24,23 +24,29 @@ typedef unsigned long long helm_sdkpy_handle;
2424
import "C"
2525

2626
import (
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

Comments
 (0)