From 987b004bf512144268f46ec6b7fe25a11af2d237 Mon Sep 17 00:00:00 2001 From: Drew Minnear Date: Tue, 24 Feb 2026 17:32:18 -0500 Subject: [PATCH 01/13] initial ginkgo integration tests are passing --- Makefile | 7 +- README.md | 8 +- src/.cursor/rules/make-ci-verification.mdc | 40 --- src/cmd/cmd_suite_test.go | 109 +++++++ src/cmd/init_test.go | 330 +++++++++++++++++++++ src/go.mod | 14 + src/go.sum | 28 ++ 7 files changed, 491 insertions(+), 45 deletions(-) delete mode 100644 src/.cursor/rules/make-ci-verification.mdc create mode 100644 src/cmd/cmd_suite_test.go create mode 100644 src/cmd/init_test.go diff --git a/Makefile b/Makefile index 556f42c..936fd59 100644 --- a/Makefile +++ b/Makefile @@ -14,6 +14,7 @@ GO_VET := $(GO_CMD) vet GO_FMT := gofmt GO_VERSION := 1.24 GOLANGCI_LINT_VERSION := v2.1.6 +GINKGO_VERSION := v2.28.1 SRC_DIR := src # Default target @@ -49,7 +50,7 @@ deps: ## Download and install Go dependencies .PHONY: test-unit test-unit: ## Run unit tests @echo "Running unit tests..." - cd $(SRC_DIR) && $(GO_TEST) -v ./... + cd $(SRC_DIR) && $(GO_TEST) -v --args --ginkgo.v ./... .PHONY: test-coverage test-coverage: ## Run unit tests with coverage report @@ -116,6 +117,10 @@ dev-setup: deps ## Set up development environment echo "Installing golangci-lint..."; \ curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/HEAD/install.sh | sh -s -- -b $$(go env GOPATH)/bin $(GOLANGCI_LINT_VERSION); \ fi + @if ! command -v ginkgo >/dev/null 2>&1; then \ + echo "Installing ginkgo CLI..."; \ + go install github.com/onsi/ginkgo/v2/ginkgo@$(GINKGO_VERSION); \ + fi @echo "Development environment ready" .PHONY: version diff --git a/README.md b/README.md index 07beca6..2778383 100644 --- a/README.md +++ b/README.md @@ -95,13 +95,13 @@ Using the prebuilt container is the easiest way to run Patternizer, as it requir #### **Initialize without secrets:** ```bash -podman run -v "$PWD:$PWD:z" -w "$PWD" quay.io/validatedpatterns/patternizer init +podman run --pull=newer -v "$PWD:$PWD:z" -w "$PWD" quay.io/validatedpatterns/patternizer init ``` #### **Initialize with secrets support:** ```bash -podman run -v "$PWD:$PWD:z" -w "$PWD" quay.io/validatedpatterns/patternizer init --with-secrets +podman run --pull=newer -v "$PWD:$PWD:z" -w "$PWD" quay.io/validatedpatterns/patternizer init --with-secrets ``` #### **Upgrade an existing pattern repository:** @@ -110,10 +110,10 @@ Use this to migrate or refresh an existing pattern repo to the latest common str ```bash # Refresh common assets, keep your Makefile unless it lacks the include -podman run -v "$PWD:$PWD:z" -w "$PWD" quay.io/validatedpatterns/patternizer upgrade +podman run --pull=newer -v "$PWD:$PWD:z" -w "$PWD" quay.io/validatedpatterns/patternizer upgrade # Replace your Makefile with the default from Patternizer -podman run -v "$PWD:$PWD:z" -w "$PWD" quay.io/validatedpatterns/patternizer upgrade --replace-makefile +podman run --pull=newer -v "$PWD:$PWD:z" -w "$PWD" quay.io/validatedpatterns/patternizer upgrade --replace-makefile ``` What upgrade does: diff --git a/src/.cursor/rules/make-ci-verification.mdc b/src/.cursor/rules/make-ci-verification.mdc deleted file mode 100644 index 076e24b..0000000 --- a/src/.cursor/rules/make-ci-verification.mdc +++ /dev/null @@ -1,40 +0,0 @@ ---- -description: lint/build/test any changes to source code -alwaysApply: true ---- - -## Go Source Code Testing Rule - -**ALWAYS use `make ci` for testing Go code changes - NEVER run manual go test commands** - -### When to Apply This Rule: -- Any changes to Go source code in the `src/` directory -- When you want to verify that changes work correctly -- Before proposing code changes to the user -- When debugging build/test issues - -### What to Use: -- ✅ **CORRECT**: `make ci` - This runs the complete CI pipeline locally (lint, build, test) -- ❌ **WRONG**: `cd src && go test ./... -v` - Manual go test commands -- ❌ **WRONG**: `go build`, `go test`, etc. - Direct go commands -- ❌ **WRONG**: `make test-unit` or other individual targets when you want full verification - -### Why This Rule Exists: -- `make ci` ensures consistency with the actual CI pipeline -- It runs linting, formatting checks, building, AND testing in the correct order -- It catches issues that manual testing might miss -- It's the same command developers use locally - -### The Command to Use: -```bash -make ci -``` - -This will: -1. Run all linting checks (`make lint`) -2. Build the binary (`make build`) -3. Run unit tests (`make test-unit`) -4. Run integration tests (`make test-integration`) -5. Generate coverage reports - -@Makefile diff --git a/src/cmd/cmd_suite_test.go b/src/cmd/cmd_suite_test.go new file mode 100644 index 0000000..784cfb4 --- /dev/null +++ b/src/cmd/cmd_suite_test.go @@ -0,0 +1,109 @@ +package cmd_test + +import ( + "bytes" + "fmt" + "os" + "os/exec" + "path/filepath" + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/onsi/gomega/gexec" + "gopkg.in/yaml.v3" + + "github.com/dminnear-rh/patternizer/internal/types" +) + +var ( + binaryPath string + resourcesPath string + projectRoot string + testPath string +) + +func TestCmd(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Cmd Suite") +} + +var _ = BeforeSuite(func() { + var err error + + wd, err := os.Getwd() + Expect(err).NotTo(HaveOccurred()) + projectRoot, err = filepath.Abs(filepath.Join(wd, "../..")) + Expect(err).NotTo(HaveOccurred()) + + resourcesPath = filepath.Join(projectRoot, "resources") + Expect(resourcesPath).To(BeADirectory(), "Could not find resources directory") + os.Setenv("PATTERNIZER_RESOURCES_DIR", resourcesPath) + + testPath = filepath.Join(projectRoot, "test") + Expect(testPath).To(BeADirectory(), "Could not find test directory") + + binaryPath, err = gexec.Build(filepath.Join(projectRoot, "src")) + Expect(err).NotTo(HaveOccurred()) +}) + +var _ = AfterSuite(func() { + gexec.CleanupBuildArtifacts() +}) + +func verifyFilesMatch(file1, file2 string) { + file1Contents, err := os.ReadFile(file1) + Expect(err).NotTo(HaveOccurred(), fmt.Sprintf("Could not read file %s", file1)) + + file2Contents, err := os.ReadFile(file2) + Expect(err).NotTo(HaveOccurred(), fmt.Sprintf("Could not read file %s", file2)) + + Expect(bytes.Equal(file1Contents, file2Contents)).To(BeTrue(), fmt.Sprintf("%s and %s have different contents", file1, file2)) +} + +func verifyGlobalValues(valuesFile string, expectedGlobalValues types.ValuesGlobal) { + f, err := os.ReadFile(valuesFile) + Expect(err).NotTo(HaveOccurred(), fmt.Sprintf("Could not read file %s", valuesFile)) + + var globalValues *types.ValuesGlobal + err = yaml.Unmarshal(f, &globalValues) + Expect(err).NotTo(HaveOccurred(), fmt.Sprintf("Could not unmarshal %s into the ValuesGlobal type", valuesFile)) + + Expect(*globalValues).To(Equal(expectedGlobalValues), fmt.Sprintf("Global values in %s differ from expected values", valuesFile)) +} + +func verifyClusterGroupValues(valuesFile string, expectedClusterGroupValues types.ValuesClusterGroup) { + f, err := os.ReadFile(valuesFile) + Expect(err).NotTo(HaveOccurred(), fmt.Sprintf("Could not read file %s", valuesFile)) + + var clusterGroupValues *types.ValuesClusterGroup + err = yaml.Unmarshal(f, &clusterGroupValues) + Expect(err).NotTo(HaveOccurred(), fmt.Sprintf("Could not unmarshal %s into the ValuesClusterGroup type", valuesFile)) + + Expect(*clusterGroupValues).To(Equal(expectedClusterGroupValues), fmt.Sprintf("Clustergroup values in %s differ from expected values", valuesFile)) +} + +func createTestDir() string { + dir, err := os.MkdirTemp("", "patternizer-test") + Expect(err).NotTo(HaveOccurred()) + return dir +} + +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) + + session, err := gexec.Start(cmd, GinkgoWriter, GinkgoWriter) + Expect(err).NotTo(HaveOccurred()) + Eventually(session).Should(gexec.Exit(0)) + return session +} + +func addDummyChart(dir, name string) { + path := filepath.Join(dir, "charts", name) + Expect(os.MkdirAll(path, 0755)).To(Succeed()) + Expect(os.WriteFile(filepath.Join(path, "Chart.yaml"), []byte("name: "+name+"\nversion: 0.1.0"), 0644)).To(Succeed()) + Expect(os.WriteFile(filepath.Join(path, "values.yaml"), []byte("replicaCount: 1"), 0644)).To(Succeed()) + Expect(os.MkdirAll(filepath.Join(path, "templates"), 0755)).To(Succeed()) +} diff --git a/src/cmd/init_test.go b/src/cmd/init_test.go new file mode 100644 index 0000000..3e41e67 --- /dev/null +++ b/src/cmd/init_test.go @@ -0,0 +1,330 @@ +package cmd_test + +import ( + "os" + "path/filepath" + + "github.com/dminnear-rh/patternizer/internal/types" + . "github.com/onsi/ginkgo/v2" +) + +var _ = Describe("patternizer init", func() { + Context("on an empty directory", Ordered, func() { + var tempDir string + + BeforeAll(func() { + tempDir = createTestDir() + _ = runCLI(tempDir, "init") + }) + + AfterAll(func() { + os.RemoveAll(tempDir) + }) + + It("should create the pattern.sh script", func() { + actual := filepath.Join(tempDir, "pattern.sh") + expected := filepath.Join(resourcesPath, "pattern.sh") + verifyFilesMatch(actual, expected) + }) + + It("should create the common Makefile", func() { + actual := filepath.Join(tempDir, "Makefile-common") + expected := filepath.Join(resourcesPath, "Makefile-common") + verifyFilesMatch(actual, expected) + }) + + It("should create the Makefile for the pattern", func() { + actual := filepath.Join(tempDir, "Makefile") + expected := filepath.Join(resourcesPath, "Makefile") + verifyFilesMatch(actual, expected) + }) + + It("should create an appropriate global values file", func() { + globalValuesFile := filepath.Join(tempDir, "values-global.yaml") + expectedGlobalValues := types.ValuesGlobal{ + Global: types.Global{ + Pattern: filepath.Base(tempDir), + SecretLoader: types.SecretLoader{ + Disabled: true, + }, + }, + Main: types.Main{ + ClusterGroupName: "prod", + MultiSourceConfig: types.MultiSourceConfig{ + Enabled: true, + ClusterGroupChartVersion: "0.9.*", + }, + }, + } + verifyGlobalValues(globalValuesFile, expectedGlobalValues) + }) + + It("should create an appropriate clustergroup values file", func() { + clusterGroupValuesFile := filepath.Join(tempDir, "values-prod.yaml") + expectedClusterGroupValues := types.ValuesClusterGroup{ + ClusterGroup: types.ClusterGroup{ + Name: "prod", + Namespaces: []types.NamespaceEntry{types.NewNamespaceEntry(filepath.Base(tempDir))}, + Subscriptions: map[string]types.Subscription{}, + Applications: map[string]types.Application{}, + }, + } + verifyClusterGroupValues(clusterGroupValuesFile, expectedClusterGroupValues) + }) + }) + + Context("on a directory containing helm charts", Ordered, func() { + var tempDir string + + BeforeAll(func() { + tempDir = createTestDir() + addDummyChart(tempDir, "test-app1") + addDummyChart(tempDir, "test-app2") + _ = runCLI(tempDir, "init") + }) + + AfterAll(func() { + os.RemoveAll(tempDir) + }) + + It("should create the pattern.sh script", func() { + actual := filepath.Join(tempDir, "pattern.sh") + expected := filepath.Join(resourcesPath, "pattern.sh") + verifyFilesMatch(actual, expected) + }) + + It("should create the common Makefile", func() { + actual := filepath.Join(tempDir, "Makefile-common") + expected := filepath.Join(resourcesPath, "Makefile-common") + verifyFilesMatch(actual, expected) + }) + + It("should create the Makefile for the pattern", func() { + actual := filepath.Join(tempDir, "Makefile") + expected := filepath.Join(resourcesPath, "Makefile") + verifyFilesMatch(actual, expected) + }) + + It("should create an appropriate global values file", func() { + globalValuesFile := filepath.Join(tempDir, "values-global.yaml") + expectedGlobalValues := types.ValuesGlobal{ + Global: types.Global{ + Pattern: filepath.Base(tempDir), + SecretLoader: types.SecretLoader{ + Disabled: true, + }, + }, + Main: types.Main{ + ClusterGroupName: "prod", + MultiSourceConfig: types.MultiSourceConfig{ + Enabled: true, + ClusterGroupChartVersion: "0.9.*", + }, + }, + } + verifyGlobalValues(globalValuesFile, expectedGlobalValues) + }) + + It("should create an appropriate clustergroup values file", func() { + clusterGroupValuesFile := filepath.Join(tempDir, "values-prod.yaml") + expectedNamespace := filepath.Base(tempDir) + expectedClusterGroupValues := types.ValuesClusterGroup{ + ClusterGroup: types.ClusterGroup{ + Name: "prod", + Namespaces: []types.NamespaceEntry{types.NewNamespaceEntry(expectedNamespace)}, + Subscriptions: map[string]types.Subscription{}, + Applications: map[string]types.Application{ + "test-app1": { + Name: "test-app1", + Namespace: expectedNamespace, + Path: "charts/test-app1", + }, + "test-app2": { + Name: "test-app2", + Namespace: expectedNamespace, + Path: "charts/test-app2", + }, + }, + }, + } + verifyClusterGroupValues(clusterGroupValuesFile, expectedClusterGroupValues) + }) + }) +}) + +var _ = Describe("patternizer init --with-secrets", func() { + Context("on an empty directory", Ordered, func() { + var tempDir string + + BeforeAll(func() { + tempDir = createTestDir() + _ = runCLI(tempDir, "init", "--with-secrets") + }) + + AfterAll(func() { + os.RemoveAll(tempDir) + }) + + It("should create the pattern.sh script", func() { + actual := filepath.Join(tempDir, "pattern.sh") + expected := filepath.Join(resourcesPath, "pattern.sh") + verifyFilesMatch(actual, expected) + }) + + It("should create the common Makefile", func() { + actual := filepath.Join(tempDir, "Makefile-common") + expected := filepath.Join(resourcesPath, "Makefile-common") + verifyFilesMatch(actual, expected) + }) + + It("should create the Makefile for the pattern", func() { + actual := filepath.Join(tempDir, "Makefile") + expected := filepath.Join(resourcesPath, "Makefile") + verifyFilesMatch(actual, expected) + }) + + It("should create an appropriate global values file", func() { + globalValuesFile := filepath.Join(tempDir, "values-global.yaml") + expectedGlobalValues := types.ValuesGlobal{ + Global: types.Global{ + Pattern: filepath.Base(tempDir), + SecretLoader: types.SecretLoader{ + Disabled: false, + }, + }, + Main: types.Main{ + ClusterGroupName: "prod", + MultiSourceConfig: types.MultiSourceConfig{ + Enabled: true, + ClusterGroupChartVersion: "0.9.*", + }, + }, + } + verifyGlobalValues(globalValuesFile, expectedGlobalValues) + }) + + It("should create an appropriate clustergroup values file", func() { + clusterGroupValuesFile := filepath.Join(tempDir, "values-prod.yaml") + expectedClusterGroupValues := types.ValuesClusterGroup{ + ClusterGroup: types.ClusterGroup{ + Name: "prod", + Namespaces: []types.NamespaceEntry{ + types.NewNamespaceEntry(filepath.Base(tempDir)), + types.NewNamespaceEntry("vault"), + types.NewNamespaceEntry("golang-external-secrets"), + }, + Subscriptions: map[string]types.Subscription{}, + Applications: map[string]types.Application{ + "vault": { + Name: "vault", + Namespace: "vault", + Chart: "hashicorp-vault", + ChartVersion: "0.1.*", + }, + "golang-external-secrets": { + Name: "golang-external-secrets", + Namespace: "golang-external-secrets", + Chart: "golang-external-secrets", + ChartVersion: "0.1.*", + }, + }, + }, + } + verifyClusterGroupValues(clusterGroupValuesFile, expectedClusterGroupValues) + }) + }) + + Context("on a directory containing helm charts", Ordered, func() { + var tempDir string + + BeforeAll(func() { + tempDir = createTestDir() + addDummyChart(tempDir, "test-app1") + addDummyChart(tempDir, "test-app2") + _ = runCLI(tempDir, "init", "--with-secrets") + }) + + AfterAll(func() { + os.RemoveAll(tempDir) + }) + + It("should create the pattern.sh script", func() { + actual := filepath.Join(tempDir, "pattern.sh") + expected := filepath.Join(resourcesPath, "pattern.sh") + verifyFilesMatch(actual, expected) + }) + + It("should create the common Makefile", func() { + actual := filepath.Join(tempDir, "Makefile-common") + expected := filepath.Join(resourcesPath, "Makefile-common") + verifyFilesMatch(actual, expected) + }) + + It("should create the Makefile for the pattern", func() { + actual := filepath.Join(tempDir, "Makefile") + expected := filepath.Join(resourcesPath, "Makefile") + verifyFilesMatch(actual, expected) + }) + + It("should create an appropriate global values file", func() { + globalValuesFile := filepath.Join(tempDir, "values-global.yaml") + expectedGlobalValues := types.ValuesGlobal{ + Global: types.Global{ + Pattern: filepath.Base(tempDir), + SecretLoader: types.SecretLoader{ + Disabled: false, + }, + }, + Main: types.Main{ + ClusterGroupName: "prod", + MultiSourceConfig: types.MultiSourceConfig{ + Enabled: true, + ClusterGroupChartVersion: "0.9.*", + }, + }, + } + verifyGlobalValues(globalValuesFile, expectedGlobalValues) + }) + + It("should create an appropriate clustergroup values file", func() { + clusterGroupValuesFile := filepath.Join(tempDir, "values-prod.yaml") + expectedNamespace := filepath.Base(tempDir) + expectedClusterGroupValues := types.ValuesClusterGroup{ + ClusterGroup: types.ClusterGroup{ + Name: "prod", + Namespaces: []types.NamespaceEntry{ + types.NewNamespaceEntry(filepath.Base(tempDir)), + types.NewNamespaceEntry("vault"), + types.NewNamespaceEntry("golang-external-secrets"), + }, + Subscriptions: map[string]types.Subscription{}, + Applications: map[string]types.Application{ + "test-app1": { + Name: "test-app1", + Namespace: expectedNamespace, + Path: "charts/test-app1", + }, + "test-app2": { + Name: "test-app2", + Namespace: expectedNamespace, + Path: "charts/test-app2", + }, + "vault": { + Name: "vault", + Namespace: "vault", + Chart: "hashicorp-vault", + ChartVersion: "0.1.*", + }, + "golang-external-secrets": { + Name: "golang-external-secrets", + Namespace: "golang-external-secrets", + Chart: "golang-external-secrets", + ChartVersion: "0.1.*", + }, + }, + }, + } + verifyClusterGroupValues(clusterGroupValuesFile, expectedClusterGroupValues) + }) + }) +}) diff --git a/src/go.mod b/src/go.mod index bd4c42b..0671449 100644 --- a/src/go.mod +++ b/src/go.mod @@ -8,9 +8,23 @@ require ( ) require ( + github.com/Masterminds/semver/v3 v3.4.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-task/slim-sprig/v3 v3.0.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/kr/pretty v0.3.1 // indirect + github.com/onsi/ginkgo/v2 v2.28.1 // indirect + github.com/onsi/gomega v1.39.1 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/spf13/pflag v1.0.6 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/mod v0.32.0 // indirect + golang.org/x/net v0.49.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.40.0 // indirect + golang.org/x/text v0.33.0 // indirect + golang.org/x/tools v0.41.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect ) diff --git a/src/go.sum b/src/go.sum index 12f8359..ada7fa7 100644 --- a/src/go.sum +++ b/src/go.sum @@ -1,5 +1,15 @@ +github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= +github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 h1:z2ogiKUYzX5Is6zr/vP9vJGqPwcdqsWjOt+V8J7+bTc= +github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= @@ -9,6 +19,10 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/onsi/ginkgo/v2 v2.28.1 h1:S4hj+HbZp40fNKuLUQOYLDgZLwNUVn19N3Atb98NCyI= +github.com/onsi/ginkgo/v2 v2.28.1/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE= +github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28= +github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= @@ -18,6 +32,20 @@ github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= +golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= +golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= +golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= +golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= +golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= From 31e45298ac4aaa7c968e485d63a9e20ff13dac32 Mon Sep 17 00:00:00 2001 From: Drew Minnear Date: Wed, 25 Feb 2026 11:41:17 -0500 Subject: [PATCH 02/13] add tests for merging with existing values-global.yaml --- src/cmd/cmd_suite_test.go | 23 +++++ src/cmd/init_test.go | 193 ++++++++++++++++++++++++++++++-------- 2 files changed, 179 insertions(+), 37 deletions(-) diff --git a/src/cmd/cmd_suite_test.go b/src/cmd/cmd_suite_test.go index 784cfb4..6af74da 100644 --- a/src/cmd/cmd_suite_test.go +++ b/src/cmd/cmd_suite_test.go @@ -51,6 +51,29 @@ var _ = AfterSuite(func() { gexec.CleanupBuildArtifacts() }) +func verifyPattenShCopied(dir string) { + actual := filepath.Join(dir, "pattern.sh") + expected := filepath.Join(resourcesPath, "pattern.sh") + verifyFilesMatch(actual, expected) + + // verify pattern.sh is executable + info, err := os.Stat(actual) + Expect(err).NotTo(HaveOccurred()) + Expect(info.Mode() & 0111).NotTo(Equal(0)) +} + +func verifyMakefileCommonCopied(dir string) { + actual := filepath.Join(dir, "Makefile-common") + expected := filepath.Join(resourcesPath, "Makefile-common") + verifyFilesMatch(actual, expected) +} + +func verifyMakefileCopied(dir string) { + actual := filepath.Join(dir, "Makefile") + expected := filepath.Join(resourcesPath, "Makefile") + verifyFilesMatch(actual, expected) +} + func verifyFilesMatch(file1, file2 string) { file1Contents, err := os.ReadFile(file1) Expect(err).NotTo(HaveOccurred(), fmt.Sprintf("Could not read file %s", file1)) diff --git a/src/cmd/init_test.go b/src/cmd/init_test.go index 3e41e67..64d8072 100644 --- a/src/cmd/init_test.go +++ b/src/cmd/init_test.go @@ -6,8 +6,16 @@ import ( "github.com/dminnear-rh/patternizer/internal/types" . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" ) +const customValuesGlobal = ` +global: + pattern: test-pattern +main: + clusterGroupName: test +` + var _ = Describe("patternizer init", func() { Context("on an empty directory", Ordered, func() { var tempDir string @@ -22,21 +30,15 @@ var _ = Describe("patternizer init", func() { }) It("should create the pattern.sh script", func() { - actual := filepath.Join(tempDir, "pattern.sh") - expected := filepath.Join(resourcesPath, "pattern.sh") - verifyFilesMatch(actual, expected) + verifyPattenShCopied(tempDir) }) It("should create the common Makefile", func() { - actual := filepath.Join(tempDir, "Makefile-common") - expected := filepath.Join(resourcesPath, "Makefile-common") - verifyFilesMatch(actual, expected) + verifyMakefileCommonCopied(tempDir) }) It("should create the Makefile for the pattern", func() { - actual := filepath.Join(tempDir, "Makefile") - expected := filepath.Join(resourcesPath, "Makefile") - verifyFilesMatch(actual, expected) + verifyMakefileCopied(tempDir) }) It("should create an appropriate global values file", func() { @@ -88,21 +90,15 @@ var _ = Describe("patternizer init", func() { }) It("should create the pattern.sh script", func() { - actual := filepath.Join(tempDir, "pattern.sh") - expected := filepath.Join(resourcesPath, "pattern.sh") - verifyFilesMatch(actual, expected) + verifyPattenShCopied(tempDir) }) It("should create the common Makefile", func() { - actual := filepath.Join(tempDir, "Makefile-common") - expected := filepath.Join(resourcesPath, "Makefile-common") - verifyFilesMatch(actual, expected) + verifyMakefileCommonCopied(tempDir) }) It("should create the Makefile for the pattern", func() { - actual := filepath.Join(tempDir, "Makefile") - expected := filepath.Join(resourcesPath, "Makefile") - verifyFilesMatch(actual, expected) + verifyMakefileCopied(tempDir) }) It("should create an appropriate global values file", func() { @@ -150,6 +146,65 @@ var _ = Describe("patternizer init", func() { verifyClusterGroupValues(clusterGroupValuesFile, expectedClusterGroupValues) }) }) + + Context("on a directory with a partially created global values file", Ordered, func() { + var tempDir string + + BeforeAll(func() { + tempDir = createTestDir() + Expect(os.WriteFile(filepath.Join(tempDir, "values-global.yaml"), []byte(customValuesGlobal), 0644)).To(Succeed()) + _ = runCLI(tempDir, "init") + }) + + AfterAll(func() { + os.RemoveAll(tempDir) + }) + + It("should create the pattern.sh script", func() { + verifyPattenShCopied(tempDir) + }) + + It("should create the common Makefile", func() { + verifyMakefileCommonCopied(tempDir) + }) + + It("should create the Makefile for the pattern", func() { + verifyMakefileCopied(tempDir) + }) + + It("should create an appropriate global values file", func() { + globalValuesFile := filepath.Join(tempDir, "values-global.yaml") + expectedGlobalValues := types.ValuesGlobal{ + Global: types.Global{ + Pattern: "test-pattern", + SecretLoader: types.SecretLoader{ + Disabled: true, + }, + }, + Main: types.Main{ + ClusterGroupName: "test", + MultiSourceConfig: types.MultiSourceConfig{ + Enabled: true, + ClusterGroupChartVersion: "0.9.*", + }, + }, + } + verifyGlobalValues(globalValuesFile, expectedGlobalValues) + }) + + It("should create an appropriate clustergroup values file", func() { + clusterGroupValuesFile := filepath.Join(tempDir, "values-test.yaml") + expectedClusterGroupValues := types.ValuesClusterGroup{ + ClusterGroup: types.ClusterGroup{ + Name: "test", + Namespaces: []types.NamespaceEntry{types.NewNamespaceEntry("test-pattern")}, + Subscriptions: map[string]types.Subscription{}, + Applications: map[string]types.Application{}, + }, + } + verifyClusterGroupValues(clusterGroupValuesFile, expectedClusterGroupValues) + }) + }) }) var _ = Describe("patternizer init --with-secrets", func() { @@ -166,21 +221,15 @@ var _ = Describe("patternizer init --with-secrets", func() { }) It("should create the pattern.sh script", func() { - actual := filepath.Join(tempDir, "pattern.sh") - expected := filepath.Join(resourcesPath, "pattern.sh") - verifyFilesMatch(actual, expected) + verifyPattenShCopied(tempDir) }) It("should create the common Makefile", func() { - actual := filepath.Join(tempDir, "Makefile-common") - expected := filepath.Join(resourcesPath, "Makefile-common") - verifyFilesMatch(actual, expected) + verifyMakefileCommonCopied(tempDir) }) It("should create the Makefile for the pattern", func() { - actual := filepath.Join(tempDir, "Makefile") - expected := filepath.Join(resourcesPath, "Makefile") - verifyFilesMatch(actual, expected) + verifyMakefileCopied(tempDir) }) It("should create an appropriate global values file", func() { @@ -249,21 +298,15 @@ var _ = Describe("patternizer init --with-secrets", func() { }) It("should create the pattern.sh script", func() { - actual := filepath.Join(tempDir, "pattern.sh") - expected := filepath.Join(resourcesPath, "pattern.sh") - verifyFilesMatch(actual, expected) + verifyPattenShCopied(tempDir) }) It("should create the common Makefile", func() { - actual := filepath.Join(tempDir, "Makefile-common") - expected := filepath.Join(resourcesPath, "Makefile-common") - verifyFilesMatch(actual, expected) + verifyMakefileCommonCopied(tempDir) }) It("should create the Makefile for the pattern", func() { - actual := filepath.Join(tempDir, "Makefile") - expected := filepath.Join(resourcesPath, "Makefile") - verifyFilesMatch(actual, expected) + verifyMakefileCopied(tempDir) }) It("should create an appropriate global values file", func() { @@ -293,7 +336,7 @@ var _ = Describe("patternizer init --with-secrets", func() { ClusterGroup: types.ClusterGroup{ Name: "prod", Namespaces: []types.NamespaceEntry{ - types.NewNamespaceEntry(filepath.Base(tempDir)), + types.NewNamespaceEntry(expectedNamespace), types.NewNamespaceEntry("vault"), types.NewNamespaceEntry("golang-external-secrets"), }, @@ -327,4 +370,80 @@ var _ = Describe("patternizer init --with-secrets", func() { verifyClusterGroupValues(clusterGroupValuesFile, expectedClusterGroupValues) }) }) + + Context("on a directory with a partially created global values file", Ordered, func() { + var tempDir string + + BeforeAll(func() { + tempDir = createTestDir() + Expect(os.WriteFile(filepath.Join(tempDir, "values-global.yaml"), []byte(customValuesGlobal), 0644)).To(Succeed()) + _ = runCLI(tempDir, "init", "--with-secrets") + }) + + AfterAll(func() { + os.RemoveAll(tempDir) + }) + + It("should create the pattern.sh script", func() { + verifyPattenShCopied(tempDir) + }) + + It("should create the common Makefile", func() { + verifyMakefileCommonCopied(tempDir) + }) + + It("should create the Makefile for the pattern", func() { + verifyMakefileCopied(tempDir) + }) + + It("should create an appropriate global values file", func() { + globalValuesFile := filepath.Join(tempDir, "values-global.yaml") + expectedGlobalValues := types.ValuesGlobal{ + Global: types.Global{ + Pattern: "test-pattern", + SecretLoader: types.SecretLoader{ + Disabled: false, + }, + }, + Main: types.Main{ + ClusterGroupName: "test", + MultiSourceConfig: types.MultiSourceConfig{ + Enabled: true, + ClusterGroupChartVersion: "0.9.*", + }, + }, + } + verifyGlobalValues(globalValuesFile, expectedGlobalValues) + }) + + It("should create an appropriate clustergroup values file", func() { + clusterGroupValuesFile := filepath.Join(tempDir, "values-test.yaml") + expectedClusterGroupValues := types.ValuesClusterGroup{ + ClusterGroup: types.ClusterGroup{ + Name: "test", + Namespaces: []types.NamespaceEntry{ + types.NewNamespaceEntry("test-pattern"), + types.NewNamespaceEntry("vault"), + types.NewNamespaceEntry("golang-external-secrets"), + }, + Subscriptions: map[string]types.Subscription{}, + Applications: map[string]types.Application{ + "vault": { + Name: "vault", + Namespace: "vault", + Chart: "hashicorp-vault", + ChartVersion: "0.1.*", + }, + "golang-external-secrets": { + Name: "golang-external-secrets", + Namespace: "golang-external-secrets", + Chart: "golang-external-secrets", + ChartVersion: "0.1.*", + }, + }, + }, + } + verifyClusterGroupValues(clusterGroupValuesFile, expectedClusterGroupValues) + }) + }) }) From f54105aac71f980a7f86828fb9ceaa1149ab0afd Mon Sep 17 00:00:00 2001 From: Drew Minnear Date: Wed, 25 Feb 2026 12:51:20 -0500 Subject: [PATCH 03/13] add init with secrets after init test case --- src/cmd/cmd_suite_test.go | 6 ++ src/cmd/init_test.go | 153 +++++++++++++++++++++++--------------- 2 files changed, 97 insertions(+), 62 deletions(-) diff --git a/src/cmd/cmd_suite_test.go b/src/cmd/cmd_suite_test.go index 6af74da..e5037c9 100644 --- a/src/cmd/cmd_suite_test.go +++ b/src/cmd/cmd_suite_test.go @@ -74,6 +74,12 @@ func verifyMakefileCopied(dir string) { verifyFilesMatch(actual, expected) } +func verifyScaffoldFilesCopied(dir string) { + verifyPattenShCopied(dir) + verifyMakefileCommonCopied(dir) + verifyMakefileCopied(dir) +} + func verifyFilesMatch(file1, file2 string) { file1Contents, err := os.ReadFile(file1) Expect(err).NotTo(HaveOccurred(), fmt.Sprintf("Could not read file %s", file1)) diff --git a/src/cmd/init_test.go b/src/cmd/init_test.go index 64d8072..9ff60d7 100644 --- a/src/cmd/init_test.go +++ b/src/cmd/init_test.go @@ -29,16 +29,8 @@ var _ = Describe("patternizer init", func() { os.RemoveAll(tempDir) }) - It("should create the pattern.sh script", func() { - verifyPattenShCopied(tempDir) - }) - - It("should create the common Makefile", func() { - verifyMakefileCommonCopied(tempDir) - }) - - It("should create the Makefile for the pattern", func() { - verifyMakefileCopied(tempDir) + It("should copy the common pattern scaffold files", func() { + verifyScaffoldFilesCopied(tempDir) }) It("should create an appropriate global values file", func() { @@ -89,16 +81,8 @@ var _ = Describe("patternizer init", func() { os.RemoveAll(tempDir) }) - It("should create the pattern.sh script", func() { - verifyPattenShCopied(tempDir) - }) - - It("should create the common Makefile", func() { - verifyMakefileCommonCopied(tempDir) - }) - - It("should create the Makefile for the pattern", func() { - verifyMakefileCopied(tempDir) + It("should copy the common pattern scaffold files", func() { + verifyScaffoldFilesCopied(tempDir) }) It("should create an appropriate global values file", func() { @@ -147,7 +131,7 @@ var _ = Describe("patternizer init", func() { }) }) - Context("on a directory with a partially created global values file", Ordered, func() { + Context("on a directory with a custom global values file", Ordered, func() { var tempDir string BeforeAll(func() { @@ -160,16 +144,8 @@ var _ = Describe("patternizer init", func() { os.RemoveAll(tempDir) }) - It("should create the pattern.sh script", func() { - verifyPattenShCopied(tempDir) - }) - - It("should create the common Makefile", func() { - verifyMakefileCommonCopied(tempDir) - }) - - It("should create the Makefile for the pattern", func() { - verifyMakefileCopied(tempDir) + It("should copy the common pattern scaffold files", func() { + verifyScaffoldFilesCopied(tempDir) }) It("should create an appropriate global values file", func() { @@ -220,16 +196,8 @@ var _ = Describe("patternizer init --with-secrets", func() { os.RemoveAll(tempDir) }) - It("should create the pattern.sh script", func() { - verifyPattenShCopied(tempDir) - }) - - It("should create the common Makefile", func() { - verifyMakefileCommonCopied(tempDir) - }) - - It("should create the Makefile for the pattern", func() { - verifyMakefileCopied(tempDir) + It("should copy the common pattern scaffold files", func() { + verifyScaffoldFilesCopied(tempDir) }) It("should create an appropriate global values file", func() { @@ -297,16 +265,8 @@ var _ = Describe("patternizer init --with-secrets", func() { os.RemoveAll(tempDir) }) - It("should create the pattern.sh script", func() { - verifyPattenShCopied(tempDir) - }) - - It("should create the common Makefile", func() { - verifyMakefileCommonCopied(tempDir) - }) - - It("should create the Makefile for the pattern", func() { - verifyMakefileCopied(tempDir) + It("should copy the common pattern scaffold files", func() { + verifyScaffoldFilesCopied(tempDir) }) It("should create an appropriate global values file", func() { @@ -371,7 +331,7 @@ var _ = Describe("patternizer init --with-secrets", func() { }) }) - Context("on a directory with a partially created global values file", Ordered, func() { + Context("on a directory with a custom global values file", Ordered, func() { var tempDir string BeforeAll(func() { @@ -384,16 +344,8 @@ var _ = Describe("patternizer init --with-secrets", func() { os.RemoveAll(tempDir) }) - It("should create the pattern.sh script", func() { - verifyPattenShCopied(tempDir) - }) - - It("should create the common Makefile", func() { - verifyMakefileCommonCopied(tempDir) - }) - - It("should create the Makefile for the pattern", func() { - verifyMakefileCopied(tempDir) + It("should copy the common pattern scaffold files", func() { + verifyScaffoldFilesCopied(tempDir) }) It("should create an appropriate global values file", func() { @@ -446,4 +398,81 @@ var _ = Describe("patternizer init --with-secrets", func() { verifyClusterGroupValues(clusterGroupValuesFile, expectedClusterGroupValues) }) }) + + Context("after running patternizer init", Ordered, func() { + var tempDir string + + BeforeAll(func() { + tempDir = createTestDir() + addDummyChart(tempDir, "test-app1") + addDummyChart(tempDir, "test-app2") + _ = runCLI(tempDir, "init") + _ = runCLI(tempDir, "init", "--with-secrets") + }) + + AfterAll(func() { + os.RemoveAll(tempDir) + }) + + It("should update the global values file to load secrets", func() { + globalValuesFile := filepath.Join(tempDir, "values-global.yaml") + expectedGlobalValues := types.ValuesGlobal{ + Global: types.Global{ + Pattern: filepath.Base(tempDir), + SecretLoader: types.SecretLoader{ + Disabled: false, + }, + }, + Main: types.Main{ + ClusterGroupName: "prod", + MultiSourceConfig: types.MultiSourceConfig{ + Enabled: true, + ClusterGroupChartVersion: "0.9.*", + }, + }, + } + verifyGlobalValues(globalValuesFile, expectedGlobalValues) + }) + + It("should update the clustergroup values file to include secrets", func() { + clusterGroupValuesFile := filepath.Join(tempDir, "values-prod.yaml") + expectedNamespace := filepath.Base(tempDir) + expectedClusterGroupValues := types.ValuesClusterGroup{ + ClusterGroup: types.ClusterGroup{ + Name: "prod", + Namespaces: []types.NamespaceEntry{ + types.NewNamespaceEntry(expectedNamespace), + types.NewNamespaceEntry("vault"), + types.NewNamespaceEntry("golang-external-secrets"), + }, + Subscriptions: map[string]types.Subscription{}, + Applications: map[string]types.Application{ + "test-app1": { + Name: "test-app1", + Namespace: expectedNamespace, + Path: "charts/test-app1", + }, + "test-app2": { + Name: "test-app2", + Namespace: expectedNamespace, + Path: "charts/test-app2", + }, + "vault": { + Name: "vault", + Namespace: "vault", + Chart: "hashicorp-vault", + ChartVersion: "0.1.*", + }, + "golang-external-secrets": { + Name: "golang-external-secrets", + Namespace: "golang-external-secrets", + Chart: "golang-external-secrets", + ChartVersion: "0.1.*", + }, + }, + }, + } + verifyClusterGroupValues(clusterGroupValuesFile, expectedClusterGroupValues) + }) + }) }) From e5b1ddd902b72e432354beb170b1fb2b1da63784 Mon Sep 17 00:00:00 2001 From: Drew Minnear Date: Wed, 25 Feb 2026 14:32:49 -0500 Subject: [PATCH 04/13] add test cases around custom clustergroup values file on init --- src/cmd/init_test.go | 163 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 160 insertions(+), 3 deletions(-) diff --git a/src/cmd/init_test.go b/src/cmd/init_test.go index 9ff60d7..e3ddb1d 100644 --- a/src/cmd/init_test.go +++ b/src/cmd/init_test.go @@ -9,13 +9,30 @@ import ( . "github.com/onsi/gomega" ) -const customValuesGlobal = ` +const customGlobalValues = ` global: pattern: test-pattern main: clusterGroupName: test ` +const customClusterGroupValues = ` +clusterGroup: + name: test + + customClusterField: user-cluster-config + + applications: + custom-user-app: + name: custom-user-app + namespace: user-namespace + path: user/path + customAppField: user-app-config + project: custom-pattern-name + +customClusterTopLevel: user-cluster-top-level +` + var _ = Describe("patternizer init", func() { Context("on an empty directory", Ordered, func() { var tempDir string @@ -136,7 +153,7 @@ var _ = Describe("patternizer init", func() { BeforeAll(func() { tempDir = createTestDir() - Expect(os.WriteFile(filepath.Join(tempDir, "values-global.yaml"), []byte(customValuesGlobal), 0644)).To(Succeed()) + Expect(os.WriteFile(filepath.Join(tempDir, "values-global.yaml"), []byte(customGlobalValues), 0644)).To(Succeed()) _ = runCLI(tempDir, "init") }) @@ -181,6 +198,68 @@ var _ = Describe("patternizer init", func() { verifyClusterGroupValues(clusterGroupValuesFile, expectedClusterGroupValues) }) }) + + Context("on a directory with custom global values and clustergroup values files", Ordered, func() { + var tempDir string + + BeforeAll(func() { + tempDir = createTestDir() + Expect(os.WriteFile(filepath.Join(tempDir, "values-global.yaml"), []byte(customGlobalValues), 0644)).To(Succeed()) + Expect(os.WriteFile(filepath.Join(tempDir, "values-test.yaml"), []byte(customClusterGroupValues), 0644)).To(Succeed()) + _ = runCLI(tempDir, "init") + }) + + AfterAll(func() { + os.RemoveAll(tempDir) + }) + + It("should copy the common pattern scaffold files", func() { + verifyScaffoldFilesCopied(tempDir) + }) + + It("should create an appropriate global values file", func() { + globalValuesFile := filepath.Join(tempDir, "values-global.yaml") + expectedGlobalValues := types.ValuesGlobal{ + Global: types.Global{ + Pattern: "test-pattern", + SecretLoader: types.SecretLoader{ + Disabled: true, + }, + }, + Main: types.Main{ + ClusterGroupName: "test", + MultiSourceConfig: types.MultiSourceConfig{ + Enabled: true, + ClusterGroupChartVersion: "0.9.*", + }, + }, + } + verifyGlobalValues(globalValuesFile, expectedGlobalValues) + }) + + It("should create an appropriate clustergroup values file", func() { + clusterGroupValuesFile := filepath.Join(tempDir, "values-test.yaml") + expectedClusterGroupValues := types.ValuesClusterGroup{ + ClusterGroup: types.ClusterGroup{ + Name: "test", + Namespaces: []types.NamespaceEntry{types.NewNamespaceEntry("test-pattern")}, + Subscriptions: map[string]types.Subscription{}, + Applications: map[string]types.Application{ + "custom-user-app": { + Name: "custom-user-app", + Namespace: "user-namespace", + Path: "user/path", + Project: "custom-pattern-name", + OtherFields: map[string]interface{}{"customAppField": "user-app-config"}, + }, + }, + OtherFields: map[string]interface{}{"customClusterField": "user-cluster-config"}, + }, + OtherFields: map[string]interface{}{"customClusterTopLevel": "user-cluster-top-level"}, + } + verifyClusterGroupValues(clusterGroupValuesFile, expectedClusterGroupValues) + }) + }) }) var _ = Describe("patternizer init --with-secrets", func() { @@ -336,7 +415,7 @@ var _ = Describe("patternizer init --with-secrets", func() { BeforeAll(func() { tempDir = createTestDir() - Expect(os.WriteFile(filepath.Join(tempDir, "values-global.yaml"), []byte(customValuesGlobal), 0644)).To(Succeed()) + Expect(os.WriteFile(filepath.Join(tempDir, "values-global.yaml"), []byte(customGlobalValues), 0644)).To(Succeed()) _ = runCLI(tempDir, "init", "--with-secrets") }) @@ -475,4 +554,82 @@ var _ = Describe("patternizer init --with-secrets", func() { verifyClusterGroupValues(clusterGroupValuesFile, expectedClusterGroupValues) }) }) + + Context("on a directory with custom global values and clustergroup values files", Ordered, func() { + var tempDir string + + BeforeAll(func() { + tempDir = createTestDir() + Expect(os.WriteFile(filepath.Join(tempDir, "values-global.yaml"), []byte(customGlobalValues), 0644)).To(Succeed()) + Expect(os.WriteFile(filepath.Join(tempDir, "values-test.yaml"), []byte(customClusterGroupValues), 0644)).To(Succeed()) + _ = runCLI(tempDir, "init", "--with-secrets") + }) + + AfterAll(func() { + os.RemoveAll(tempDir) + }) + + It("should copy the common pattern scaffold files", func() { + verifyScaffoldFilesCopied(tempDir) + }) + + It("should create an appropriate global values file", func() { + globalValuesFile := filepath.Join(tempDir, "values-global.yaml") + expectedGlobalValues := types.ValuesGlobal{ + Global: types.Global{ + Pattern: "test-pattern", + SecretLoader: types.SecretLoader{ + Disabled: false, + }, + }, + Main: types.Main{ + ClusterGroupName: "test", + MultiSourceConfig: types.MultiSourceConfig{ + Enabled: true, + ClusterGroupChartVersion: "0.9.*", + }, + }, + } + verifyGlobalValues(globalValuesFile, expectedGlobalValues) + }) + + It("should create an appropriate clustergroup values file", func() { + clusterGroupValuesFile := filepath.Join(tempDir, "values-test.yaml") + expectedClusterGroupValues := types.ValuesClusterGroup{ + ClusterGroup: types.ClusterGroup{ + Name: "test", + Namespaces: []types.NamespaceEntry{ + types.NewNamespaceEntry("test-pattern"), + types.NewNamespaceEntry("vault"), + types.NewNamespaceEntry("golang-external-secrets"), + }, + Subscriptions: map[string]types.Subscription{}, + Applications: map[string]types.Application{ + "custom-user-app": { + Name: "custom-user-app", + Namespace: "user-namespace", + Path: "user/path", + Project: "custom-pattern-name", + OtherFields: map[string]interface{}{"customAppField": "user-app-config"}, + }, + "vault": { + Name: "vault", + Namespace: "vault", + Chart: "hashicorp-vault", + ChartVersion: "0.1.*", + }, + "golang-external-secrets": { + Name: "golang-external-secrets", + Namespace: "golang-external-secrets", + Chart: "golang-external-secrets", + ChartVersion: "0.1.*", + }, + }, + OtherFields: map[string]interface{}{"customClusterField": "user-cluster-config"}, + }, + OtherFields: map[string]interface{}{"customClusterTopLevel": "user-cluster-top-level"}, + } + verifyClusterGroupValues(clusterGroupValuesFile, expectedClusterGroupValues) + }) + }) }) From 6a6f1edb3566f80774acd06dd3e0192a7b302ddf Mon Sep 17 00:00:00 2001 From: Drew Minnear Date: Thu, 26 Feb 2026 11:31:31 -0500 Subject: [PATCH 05/13] bump go version --- .github/workflows/lint-test.yaml | 2 +- Containerfile | 2 +- Makefile | 4 +- README.md | 2 +- src/.golangci.yml | 2 +- src/cmd/cmd_suite_test.go | 28 +++++--- src/cmd/init.go | 6 +- src/cmd/init_test.go | 101 ++++++++++++++++++++------- src/cmd/upgrade.go | 4 +- src/cmd/upgrade_test.go | 9 +++ src/go.mod | 9 ++- src/go.sum | 33 ++++++++- src/internal/pattern/pattern.go | 4 +- src/internal/pattern/pattern_test.go | 2 +- src/main.go | 2 +- src/main_test.go | 4 +- 16 files changed, 153 insertions(+), 61 deletions(-) create mode 100644 src/cmd/upgrade_test.go diff --git a/.github/workflows/lint-test.yaml b/.github/workflows/lint-test.yaml index 4762908..b7bc26c 100644 --- a/.github/workflows/lint-test.yaml +++ b/.github/workflows/lint-test.yaml @@ -5,7 +5,7 @@ on: branches: ["main"] env: - GO_VERSION: '1.24' + GO_VERSION: '1.25' jobs: lint: diff --git a/Containerfile b/Containerfile index f50acf6..5d02f6e 100644 --- a/Containerfile +++ b/Containerfile @@ -1,4 +1,4 @@ -ARG GO_VERSION=1.24 +ARG GO_VERSION=1.25.7 ARG GOARCH=amd64 # Build stage diff --git a/Makefile b/Makefile index 936fd59..04c8f55 100644 --- a/Makefile +++ b/Makefile @@ -12,8 +12,8 @@ GO_TEST := $(GO_CMD) test GO_CLEAN := $(GO_CMD) clean GO_VET := $(GO_CMD) vet GO_FMT := gofmt -GO_VERSION := 1.24 -GOLANGCI_LINT_VERSION := v2.1.6 +GO_VERSION := 1.25.7 +GOLANGCI_LINT_VERSION := v2.10.1 GINKGO_VERSION := v2.28.1 SRC_DIR := src diff --git a/README.md b/README.md index 2778383..1136e64 100644 --- a/README.md +++ b/README.md @@ -168,7 +168,7 @@ This section is for developers who want to contribute to the Patternizer project ```bash # 1. Clone the repository -git clone https://github.com/dminnear-rh/patternizer.git +git clone https://github.com/validatedpatterns/patternizer.git cd patternizer # 2. Set up the development environment (installs tools) diff --git a/src/.golangci.yml b/src/.golangci.yml index ea5d997..dfd39f4 100644 --- a/src/.golangci.yml +++ b/src/.golangci.yml @@ -42,7 +42,7 @@ formatters: simplify: true goimports: local-prefixes: - - github.com/dminnear-rh/patternizer + - github.com/validatedpatterns/patternizer exclusions: generated: lax paths: diff --git a/src/cmd/cmd_suite_test.go b/src/cmd/cmd_suite_test.go index e5037c9..5bb2bf5 100644 --- a/src/cmd/cmd_suite_test.go +++ b/src/cmd/cmd_suite_test.go @@ -13,7 +13,7 @@ import ( "github.com/onsi/gomega/gexec" "gopkg.in/yaml.v3" - "github.com/dminnear-rh/patternizer/internal/types" + "github.com/validatedpatterns/patternizer/internal/types" ) var ( @@ -33,7 +33,7 @@ var _ = BeforeSuite(func() { wd, err := os.Getwd() Expect(err).NotTo(HaveOccurred()) - projectRoot, err = filepath.Abs(filepath.Join(wd, "../..")) + projectRoot, err = filepath.Abs(filepath.Join(wd, "..", "..")) Expect(err).NotTo(HaveOccurred()) resourcesPath = filepath.Join(projectRoot, "resources") @@ -59,7 +59,7 @@ func verifyPattenShCopied(dir string) { // verify pattern.sh is executable info, err := os.Stat(actual) Expect(err).NotTo(HaveOccurred()) - Expect(info.Mode() & 0111).NotTo(Equal(0)) + Expect(info.Mode() & 0o111).NotTo(Equal(0)) } func verifyMakefileCommonCopied(dir string) { @@ -80,6 +80,12 @@ func verifyScaffoldFilesCopied(dir string) { verifyMakefileCopied(dir) } +func verifySecretTemplateCopied(dir string) { + actual := filepath.Join(dir, "values-secret.yaml.template") + expected := filepath.Join(dir, "values-secret.yaml.template") + verifyFilesMatch(actual, expected) +} + func verifyFilesMatch(file1, file2 string) { file1Contents, err := os.ReadFile(file1) Expect(err).NotTo(HaveOccurred(), fmt.Sprintf("Could not read file %s", file1)) @@ -90,7 +96,7 @@ func verifyFilesMatch(file1, file2 string) { Expect(bytes.Equal(file1Contents, file2Contents)).To(BeTrue(), fmt.Sprintf("%s and %s have different contents", file1, file2)) } -func verifyGlobalValues(valuesFile string, expectedGlobalValues types.ValuesGlobal) { +func verifyGlobalValues(valuesFile string, expectedGlobalValues *types.ValuesGlobal) { f, err := os.ReadFile(valuesFile) Expect(err).NotTo(HaveOccurred(), fmt.Sprintf("Could not read file %s", valuesFile)) @@ -98,10 +104,10 @@ func verifyGlobalValues(valuesFile string, expectedGlobalValues types.ValuesGlob err = yaml.Unmarshal(f, &globalValues) Expect(err).NotTo(HaveOccurred(), fmt.Sprintf("Could not unmarshal %s into the ValuesGlobal type", valuesFile)) - Expect(*globalValues).To(Equal(expectedGlobalValues), fmt.Sprintf("Global values in %s differ from expected values", valuesFile)) + Expect(*globalValues).To(Equal(*expectedGlobalValues), fmt.Sprintf("Global values in %s differ from expected values", valuesFile)) } -func verifyClusterGroupValues(valuesFile string, expectedClusterGroupValues types.ValuesClusterGroup) { +func verifyClusterGroupValues(valuesFile string, expectedClusterGroupValues *types.ValuesClusterGroup) { f, err := os.ReadFile(valuesFile) Expect(err).NotTo(HaveOccurred(), fmt.Sprintf("Could not read file %s", valuesFile)) @@ -109,7 +115,7 @@ func verifyClusterGroupValues(valuesFile string, expectedClusterGroupValues type err = yaml.Unmarshal(f, &clusterGroupValues) Expect(err).NotTo(HaveOccurred(), fmt.Sprintf("Could not unmarshal %s into the ValuesClusterGroup type", valuesFile)) - Expect(*clusterGroupValues).To(Equal(expectedClusterGroupValues), fmt.Sprintf("Clustergroup values in %s differ from expected values", valuesFile)) + Expect(*clusterGroupValues).To(Equal(*expectedClusterGroupValues), fmt.Sprintf("Clustergroup values in %s differ from expected values", valuesFile)) } func createTestDir() string { @@ -131,8 +137,8 @@ func runCLI(dir string, args ...string) *gexec.Session { func addDummyChart(dir, name string) { path := filepath.Join(dir, "charts", name) - Expect(os.MkdirAll(path, 0755)).To(Succeed()) - Expect(os.WriteFile(filepath.Join(path, "Chart.yaml"), []byte("name: "+name+"\nversion: 0.1.0"), 0644)).To(Succeed()) - Expect(os.WriteFile(filepath.Join(path, "values.yaml"), []byte("replicaCount: 1"), 0644)).To(Succeed()) - Expect(os.MkdirAll(filepath.Join(path, "templates"), 0755)).To(Succeed()) + Expect(os.MkdirAll(path, 0o755)).To(Succeed()) + Expect(os.WriteFile(filepath.Join(path, "Chart.yaml"), []byte("name: "+name+"\nversion: 0.1.0"), 0o644)).To(Succeed()) + Expect(os.WriteFile(filepath.Join(path, "values.yaml"), []byte("replicaCount: 1"), 0o644)).To(Succeed()) + Expect(os.MkdirAll(filepath.Join(path, "templates"), 0o755)).To(Succeed()) } diff --git a/src/cmd/init.go b/src/cmd/init.go index 040e272..7fa50ff 100644 --- a/src/cmd/init.go +++ b/src/cmd/init.go @@ -5,9 +5,9 @@ import ( "os" "path/filepath" - "github.com/dminnear-rh/patternizer/internal/fileutils" - "github.com/dminnear-rh/patternizer/internal/helm" - "github.com/dminnear-rh/patternizer/internal/pattern" + "github.com/validatedpatterns/patternizer/internal/fileutils" + "github.com/validatedpatterns/patternizer/internal/helm" + "github.com/validatedpatterns/patternizer/internal/pattern" ) // runInit handles the initialization logic for the init command. diff --git a/src/cmd/init_test.go b/src/cmd/init_test.go index e3ddb1d..a11893c 100644 --- a/src/cmd/init_test.go +++ b/src/cmd/init_test.go @@ -4,9 +4,10 @@ import ( "os" "path/filepath" - "github.com/dminnear-rh/patternizer/internal/types" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + + "github.com/validatedpatterns/patternizer/internal/types" ) const customGlobalValues = ` @@ -33,6 +34,16 @@ clusterGroup: customClusterTopLevel: user-cluster-top-level ` +const customSecretTemplate = ` +version: "2.0" + +secrets: + - name: customSecret + fields: + - name: test + value: test +` + var _ = Describe("patternizer init", func() { Context("on an empty directory", Ordered, func() { var tempDir string @@ -67,7 +78,7 @@ var _ = Describe("patternizer init", func() { }, }, } - verifyGlobalValues(globalValuesFile, expectedGlobalValues) + verifyGlobalValues(globalValuesFile, &expectedGlobalValues) }) It("should create an appropriate clustergroup values file", func() { @@ -80,7 +91,7 @@ var _ = Describe("patternizer init", func() { Applications: map[string]types.Application{}, }, } - verifyClusterGroupValues(clusterGroupValuesFile, expectedClusterGroupValues) + verifyClusterGroupValues(clusterGroupValuesFile, &expectedClusterGroupValues) }) }) @@ -119,7 +130,7 @@ var _ = Describe("patternizer init", func() { }, }, } - verifyGlobalValues(globalValuesFile, expectedGlobalValues) + verifyGlobalValues(globalValuesFile, &expectedGlobalValues) }) It("should create an appropriate clustergroup values file", func() { @@ -144,7 +155,7 @@ var _ = Describe("patternizer init", func() { }, }, } - verifyClusterGroupValues(clusterGroupValuesFile, expectedClusterGroupValues) + verifyClusterGroupValues(clusterGroupValuesFile, &expectedClusterGroupValues) }) }) @@ -153,7 +164,7 @@ var _ = Describe("patternizer init", func() { BeforeAll(func() { tempDir = createTestDir() - Expect(os.WriteFile(filepath.Join(tempDir, "values-global.yaml"), []byte(customGlobalValues), 0644)).To(Succeed()) + Expect(os.WriteFile(filepath.Join(tempDir, "values-global.yaml"), []byte(customGlobalValues), 0o644)).To(Succeed()) _ = runCLI(tempDir, "init") }) @@ -182,7 +193,7 @@ var _ = Describe("patternizer init", func() { }, }, } - verifyGlobalValues(globalValuesFile, expectedGlobalValues) + verifyGlobalValues(globalValuesFile, &expectedGlobalValues) }) It("should create an appropriate clustergroup values file", func() { @@ -195,7 +206,7 @@ var _ = Describe("patternizer init", func() { Applications: map[string]types.Application{}, }, } - verifyClusterGroupValues(clusterGroupValuesFile, expectedClusterGroupValues) + verifyClusterGroupValues(clusterGroupValuesFile, &expectedClusterGroupValues) }) }) @@ -204,8 +215,8 @@ var _ = Describe("patternizer init", func() { BeforeAll(func() { tempDir = createTestDir() - Expect(os.WriteFile(filepath.Join(tempDir, "values-global.yaml"), []byte(customGlobalValues), 0644)).To(Succeed()) - Expect(os.WriteFile(filepath.Join(tempDir, "values-test.yaml"), []byte(customClusterGroupValues), 0644)).To(Succeed()) + Expect(os.WriteFile(filepath.Join(tempDir, "values-global.yaml"), []byte(customGlobalValues), 0o644)).To(Succeed()) + Expect(os.WriteFile(filepath.Join(tempDir, "values-test.yaml"), []byte(customClusterGroupValues), 0o644)).To(Succeed()) _ = runCLI(tempDir, "init") }) @@ -234,7 +245,7 @@ var _ = Describe("patternizer init", func() { }, }, } - verifyGlobalValues(globalValuesFile, expectedGlobalValues) + verifyGlobalValues(globalValuesFile, &expectedGlobalValues) }) It("should create an appropriate clustergroup values file", func() { @@ -257,7 +268,7 @@ var _ = Describe("patternizer init", func() { }, OtherFields: map[string]interface{}{"customClusterTopLevel": "user-cluster-top-level"}, } - verifyClusterGroupValues(clusterGroupValuesFile, expectedClusterGroupValues) + verifyClusterGroupValues(clusterGroupValuesFile, &expectedClusterGroupValues) }) }) }) @@ -279,6 +290,10 @@ var _ = Describe("patternizer init --with-secrets", func() { verifyScaffoldFilesCopied(tempDir) }) + It("should copy the secrets template file", func() { + verifySecretTemplateCopied(tempDir) + }) + It("should create an appropriate global values file", func() { globalValuesFile := filepath.Join(tempDir, "values-global.yaml") expectedGlobalValues := types.ValuesGlobal{ @@ -296,7 +311,7 @@ var _ = Describe("patternizer init --with-secrets", func() { }, }, } - verifyGlobalValues(globalValuesFile, expectedGlobalValues) + verifyGlobalValues(globalValuesFile, &expectedGlobalValues) }) It("should create an appropriate clustergroup values file", func() { @@ -326,7 +341,7 @@ var _ = Describe("patternizer init --with-secrets", func() { }, }, } - verifyClusterGroupValues(clusterGroupValuesFile, expectedClusterGroupValues) + verifyClusterGroupValues(clusterGroupValuesFile, &expectedClusterGroupValues) }) }) @@ -348,6 +363,10 @@ var _ = Describe("patternizer init --with-secrets", func() { verifyScaffoldFilesCopied(tempDir) }) + It("should copy the secrets template file", func() { + verifySecretTemplateCopied(tempDir) + }) + It("should create an appropriate global values file", func() { globalValuesFile := filepath.Join(tempDir, "values-global.yaml") expectedGlobalValues := types.ValuesGlobal{ @@ -365,7 +384,7 @@ var _ = Describe("patternizer init --with-secrets", func() { }, }, } - verifyGlobalValues(globalValuesFile, expectedGlobalValues) + verifyGlobalValues(globalValuesFile, &expectedGlobalValues) }) It("should create an appropriate clustergroup values file", func() { @@ -406,7 +425,7 @@ var _ = Describe("patternizer init --with-secrets", func() { }, }, } - verifyClusterGroupValues(clusterGroupValuesFile, expectedClusterGroupValues) + verifyClusterGroupValues(clusterGroupValuesFile, &expectedClusterGroupValues) }) }) @@ -415,7 +434,7 @@ var _ = Describe("patternizer init --with-secrets", func() { BeforeAll(func() { tempDir = createTestDir() - Expect(os.WriteFile(filepath.Join(tempDir, "values-global.yaml"), []byte(customGlobalValues), 0644)).To(Succeed()) + Expect(os.WriteFile(filepath.Join(tempDir, "values-global.yaml"), []byte(customGlobalValues), 0o644)).To(Succeed()) _ = runCLI(tempDir, "init", "--with-secrets") }) @@ -427,6 +446,10 @@ var _ = Describe("patternizer init --with-secrets", func() { verifyScaffoldFilesCopied(tempDir) }) + It("should copy the secrets template file", func() { + verifySecretTemplateCopied(tempDir) + }) + It("should create an appropriate global values file", func() { globalValuesFile := filepath.Join(tempDir, "values-global.yaml") expectedGlobalValues := types.ValuesGlobal{ @@ -444,7 +467,7 @@ var _ = Describe("patternizer init --with-secrets", func() { }, }, } - verifyGlobalValues(globalValuesFile, expectedGlobalValues) + verifyGlobalValues(globalValuesFile, &expectedGlobalValues) }) It("should create an appropriate clustergroup values file", func() { @@ -474,7 +497,7 @@ var _ = Describe("patternizer init --with-secrets", func() { }, }, } - verifyClusterGroupValues(clusterGroupValuesFile, expectedClusterGroupValues) + verifyClusterGroupValues(clusterGroupValuesFile, &expectedClusterGroupValues) }) }) @@ -493,6 +516,10 @@ var _ = Describe("patternizer init --with-secrets", func() { os.RemoveAll(tempDir) }) + It("should copy the secrets template file", func() { + verifySecretTemplateCopied(tempDir) + }) + It("should update the global values file to load secrets", func() { globalValuesFile := filepath.Join(tempDir, "values-global.yaml") expectedGlobalValues := types.ValuesGlobal{ @@ -510,7 +537,7 @@ var _ = Describe("patternizer init --with-secrets", func() { }, }, } - verifyGlobalValues(globalValuesFile, expectedGlobalValues) + verifyGlobalValues(globalValuesFile, &expectedGlobalValues) }) It("should update the clustergroup values file to include secrets", func() { @@ -551,7 +578,7 @@ var _ = Describe("patternizer init --with-secrets", func() { }, }, } - verifyClusterGroupValues(clusterGroupValuesFile, expectedClusterGroupValues) + verifyClusterGroupValues(clusterGroupValuesFile, &expectedClusterGroupValues) }) }) @@ -560,8 +587,8 @@ var _ = Describe("patternizer init --with-secrets", func() { BeforeAll(func() { tempDir = createTestDir() - Expect(os.WriteFile(filepath.Join(tempDir, "values-global.yaml"), []byte(customGlobalValues), 0644)).To(Succeed()) - Expect(os.WriteFile(filepath.Join(tempDir, "values-test.yaml"), []byte(customClusterGroupValues), 0644)).To(Succeed()) + Expect(os.WriteFile(filepath.Join(tempDir, "values-global.yaml"), []byte(customGlobalValues), 0o644)).To(Succeed()) + Expect(os.WriteFile(filepath.Join(tempDir, "values-test.yaml"), []byte(customClusterGroupValues), 0o644)).To(Succeed()) _ = runCLI(tempDir, "init", "--with-secrets") }) @@ -573,6 +600,10 @@ var _ = Describe("patternizer init --with-secrets", func() { verifyScaffoldFilesCopied(tempDir) }) + It("should copy the secrets template file", func() { + verifySecretTemplateCopied(tempDir) + }) + It("should create an appropriate global values file", func() { globalValuesFile := filepath.Join(tempDir, "values-global.yaml") expectedGlobalValues := types.ValuesGlobal{ @@ -590,7 +621,7 @@ var _ = Describe("patternizer init --with-secrets", func() { }, }, } - verifyGlobalValues(globalValuesFile, expectedGlobalValues) + verifyGlobalValues(globalValuesFile, &expectedGlobalValues) }) It("should create an appropriate clustergroup values file", func() { @@ -629,7 +660,27 @@ var _ = Describe("patternizer init --with-secrets", func() { }, OtherFields: map[string]interface{}{"customClusterTopLevel": "user-cluster-top-level"}, } - verifyClusterGroupValues(clusterGroupValuesFile, expectedClusterGroupValues) + verifyClusterGroupValues(clusterGroupValuesFile, &expectedClusterGroupValues) + }) + }) + + Context("on a directory with a custom secret template", Ordered, func() { + var tempDir string + + BeforeAll(func() { + tempDir = createTestDir() + Expect(os.WriteFile(filepath.Join(tempDir, "values-secret.yaml.template"), []byte(customSecretTemplate), 0o644)).To(Succeed()) + _ = runCLI(tempDir, "init", "--with-secrets") + }) + + AfterAll(func() { + os.RemoveAll(tempDir) + }) + + It("should not modify the secrets template file", func() { + actual, err := os.ReadFile(filepath.Join(tempDir, "values-secret.yaml.template")) + Expect(err).NotTo(HaveOccurred()) + Expect(string(actual)).To(Equal(customSecretTemplate)) }) }) }) diff --git a/src/cmd/upgrade.go b/src/cmd/upgrade.go index 325707c..e03a1fa 100644 --- a/src/cmd/upgrade.go +++ b/src/cmd/upgrade.go @@ -5,8 +5,8 @@ import ( "os" "path/filepath" - "github.com/dminnear-rh/patternizer/internal/fileutils" - "github.com/dminnear-rh/patternizer/internal/pattern" + "github.com/validatedpatterns/patternizer/internal/fileutils" + "github.com/validatedpatterns/patternizer/internal/pattern" ) // runUpgrade handles the upgrade logic for the upgrade command. diff --git a/src/cmd/upgrade_test.go b/src/cmd/upgrade_test.go new file mode 100644 index 0000000..f771848 --- /dev/null +++ b/src/cmd/upgrade_test.go @@ -0,0 +1,9 @@ +package cmd_test + +import ( + . "github.com/onsi/ginkgo/v2" +) + +var _ = Describe("patternizer upgrade", func() { + +}) diff --git a/src/go.mod b/src/go.mod index 0671449..7be8329 100644 --- a/src/go.mod +++ b/src/go.mod @@ -1,8 +1,10 @@ -module github.com/dminnear-rh/patternizer +module github.com/validatedpatterns/patternizer -go 1.24.4 +go 1.25.7 require ( + github.com/onsi/ginkgo/v2 v2.28.1 + github.com/onsi/gomega v1.39.1 github.com/spf13/cobra v1.9.1 gopkg.in/yaml.v3 v3.0.1 ) @@ -14,9 +16,6 @@ require ( github.com/google/go-cmp v0.7.0 // indirect github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/kr/pretty v0.3.1 // indirect - github.com/onsi/ginkgo/v2 v2.28.1 // indirect - github.com/onsi/gomega v1.39.1 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/spf13/pflag v1.0.6 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect diff --git a/src/go.sum b/src/go.sum index ada7fa7..ec6f177 100644 --- a/src/go.sum +++ b/src/go.sum @@ -1,17 +1,28 @@ github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gkampitakis/ciinfo v0.3.2 h1:JcuOPk8ZU7nZQjdUhctuhQofk7BGHuIy0c9Ez8BNhXs= +github.com/gkampitakis/ciinfo v0.3.2/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo= +github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M= +github.com/gkampitakis/go-diff v1.3.2/go.mod h1:LLgOrpqleQe26cte8s36HTWcTmMEur6OPYerdAAS9tk= +github.com/gkampitakis/go-snaps v0.5.15 h1:amyJrvM1D33cPHwVrjo9jQxX8g/7E2wYdZ+01KS3zGE= +github.com/gkampitakis/go-snaps v0.5.15/go.mod h1:HNpx/9GoKisdhw9AFOBT1N7DBs9DiHo/hGheFGBZ+mc= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= +github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 h1:z2ogiKUYzX5Is6zr/vP9vJGqPwcdqsWjOt+V8J7+bTc= github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/joshdk/go-junit v1.0.0 h1:S86cUKIdwBHWwA6xCmFlf3RTLfVXYQfvanM5Uh+K6GE= +github.com/joshdk/go-junit v1.0.0/go.mod h1:TiiV0PqkaNfFXjEiyjWM3XXrhVyCa1K4Zfga6W52ung= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -19,12 +30,16 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo= +github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg= +github.com/mfridman/tparse v0.18.0 h1:wh6dzOKaIwkUGyKgOntDW4liXSo37qg5AXbIhkMV3vE= +github.com/mfridman/tparse v0.18.0/go.mod h1:gEvqZTuCgEhPbYk/2lS3Kcxg1GmTxxU7kTC8DvP0i/A= github.com/onsi/ginkgo/v2 v2.28.1 h1:S4hj+HbZp40fNKuLUQOYLDgZLwNUVn19N3Atb98NCyI= github.com/onsi/ginkgo/v2 v2.28.1/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE= github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28= github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -32,6 +47,16 @@ github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= @@ -46,6 +71,8 @@ golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= +google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A= +google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/src/internal/pattern/pattern.go b/src/internal/pattern/pattern.go index 0e5a965..aa95726 100644 --- a/src/internal/pattern/pattern.go +++ b/src/internal/pattern/pattern.go @@ -7,8 +7,8 @@ import ( "gopkg.in/yaml.v3" - "github.com/dminnear-rh/patternizer/internal/fileutils" - "github.com/dminnear-rh/patternizer/internal/types" + "github.com/validatedpatterns/patternizer/internal/fileutils" + "github.com/validatedpatterns/patternizer/internal/types" ) // GetPatternNameAndRepoRoot returns the pattern name and repository root directory. diff --git a/src/internal/pattern/pattern_test.go b/src/internal/pattern/pattern_test.go index 6f50db2..7a57e8a 100644 --- a/src/internal/pattern/pattern_test.go +++ b/src/internal/pattern/pattern_test.go @@ -7,7 +7,7 @@ import ( "gopkg.in/yaml.v3" - "github.com/dminnear-rh/patternizer/internal/types" + "github.com/validatedpatterns/patternizer/internal/types" ) // TestProcessGlobalValuesPreservesFields tests that ProcessGlobalValues preserves existing user fields. diff --git a/src/main.go b/src/main.go index 97b28fb..77bc9c2 100644 --- a/src/main.go +++ b/src/main.go @@ -1,6 +1,6 @@ package main -import "github.com/dminnear-rh/patternizer/cmd" +import "github.com/validatedpatterns/patternizer/cmd" func main() { cmd.Execute() diff --git a/src/main_test.go b/src/main_test.go index 205e5ac..f29af14 100644 --- a/src/main_test.go +++ b/src/main_test.go @@ -4,8 +4,8 @@ import ( "os" "testing" - "github.com/dminnear-rh/patternizer/internal/fileutils" - "github.com/dminnear-rh/patternizer/internal/types" + "github.com/validatedpatterns/patternizer/internal/fileutils" + "github.com/validatedpatterns/patternizer/internal/types" ) func TestGetResourcePath(t *testing.T) { From ebdfb8c1a335b4156cf112500263bc5eaa7ddef5 Mon Sep 17 00:00:00 2001 From: Drew Minnear Date: Thu, 26 Feb 2026 14:38:25 -0500 Subject: [PATCH 06/13] add tests for upgrade --- Makefile | 26 +- resources/Makefile-common | 8 +- resources/ansible.cfg | 3 + src/cmd/cmd_suite_test.go | 7 + src/cmd/upgrade_test.go | 118 ++++ src/internal/pattern/pattern.go | 5 - src/internal/types/clustergroup.go | 1 - ...ected_values_custom_cluster_overwrite.yaml | 35 - test/expected_values_global.yaml | 9 - test/expected_values_global_custom.yaml | 9 - test/expected_values_global_overwrite.yaml | 14 - test/expected_values_global_with_secrets.yaml | 9 - test/expected_values_prod.yaml | 14 - test/expected_values_prod_with_secrets.yaml | 26 - ...expected_values_renamed_cluster_group.yaml | 26 - test/initial_ansible_cfg_overwrite | 7 - test/initial_makefile_overwrite | 6 - test/initial_makefile_pattern_overwrite | 6 - test/initial_pattern_sh_overwrite | 4 - ...itial_values_custom_cluster_overwrite.yaml | 12 - test/initial_values_global_custom.yaml | 4 - test/initial_values_global_overwrite.yaml | 14 - ...tial_values_secret_template_overwrite.yaml | 12 - test/integration_test.sh | 597 ------------------ 24 files changed, 134 insertions(+), 838 deletions(-) delete mode 100644 test/expected_values_custom_cluster_overwrite.yaml delete mode 100644 test/expected_values_global.yaml delete mode 100644 test/expected_values_global_custom.yaml delete mode 100644 test/expected_values_global_overwrite.yaml delete mode 100644 test/expected_values_global_with_secrets.yaml delete mode 100644 test/expected_values_prod.yaml delete mode 100644 test/expected_values_prod_with_secrets.yaml delete mode 100644 test/expected_values_renamed_cluster_group.yaml delete mode 100644 test/initial_ansible_cfg_overwrite delete mode 100644 test/initial_makefile_overwrite delete mode 100644 test/initial_makefile_pattern_overwrite delete mode 100644 test/initial_pattern_sh_overwrite delete mode 100644 test/initial_values_custom_cluster_overwrite.yaml delete mode 100644 test/initial_values_global_custom.yaml delete mode 100644 test/initial_values_global_overwrite.yaml delete mode 100644 test/initial_values_secret_template_overwrite.yaml delete mode 100755 test/integration_test.sh diff --git a/Makefile b/Makefile index 04c8f55..12d4501 100644 --- a/Makefile +++ b/Makefile @@ -50,7 +50,7 @@ deps: ## Download and install Go dependencies .PHONY: test-unit test-unit: ## Run unit tests @echo "Running unit tests..." - cd $(SRC_DIR) && $(GO_TEST) -v --args --ginkgo.v ./... + ginkgo -v ./... .PHONY: test-coverage test-coverage: ## Run unit tests with coverage report @@ -58,19 +58,8 @@ test-coverage: ## Run unit tests with coverage report cd $(SRC_DIR) && $(GO_TEST) ./... -coverprofile=coverage.out cd $(SRC_DIR) && $(GO_CMD) tool cover -func=coverage.out -.PHONY: shellcheck -shellcheck: ## Run shellcheck on integration test script - @echo "Running shellcheck on integration test script..." - @podman run --pull always -v "$(PWD):/mnt:z" docker.io/koalaman/shellcheck:stable test/integration_test.sh - @echo "Shellcheck passed" - -.PHONY: test-integration -test-integration: build shellcheck ## Run integration tests - @echo "Running integration tests..." - PATTERNIZER_BINARY=./$(SRC_DIR)/$(NAME) ./test/integration_test.sh - .PHONY: test -test: test-unit test-integration ## Run all tests (unit + integration) +test: test-unit ## Run all tests (unit + integration) .PHONY: lint lint: lint-fmt lint-vet lint-golangci ## Run all linting checks @@ -123,17 +112,6 @@ dev-setup: deps ## Set up development environment fi @echo "Development environment ready" -.PHONY: version -version: ## Show version information - @echo "Go version: $$(go version)" - @echo "Git commit: $$(git rev-parse --short HEAD 2>/dev/null || echo 'unknown')" - @echo "Build date: $$(date -u +%Y-%m-%dT%H:%M:%SZ)" - -.PHONY: docs -docs: ## Generate Go documentation - @echo "Generating documentation..." - cd $(SRC_DIR) && $(GO_CMD) doc -all ./... - .PHONY: check check: lint-fmt lint-vet build test-unit ## Quick check (format, vet, build, unit tests) diff --git a/resources/Makefile-common b/resources/Makefile-common index 537ca88..41f47f9 100644 --- a/resources/Makefile-common +++ b/resources/Makefile-common @@ -1,6 +1,6 @@ MAKEFLAGS += --no-print-directory -ANSIBLE_STDOUT_CALLBACK ?= null # null silences all ansible output. Override this with default, minimal, oneline, etc. when debugging. -ANSIBLE_RUN := ANSIBLE_STDOUT_CALLBACK=$(ANSIBLE_STDOUT_CALLBACK) ansible-playbook $(EXTRA_PLAYBOOK_OPTS) +ANSIBLE_STDOUT_CALLBACK ?= rhvp.cluser_utils.readable # null silences all ansible output. Override this with default, minimal, oneline, etc. when debugging. +ANSIBLE_RUN ?= ANSIBLE_STDOUT_CALLBACK=$(ANSIBLE_STDOUT_CALLBACK) ansible-playbook $(EXTRA_PLAYBOOK_OPTS) DOCS_URL := https://validatedpatterns.io/blog/2025-08-29-new-common-makefile-structure/ .PHONY: help @@ -20,9 +20,9 @@ operator-deploy operator-upgrade: ## Installs/updates the pattern on a cluster ( .PHONY: install install: pattern-install ## Installs the pattern onto a cluster (Loads secrets as well if configured) -.PHONY: uninstall ## Prints a notice that patterns cannot currently be uninstalled +.PHONY: uninstall ## (Experimental) Tries to uninstall the pattern. Not always possible for a clean uninstall due to storage finalizers, CSVs in openshift-operators namespace, and other conditions. See https://validatedpatterns.io/blog/2026-02-16-pattern-uninstall/. uninstall: - @echo "Uninstall is not possible at the moment so this target is empty. We are working to implement it as well as we can." + @$(ANSIBLE_RUN) rhvp.cluster_utils.uninstall .PHONY: pattern-install pattern-install: diff --git a/resources/ansible.cfg b/resources/ansible.cfg index ac1ce92..528a8cb 100644 --- a/resources/ansible.cfg +++ b/resources/ansible.cfg @@ -10,3 +10,6 @@ filter_plugins=~/.ansible/plugins/filter:./ansible/plugins/filter:/usr/share/ans # use the collections from the util. container, # change below if you want to test local collections collections_path=/usr/share/ansible/collections + +[inventory] +inventory_unparsed_warning=False diff --git a/src/cmd/cmd_suite_test.go b/src/cmd/cmd_suite_test.go index 5bb2bf5..1375793 100644 --- a/src/cmd/cmd_suite_test.go +++ b/src/cmd/cmd_suite_test.go @@ -74,10 +74,17 @@ func verifyMakefileCopied(dir string) { verifyFilesMatch(actual, expected) } +func verifyAnsibleCfgCopied(dir string) { + actual := filepath.Join(dir, "ansible.cfg") + expected := filepath.Join(resourcesPath, "ansible.cfg") + verifyFilesMatch(actual, expected) +} + func verifyScaffoldFilesCopied(dir string) { verifyPattenShCopied(dir) verifyMakefileCommonCopied(dir) verifyMakefileCopied(dir) + verifyAnsibleCfgCopied(dir) } func verifySecretTemplateCopied(dir string) { diff --git a/src/cmd/upgrade_test.go b/src/cmd/upgrade_test.go index f771848..0b5dc58 100644 --- a/src/cmd/upgrade_test.go +++ b/src/cmd/upgrade_test.go @@ -1,9 +1,127 @@ package cmd_test import ( + "errors" + "os" + "os/exec" + "path/filepath" + "strings" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/onsi/gomega/gexec" ) +func cloneMCGWithCommon(dir string) { + cmd := exec.Command("git", "init") + cmd.Dir = dir + session, err := gexec.Start(cmd, GinkgoWriter, GinkgoWriter) + Expect(err).NotTo(HaveOccurred()) + Eventually(session).Should(gexec.Exit(0), "Could not init git repo") + + cmd = exec.Command("git", "remote", "add", "origin", "https://github.com/validatedpatterns/multicloud-gitops.git") + cmd.Dir = dir + session, err = gexec.Start(cmd, GinkgoWriter, GinkgoWriter) + Expect(err).NotTo(HaveOccurred()) + Eventually(session).Should(gexec.Exit(0), "Could not add origin to git repo") + + cmd = exec.Command("git", "fetch", "--depth=1", "origin", "02954705e3d58e4823cd195beb8c31418f730830") + cmd.Dir = dir + session, err = gexec.Start(cmd, GinkgoWriter, GinkgoWriter) + Expect(err).NotTo(HaveOccurred()) + Eventually(session).Should(gexec.Exit(0), "Could not fetch SHA for last MCG commit before patternizer was run") + + cmd = exec.Command("git", "checkout", "02954705e3d58e4823cd195beb8c31418f730830") + cmd.Dir = dir + session, err = gexec.Start(cmd, GinkgoWriter, GinkgoWriter) + Expect(err).NotTo(HaveOccurred()) + Eventually(session).Should(gexec.Exit(0), "Could not checkout SHA for last MCG commit before patternizer was run") +} + var _ = Describe("patternizer upgrade", func() { + Context("on a repo using common", Ordered, func() { + var tempDir, oldMakefile string + + BeforeAll(func() { + tempDir = createTestDir() + cloneMCGWithCommon(tempDir) + f, err := os.ReadFile(filepath.Join(tempDir, "Makefile")) + Expect(err).NotTo(HaveOccurred()) + oldMakefile = string(f) + _ = runCLI(tempDir, "upgrade") + }) + + AfterAll(func() { + os.RemoveAll(tempDir) + }) + + It("should copy the common scaffold files (except Makefile)", func() { + verifyPattenShCopied(tempDir) + verifyMakefileCommonCopied(tempDir) + verifyAnsibleCfgCopied(tempDir) + }) + + It("should inject the include for Makefile-common into the existing Makefile", func() { + f, err := os.ReadFile(filepath.Join(tempDir, "Makefile")) + Expect(err).NotTo(HaveOccurred()) + actualMakefile := string(f) + Expect(strings.Contains(actualMakefile, oldMakefile)).To(BeTrue(), "Could not find contents of existing Makefile in updated Makefile") + Expect(strings.Contains(actualMakefile, "include Makefile-common\n")).To(BeTrue(), "Could not find include for Makefile-common in updated Makefile") + }) + + It("should remove the old common directory", func() { + _, err := os.Stat(filepath.Join(tempDir, "common")) + Expect(errors.Is(err, os.ErrNotExist)).To(BeTrue(), "Common directory should have been removed but was not") + }) + }) + + Context("when run multiple times", Ordered, func() { + var tempDir, expectedMakefile string + + BeforeAll(func() { + tempDir = createTestDir() + cloneMCGWithCommon(tempDir) + _ = runCLI(tempDir, "upgrade") + f, err := os.ReadFile(filepath.Join(tempDir, "Makefile")) + Expect(err).NotTo(HaveOccurred()) + expectedMakefile = string(f) + _ = runCLI(tempDir, "upgrade") + }) + + AfterAll(func() { + os.RemoveAll(tempDir) + }) + + It("should not update Makefiles that already include Makefile-common", func() { + f, err := os.ReadFile(filepath.Join(tempDir, "Makefile")) + Expect(err).NotTo(HaveOccurred()) + actualMakefile := string(f) + Expect(actualMakefile).To(Equal(expectedMakefile), "Makefiles that already contain the include for Makefile-common should not be updated") + }) + }) +}) + +var _ = Describe("patternizer upgrade --replace-makefile", func() { + Context("on a repo using common", Ordered, func() { + var tempDir string + + BeforeAll(func() { + tempDir = createTestDir() + cloneMCGWithCommon(tempDir) + _ = runCLI(tempDir, "upgrade", "--replace-makefile") + }) + + AfterAll(func() { + os.RemoveAll(tempDir) + }) + + It("should update the common scaffold files (including Makefile)", func() { + verifyScaffoldFilesCopied(tempDir) + }) + It("should remove the old common directory", func() { + _, err := os.Stat(filepath.Join(tempDir, "common")) + Expect(errors.Is(err, os.ErrNotExist)).To(BeTrue(), "Common directory should have been removed but was not") + }) + }) }) diff --git a/src/internal/pattern/pattern.go b/src/internal/pattern/pattern.go index aa95726..02bc709 100644 --- a/src/internal/pattern/pattern.go +++ b/src/internal/pattern/pattern.go @@ -141,11 +141,6 @@ func mergeClusterGroupValues(defaults, existing *types.ValuesClusterGroup) { defaults.ClusterGroup.Projects = mergedProjects - // Preserve other fields from existing - if existing.ClusterGroup.IsHubCluster { - defaults.ClusterGroup.IsHubCluster = existing.ClusterGroup.IsHubCluster - } - // Merge subscriptions for key, sub := range existing.ClusterGroup.Subscriptions { defaults.ClusterGroup.Subscriptions[key] = sub diff --git a/src/internal/types/clustergroup.go b/src/internal/types/clustergroup.go index 3fad2e7..031f27f 100644 --- a/src/internal/types/clustergroup.go +++ b/src/internal/types/clustergroup.go @@ -85,7 +85,6 @@ type Subscription struct { // ClusterGroup holds the detailed configuration for the cluster group. type ClusterGroup struct { Name string `yaml:"name"` - IsHubCluster bool `yaml:"isHubCluster,omitempty"` Namespaces []NamespaceEntry `yaml:"namespaces"` Projects []string `yaml:"projects,omitempty"` Subscriptions map[string]Subscription `yaml:"subscriptions"` diff --git a/test/expected_values_custom_cluster_overwrite.yaml b/test/expected_values_custom_cluster_overwrite.yaml deleted file mode 100644 index cf4d73a..0000000 --- a/test/expected_values_custom_cluster_overwrite.yaml +++ /dev/null @@ -1,35 +0,0 @@ -clusterGroup: - name: custom-cluster - isHubCluster: true - customClusterField: user-cluster-config - namespaces: - - custom-pattern-name - - vault - - golang-external-secrets - subscriptions: {} - applications: - custom-user-app: - customAppField: user-app-config - name: custom-user-app - namespace: user-namespace - path: user/path - project: custom-pattern-name - golang-external-secrets: - name: golang-external-secrets - namespace: golang-external-secrets - chart: golang-external-secrets - chartVersion: 0.1.* - simple: - name: simple - namespace: custom-pattern-name - path: charts/simple - trivial: - name: trivial - namespace: custom-pattern-name - path: charts/trivial - vault: - name: vault - namespace: vault - chart: hashicorp-vault - chartVersion: 0.1.* -customClusterTopLevel: user-cluster-top-level diff --git a/test/expected_values_global.yaml b/test/expected_values_global.yaml deleted file mode 100644 index 75f5a1a..0000000 --- a/test/expected_values_global.yaml +++ /dev/null @@ -1,9 +0,0 @@ -global: - pattern: trivial-pattern - secretLoader: - disabled: true -main: - clusterGroupName: prod - multiSourceConfig: - enabled: true - clusterGroupChartVersion: 0.9.* diff --git a/test/expected_values_global_custom.yaml b/test/expected_values_global_custom.yaml deleted file mode 100644 index 90cadba..0000000 --- a/test/expected_values_global_custom.yaml +++ /dev/null @@ -1,9 +0,0 @@ -global: - pattern: renamed-pattern - secretLoader: - disabled: false -main: - clusterGroupName: renamed-cluster-group - multiSourceConfig: - enabled: true - clusterGroupChartVersion: 0.9.* diff --git a/test/expected_values_global_overwrite.yaml b/test/expected_values_global_overwrite.yaml deleted file mode 100644 index 1c74638..0000000 --- a/test/expected_values_global_overwrite.yaml +++ /dev/null @@ -1,14 +0,0 @@ -global: - pattern: custom-pattern-name - customGlobalField: user-custom-value - secretLoader: - disabled: false - customSecretField: user-secret-config -main: - clusterGroupName: custom-cluster - customMainField: user-main-value - multiSourceConfig: - enabled: true - clusterGroupChartVersion: 0.8.* - customMultiSourceField: user-multisource-config -customTopLevelField: user-top-level-value diff --git a/test/expected_values_global_with_secrets.yaml b/test/expected_values_global_with_secrets.yaml deleted file mode 100644 index 76167bc..0000000 --- a/test/expected_values_global_with_secrets.yaml +++ /dev/null @@ -1,9 +0,0 @@ -global: - pattern: trivial-pattern - secretLoader: - disabled: false -main: - clusterGroupName: prod - multiSourceConfig: - enabled: true - clusterGroupChartVersion: 0.9.* diff --git a/test/expected_values_prod.yaml b/test/expected_values_prod.yaml deleted file mode 100644 index 19000ad..0000000 --- a/test/expected_values_prod.yaml +++ /dev/null @@ -1,14 +0,0 @@ -clusterGroup: - name: prod - namespaces: - - trivial-pattern - subscriptions: {} - applications: - simple: - name: simple - namespace: trivial-pattern - path: charts/simple - trivial: - name: trivial - namespace: trivial-pattern - path: charts/trivial diff --git a/test/expected_values_prod_with_secrets.yaml b/test/expected_values_prod_with_secrets.yaml deleted file mode 100644 index c98df81..0000000 --- a/test/expected_values_prod_with_secrets.yaml +++ /dev/null @@ -1,26 +0,0 @@ -clusterGroup: - name: prod - namespaces: - - trivial-pattern - - vault - - golang-external-secrets - subscriptions: {} - applications: - golang-external-secrets: - name: golang-external-secrets - namespace: golang-external-secrets - chart: golang-external-secrets - chartVersion: 0.1.* - simple: - name: simple - namespace: trivial-pattern - path: charts/simple - trivial: - name: trivial - namespace: trivial-pattern - path: charts/trivial - vault: - name: vault - namespace: vault - chart: hashicorp-vault - chartVersion: 0.1.* diff --git a/test/expected_values_renamed_cluster_group.yaml b/test/expected_values_renamed_cluster_group.yaml deleted file mode 100644 index 307ce06..0000000 --- a/test/expected_values_renamed_cluster_group.yaml +++ /dev/null @@ -1,26 +0,0 @@ -clusterGroup: - name: renamed-cluster-group - namespaces: - - renamed-pattern - - vault - - golang-external-secrets - subscriptions: {} - applications: - golang-external-secrets: - name: golang-external-secrets - namespace: golang-external-secrets - chart: golang-external-secrets - chartVersion: 0.1.* - simple: - name: simple - namespace: renamed-pattern - path: charts/simple - trivial: - name: trivial - namespace: renamed-pattern - path: charts/trivial - vault: - name: vault - namespace: vault - chart: hashicorp-vault - chartVersion: 0.1.* diff --git a/test/initial_ansible_cfg_overwrite b/test/initial_ansible_cfg_overwrite deleted file mode 100644 index ab2af55..0000000 --- a/test/initial_ansible_cfg_overwrite +++ /dev/null @@ -1,7 +0,0 @@ -[defaults] -display_skipped_hosts=False -localhost_warning=True -retry_files_enabled=False -library=~/.ansible/plugins/modules:./ansible/plugins/modules:./common/ansible/plugins/modules:/usr/share/ansible/plugins/modules -roles_path=~/.ansible/roles:./ansible/roles:./common/ansible/roles:/usr/share/ansible/roles:/etc/ansible/roles -filter_plugins=~/.ansible/plugins/filter:./ansible/plugins/filter:./common/ansible/plugins/filter:/usr/share/ansible/plugins/filter diff --git a/test/initial_makefile_overwrite b/test/initial_makefile_overwrite deleted file mode 100644 index 666754d..0000000 --- a/test/initial_makefile_overwrite +++ /dev/null @@ -1,6 +0,0 @@ -# Custom user Makefile -# This should NOT be overwritten - -.PHONY: custom-target -custom-target: - @echo "This is a custom user target" diff --git a/test/initial_makefile_pattern_overwrite b/test/initial_makefile_pattern_overwrite deleted file mode 100644 index 1f6cb3c..0000000 --- a/test/initial_makefile_pattern_overwrite +++ /dev/null @@ -1,6 +0,0 @@ -# Old Makefile-common content -# This SHOULD be overwritten - -.PHONY: old-target -old-target: - @echo "This is old content" diff --git a/test/initial_pattern_sh_overwrite b/test/initial_pattern_sh_overwrite deleted file mode 100644 index 62c2639..0000000 --- a/test/initial_pattern_sh_overwrite +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash -# Old pattern.sh content -# This SHOULD be overwritten -echo "Old pattern.sh script" diff --git a/test/initial_values_custom_cluster_overwrite.yaml b/test/initial_values_custom_cluster_overwrite.yaml deleted file mode 100644 index 082a5fc..0000000 --- a/test/initial_values_custom_cluster_overwrite.yaml +++ /dev/null @@ -1,12 +0,0 @@ -clusterGroup: - name: custom-cluster - isHubCluster: true - customClusterField: user-cluster-config - applications: - custom-user-app: - name: custom-user-app - namespace: user-namespace - path: user/path - customAppField: user-app-config - project: custom-pattern-name -customClusterTopLevel: user-cluster-top-level diff --git a/test/initial_values_global_custom.yaml b/test/initial_values_global_custom.yaml deleted file mode 100644 index 0f0169a..0000000 --- a/test/initial_values_global_custom.yaml +++ /dev/null @@ -1,4 +0,0 @@ -global: - pattern: renamed-pattern -main: - clusterGroupName: renamed-cluster-group diff --git a/test/initial_values_global_overwrite.yaml b/test/initial_values_global_overwrite.yaml deleted file mode 100644 index a347cf8..0000000 --- a/test/initial_values_global_overwrite.yaml +++ /dev/null @@ -1,14 +0,0 @@ -global: - pattern: custom-pattern-name - customGlobalField: user-custom-value - secretLoader: - disabled: true - customSecretField: user-secret-config -main: - clusterGroupName: custom-cluster - customMainField: user-main-value - multiSourceConfig: - enabled: true - clusterGroupChartVersion: 0.8.* - customMultiSourceField: user-multisource-config -customTopLevelField: user-top-level-value diff --git a/test/initial_values_secret_template_overwrite.yaml b/test/initial_values_secret_template_overwrite.yaml deleted file mode 100644 index de1e5bc..0000000 --- a/test/initial_values_secret_template_overwrite.yaml +++ /dev/null @@ -1,12 +0,0 @@ -# Custom user secrets template -# This should NOT be overwritten - -version: "2.0" -secrets: - - name: aws-creds - fields: - - name: aws_access_key_id - value: "An aws access key" - - - name: aws_secret_access_key - value: "An aws access secret key" diff --git a/test/integration_test.sh b/test/integration_test.sh deleted file mode 100755 index d74e1e1..0000000 --- a/test/integration_test.sh +++ /dev/null @@ -1,597 +0,0 @@ -#!/bin/bash - -set -euo pipefail - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -NC='\033[0m' # No Color - -# Test configuration -PATTERNIZER_BINARY="${PATTERNIZER_BINARY:-./src/patternizer}" -TEST_REPO_URL="https://github.com/dminnear-rh/trivial-pattern.git" -TEST_DIR="/tmp/patternizer-integration-test" -TEST_DIR_SECRETS="/tmp/patternizer-integration-test-secrets" -TEST_DIR_CUSTOM="/tmp/patternizer-integration-test-custom" -TEST_DIR_SEQUENTIAL="/tmp/patternizer-integration-test-sequential" -TEST_DIR_OVERWRITE="/tmp/patternizer-integration-test-overwrite" -TEST_DIR_MIXED="/tmp/patternizer-integration-test-mixed" -TEST_DIR_UPGRADE="/tmp/patternizer-integration-test-upgrade" -TEST_DIR_UPGRADE_INCLUDE="/tmp/patternizer-integration-test-upgrade-include" -TEST_DIR_UPGRADE_REPLACE="/tmp/patternizer-integration-test-upgrade-replace" -TEST_DIR_UPGRADE_NOMAKEFILE="/tmp/patternizer-integration-test-upgrade-nomakefile" -REPO_NAME=$(basename -s .git "$TEST_REPO_URL") -SHARED_CLONE_PARENT="/tmp/patternizer-shared-clone" -SHARED_REPO_DIR="$SHARED_CLONE_PARENT/$REPO_NAME" - -echo -e "${YELLOW}Starting patternizer integration tests...${NC}" - -# Clean up any previous test runs -if [ -d "$TEST_DIR" ]; then - rm -rf "$TEST_DIR" -fi -if [ -d "$TEST_DIR_SECRETS" ]; then - rm -rf "$TEST_DIR_SECRETS" -fi -if [ -d "$TEST_DIR_CUSTOM" ]; then - rm -rf "$TEST_DIR_CUSTOM" -fi -if [ -d "$TEST_DIR_SEQUENTIAL" ]; then - rm -rf "$TEST_DIR_SEQUENTIAL" -fi -if [ -d "$TEST_DIR_OVERWRITE" ]; then - rm -rf "$TEST_DIR_OVERWRITE" -fi -if [ -d "$TEST_DIR_MIXED" ]; then - rm -rf "$TEST_DIR_MIXED" -fi -if [ -d "$TEST_DIR_UPGRADE" ]; then - rm -rf "$TEST_DIR_UPGRADE" -fi -if [ -d "$TEST_DIR_UPGRADE_INCLUDE" ]; then - rm -rf "$TEST_DIR_UPGRADE_INCLUDE" -fi -if [ -d "$TEST_DIR_UPGRADE_REPLACE" ]; then - rm -rf "$TEST_DIR_UPGRADE_REPLACE" -fi -if [ -d "$TEST_DIR_UPGRADE_NOMAKEFILE" ]; then - rm -rf "$TEST_DIR_UPGRADE_NOMAKEFILE" -fi - -# Clean up any previous shared clone -if [ -d "$SHARED_CLONE_PARENT" ]; then - rm -rf "$SHARED_CLONE_PARENT" -fi - -# Convert PATTERNIZER_BINARY to absolute path before changing directories -PATTERNIZER_BINARY=$(realpath "$PATTERNIZER_BINARY") - -# Get the absolute path to the repository root (where resource files are located) -REPO_ROOT=$(pwd) - -# Export resources directory so patternizer can find resource files -export PATTERNIZER_RESOURCES_DIR="$REPO_ROOT/resources" - -# Set absolute paths to expected files -EXPECTED_VALUES_CUSTOM_CLUSTER_OVERWRITE="$REPO_ROOT/test/expected_values_custom_cluster_overwrite.yaml" -EXPECTED_VALUES_GLOBAL_CUSTOM="$REPO_ROOT/test/expected_values_global_custom.yaml" -EXPECTED_VALUES_GLOBAL_OVERWRITE="$REPO_ROOT/test/expected_values_global_overwrite.yaml" -EXPECTED_VALUES_GLOBAL_WITH_SECRETS="$REPO_ROOT/test/expected_values_global_with_secrets.yaml" -EXPECTED_VALUES_GLOBAL="$REPO_ROOT/test/expected_values_global.yaml" -EXPECTED_VALUES_PROD_WITH_SECRETS="$REPO_ROOT/test/expected_values_prod_with_secrets.yaml" -EXPECTED_VALUES_PROD="$REPO_ROOT/test/expected_values_prod.yaml" -EXPECTED_VALUES_RENAMED_CLUSTER_GROUP="$REPO_ROOT/test/expected_values_renamed_cluster_group.yaml" -INITIAL_MAKEFILE_OVERWRITE="$REPO_ROOT/test/initial_makefile_overwrite" -INITIAL_MAKEFILE_PATTERN_OVERWRITE="$REPO_ROOT/test/initial_makefile_pattern_overwrite" -INITIAL_PATTERN_SH_OVERWRITE="$REPO_ROOT/test/initial_pattern_sh_overwrite" -INITIAL_VALUES_CUSTOM_CLUSTER_OVERWRITE="$REPO_ROOT/test/initial_values_custom_cluster_overwrite.yaml" -INITIAL_VALUES_GLOBAL_CUSTOM="$REPO_ROOT/test/initial_values_global_custom.yaml" -INITIAL_VALUES_GLOBAL_OVERWRITE="$REPO_ROOT/test/initial_values_global_overwrite.yaml" -INITIAL_VALUES_SECRET_TEMPLATE_OVERWRITE="$REPO_ROOT/test/initial_values_secret_template_overwrite.yaml" -INITIAL_ANSIBLE_CFG_OVERWRITE="$REPO_ROOT/test/initial_ansible_cfg_overwrite" - -# Set paths for expected resource files -EXPECTED_MAKEFILE="$PATTERNIZER_RESOURCES_DIR/Makefile" -EXPECTED_MAKEFILE_COMMON="$PATTERNIZER_RESOURCES_DIR/Makefile-common" -EXPECTED_PATTERN_SH="$PATTERNIZER_RESOURCES_DIR/pattern.sh" -EXPECTED_VALUES_SECRET_TEMPLATE="$PATTERNIZER_RESOURCES_DIR/values-secret.yaml.template" -EXPECTED_ANSIBLE_CFG="$PATTERNIZER_RESOURCES_DIR/ansible.cfg" - -# Check if patternizer binary exists and is executable -if [ ! -x "$PATTERNIZER_BINARY" ]; then - echo -e "${RED}ERROR: Patternizer binary not found or not executable at: $PATTERNIZER_BINARY${NC}" - exit 1 -fi - -# Perform a single shallow clone of the source repository into a shared location -mkdir -p "$SHARED_CLONE_PARENT" -cd "$SHARED_CLONE_PARENT" -git clone --depth 1 "$TEST_REPO_URL" -cd "$REPO_ROOT" - -# Helper to prepare a test repository copy and cd into it -prepare_and_enter_repo() { - local dest_dir="$1" - local header_msg="$2" - test_header "$header_msg" - cd "$REPO_ROOT" - mkdir -p "$dest_dir" - rm -rf "${dest_dir:?}/${REPO_NAME:?}" - cp -a "$SHARED_REPO_DIR" "$dest_dir/" - cd "$dest_dir/$REPO_NAME" -} - -# Function to compare YAML files (ignoring whitespace differences) -compare_yaml() { - local expected_file="$1" - local actual_file="$2" - local description="$3" - - if [ ! -f "$actual_file" ]; then - echo -e "${RED}FAIL: $description - file not created: $actual_file${NC}" - return 1 - fi - - # Normalize YAML by sorting and removing empty lines/spaces - normalize_yaml() { - python3 -c " -import yaml, sys -try: - with open('$1', 'r') as f: - data = yaml.safe_load(f) - print(yaml.dump(data, default_flow_style=False, sort_keys=True)) -except Exception as e: - print(f'Error processing $1: {e}', file=sys.stderr) - sys.exit(1) -" - } - - # Compare normalized YAML - if normalize_yaml "$expected_file" | diff -u - <(normalize_yaml "$actual_file") > /dev/null; then - echo -e "${GREEN}PASS: $description${NC}" - return 0 - else - echo -e "${RED}FAIL: $description${NC}" - echo "Expected content (normalized):" - normalize_yaml "$expected_file" - echo "" - echo "Actual content (normalized):" - normalize_yaml "$actual_file" - echo "" - echo "Diff:" - normalize_yaml "$expected_file" | diff -u - <(normalize_yaml "$actual_file") || true - return 1 - fi -} - -# Function to print test section headers -test_header() { - echo -e "${YELLOW}$1${NC}" -} - -# Function to print test pass messages -test_pass() { - echo -e "${GREEN}PASS: $1${NC}" -} - -# Function to print test fail messages and exit -test_fail() { - echo -e "${RED}FAIL: $1${NC}" - exit 1 -} - -# Function to compare two files exactly with diff, showing differences on failure -compare_files() { - local expected_file="$1" - local actual_file="$2" - local description="$3" - - if [ ! -f "$actual_file" ]; then - test_fail "$description - file not created: $actual_file" - fi - - if [ ! -f "$expected_file" ]; then - test_fail "$description - expected file not found: $expected_file" - fi - - if diff "$expected_file" "$actual_file" > /dev/null; then - test_pass "$description" - return 0 - else - echo -e "${RED}FAIL: $description${NC}" - echo "Expected file: $expected_file" - echo "Actual file: $actual_file" - echo "Diff:" - diff "$expected_file" "$actual_file" || true - exit 1 - fi -} - -# Function to check file exists -check_file_exists() { - local file="$1" - local description="$2" - - if [ -f "$file" ]; then - test_pass "$description" - return 0 - else - test_fail "$description - file not found: $file" - fi -} - -# -# Test 1: Basic initialization (without secrets) -# -prepare_and_enter_repo "$TEST_DIR" "=== Test 1: Basic initialization (without secrets) ===" - -test_header "Running patternizer init..." -"$PATTERNIZER_BINARY" init - -test_header "Running verification tests..." - -# Test 1.1: Check values-global.yaml -compare_yaml "$EXPECTED_VALUES_GLOBAL" "values-global.yaml" "values-global.yaml content" - -# Test 1.2: Check values-prod.yaml -compare_yaml "$EXPECTED_VALUES_PROD" "values-prod.yaml" "values-prod.yaml content" - -# Test 1.3: Verify pattern.sh is executable -if [ -x "pattern.sh" ]; then - echo -e "${GREEN}PASS: pattern.sh is executable${NC}" -else - echo -e "${RED}FAIL: pattern.sh is not executable${NC}" - exit 1 -fi - -# Test 1.4: Check Makefile has exact expected content -compare_files "$EXPECTED_MAKEFILE" "Makefile" "Makefile has expected content (init without secrets)" - -# Test 1.5: Check Makefile-common has exact expected content -compare_files "$EXPECTED_MAKEFILE_COMMON" "Makefile-common" "Makefile-common has expected content (init without secrets)" - -# Test 1.6: Check ansible.cfg has exact expected content -compare_files "$EXPECTED_ANSIBLE_CFG" "ansible.cfg" "ansible.cfg has expected content (init without secrets)" - -test_pass "=== Test 1: Basic initialization PASSED ===" - -# -# Test 2: Initialization with secrets -# -prepare_and_enter_repo "$TEST_DIR_SECRETS" "=== Test 2: Initialization with secrets ===" - -test_header "Running patternizer init --with-secrets..." -"$PATTERNIZER_BINARY" init --with-secrets - -test_header "Running verification tests for secrets..." - -# Test 2.1: Check values-global.yaml (secretLoader.disabled should be false with secrets) -compare_yaml "$EXPECTED_VALUES_GLOBAL_WITH_SECRETS" "values-global.yaml" "values-global.yaml content (with secrets)" - -# Test 2.2: Check values-prod.yaml with secrets applications -compare_yaml "$EXPECTED_VALUES_PROD_WITH_SECRETS" "values-prod.yaml" "values-prod.yaml content (with secrets)" - -# Test 2.3: Verify pattern.sh is executable -if [ -x "pattern.sh" ]; then - echo -e "${GREEN}PASS: pattern.sh is executable (with secrets)${NC}" -else - echo -e "${RED}FAIL: pattern.sh is not executable (with secrets)${NC}" - exit 1 -fi - -# Test 2.4: Check values-secret.yaml.template has exact expected content -compare_files "$EXPECTED_VALUES_SECRET_TEMPLATE" "values-secret.yaml.template" "values-secret.yaml.template has expected content" - -# Test 2.5: Check Makefile has exact expected content -compare_files "$EXPECTED_MAKEFILE" "Makefile" "Makefile has expected content (init with secrets)" - -# Test 2.6: Check Makefile-common has exact expected content -compare_files "$EXPECTED_MAKEFILE_COMMON" "Makefile-common" "Makefile-common has expected content (init with secrets)" - -# Test 2.7: Check ansible.cfg has exact expected content -compare_files "$EXPECTED_ANSIBLE_CFG" "ansible.cfg" "ansible.cfg has expected content (init with secrets)" - -test_pass "=== Test 2: Initialization with secrets PASSED ===" - -# -# Test 3: Custom pattern and cluster group names (merging test with secrets) -# -prepare_and_enter_repo "$TEST_DIR_CUSTOM" "=== Test 3: Custom pattern and cluster group names (with secrets) ===" - -test_header "Setting up initial values-global.yaml with custom names..." -cp "$INITIAL_VALUES_GLOBAL_CUSTOM" "values-global.yaml" - -test_header "Running patternizer init --with-secrets (should preserve custom names)..." -"$PATTERNIZER_BINARY" init --with-secrets - -test_header "Running verification tests for custom names..." - -# Test 3.1: Check values-global.yaml preserves custom names and adds multiSourceConfig -compare_yaml "$EXPECTED_VALUES_GLOBAL_CUSTOM" "values-global.yaml" "values-global.yaml content (custom names)" - -# Test 3.2: Check custom cluster group file is created with correct content -compare_yaml "$EXPECTED_VALUES_RENAMED_CLUSTER_GROUP" "values-renamed-cluster-group.yaml" "values-renamed-cluster-group.yaml content" - -# Test 3.3: Verify pattern.sh is executable -if [ -x "pattern.sh" ]; then - echo -e "${GREEN}PASS: pattern.sh is executable (custom names)${NC}" -else - echo -e "${RED}FAIL: pattern.sh is not executable (custom names)${NC}" - exit 1 -fi - -# Test 3.4: Check values-secret.yaml.template has exact expected content -compare_files "$EXPECTED_VALUES_SECRET_TEMPLATE" "values-secret.yaml.template" "values-secret.yaml.template has expected content (custom names)" - -# Test 3.5: Check Makefile has exact expected content -compare_files "$EXPECTED_MAKEFILE" "Makefile" "Makefile has expected content (custom names with secrets)" - -# Test 3.6: Check Makefile-common has exact expected content -compare_files "$EXPECTED_MAKEFILE_COMMON" "Makefile-common" "Makefile-common has expected content (custom names with secrets)" - -# Test 3.7: Check ansible.cfg has exact expected content -compare_files "$EXPECTED_ANSIBLE_CFG" "ansible.cfg" "ansible.cfg has expected content (custom names with secrets)" - -test_pass "=== Test 3: Custom pattern and cluster group names (with secrets) PASSED ===" - -# -# Test 4: Sequential execution (init followed by init --with-secrets) -# -prepare_and_enter_repo "$TEST_DIR_SEQUENTIAL" "=== Test 4: Sequential execution (init + init --with-secrets) ===" - -test_header "Running patternizer init (first)..." -"$PATTERNIZER_BINARY" init - -test_header "Running patternizer init --with-secrets (second)..." -"$PATTERNIZER_BINARY" init --with-secrets - -test_header "Running verification tests for sequential execution..." - -# Test 4.1: Check values-global.yaml (should have secretLoader.disabled=false after --with-secrets) -compare_yaml "$EXPECTED_VALUES_GLOBAL_WITH_SECRETS" "values-global.yaml" "values-global.yaml content (sequential)" - -# Test 4.2: Check values-prod.yaml matches the --with-secrets output -compare_yaml "$EXPECTED_VALUES_PROD_WITH_SECRETS" "values-prod.yaml" "values-prod.yaml content (sequential, should match --with-secrets)" - -# Test 4.3: Verify pattern.sh is executable -if [ -x "pattern.sh" ]; then - echo -e "${GREEN}PASS: pattern.sh is executable (sequential)${NC}" -else - echo -e "${RED}FAIL: pattern.sh is not executable (sequential)${NC}" - exit 1 -fi - -# Test 4.4: Check values-secret.yaml.template has exact expected content -compare_files "$EXPECTED_VALUES_SECRET_TEMPLATE" "values-secret.yaml.template" "values-secret.yaml.template has expected content (sequential)" - -# Test 4.5: Check Makefile has exact expected content -compare_files "$EXPECTED_MAKEFILE" "Makefile" "Makefile has expected content (sequential execution)" - -# Test 4.6: Check Makefile-common has exact expected content -compare_files "$EXPECTED_MAKEFILE_COMMON" "Makefile-common" "Makefile-common has expected content (sequential execution)" - -# Test 4.7: Check ansible.cfg has exact expected content -compare_files "$EXPECTED_ANSIBLE_CFG" "ansible.cfg" "ansible.cfg has expected content (sequential execution)" - - -test_pass "=== Test 4: Sequential execution PASSED ===" - -# -# Test 5: File overwrite behavior with existing custom files -# -prepare_and_enter_repo "$TEST_DIR_OVERWRITE" "=== Test 5: File overwrite behavior with existing custom files ===" - -test_header "Setting up existing custom files..." - -# Copy initial files to set up the test scenario -cp "$INITIAL_VALUES_GLOBAL_OVERWRITE" "values-global.yaml" -cp "$INITIAL_VALUES_CUSTOM_CLUSTER_OVERWRITE" "values-custom-cluster.yaml" -cp "$INITIAL_MAKEFILE_OVERWRITE" "Makefile" -cp "$INITIAL_MAKEFILE_PATTERN_OVERWRITE" "Makefile-common" -cp "$INITIAL_PATTERN_SH_OVERWRITE" "pattern.sh" -cp "$INITIAL_VALUES_SECRET_TEMPLATE_OVERWRITE" "values-secret.yaml.template" -cp "$INITIAL_ANSIBLE_CFG_OVERWRITE" "ansible.cfg" - -# Make pattern.sh executable to match real scenarios -chmod +x "pattern.sh" - -test_header "Running patternizer init --with-secrets..." -"$PATTERNIZER_BINARY" init --with-secrets - -test_header "Verifying file overwrite behavior..." - -# Test 5.1: values-global.yaml should preserve custom fields and merge with defaults -compare_yaml "$EXPECTED_VALUES_GLOBAL_OVERWRITE" "values-global.yaml" "values-global.yaml content (preserves custom fields with --with-secrets)" - -# Test 5.2: values-custom-cluster.yaml should preserve custom fields and merge with defaults -compare_yaml "$EXPECTED_VALUES_CUSTOM_CLUSTER_OVERWRITE" "values-custom-cluster.yaml" "values-custom-cluster.yaml content (preserves custom fields)" - -# Test 5.3: Makefile should NOT be overwritten -compare_files "$INITIAL_MAKEFILE_OVERWRITE" "Makefile" "Makefile was not overwritten (content preserved)" - -# Test 5.4: Makefile-common SHOULD be overwritten with exact expected content -compare_files "$EXPECTED_MAKEFILE_COMMON" "Makefile-common" "Makefile-common was overwritten with correct content" - -# Test 5.5: pattern.sh SHOULD be overwritten with exact expected content and be executable -compare_files "$EXPECTED_PATTERN_SH" "pattern.sh" "pattern.sh was overwritten with correct content" - -# Verify it's executable -if [ -x "pattern.sh" ]; then - test_pass "pattern.sh is executable" -else - test_fail "pattern.sh is not executable" -fi - -# Test 5.6: values-secret.yaml.template should NOT be overwritten -compare_files "$INITIAL_VALUES_SECRET_TEMPLATE_OVERWRITE" "values-secret.yaml.template" "values-secret.yaml.template was not overwritten (content preserved)" - -# Test 5.7: ansible.cfg SHOULD be overwritten with exact expected content -compare_files "$EXPECTED_ANSIBLE_CFG" "ansible.cfg" "ansible.cfg was overwritten with correct content" - - -test_pass "=== Test 5: File overwrite behavior PASSED ===" - -# -# Test 6: Mixed file overwrite behavior (some files exist, some don't) -# -prepare_and_enter_repo "$TEST_DIR_MIXED" "=== Test 6: Mixed file overwrite behavior ===" - -test_header "Setting up partial existing files..." - -# Only create some files to test mixed scenarios - -# Copy initial files for mixed scenario -cp "$INITIAL_MAKEFILE_OVERWRITE" "Makefile" -cp "$INITIAL_VALUES_SECRET_TEMPLATE_OVERWRITE" "values-secret.yaml.template" - -# Don't create values-global.yaml, values-prod.yaml (should be created) -# Don't create Makefile-common, pattern.sh (should be created/overwritten) - -test_header "Running patternizer init --with-secrets on mixed repository..." -"$PATTERNIZER_BINARY" init --with-secrets - -test_header "Verifying mixed overwrite behavior..." - -# Test 6.1: Files that should be created with exact expected content -check_file_exists "values-global.yaml" "values-global.yaml created when missing" -check_file_exists "values-prod.yaml" "values-prod.yaml created when missing" - -compare_files "$EXPECTED_MAKEFILE_COMMON" "Makefile-common" "Makefile-common created with correct content" - -compare_files "$EXPECTED_PATTERN_SH" "pattern.sh" "pattern.sh created with correct content" - -compare_files "$EXPECTED_ANSIBLE_CFG" "ansible.cfg" "ansible.cfg created with correct content" - -# Test 6.2: Files that should be preserved -compare_files "$INITIAL_MAKEFILE_OVERWRITE" "Makefile" "Existing Makefile preserved in mixed scenario" - -compare_files "$INITIAL_VALUES_SECRET_TEMPLATE_OVERWRITE" "values-secret.yaml.template" "Existing values-secret.yaml.template preserved in mixed scenario" - -# Test 6.3: Verify pattern.sh is executable -if [ -x "pattern.sh" ]; then - test_pass "pattern.sh is executable in mixed scenario" -else - test_fail "pattern.sh is not executable in mixed scenario" -fi - -test_pass "=== Test 6: Mixed file overwrite behavior PASSED ===" - -# -# Test 7: Upgrade without --replace-makefile, inject include on first line -# -prepare_and_enter_repo "$TEST_DIR_UPGRADE" "=== Test 7: Upgrade (no replace, inject include) ===" - -# Simulate legacy structure -mkdir -p common -ln -s common/pattern.sh pattern.sh - -# Create a simple Makefile without include -cat > Makefile <<'EOF' -all: - @echo hello -EOF - -# Run upgrade -"$PATTERNIZER_BINARY" upgrade - -# Verify common/ removed and pattern.sh replaced (not symlink) -if [ -d common ]; then - test_fail "common directory was not removed by upgrade" -fi -if [ -L pattern.sh ]; then - test_fail "pattern.sh symlink was not removed by upgrade" -fi - -# Verify pattern.sh and Makefile-common contents -compare_files "$EXPECTED_PATTERN_SH" "pattern.sh" "pattern.sh copied during upgrade" -compare_files "$EXPECTED_MAKEFILE_COMMON" "Makefile-common" "Makefile-common copied during upgrade" -compare_files "$EXPECTED_ANSIBLE_CFG" "ansible.cfg" "ansible.cfg copied during upgrade" - -# Verify Makefile first line and content -EXPECTED_UPGRADE_MF=$(mktemp) -printf "include Makefile-common\nall:\n\t@echo hello\n" > "$EXPECTED_UPGRADE_MF" -compare_files "$EXPECTED_UPGRADE_MF" "Makefile" "Makefile injected include at first line" -rm -f "$EXPECTED_UPGRADE_MF" - -test_pass "=== Test 7: Upgrade (no replace, inject include) PASSED ===" - -# -# Test 8: Upgrade without --replace-makefile, include already exists elsewhere -# -prepare_and_enter_repo "$TEST_DIR_UPGRADE_INCLUDE" "=== Test 8: Upgrade (no replace, include present) ===" - -# Legacy bits -mkdir -p common -ln -s common/pattern.sh pattern.sh - -# Makefile already contains include, not on first line -cat > Makefile <<'EOF' -foo: - @echo foo -include Makefile-common -bar: - @echo bar -EOF -cp Makefile /tmp/expected_makefile_include_present - -# Run upgrade -"$PATTERNIZER_BINARY" upgrade - -# Verify removals and copies -if [ -d common ]; then - test_fail "common directory was not removed by upgrade (include-present case)" -fi -if [ -L pattern.sh ]; then - test_fail "pattern.sh symlink was not removed by upgrade (include-present case)" -fi -compare_files "$EXPECTED_PATTERN_SH" "pattern.sh" "pattern.sh copied during upgrade (include-present)" -compare_files "$EXPECTED_MAKEFILE_COMMON" "Makefile-common" "Makefile-common copied during upgrade (include-present)" - -# Verify Makefile unchanged -compare_files "/tmp/expected_makefile_include_present" "Makefile" "Makefile unchanged when include already present" -rm -f /tmp/expected_makefile_include_present - -test_pass "=== Test 8: Upgrade (no replace, include present) PASSED ===" - -# -# Test 9: Upgrade with --replace-makefile replaces Makefile exactly -# -prepare_and_enter_repo "$TEST_DIR_UPGRADE_REPLACE" "=== Test 9: Upgrade (--replace-makefile) ===" - -# Create a custom Makefile to be overwritten -echo "custom: ; @echo custom" > Makefile - -# Run upgrade with flag -"$PATTERNIZER_BINARY" upgrade --replace-makefile - -# Verify Makefile replaced and other files copied -compare_files "$EXPECTED_MAKEFILE" "Makefile" "Makefile replaced during upgrade --replace-makefile" -compare_files "$EXPECTED_MAKEFILE_COMMON" "Makefile-common" "Makefile-common copied during upgrade --replace-makefile" -compare_files "$EXPECTED_PATTERN_SH" "pattern.sh" "pattern.sh copied during upgrade --replace-makefile" - -test_pass "=== Test 9: Upgrade (--replace-makefile) PASSED ===" - -# -# Test 10: Upgrade without existing Makefile creates default Makefile -# -prepare_and_enter_repo "$TEST_DIR_UPGRADE_NOMAKEFILE" "=== Test 10: Upgrade (no Makefile present) ===" - -# Ensure no Makefile exists -rm -f Makefile - -# Run upgrade -"$PATTERNIZER_BINARY" upgrade - -# Verify Makefile created and matches default -compare_files "$EXPECTED_MAKEFILE" "Makefile" "Makefile created during upgrade when missing" -compare_files "$EXPECTED_MAKEFILE_COMMON" "Makefile-common" "Makefile-common copied during upgrade when missing" -compare_files "$EXPECTED_PATTERN_SH" "pattern.sh" "pattern.sh copied during upgrade when missing" - -test_pass "=== Test 10: Upgrade (no Makefile present) PASSED ===" - -test_pass "All integration tests passed!" - -# Clean up -cd "$REPO_ROOT" -rm -rf "$TEST_DIR" "$TEST_DIR_SECRETS" "$TEST_DIR_CUSTOM" "$TEST_DIR_SEQUENTIAL" "$TEST_DIR_OVERWRITE" "$TEST_DIR_MIXED" "$TEST_DIR_UPGRADE" "$TEST_DIR_UPGRADE_INCLUDE" "$TEST_DIR_UPGRADE_REPLACE" "$TEST_DIR_UPGRADE_NOMAKEFILE" "$SHARED_CLONE_PARENT" From ff87d53f058146fbb79c9c0f42e63f840698f84c Mon Sep 17 00:00:00 2001 From: Drew Minnear Date: Thu, 26 Feb 2026 15:11:00 -0500 Subject: [PATCH 07/13] update docs --- README.md | 84 ++++++++------------------------------- src/cmd/cmd_suite_test.go | 4 -- 2 files changed, 17 insertions(+), 71 deletions(-) diff --git a/README.md b/README.md index 1136e64..8cf19a8 100644 --- a/README.md +++ b/README.md @@ -4,12 +4,11 @@ [![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) -**Patternizer** is a command-line tool that bootstraps a Git repository containing Helm charts into a ready-to-use Validated Pattern. It automatically generates the necessary scaffolding, configuration files, and utility scripts, so you can get your pattern up and running in minutes. +**Patternizer** is a command-line tool that bootstraps a Git repository containing Helm charts into a ready-to-use Validated Pattern. It automatically generates the necessary scaffolding, configuration files, and utility scripts, so you can get your pattern up and running in minutes. It can also be used to upgrade existing patterns as described in [the Validated Pattern's blog](https://validatedpatterns.io/blog/2025-08-29-new-common-makefile-structure/). -> **Note:** This tool was developed with assistance from [Cursor](https://cursor.sh), an AI-powered code editor. +> **Note:** This repo was developed with AI tools including [Cursor](https://cursor.com/), [Claude](https://claude.ai/login) and [Gemini](https://gemini.google.com/app). - [Patternizer](#patternizer) - - [Features](#features) - [Quick Start](#quick-start) - [Example Workflow](#example-workflow) - [Usage Details](#usage-details) @@ -22,25 +21,10 @@ - [Development \& Contributing](#development--contributing) - [Prerequisites](#prerequisites) - [Local Development Workflow](#local-development-workflow) - - [Common Makefile Targets](#common-makefile-targets) - - [Testing Strategy](#testing-strategy) - - [Architecture](#architecture) - - [CI/CD Pipeline](#cicd-pipeline) - [How to Contribute](#how-to-contribute) -## Features - -- 🚀 **CLI-first design** with intuitive commands and help system -- 📦 **Container-native** for consistent execution across all environments -- 🔍 **Auto-discovery** of Helm charts and Git repository metadata -- 🔐 **Optional secrets integration** with Vault and External Secrets -- 🏗️ **Makefile-driven** utility scripts for easy pattern management -- ♻️ **Upgrade command** to refresh existing pattern repositories to the latest common structure - ## Quick Start -This guide assumes you have a Git repository containing one or more Helm charts. - **Prerequisites:** - Podman or Docker @@ -49,8 +33,7 @@ This guide assumes you have a Git repository containing one or more Helm charts. Navigate to your repository's root directory and run the initialization command: ```bash -# In the root of your pattern-repo -podman run -v "$PWD:$PWD:z" -w "$PWD" quay.io/validatedpatterns/patternizer init +podman run --pull=newer -v "$PWD:$PWD:z" -w "$PWD" quay.io/validatedpatterns/patternizer init ``` This single command will generate all the necessary files to turn your repository into a Validated Pattern. @@ -68,7 +51,7 @@ This single command will generate all the necessary files to turn your repositor 2. **Initialize the pattern using Patternizer:** ```bash - podman run -v "$PWD:$PWD:z" -w "$PWD" quay.io/validatedpatterns/patternizer init + podman run --pull=newer -v "$PWD:$PWD:z" -w "$PWD" quay.io/validatedpatterns/patternizer init ``` 3. **Review, commit, and push the generated files:** @@ -80,7 +63,13 @@ This single command will generate all the necessary files to turn your repositor git push -u origin initialize-pattern ``` -4. **Install the pattern:** +4. **Login to an OpenShift cluster:** + + ```bash + export KUBECONFIG=/path/to/cluster/kubeconfig + ``` + +5. **Install the pattern:** ```bash ./pattern.sh make install @@ -90,7 +79,7 @@ This single command will generate all the necessary files to turn your repositor ### Container Usage (Recommended) -Using the prebuilt container is the easiest way to run Patternizer, as it requires no local installation. The `-v "$PWD:$PWD:z" -w "$PWD"` flag mounts your current directory into the container's `/repo` workspace. +Using the prebuilt container is the easiest way to run Patternizer, as it requires no local installation. #### **Initialize without secrets:** @@ -119,10 +108,9 @@ podman run --pull=newer -v "$PWD:$PWD:z" -w "$PWD" quay.io/validatedpatterns/pat What upgrade does: - Removes the `common/` directory if it exists -- Removes `./pattern.sh` if it exists (symlink or file) -- Copies `resources/Makefile-common` and `resources/pattern.sh` into the repo root +- Updates `ansible.cfg`, `Makefile-common`, and `pattern.sh` to the latest versions from [the resources directory](./resources/) - Makefile handling: - - If `--replace-makefile` is set: copies the default `Makefile` into the repo root (overwriting any existing one) + - If `--replace-makefile` is set: replaces an existing Makefile, if present, to [`Makefile`](./resources/Makefile) from the resources directory - If not set: - If no `Makefile` exists: copies the default `Makefile` - If a `Makefile` exists and already contains `include Makefile-common` anywhere: leaves it unchanged @@ -147,6 +135,7 @@ Running `patternizer init` creates the following: - `pattern.sh`: A utility script for common pattern operations (`install`, `upgrade`, etc.). - `Makefile`: A simple Makefile that includes `Makefile-common`. - `Makefile-common`: The core Makefile with all pattern-related targets. +- `ansible.cfg`: Configuration for the ansible installation used when `./pattern.sh` is called Using the `--with-secrets` flag additionally creates: @@ -159,7 +148,7 @@ This section is for developers who want to contribute to the Patternizer project ### Prerequisites -- Go (see `go.mod` for version) +- Go (see [`go.mod`](./src/go.mod) for version) - Podman or Docker - Git - Make @@ -171,7 +160,7 @@ This section is for developers who want to contribute to the Patternizer project git clone https://github.com/validatedpatterns/patternizer.git cd patternizer -# 2. Set up the development environment (installs tools) +# 2. Set up the development environment make dev-setup # 3. Make your changes... @@ -180,45 +169,6 @@ make dev-setup make ci ``` -### Common Makefile Targets - -The `Makefile` is the single source of truth for all development and CI tasks. - -- `make help`: Show all available targets. -- `make check`: Quick feedback loop (format, vet, build, unit tests). -- `make build`: Build the `patternizer` binary. -- `make test`: Run all tests (unit and integration). -- `make test-unit`: Run unit tests only. -- `make test-integration`: Run integration tests only. -- `make lint`: Run all code quality checks. -- `make amd64`: Build the amd64 container image locally. -- `make arm64`: Build the arm64 container image locally. - -### Testing Strategy - -Patternizer has a comprehensive test suite to ensure stability and correctness. - -- **Unit Tests:** Located alongside the code they test (e.g., `src/internal/helm/helm_test.go`), these tests cover individual functions and packages in isolation. They validate Helm chart detection, Git URL parsing, and YAML processing logic. -- **Integration Tests:** The integration test suite (`test/integration_test.sh`) validates the end-to-end CLI workflow against a real Git repository. Key scenarios include: - 1. **Basic Init:** Validates default file generation without secrets. - 2. **Init with Secrets:** Ensures secrets-related applications and files are correctly added. - 3. **Configuration Preservation:** Verifies that existing custom values are preserved when the tool is re-run. - 4. **Sequential Execution:** Tests running `init` and then `init --with-secrets` to ensure a clean upgrade. - 5. **Selective File Overwriting:** Confirms that running `init` on a repository with pre-existing custom files correctly **merges YAML configurations**, preserves user-modified files (like `Makefile` and `values-secret.yaml.template`), and only overwrites essential, generated scripts (`pattern.sh`, `Makefile-common`). - 6. **Mixed State Handling:** Validates that the tool correctly initializes a partially-configured repository, **creating files that are missing** while leaving existing ones untouched. - 7. **Upgrade (no replace):** Removes legacy `common/` and `pattern.sh` symlink, copies `Makefile-common`/`pattern.sh`, and injects `include Makefile-common` at the top of `Makefile` when missing. - 8. **Upgrade (include present):** Leaves the existing `Makefile` unchanged when it already contains `include Makefile-common` anywhere. - 9. **Upgrade with `--replace-makefile`:** Replaces `Makefile` with the default and refreshes common assets. - 10. **Upgrade (no Makefile present):** Creates the default `Makefile` and refreshes common assets when a `Makefile` does not exist. - -### Architecture - -The CLI is organized into focused packages following Go best practices, with a clean separation of concerns between command-line logic (`cmd`), core business logic (`internal`), and file operations (`fileutils`). This modular design makes the codebase maintainable, testable, and extensible. - -### CI/CD Pipeline - -The GitHub Actions pipeline (`.github/workflows/ci.yaml`) runs on every push and pull request. It uses the same `Makefile` targets that developers use locally, ensuring perfect consistency between local and CI environments. - ### How to Contribute 1. Fork the repository. diff --git a/src/cmd/cmd_suite_test.go b/src/cmd/cmd_suite_test.go index 1375793..7d7e62b 100644 --- a/src/cmd/cmd_suite_test.go +++ b/src/cmd/cmd_suite_test.go @@ -20,7 +20,6 @@ var ( binaryPath string resourcesPath string projectRoot string - testPath string ) func TestCmd(t *testing.T) { @@ -40,9 +39,6 @@ var _ = BeforeSuite(func() { Expect(resourcesPath).To(BeADirectory(), "Could not find resources directory") os.Setenv("PATTERNIZER_RESOURCES_DIR", resourcesPath) - testPath = filepath.Join(projectRoot, "test") - Expect(testPath).To(BeADirectory(), "Could not find test directory") - binaryPath, err = gexec.Build(filepath.Join(projectRoot, "src")) Expect(err).NotTo(HaveOccurred()) }) From ce991e601677a89604eed1df2bdd89f5a950d2e4 Mon Sep 17 00:00:00 2001 From: Drew Minnear Date: Thu, 26 Feb 2026 15:16:26 -0500 Subject: [PATCH 08/13] format updates --- README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 8cf19a8..8b2912f 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ # Patternizer -![Version: 0.2.1](https://img.shields.io/badge/Version-0.2.1-informational?style=flat-square) +![Version: 1.0.0](https://img.shields.io/badge/Version-1.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) **Patternizer** is a command-line tool that bootstraps a Git repository containing Helm charts into a ready-to-use Validated Pattern. It automatically generates the necessary scaffolding, configuration files, and utility scripts, so you can get your pattern up and running in minutes. It can also be used to upgrade existing patterns as described in [the Validated Pattern's blog](https://validatedpatterns.io/blog/2025-08-29-new-common-makefile-structure/). -> **Note:** This repo was developed with AI tools including [Cursor](https://cursor.com/), [Claude](https://claude.ai/login) and [Gemini](https://gemini.google.com/app). +**Note:** This repo was developed with AI tools including [Cursor](https://cursor.com/), [Claude](https://claude.ai/login) and [Gemini](https://gemini.google.com/app). - [Patternizer](#patternizer) - [Quick Start](#quick-start) @@ -40,7 +40,7 @@ This single command will generate all the necessary files to turn your repositor ## Example Workflow -1. **Clone or create your pattern repository:** +1. **Clone or create your pattern repository** ```bash git clone https://github.com/your-org/your-pattern.git @@ -48,13 +48,13 @@ This single command will generate all the necessary files to turn your repositor git checkout -b initialize-pattern ``` -2. **Initialize the pattern using Patternizer:** +2. **Initialize the pattern using Patternizer** ```bash podman run --pull=newer -v "$PWD:$PWD:z" -w "$PWD" quay.io/validatedpatterns/patternizer init ``` -3. **Review, commit, and push the generated files:** +3. **Review, commit, and push the generated files** ```bash git status @@ -63,13 +63,13 @@ This single command will generate all the necessary files to turn your repositor git push -u origin initialize-pattern ``` -4. **Login to an OpenShift cluster:** +4. **Login to an OpenShift cluster** ```bash export KUBECONFIG=/path/to/cluster/kubeconfig ``` -5. **Install the pattern:** +5. **Install the pattern** ```bash ./pattern.sh make install From 919f6cc653277349c9ae8251241afe608091c77a Mon Sep 17 00:00:00 2001 From: Drew Minnear Date: Thu, 26 Feb 2026 15:25:25 -0500 Subject: [PATCH 09/13] clean unneccessary comment from makefile-common --- resources/Makefile-common | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/Makefile-common b/resources/Makefile-common index 41f47f9..7a18c0e 100644 --- a/resources/Makefile-common +++ b/resources/Makefile-common @@ -1,5 +1,5 @@ MAKEFLAGS += --no-print-directory -ANSIBLE_STDOUT_CALLBACK ?= rhvp.cluser_utils.readable # null silences all ansible output. Override this with default, minimal, oneline, etc. when debugging. +ANSIBLE_STDOUT_CALLBACK ?= rhvp.cluser_utils.readable ANSIBLE_RUN ?= ANSIBLE_STDOUT_CALLBACK=$(ANSIBLE_STDOUT_CALLBACK) ansible-playbook $(EXTRA_PLAYBOOK_OPTS) DOCS_URL := https://validatedpatterns.io/blog/2025-08-29-new-common-makefile-structure/ From 84afa9da87c420c9c358087d8b5a9c8621da8f45 Mon Sep 17 00:00:00 2001 From: Drew Minnear Date: Thu, 26 Feb 2026 15:50:38 -0500 Subject: [PATCH 10/13] update pattern.sh to latest from mcg --- resources/pattern.sh | 54 +++++++++++++++++++++++++------------------- 1 file changed, 31 insertions(+), 23 deletions(-) diff --git a/resources/pattern.sh b/resources/pattern.sh index d6daa15..e7f16e5 100755 --- a/resources/pattern.sh +++ b/resources/pattern.sh @@ -1,19 +1,20 @@ #!/bin/bash +set -euo pipefail function is_available { - command -v $1 >/dev/null 2>&1 || { echo >&2 "$1 is required but it's not installed. Aborting."; exit 1; } + command -v "$1" >/dev/null 2>&1 || { echo >&2 "$1 is required but it's not installed. Aborting."; exit 1; } } function version { - echo "$@" | awk -F. '{ printf("%d%03d%03d%03d\n", $1,$2,$3,$4); }' + echo "$1" | awk -F. '{ printf("%d%03d%03d%03d\n", $1,$2,$3,$4); }' } -if [ -z "$PATTERN_UTILITY_CONTAINER" ]; then +if [ -z "${PATTERN_UTILITY_CONTAINER:-}" ]; then PATTERN_UTILITY_CONTAINER="quay.io/validatedpatterns/utility-container" fi # If PATTERN_DISCONNECTED_HOME is set it will be used to populate both PATTERN_UTILITY_CONTAINER # and PATTERN_INSTALL_CHART automatically -if [ -n "${PATTERN_DISCONNECTED_HOME}" ]; then +if [ -n "${PATTERN_DISCONNECTED_HOME:-}" ]; then PATTERN_UTILITY_CONTAINER="${PATTERN_DISCONNECTED_HOME}/utility-container" PATTERN_INSTALL_CHART="oci://${PATTERN_DISCONNECTED_HOME}/pattern-install" echo "PATTERN_DISCONNECTED_HOME is set to ${PATTERN_DISCONNECTED_HOME}" @@ -23,10 +24,10 @@ if [ -n "${PATTERN_DISCONNECTED_HOME}" ]; then fi readonly commands=(podman) -for cmd in ${commands[@]}; do is_available "$cmd"; done +for cmd in "${commands[@]}"; do is_available "$cmd"; done UNSUPPORTED_PODMAN_VERSIONS="1.6 1.5" -PODMAN_VERSION_STR=$(podman --version) +PODMAN_VERSION_STR=$(podman --version) || { echo "Failed to get podman version"; exit 1; } for i in ${UNSUPPORTED_PODMAN_VERSIONS}; do # We add a space if echo "${PODMAN_VERSION_STR}" | grep -q -E "\b${i}"; then @@ -41,19 +42,20 @@ done PODMAN_VERSION=$(echo "${PODMAN_VERSION_STR}" | awk '{ print $NF }') # podman < 4.3.0 do not support keep-id:uid=... -if [ $(version "${PODMAN_VERSION}") -lt $(version "4.3.0") ]; then - PODMAN_ARGS="-v ${HOME}:/root" +PODMAN_ARGS=() +if [ "$(version "${PODMAN_VERSION}")" -lt "$(version "4.3.0")" ]; then + PODMAN_ARGS=(-v "${HOME}:/root") else # We do not rely on bash's $UID and $GID because on MacOSX $GID is not set MYNAME=$(id -n -u) MYUID=$(id -u) MYGID=$(id -g) - PODMAN_ARGS="--passwd-entry ${MYNAME}:x:${MYUID}:${MYGID}::/pattern-home:/bin/bash --user ${MYUID}:${MYGID} --userns keep-id:uid=${MYUID},gid=${MYGID}" - + PODMAN_ARGS=(--passwd-entry "${MYNAME}:x:${MYUID}:${MYGID}::/pattern-home:/bin/bash" --user "${MYUID}:${MYGID}" --userns "keep-id:uid=${MYUID},gid=${MYGID}") fi -if [ -n "$KUBECONFIG" ]; then - if [[ ! "${KUBECONFIG}" =~ ^$HOME* ]]; then +if [ -n "${KUBECONFIG:-}" ]; then + # Check if KUBECONFIG path starts with HOME directory + if [[ ! "${KUBECONFIG}" =~ ^"${HOME}" ]]; then echo "${KUBECONFIG} is pointing outside of the HOME folder, this will make it unavailable from the container." echo "Please move it somewhere inside your $HOME folder, as that is what gets bind-mounted inside the container" exit 1 @@ -62,20 +64,26 @@ fi # Detect if we use podman machine. If we do not then we bind mount local host ssl folders # if we are using podman machine then we do not bind mount anything (for now!) -REMOTE_PODMAN=$(podman system connection list | tail -n +2 | wc -l) -if [ $REMOTE_PODMAN -eq 0 ]; then # If we are not using podman machine we check the hosts folders +REMOTE_PODMAN=$(podman system connection list | tail -n +2 | wc -l) || REMOTE_PODMAN=0 +PKI_HOST_MOUNT_ARGS=() +if [ "${REMOTE_PODMAN}" -eq 0 ]; then # If we are not using podman machine we check the hosts folders # We check /etc/pki/tls because on ubuntu /etc/pki/fwupd sometimes # exists but not /etc/pki/tls and we do not want to bind mount in such a case # as it would find no certificates at all. if [ -d /etc/pki/tls ]; then - PKI_HOST_MOUNT_ARGS="-v /etc/pki:/etc/pki:ro" + PKI_HOST_MOUNT_ARGS=(-v /etc/pki:/etc/pki:ro) elif [ -d /etc/ssl ]; then - PKI_HOST_MOUNT_ARGS="-v /etc/ssl:/etc/ssl:ro" + PKI_HOST_MOUNT_ARGS=(-v /etc/ssl:/etc/ssl:ro) else - PKI_HOST_MOUNT_ARGS="-v /usr/share/ca-certificates:/usr/share/ca-certificates:ro" + PKI_HOST_MOUNT_ARGS=(-v /usr/share/ca-certificates:/usr/share/ca-certificates:ro) fi -else - PKI_HOST_MOUNT_ARGS="" +fi + +# Parse EXTRA_ARGS into an array if set +EXTRA_ARGS_ARRAY=() +if [ -n "${EXTRA_ARGS:-}" ]; then + # shellcheck disable=SC2206 + EXTRA_ARGS_ARRAY=(${EXTRA_ARGS}) fi # Copy Kubeconfig from current environment. The utilities will pick up ~/.kube/config if set so it's not mandatory @@ -106,12 +114,12 @@ podman run -it --rm --pull=newer \ -e TOKEN_SECRET \ -e UUID_FILE \ -e VALUES_SECRET \ - ${PKI_HOST_MOUNT_ARGS} \ + "${PKI_HOST_MOUNT_ARGS[@]}" \ -v "$(pwd -P)":"$(pwd -P)" \ -v "${HOME}":"${HOME}" \ -v "${HOME}":/pattern-home \ - ${PODMAN_ARGS} \ - ${EXTRA_ARGS} \ + "${PODMAN_ARGS[@]}" \ + "${EXTRA_ARGS_ARRAY[@]}" \ -w "$(pwd -P)" \ "$PATTERN_UTILITY_CONTAINER" \ - $@ + "$@" From a3b044b0edeece096cf92d74c15b4629e9c68cb1 Mon Sep 17 00:00:00 2001 From: Drew Minnear Date: Thu, 26 Feb 2026 15:52:18 -0500 Subject: [PATCH 11/13] fix typo --- resources/Makefile-common | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/Makefile-common b/resources/Makefile-common index 7a18c0e..a5da057 100644 --- a/resources/Makefile-common +++ b/resources/Makefile-common @@ -1,5 +1,5 @@ MAKEFLAGS += --no-print-directory -ANSIBLE_STDOUT_CALLBACK ?= rhvp.cluser_utils.readable +ANSIBLE_STDOUT_CALLBACK ?= rhvp.cluster_utils.readable ANSIBLE_RUN ?= ANSIBLE_STDOUT_CALLBACK=$(ANSIBLE_STDOUT_CALLBACK) ansible-playbook $(EXTRA_PLAYBOOK_OPTS) DOCS_URL := https://validatedpatterns.io/blog/2025-08-29-new-common-makefile-structure/ From 3614262311070b994fa50ec9d1efa677c9b9ef53 Mon Sep 17 00:00:00 2001 From: Drew Minnear Date: Thu, 26 Feb 2026 17:53:14 -0500 Subject: [PATCH 12/13] update github workflow --- .github/workflows/lint-test.yaml | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/.github/workflows/lint-test.yaml b/.github/workflows/lint-test.yaml index b7bc26c..92a0f94 100644 --- a/.github/workflows/lint-test.yaml +++ b/.github/workflows/lint-test.yaml @@ -12,12 +12,12 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: persist-credentials: false - name: Set up Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v6 with: go-version: ${{ env.GO_VERSION }} cache-dependency-path: src/go.sum @@ -29,22 +29,19 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v6 with: go-version: ${{ env.GO_VERSION }} cache-dependency-path: src/go.sum - - name: Build binary - run: make build + - name: Install ginkgo + run: make dev-setup - - name: Run unit tests - run: make test-unit + - name: Run tests + run: make test - name: Generate test coverage report run: make test-coverage - - - name: Run integration tests - run: make test-integration From 76e5df520449ed4176342e8b0bf7f8848eb3d56f Mon Sep 17 00:00:00 2001 From: Drew Minnear Date: Fri, 27 Feb 2026 13:12:08 -0500 Subject: [PATCH 13/13] update makefile docstring for uninstall --- resources/Makefile-common | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/Makefile-common b/resources/Makefile-common index a5da057..0e56443 100644 --- a/resources/Makefile-common +++ b/resources/Makefile-common @@ -20,8 +20,8 @@ operator-deploy operator-upgrade: ## Installs/updates the pattern on a cluster ( .PHONY: install install: pattern-install ## Installs the pattern onto a cluster (Loads secrets as well if configured) -.PHONY: uninstall ## (Experimental) Tries to uninstall the pattern. Not always possible for a clean uninstall due to storage finalizers, CSVs in openshift-operators namespace, and other conditions. See https://validatedpatterns.io/blog/2026-02-16-pattern-uninstall/. -uninstall: +.PHONY: +uninstall: uninstall ## (EXPERIMENTAL) See https://validatedpatterns.io/blog/2026-02-16-pattern-uninstall/. @$(ANSIBLE_RUN) rhvp.cluster_utils.uninstall .PHONY: pattern-install