Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 0 additions & 13 deletions Containerfile
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,5 @@ FROM registry.access.redhat.com/ubi10/ubi-minimal:10.0

COPY --from=builder /build/patternizer /usr/local/bin/patternizer

ARG PATTERNIZER_RESOURCES_DIR=/tmp/resources
WORKDIR ${PATTERNIZER_RESOURCES_DIR}

COPY resources/* .

ARG PATTERNIZER_SKILLS_DIR=/tmp/skills
WORKDIR ${PATTERNIZER_SKILLS_DIR}

COPY skills/ .

ENV PATTERNIZER_RESOURCES_DIR=${PATTERNIZER_RESOURCES_DIR}
ENV PATTERNIZER_SKILLS_DIR=${PATTERNIZER_SKILLS_DIR}

ENTRYPOINT ["patternizer"]
CMD ["help"]
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Patternizer

![Version: 1.3.1](https://img.shields.io/badge/Version-1.3.1-informational?style=flat-square)
![Version: 2.0.0](https://img.shields.io/badge/Version-2.0.0-informational?style=flat-square)
[![Quay Repository](https://img.shields.io/badge/Quay.io-patternizer-blue?logo=quay)](https://quay.io/repository/validatedpatterns/patternizer)
[![CI Pipeline](https://github.com/validatedpatterns/patternizer/actions/workflows/build-push.yaml/badge.svg?branch=main)](https://github.com/validatedpatterns/patternizer/actions/workflows/build-push.yaml)

Expand Down
15 changes: 4 additions & 11 deletions src/cmd/cmd_suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ var (
binaryPath string
resourcesPath string
skillsPath string
projectRoot string
)

func TestCmd(t *testing.T) {
Expand All @@ -33,18 +32,16 @@ var _ = BeforeSuite(func() {

wd, err := os.Getwd()
Expect(err).NotTo(HaveOccurred())
projectRoot, err = filepath.Abs(filepath.Join(wd, "..", ".."))
srcRoot, err := filepath.Abs(filepath.Join(wd, ".."))
Expect(err).NotTo(HaveOccurred())

resourcesPath = filepath.Join(projectRoot, "resources")
resourcesPath = filepath.Join(srcRoot, "internal", "embedded", "resources")
Expect(resourcesPath).To(BeADirectory(), "Could not find resources directory")
os.Setenv("PATTERNIZER_RESOURCES_DIR", resourcesPath)

skillsPath = filepath.Join(projectRoot, "skills")
skillsPath = filepath.Join(srcRoot, "internal", "embedded", "skills")
Expect(skillsPath).To(BeADirectory(), "Could not find skills directory")
os.Setenv("PATTERNIZER_SKILLS_DIR", skillsPath)

binaryPath, err = gexec.Build(filepath.Join(projectRoot, "src"))
binaryPath, err = gexec.Build(srcRoot)
Expect(err).NotTo(HaveOccurred())
})

Expand Down Expand Up @@ -135,10 +132,6 @@ func createTestDir() string {
func runCLI(dir string, args ...string) *gexec.Session {
cmd := exec.Command(binaryPath, args...)
cmd.Dir = dir
cmd.Env = append(os.Environ(),
"PATTERNIZER_RESOURCES_DIR="+resourcesPath,
"PATTERNIZER_SKILLS_DIR="+skillsPath,
)

session, err := gexec.Start(cmd, GinkgoWriter, GinkgoWriter)
Expect(err).NotTo(HaveOccurred())
Expand Down
28 changes: 6 additions & 22 deletions src/cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"os"
"path/filepath"

"github.com/validatedpatterns/patternizer/internal/embedded"
"github.com/validatedpatterns/patternizer/internal/fileutils"
"github.com/validatedpatterns/patternizer/internal/helm"
"github.com/validatedpatterns/patternizer/internal/pattern"
Expand Down Expand Up @@ -35,44 +36,27 @@ func runInit(withSecrets bool) error {
return fmt.Errorf("error processing cluster group values: %w", err)
}

// Copy pattern.sh and Makefile from resources
resourcesDir, err := fileutils.GetResourcesPath()
if err != nil {
return fmt.Errorf("error getting resource path: %w", err)
}

patternShSrc := filepath.Join(resourcesDir, "pattern.sh")
patternShDst := filepath.Join(repoRoot, "pattern.sh")
if err := fileutils.CopyFile(patternShSrc, patternShDst); err != nil {
if err := fileutils.WriteEmbeddedFile(embedded.Resources, "resources/pattern.sh", filepath.Join(repoRoot, "pattern.sh"), 0o755); err != nil {
return fmt.Errorf("error copying pattern.sh: %w", err)
}

// Always copy ansible.cfg to the pattern repo
ansibleCfgSrc := filepath.Join(resourcesDir, "ansible.cfg")
ansibleCfgDst := filepath.Join(repoRoot, "ansible.cfg")
if err := fileutils.CopyFile(ansibleCfgSrc, ansibleCfgDst); err != nil {
if err := fileutils.WriteEmbeddedFile(embedded.Resources, "resources/ansible.cfg", filepath.Join(repoRoot, "ansible.cfg"), 0o644); err != nil {
return fmt.Errorf("error copying ansible.cfg: %w", err)
}

// Always copy Makefile-common to the pattern repo
makefilePatternSrc := filepath.Join(resourcesDir, "Makefile-common")
makefilePatternDst := filepath.Join(repoRoot, "Makefile-common")
if err := fileutils.CopyFile(makefilePatternSrc, makefilePatternDst); err != nil {
if err := fileutils.WriteEmbeddedFile(embedded.Resources, "resources/Makefile-common", filepath.Join(repoRoot, "Makefile-common"), 0o644); err != nil {
return fmt.Errorf("error copying Makefile-common: %w", err)
}

// Create a simple Makefile that includes Makefile-common (only if it doesn't exist)
makefileSrc := filepath.Join(resourcesDir, "Makefile")
makefileDst := filepath.Join(repoRoot, "Makefile")
if _, err := os.Stat(makefileDst); os.IsNotExist(err) {
if err := fileutils.CopyFile(makefileSrc, makefileDst); err != nil {
if err := fileutils.WriteEmbeddedFile(embedded.Resources, "resources/Makefile", makefileDst, 0o644); err != nil {
return fmt.Errorf("error copying Makefile: %w", err)
}
}

// Handle secrets setup if requested
if withSecrets {
if err := fileutils.HandleSecretsSetup(resourcesDir, repoRoot); err != nil {
if err := fileutils.HandleSecretsSetup(embedded.Resources, repoRoot); err != nil {
return fmt.Errorf("error setting up secrets: %w", err)
}
}
Expand Down
24 changes: 6 additions & 18 deletions src/cmd/upgrade.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"os"
"path/filepath"

"github.com/validatedpatterns/patternizer/internal/embedded"
"github.com/validatedpatterns/patternizer/internal/fileutils"
"github.com/validatedpatterns/patternizer/internal/pattern"
)
Expand All @@ -31,43 +32,30 @@ func runUpgrade(replaceMakefile bool) error {
return fmt.Errorf("error removing pattern.sh: %w", err)
}

// Copy resources into repo root
resourcesDir, err := fileutils.GetResourcesPath()
if err != nil {
return fmt.Errorf("error getting resource path: %w", err)
}

// Copy pattern.sh
if err := fileutils.CopyFile(filepath.Join(resourcesDir, "pattern.sh"), patternShPath); err != nil {
if err := fileutils.WriteEmbeddedFile(embedded.Resources, "resources/pattern.sh", patternShPath, 0o755); err != nil {
return fmt.Errorf("error copying pattern.sh: %w", err)
}

// Copy Makefile-common
if err := fileutils.CopyFile(filepath.Join(resourcesDir, "Makefile-common"), filepath.Join(repoRoot, "Makefile-common")); err != nil {
if err := fileutils.WriteEmbeddedFile(embedded.Resources, "resources/Makefile-common", filepath.Join(repoRoot, "Makefile-common"), 0o644); err != nil {
return fmt.Errorf("error copying Makefile-common: %w", err)
}

// Copy ansible.cfg
if err := fileutils.CopyFile(filepath.Join(resourcesDir, "ansible.cfg"), filepath.Join(repoRoot, "ansible.cfg")); err != nil {
if err := fileutils.WriteEmbeddedFile(embedded.Resources, "resources/ansible.cfg", filepath.Join(repoRoot, "ansible.cfg"), 0o644); err != nil {
return fmt.Errorf("error copying ansible.cfg: %w", err)
}

// Makefile handling
makefileSrc := filepath.Join(resourcesDir, "Makefile")
makefileDst := filepath.Join(repoRoot, "Makefile")

if replaceMakefile {
if err := fileutils.CopyFile(makefileSrc, makefileDst); err != nil {
if err := fileutils.WriteEmbeddedFile(embedded.Resources, "resources/Makefile", makefileDst, 0o644); err != nil {
return fmt.Errorf("error replacing Makefile: %w", err)
}
} else {
// If Makefile doesn't exist, copy it
if _, err := os.Stat(makefileDst); os.IsNotExist(err) {
if err := fileutils.CopyFile(makefileSrc, makefileDst); err != nil {
if err := fileutils.WriteEmbeddedFile(embedded.Resources, "resources/Makefile", makefileDst, 0o644); err != nil {
return fmt.Errorf("error copying Makefile: %w", err)
}
} else if err == nil {
// If Makefile exists, check for include and prepend if missing
hasInclude, err := fileutils.FileContainsIncludeMakefileCommon(makefileDst)
if err != nil {
return fmt.Errorf("error checking Makefile for include: %w", err)
Expand Down
9 changes: 9 additions & 0 deletions src/internal/embedded/embedded.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package embedded

import "embed"

//go:embed resources/*
var Resources embed.FS

//go:embed all:skills
var Skills embed.FS
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
47 changes: 28 additions & 19 deletions src/internal/fileutils/fileutils.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package fileutils
import (
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"strings"
Expand Down Expand Up @@ -49,39 +50,47 @@ func CopyFile(src, dst string) error {
}

// HandleSecretsSetup handles the setup for secrets usage by copying the secrets template.
func HandleSecretsSetup(resourcesDir, repoRoot string) (err error) {
// Copy the values-secret.yaml.template file to the pattern root only if it doesn't already exist
secretsTemplateSrc := filepath.Join(resourcesDir, "values-secret.yaml.template")
func HandleSecretsSetup(fsys fs.FS, repoRoot string) error {
secretsTemplateDst := filepath.Join(repoRoot, "values-secret.yaml.template")

if _, err := os.Stat(secretsTemplateDst); os.IsNotExist(err) {
if err = CopyFile(secretsTemplateSrc, secretsTemplateDst); err != nil {
if err = WriteEmbeddedFile(fsys, "resources/values-secret.yaml.template", secretsTemplateDst, 0o644); err != nil {
return fmt.Errorf("error copying secrets template: %w", err)
}
}

return nil
}

// GetResourcesPath returns the path to the resources directory.
// It checks the PATTERNIZER_RESOURCES_DIR environment variable first,
// and falls back to the current working directory.
func GetResourcesPath() (path string, err error) {
path = os.Getenv("PATTERNIZER_RESOURCES_DIR")
if path != "" {
return path, nil
// WriteEmbeddedFile reads a file from an embedded FS and writes it to disk with the given mode.
func WriteEmbeddedFile(fsys fs.FS, srcPath, dstPath string, mode os.FileMode) error {
data, err := fs.ReadFile(fsys, srcPath)
if err != nil {
return fmt.Errorf("reading embedded file %s: %w", srcPath, err)
}

return "", fmt.Errorf("PATTERNIZER_RESOURCES_DIR environment variable is not set")
if err := os.WriteFile(dstPath, data, mode); err != nil {
return err
}
return os.Chmod(dstPath, mode)
}

func GetSkillsPath() (string, error) {
path := os.Getenv("PATTERNIZER_SKILLS_DIR")
if path != "" {
return path, nil
}
// WriteEmbeddedDir recursively copies an embedded directory tree to disk.
func WriteEmbeddedDir(fsys fs.FS, srcDir, dstDir string) error {
return fs.WalkDir(fsys, srcDir, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
relPath, err := filepath.Rel(srcDir, path)
if err != nil {
return err
}
target := filepath.Join(dstDir, relPath)

return "", fmt.Errorf("PATTERNIZER_SKILLS_DIR environment variable is not set")
if d.IsDir() {
return os.MkdirAll(target, 0o755)
}
return WriteEmbeddedFile(fsys, path, target, 0o644)
})
}

// CopyDir recursively copies the contents of src into dst.
Expand Down
81 changes: 38 additions & 43 deletions src/internal/fileutils/fileutils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"path/filepath"
"runtime"
"strings"
"testing/fstest"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
Expand Down Expand Up @@ -40,70 +41,64 @@ var _ = Describe("CopyFile", func() {

var _ = Describe("HandleSecretsSetup", func() {
It("should copy template when missing and not overwrite when present", func() {
resources := GinkgoT().TempDir()
repoRoot := GinkgoT().TempDir()

templatePath := filepath.Join(resources, "values-secret.yaml.template")
originalContent := "foo: bar\n"
Expect(os.WriteFile(templatePath, []byte(originalContent), 0o644)).To(Succeed())
fsys := fstest.MapFS{
"resources/values-secret.yaml.template": &fstest.MapFile{Data: []byte("foo: bar\n")},
}

// First call should copy
Expect(HandleSecretsSetup(resources, repoRoot)).To(Succeed())
Expect(HandleSecretsSetup(fsys, repoRoot)).To(Succeed())
copied := filepath.Join(repoRoot, "values-secret.yaml.template")
data, err := os.ReadFile(copied)
Expect(err).NotTo(HaveOccurred())
Expect(string(data)).To(Equal(originalContent))
Expect(string(data)).To(Equal("foo: bar\n"))

// Change the source and call again; destination should remain unchanged
Expect(os.WriteFile(templatePath, []byte("baz: qux\n"), 0o644)).To(Succeed())
Expect(HandleSecretsSetup(resources, repoRoot)).To(Succeed())
Expect(HandleSecretsSetup(fsys, repoRoot)).To(Succeed())
data2, err := os.ReadFile(copied)
Expect(err).NotTo(HaveOccurred())
Expect(string(data2)).To(Equal(originalContent))
Expect(string(data2)).To(Equal("foo: bar\n"))
})
})

var _ = Describe("GetResourcesPath", func() {
It("should return the path when the environment variable is set", func() {
old := os.Getenv("PATTERNIZER_RESOURCES_DIR")
DeferCleanup(func() { os.Setenv("PATTERNIZER_RESOURCES_DIR", old) })
var _ = Describe("WriteEmbeddedFile", func() {
It("should write file contents with the specified mode", func() {
dir := GinkgoT().TempDir()
dst := filepath.Join(dir, "out.txt")

tmp := GinkgoT().TempDir()
Expect(os.Setenv("PATTERNIZER_RESOURCES_DIR", tmp)).To(Succeed())
got, err := GetResourcesPath()
Expect(err).NotTo(HaveOccurred())
Expect(got).To(Equal(tmp))
})
fsys := fstest.MapFS{
"resources/test.txt": &fstest.MapFile{Data: []byte("hello embedded")},
}

It("should return an error when the environment variable is unset", func() {
old := os.Getenv("PATTERNIZER_RESOURCES_DIR")
DeferCleanup(func() { os.Setenv("PATTERNIZER_RESOURCES_DIR", old) })
Expect(WriteEmbeddedFile(fsys, "resources/test.txt", dst, 0o755)).To(Succeed())

Expect(os.Unsetenv("PATTERNIZER_RESOURCES_DIR")).To(Succeed())
_, err := GetResourcesPath()
Expect(err).To(HaveOccurred())
data, err := os.ReadFile(dst)
Expect(err).NotTo(HaveOccurred())
Expect(string(data)).To(Equal("hello embedded"))

info, err := os.Stat(dst)
Expect(err).NotTo(HaveOccurred())
Expect(info.Mode().Perm()).To(Equal(os.FileMode(0o755).Perm()))
})
})

var _ = Describe("GetSkillsPath", func() {
It("should return the path when the environment variable is set", func() {
old := os.Getenv("PATTERNIZER_SKILLS_DIR")
DeferCleanup(func() { os.Setenv("PATTERNIZER_SKILLS_DIR", old) })
var _ = Describe("WriteEmbeddedDir", func() {
It("should recursively write an embedded directory tree", func() {
dst := filepath.Join(GinkgoT().TempDir(), "output")

tmp := GinkgoT().TempDir()
Expect(os.Setenv("PATTERNIZER_SKILLS_DIR", tmp)).To(Succeed())
got, err := GetSkillsPath()
Expect(err).NotTo(HaveOccurred())
Expect(got).To(Equal(tmp))
})
fsys := fstest.MapFS{
"skills/myskill/SKILL.md": &fstest.MapFile{Data: []byte("skill content")},
"skills/myskill/reference.md": &fstest.MapFile{Data: []byte("ref content")},
}

It("should return an error when the environment variable is unset", func() {
old := os.Getenv("PATTERNIZER_SKILLS_DIR")
DeferCleanup(func() { os.Setenv("PATTERNIZER_SKILLS_DIR", old) })
Expect(WriteEmbeddedDir(fsys, "skills/myskill", dst)).To(Succeed())

Expect(os.Unsetenv("PATTERNIZER_SKILLS_DIR")).To(Succeed())
_, err := GetSkillsPath()
Expect(err).To(HaveOccurred())
got1, err := os.ReadFile(filepath.Join(dst, "SKILL.md"))
Expect(err).NotTo(HaveOccurred())
Expect(string(got1)).To(Equal("skill content"))

got2, err := os.ReadFile(filepath.Join(dst, "reference.md"))
Expect(err).NotTo(HaveOccurred())
Expect(string(got2)).To(Equal("ref content"))
})
})

Expand Down
Loading
Loading