diff --git a/internal/chartfs/chartfs.go b/internal/chartfs/chartfs.go index 0d5d77da5..09f71b553 100644 --- a/internal/chartfs/chartfs.go +++ b/internal/chartfs/chartfs.go @@ -1,6 +1,8 @@ package chartfs import ( + "fmt" + "io" "io/fs" "os" "path/filepath" @@ -99,6 +101,65 @@ func (c *ChartFS) GetAllCharts() ([]chart.Chart, error) { return charts, nil } +func (c *ChartFS) extractChartDir(destDir, chartDir string) error { + return fs.WalkDir(c.fsys, chartDir, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return fmt.Errorf("walking %q: %w", path, err) + } + target := filepath.Join(destDir, path) + if d.IsDir() { + if err := os.MkdirAll(target, 0o755); err != nil { + return fmt.Errorf("creating directory %q: %w", target, err) + } + if err := os.Chmod(target, 0o755); err != nil { + return fmt.Errorf("setting directory permissions on %q: %w", target, err) + } + return nil + } + return c.extractFile(target, path) + }) +} + +func (c *ChartFS) extractFile(target, srcPath string) error { + if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil { + return fmt.Errorf("creating parent directory for %q: %w", target, err) + } + src, err := c.fsys.Open(srcPath) + if err != nil { + return fmt.Errorf("opening source %q: %w", srcPath, err) + } + defer src.Close() + + dst, err := os.OpenFile(target, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o644) + if err != nil { + return fmt.Errorf("creating file %q: %w", target, err) + } + defer dst.Close() + + if _, err := io.Copy(dst, src); err != nil { + return fmt.Errorf("writing file %q: %w", target, err) + } + if err := os.Chmod(target, 0o644); err != nil { + return fmt.Errorf("setting file permissions on %q: %w", target, err) + } + return nil +} + +// ExtractTo extracts all Helm chart directories from the embedded filesystem +// to the destination directory on disk. +func (c *ChartFS) ExtractTo(destDir string) error { + chartDirs, err := c.walkAndFindChartDirs(c.fsys, ".") + if err != nil { + return fmt.Errorf("discovering chart directories: %w", err) + } + for _, chartDir := range chartDirs { + if err := c.extractChartDir(destDir, chartDir); err != nil { + return fmt.Errorf("extracting chart %q: %w", chartDir, err) + } + } + return nil +} + // WithBaseDir returns a new ChartFS that is rooted at the given base directory. func (c *ChartFS) WithBaseDir(baseDir string) (*ChartFS, error) { sub, err := fs.Sub(c.fsys, baseDir) diff --git a/internal/chartfs/chartfs_test.go b/internal/chartfs/chartfs_test.go index 19f3a3f8d..651c1a7b7 100644 --- a/internal/chartfs/chartfs_test.go +++ b/internal/chartfs/chartfs_test.go @@ -2,6 +2,7 @@ package chartfs import ( "os" + "path/filepath" "testing" o "github.com/onsi/gomega" @@ -45,3 +46,83 @@ func TestNewChartFS(t *testing.T) { g.Expect(len(charts)).To(o.BeNumerically(">", 1)) }) } + +func TestChartFS_ExtractTo(t *testing.T) { + g := o.NewWithT(t) + + c := New(os.DirFS("../../test")) + + destDir := t.TempDir() + err := c.ExtractTo(destDir) + g.Expect(err).To(o.Succeed()) + + t.Run("chart directories are extracted", func(t *testing.T) { + g := o.NewWithT(t) + expectedCharts := []string{ + "charts/helmet-foundation", + "charts/helmet-infrastructure", + "charts/helmet-integrations", + "charts/helmet-networking", + "charts/helmet-operators", + "charts/helmet-product-a", + "charts/helmet-product-b", + "charts/helmet-product-c", + "charts/helmet-product-d", + "charts/helmet-storage", + "charts/testing", + } + for _, chartDir := range expectedCharts { + chartYaml := filepath.Join(destDir, chartDir, "Chart.yaml") + g.Expect(chartYaml).To(o.BeAnExistingFile()) + } + }) + + t.Run("chart contents are correct", func(t *testing.T) { + g := o.NewWithT(t) + srcBytes, err := os.ReadFile("../../test/charts/helmet-product-a/Chart.yaml") + g.Expect(err).To(o.Succeed()) + + dstBytes, err := os.ReadFile( + filepath.Join(destDir, "charts/helmet-product-a/Chart.yaml"), + ) + g.Expect(err).To(o.Succeed()) + g.Expect(dstBytes).To(o.Equal(srcBytes)) + }) + + t.Run("template files are extracted", func(t *testing.T) { + g := o.NewWithT(t) + notesPath := filepath.Join( + destDir, "charts/helmet-product-a/templates/NOTES.txt", + ) + g.Expect(notesPath).To(o.BeAnExistingFile()) + }) + + t.Run("non-chart files are excluded", func(t *testing.T) { + g := o.NewWithT(t) + g.Expect(filepath.Join(destDir, "values.yaml.tpl")). + ToNot(o.BeAnExistingFile()) + g.Expect(filepath.Join(destDir, "config.yaml")). + ToNot(o.BeAnExistingFile()) + }) + + t.Run("non-chart directories are excluded", func(t *testing.T) { + g := o.NewWithT(t) + g.Expect(filepath.Join(destDir, "charts/_common")). + ToNot(o.BeADirectory()) + }) + + t.Run("file permissions are correct", func(t *testing.T) { + g := o.NewWithT(t) + chartYaml := filepath.Join( + destDir, "charts/helmet-product-a/Chart.yaml", + ) + info, err := os.Stat(chartYaml) + g.Expect(err).To(o.Succeed()) + g.Expect(info.Mode().Perm()).To(o.Equal(os.FileMode(0o644))) + + chartDir := filepath.Join(destDir, "charts/helmet-product-a") + dirInfo, err := os.Stat(chartDir) + g.Expect(err).To(o.Succeed()) + g.Expect(dirInfo.Mode().Perm()).To(o.Equal(os.FileMode(0o755))) + }) +}