@@ -3,6 +3,7 @@ package helm
33import (
44 "fmt"
55 "os"
6+ "os/exec"
67 "path/filepath"
78 "strings"
89 "time"
@@ -15,6 +16,7 @@ import (
1516 "helm.sh/helm/v3/pkg/cli"
1617 "helm.sh/helm/v3/pkg/downloader"
1718 "helm.sh/helm/v3/pkg/getter"
19+ "helm.sh/helm/v3/pkg/registry"
1820 "helm.sh/helm/v3/pkg/repo"
1921)
2022
@@ -104,8 +106,14 @@ func HelmInstall(
104106 return handleInstallationSuccess (rel , namespace )
105107}
106108
107- // loadChart determines the chart source and loads it appropriately
109+ // LoadChart determines the chart source and loads it appropriately
108110func LoadChart (chartRef , repoURL , version string , settings * cli.EnvSettings ) (* chart.Chart , error ) {
111+ // Check if it's an OCI registry reference
112+ if strings .HasPrefix (chartRef , "oci://" ) {
113+ fmt .Printf ("🐳 Loading OCI chart from registry...\n " )
114+ return LoadOCIChart (chartRef , version , settings , false ) // You might want to make debug configurable
115+ }
116+
109117 if repoURL != "" {
110118 fmt .Printf ("🌐 Loading remote chart from repository...\n " )
111119 return LoadRemoteChart (chartRef , repoURL , version , settings )
@@ -116,9 +124,327 @@ func LoadChart(chartRef, repoURL, version string, settings *cli.EnvSettings) (*c
116124 return LoadFromLocalRepo (chartRef , version , settings )
117125 }
118126
127+ // Handle local chart file or directory
119128 return loader .Load (chartRef )
120129}
121130
131+ // LoadOCIChart loads a chart from an OCI registry
132+ func LoadOCIChart (chartRef , version string , settings * cli.EnvSettings , debug bool ) (* chart.Chart , error ) {
133+ if debug {
134+ pterm .Printf ("Loading OCI chart: %s (version: %s)\n " , chartRef , version )
135+ }
136+
137+ // Ensure cache directory exists
138+ if err := ensureHelmCacheDir (settings .RepositoryCache ); err != nil {
139+ return nil , fmt .Errorf ("failed to create helm cache directory: %w" , err )
140+ }
141+
142+ // Create registry client
143+ registryClient , err := newRegistryClient (debug )
144+ if err != nil {
145+ return nil , fmt .Errorf ("failed to create registry client: %w" , err )
146+ }
147+
148+ // Create action configuration with registry client
149+ actionConfig := & action.Configuration {
150+ RegistryClient : registryClient ,
151+ }
152+
153+ // Create pull action
154+ pull := action .NewPullWithOpts (action .WithConfig (actionConfig ))
155+ pull .Settings = settings
156+ pull .Version = version
157+ pull .Untar = false // Keep as .tgz file
158+ pull .DestDir = settings .RepositoryCache
159+
160+ // Run the pull command
161+ fmt .Printf ("⬇️ Pulling OCI chart: %s...\n " , chartRef )
162+ downloadedFile , err := pull .Run (chartRef )
163+ if err != nil {
164+ // The error might be about the file path, not the pull itself
165+ if debug {
166+ fmt .Printf ("⚠️ Pull returned error but may have succeeded: %v\n " , err )
167+ fmt .Printf ("⚠️ Downloaded file path from pull.Run(): %s\n " , downloadedFile )
168+ }
169+
170+ // Continue to try loading the chart anyway
171+ return findAndLoadChartFromCache (chartRef , settings , debug )
172+ }
173+
174+ if debug {
175+ fmt .Printf ("✅ Pull reported success, downloaded to: %s\n " , downloadedFile )
176+ }
177+
178+ // Try to find and load the chart
179+ return findAndLoadChartFromCache (chartRef , settings , debug )
180+ }
181+
182+ // Helper function to ensure helm cache directory exists
183+ func ensureHelmCacheDir (cacheDir string ) error {
184+ if _ , err := os .Stat (cacheDir ); os .IsNotExist (err ) {
185+ fmt .Printf ("📁 Creating helm cache directory: %s\n " , cacheDir )
186+ if err := os .MkdirAll (cacheDir , 0755 ); err != nil {
187+ return fmt .Errorf ("failed to create directory %s: %w" , cacheDir , err )
188+ }
189+ } else if err != nil {
190+ return fmt .Errorf ("failed to check cache directory: %w" , err )
191+ }
192+ return nil
193+ }
194+
195+ // Helper function to find and load chart from cache
196+ func findAndLoadChartFromCache (chartRef string , settings * cli.EnvSettings , debug bool ) (* chart.Chart , error ) {
197+ // Ensure directory exists (double-check)
198+ if err := ensureHelmCacheDir (settings .RepositoryCache ); err != nil {
199+ return nil , err
200+ }
201+
202+ // List all files in cache directory
203+ files , err := os .ReadDir (settings .RepositoryCache )
204+ if err != nil {
205+ return nil , fmt .Errorf ("failed to read cache directory %s: %w" , settings .RepositoryCache , err )
206+ }
207+
208+ if debug {
209+ fmt .Printf ("📁 Searching for chart in cache directory: %s\n " , settings .RepositoryCache )
210+ fmt .Printf ("📁 Files found (%d):\n " , len (files ))
211+ for i , file := range files {
212+ info , _ := file .Info ()
213+ fmt .Printf (" %d. %s (size: %d)\n " , i + 1 , file .Name (), info .Size ())
214+ }
215+ }
216+
217+ // If no files found, try a different approach
218+ if len (files ) == 0 {
219+ fmt .Println ("⚠️ No files found in cache, attempting direct helm CLI pull..." )
220+ return pullWithHelmCLI (chartRef , settings , debug )
221+ }
222+
223+ // Extract chart name from OCI reference
224+ ref := strings .TrimPrefix (chartRef , "oci://" )
225+ baseName := filepath .Base (ref )
226+
227+ // Remove tag if present
228+ chartName := baseName
229+ if idx := strings .LastIndex (chartName , ":" ); idx != - 1 {
230+ chartName = chartName [:idx ]
231+ }
232+
233+ if debug {
234+ fmt .Printf ("🔍 Looking for chart matching: %s\n " , chartName )
235+ }
236+
237+ // Look for .tgz files (most common)
238+ for _ , file := range files {
239+ if ! file .IsDir () && strings .HasSuffix (strings .ToLower (file .Name ()), ".tgz" ) {
240+ fullPath := filepath .Join (settings .RepositoryCache , file .Name ())
241+
242+ if debug {
243+ fmt .Printf (" Trying .tgz file: %s\n " , file .Name ())
244+ }
245+
246+ chartObj , err := loader .Load (fullPath )
247+ if err == nil {
248+ if debug {
249+ fmt .Printf ("✅ Successfully loaded chart from: %s\n " , fullPath )
250+ }
251+ return chartObj , nil
252+ }
253+
254+ if debug {
255+ fmt .Printf ("❌ Failed to load as chart: %v\n " , err )
256+ }
257+ }
258+ }
259+
260+ // Try any file (might not have .tgz extension)
261+ for _ , file := range files {
262+ if ! file .IsDir () {
263+ fullPath := filepath .Join (settings .RepositoryCache , file .Name ())
264+
265+ if debug {
266+ fmt .Printf (" Trying any file: %s\n " , file .Name ())
267+ }
268+
269+ chartObj , err := loader .Load (fullPath )
270+ if err == nil {
271+ if debug {
272+ fmt .Printf ("✅ Successfully loaded chart from: %s\n " , fullPath )
273+ }
274+ return chartObj , nil
275+ }
276+ }
277+ }
278+
279+ return nil , fmt .Errorf ("no valid chart file found in cache directory: %s" , settings .RepositoryCache )
280+ }
281+
282+ // Fallback function using helm CLI directly
283+ func pullWithHelmCLI (chartRef string , settings * cli.EnvSettings , debug bool ) (* chart.Chart , error ) {
284+ fmt .Printf ("🔄 Using helm CLI for OCI pull...\n " )
285+
286+ // Ensure cache directory exists
287+ if err := ensureHelmCacheDir (settings .RepositoryCache ); err != nil {
288+ return nil , err
289+ }
290+
291+ // Create a temporary directory
292+ tempDir , err := os .MkdirTemp ("" , "helm-oci-*" )
293+ if err != nil {
294+ return nil , fmt .Errorf ("failed to create temp directory: %w" , err )
295+ }
296+ defer os .RemoveAll (tempDir )
297+
298+ // Build helm command
299+ args := []string {"pull" , chartRef , "--destination" , tempDir }
300+
301+ // Add version if specified
302+ if strings .Contains (chartRef , ":" ) {
303+ // Version might be in the chartRef itself
304+ fmt .Printf ("📦 Chart reference includes version/tag\n " )
305+ } else {
306+ // Parse version from chartRef or use default
307+ ref := strings .TrimPrefix (chartRef , "oci://" )
308+ if idx := strings .LastIndex (ref , ":" ); idx != - 1 {
309+ version := ref [idx + 1 :]
310+ args = append (args , "--version" , version )
311+ }
312+ }
313+
314+ if debug {
315+ args = append (args , "--debug" )
316+ }
317+
318+ // Execute helm pull
319+ cmd := exec .Command ("helm" , args ... )
320+ cmd .Env = os .Environ ()
321+
322+ // Enable OCI experimental feature
323+ cmd .Env = append (cmd .Env , "HELM_EXPERIMENTAL_OCI=1" )
324+
325+ // Handle GitHub Container Registry authentication
326+ if strings .Contains (chartRef , "ghcr.io" ) {
327+ fmt .Println ("🔑 Detected GHCR registry" )
328+ if token := os .Getenv ("GITHUB_TOKEN" ); token != "" {
329+ fmt .Println ("🔑 Using GITHUB_TOKEN for authentication" )
330+ // Helm should automatically use GITHUB_TOKEN for ghcr.io
331+ cmd .Env = append (cmd .Env , "GITHUB_TOKEN=" + token )
332+ }
333+ }
334+
335+ output , err := cmd .CombinedOutput ()
336+ if debug {
337+ fmt .Printf ("📋 Helm CLI output:\n %s\n " , output )
338+ }
339+
340+ if err != nil {
341+ return nil , fmt .Errorf ("helm CLI pull failed: %w\n Output: %s" , err , output )
342+ }
343+
344+ // Find and load the downloaded chart
345+ files , err := os .ReadDir (tempDir )
346+ if err != nil {
347+ return nil , fmt .Errorf ("failed to read temp directory: %w" , err )
348+ }
349+
350+ for _ , file := range files {
351+ if ! file .IsDir () {
352+ fullPath := filepath .Join (tempDir , file .Name ())
353+ fmt .Printf ("📦 Attempting to load: %s\n " , file .Name ())
354+
355+ chartObj , err := loader .Load (fullPath )
356+ if err == nil {
357+ fmt .Printf ("✅ Successfully loaded chart\n " )
358+
359+ // Copy to cache directory for future use
360+ cachePath := filepath .Join (settings .RepositoryCache , file .Name ())
361+ if err := copyFile (fullPath , cachePath ); err == nil && debug {
362+ fmt .Printf ("📁 Copied to cache: %s\n " , cachePath )
363+ }
364+
365+ return chartObj , nil
366+ }
367+
368+ if debug {
369+ fmt .Printf ("❌ Failed to load: %v\n " , err )
370+ }
371+ }
372+ }
373+
374+ return nil , fmt .Errorf ("no chart file found after helm pull" )
375+ }
376+
377+ // Helper function to copy file
378+ func copyFile (src , dst string ) error {
379+ input , err := os .ReadFile (src )
380+ if err != nil {
381+ return err
382+ }
383+ return os .WriteFile (dst , input , 0644 )
384+ }
385+
386+ // newRegistryClient creates a registry client for OCI operations
387+ func newRegistryClient (debug bool ) (* registry.Client , error ) {
388+ // Create registry client options
389+ opts := []registry.ClientOption {
390+ registry .ClientOptWriter (os .Stderr ), // Use stderr for debug output
391+ registry .ClientOptDebug (debug ),
392+ }
393+
394+ // Try multiple credential sources
395+ helmConfig := helmHome ()
396+
397+ // Check for Docker config in multiple locations
398+ possibleCredFiles := []string {
399+ filepath .Join (helmConfig , "config.json" ),
400+ filepath .Join (os .Getenv ("HOME" ), ".docker/config.json" ),
401+ "/etc/docker/config.json" ,
402+ filepath .Join (os .Getenv ("HOME" ), ".helm/registry/config.json" ),
403+ }
404+
405+ for _ , credFile := range possibleCredFiles {
406+ if _ , err := os .Stat (credFile ); err == nil {
407+ opts = append (opts , registry .ClientOptCredentialsFile (credFile ))
408+ if debug {
409+ pterm .Printf ("Using credentials file: %s\n " , credFile )
410+ }
411+ break
412+ }
413+ }
414+
415+ // Also check for environment variables
416+ if auth := os .Getenv ("HELM_REGISTRY_CONFIG" ); auth != "" {
417+ opts = append (opts , registry .ClientOptCredentialsFile (auth ))
418+ }
419+
420+ // Create and return the registry client
421+ client , err := registry .NewClient (opts ... )
422+ if err != nil {
423+ return nil , fmt .Errorf ("failed to create registry client: %w" , err )
424+ }
425+
426+ return client , nil
427+ }
428+
429+ // helmHome gets the Helm home directory
430+ func helmHome () string {
431+ if home := os .Getenv ("HELM_HOME" ); home != "" {
432+ return home
433+ }
434+ if home := os .Getenv ("HELM_CONFIG_HOME" ); home != "" {
435+ return home
436+ }
437+ userHome , _ := os .UserHomeDir ()
438+ helmPath := filepath .Join (userHome , ".helm" )
439+
440+ // Ensure directory exists
441+ if _ , err := os .Stat (helmPath ); os .IsNotExist (err ) {
442+ os .MkdirAll (helmPath , 0755 )
443+ }
444+
445+ return helmPath
446+ }
447+
122448// loadFromLocalRepo loads a chart from a local repository
123449func LoadFromLocalRepo (chartRef , version string , settings * cli.EnvSettings ) (* chart.Chart , error ) {
124450 repoName := strings .Split (chartRef , "/" )[0 ]
0 commit comments