Skip to content

Commit cd39408

Browse files
authored
Merge pull request #329 from clouddrove/feat/OCI-chart
Fix OCI Chart Loading in GitHub Actions
2 parents f43b7a7 + 3e9f92c commit cd39408

File tree

2 files changed

+334
-43
lines changed

2 files changed

+334
-43
lines changed

internal/helm/install.go

Lines changed: 327 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package helm
33
import (
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
108110
func 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\nOutput: %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
123449
func LoadFromLocalRepo(chartRef, version string, settings *cli.EnvSettings) (*chart.Chart, error) {
124450
repoName := strings.Split(chartRef, "/")[0]

0 commit comments

Comments
 (0)