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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 22 additions & 9 deletions Makefile-pattern
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,28 @@ help: ## This help message
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n"} /^(\s|[a-zA-Z_0-9-])+:.*?##/ { printf " \033[36m%-35s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)

.PHONY: install
USE_SECRETS ?= false
install: operator-deploy ## Install the pattern (USE_SECRETS=true to load secrets)
@if [ "$(USE_SECRETS)" != "false" ]; then \
$(MAKE) post-install; \
# Dynamically read secretLoader.disabled from values-global.yaml to determine LOAD_SECRETS
# If secretLoader.disabled is true, LOAD_SECRETS should be false (secrets disabled)
# If secretLoader.disabled is false, LOAD_SECRETS should be true (secrets enabled)
LOAD_SECRETS := $(shell if [ -f values-global.yaml ]; then \
YQ_OUTPUT=$$(yq -r '.global.secretLoader.disabled' values-global.yaml 2>/dev/null); \
YQ_EXIT=$$?; \
if [ $$YQ_EXIT -eq 0 ] && [ "$$YQ_OUTPUT" != "null" ]; then \
DISABLED="$$YQ_OUTPUT"; \
else \
DISABLED="true"; \
fi; \
if [ "$$DISABLED" = "false" ]; then echo "true"; else echo "false"; fi; \
else \
echo "false"; \
fi)
install: operator-deploy ## Install the pattern (LOAD_SECRETS determined by global.secretLoader.disabled in values-global.yaml)
@echo "Secrets configuration: global.secretLoader.disabled=$$(yq -r '.global.secretLoader.disabled // true' values-global.yaml 2>/dev/null || echo 'true'), LOAD_SECRETS=$(LOAD_SECRETS)"
@if [ "$(LOAD_SECRETS)" != "false" ]; then \
echo "Loading secrets..."; \
$(MAKE) load-secrets; \
else \
echo "Skipping secrets loading (disabled)"; \
fi
@echo "Installed"

Expand Down Expand Up @@ -155,11 +173,6 @@ validate-cluster:
echo "OK";\
fi

.PHONY: post-install
post-install:
make load-secrets
@echo "Done"

.PHONY: load-secrets
load-secrets: ## Load secrets into the configured backend
@PATTERN_NAME=$(NAME); \
Expand Down
36 changes: 22 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
[![Quay Repository](https://img.shields.io/badge/Quay.io-patternizer-blue?logo=quay)](https://quay.io/repository/dminnear/patternizer)
[![CI Pipeline](https://github.com/dminnear-rh/patternizer/actions/workflows/ci.yaml/badge.svg?branch=main)](https://github.com/dminnear-rh/patternizer/actions/workflows/ci.yaml)

> **Note:** This tool was developed with assistance from [Cursor](https://cursor.sh), an AI-powered code editor.

**Patternizer** is a CLI tool and container utility designed to bootstrap Validated Pattern repositories. It automatically generates the necessary `values-global.yaml` and `values-<cluster_group>.yaml` files by inspecting Git repositories, discovering Helm charts, and applying sensible defaults.

The tool provides both a standalone CLI and containerized execution for maximum flexibility and consistency across environments.
Expand Down Expand Up @@ -61,17 +63,18 @@ patternizer init help

The `patternizer init` command generates:

- `values-global.yaml` - Global pattern configuration
- `values-global.yaml` - Global pattern configuration with `global.secretLoader.disabled: true`
- `values-<cluster_group>.yaml` - Cluster group-specific configuration
- `pattern.sh` - Utility script for pattern operations
- `Makefile` - Self-contained Makefile with inlined scripts for pattern management
- `Makefile` - Simple include-based Makefile that includes `Makefile-pattern`
- `Makefile-pattern` - Contains all pattern targets and dynamically reads secrets config from `values-global.yaml`

When using `--with-secrets`:
- `values-secret.yaml.template` - Template for secrets configuration
- Modified `pattern.sh` with `USE_SECRETS=true` as default
- Modified `Makefile` with `USE_SECRETS=true` as default
- `values-global.yaml` with `global.secretLoader.disabled: false` (enables secrets)
- Additional applications (vault, golang-external-secrets) in cluster group values

Both `pattern.sh` and `Makefile` provide equivalent functionality for pattern installation and management, with the Makefile offering a more traditional build tool approach.
The secrets loading behavior is controlled entirely by the `global.secretLoader.disabled` field in `values-global.yaml`.

---

Expand Down Expand Up @@ -109,7 +112,7 @@ podman run --rm -it -v .:/repo:z quay.io/dminnear/patternizer init --with-secret

3. **Review generated files:**
```bash
ls -la values-*.yaml pattern.sh
ls -la values-*.yaml pattern.sh Makefile*
```

4. **Install the pattern:**
Expand Down Expand Up @@ -217,6 +220,10 @@ The project includes comprehensive unit tests across multiple packages:
- Adds new applications while maintaining existing ones
- Maintains custom fields within applications and subscriptions
- `TestProcessGlobalValuesWithNewFile()` - New file creation with proper defaults
- `TestProcessGlobalValuesWithSecrets()` - Validates secrets configuration:
- Tests `ProcessGlobalValues` with `withSecrets=true`
- Verifies `global.secretLoader.disabled: false` is set correctly
- Ensures secrets-enabled configuration is properly generated

### Integration Tests

Expand All @@ -225,29 +232,30 @@ The integration test suite (`test/integration_test.sh`) validates the complete C
**Test 1: Basic Initialization (Without Secrets)**
- Clones the [trivial-pattern](https://github.com/dminnear-rh/trivial-pattern) repository
- Runs `patternizer init` and validates generated files
- Verifies `values-global.yaml` and `values-prod.yaml` content using YAML normalization
- Ensures `pattern.sh` is created with `USE_SECRETS=false` and executable permissions
- Validates `Makefile` is created with `USE_SECRETS=false` and contains proper functionality
- Verifies `values-global.yaml` contains `global.secretLoader.disabled: true`
- Validates `values-prod.yaml` content using YAML normalization
- Ensures `pattern.sh` is created and executable
- Validates `Makefile` (include-based) and `Makefile-pattern` are created

**Test 2: Initialization with Secrets**
- Tests `patternizer init --with-secrets` functionality
- Verifies `values-global.yaml` contains `global.secretLoader.disabled: false`
- Validates secrets-specific applications (vault, golang-external-secrets) are added
- Verifies additional namespaces and `values-secret.yaml.template` are created
- Ensures `pattern.sh` is configured with `USE_SECRETS=true`
- Validates `Makefile` is configured with `USE_SECRETS=true` and contains secrets support
- Ensures `pattern.sh` and both Makefile files are properly generated

**Test 3: Custom Pattern and Cluster Group Names**
- Tests field preservation and intelligent merging of existing configurations
- Pre-populates custom `values-global.yaml` with renamed pattern and cluster group
- Verifies custom names are preserved while adding missing default configurations
- Validates custom cluster group file generation (e.g., `values-renamed-cluster-group.yaml`)
- Ensures both `pattern.sh` and `Makefile` are configured correctly for the custom setup
- Ensures `global.secretLoader.disabled: false` is set correctly with `--with-secrets`

**Test 4: Sequential Execution**
- Tests running `patternizer init` followed by `patternizer init --with-secrets`
- Validates that the second command properly upgrades the configuration
- Ensures final state matches direct `--with-secrets` execution
- Verifies both `pattern.sh` and `Makefile` are updated correctly in the sequential workflow
- Ensures `global.secretLoader.disabled` transitions from `true` to `false`
- Verifies final state matches direct `--with-secrets` execution

Run integration tests locally:
```bash
Expand Down
3 changes: 0 additions & 3 deletions pattern.sh
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,6 @@ else
PKI_HOST_MOUNT_ARGS=""
fi

: "${USE_SECRETS:=false}"

# Copy Kubeconfig from current environment. The utilities will pick up ~/.kube/config if set so it's not mandatory
# $HOME is mounted as itself for any files that are referenced with absolute paths
# $HOME is mounted to /root because the UID in the container is 0 and that's where SSH looks for credentials
Expand All @@ -105,7 +103,6 @@ podman run -it --rm --pull=newer \
-e K8S_AUTH_USERNAME \
-e K8S_AUTH_PASSWORD \
-e K8S_AUTH_TOKEN \
-e USE_SECRETS="$USE_SECRETS" \
${PKI_HOST_MOUNT_ARGS} \
-v "${HOME}":"${HOME}" \
-v "${HOME}":/pattern-home \
Expand Down
40 changes: 40 additions & 0 deletions src/.cursor/rules/make-ci-verification.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
---
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
25 changes: 12 additions & 13 deletions src/cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package cmd

import (
"fmt"
"os"
"path/filepath"

"github.com/dminnear-rh/patternizer/internal/fileutils"
Expand All @@ -24,7 +25,7 @@ func runInit(withSecrets bool) error {
}

// Process values-global.yaml
actualPatternName, clusterGroupName, err := pattern.ProcessGlobalValues(patternName, repoRoot)
actualPatternName, clusterGroupName, err := pattern.ProcessGlobalValues(patternName, repoRoot, withSecrets)
if err != nil {
return fmt.Errorf("error processing global values: %w", err)
}
Expand All @@ -46,21 +47,19 @@ func runInit(withSecrets bool) error {
return fmt.Errorf("error copying pattern.sh: %w", err)
}

// Set USE_SECRETS in pattern.sh based on the flag
if err := fileutils.ModifyPatternShScript(patternShDst, withSecrets); err != nil {
return fmt.Errorf("error modifying pattern.sh: %w", err)
// Always copy Makefile-pattern to the pattern repo
makefilePatternSrc := filepath.Join(resourcesDir, "Makefile-pattern")
makefilePatternDst := filepath.Join(repoRoot, "Makefile-pattern")
if err := fileutils.CopyFile(makefilePatternSrc, makefilePatternDst); err != nil {
return fmt.Errorf("error copying Makefile-pattern: %w", err)
}

// Copy and modify Makefile
makefileSrc := filepath.Join(resourcesDir, "Makefile-pattern")
// Create a simple Makefile that includes Makefile-pattern (only if it doesn't exist)
makefileDst := filepath.Join(repoRoot, "Makefile")
if err := fileutils.CopyFile(makefileSrc, makefileDst); err != nil {
return fmt.Errorf("error copying Makefile: %w", err)
}

// Set USE_SECRETS in Makefile based on the flag
if err := fileutils.ModifyMakefileScript(makefileDst, withSecrets); err != nil {
return fmt.Errorf("error modifying Makefile: %w", err)
if _, err := os.Stat(makefileDst); os.IsNotExist(err) {
if err := fileutils.CreateIncludeMakefile(makefileDst); err != nil {
return fmt.Errorf("error creating Makefile: %w", err)
}
}

// Handle secrets setup if requested
Expand Down
120 changes: 15 additions & 105 deletions src/internal/fileutils/fileutils.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
package fileutils

import (
"bufio"
"fmt"
"io"
"os"
"path/filepath"
"regexp"
"strings"
)

// CopyFile copies a file from src to dst. If dst already exists, it will be overwritten.
Expand Down Expand Up @@ -48,104 +45,7 @@ func CopyFile(src, dst string) error {
return nil
}

// ModifyPatternShScript modifies the pattern.sh script to set USE_SECRETS to the desired value.
func ModifyPatternShScript(patternShPath string, useSecrets bool) error {
file, err := os.Open(patternShPath)
if err != nil {
return err
}

var lines []string
scanner := bufio.NewScanner(file)
for scanner.Scan() {
lines = append(lines, scanner.Text())
}
file.Close()

if err := scanner.Err(); err != nil {
return err
}

// Regex to match the USE_SECRETS line
regex := regexp.MustCompile(`^\s*:\s*"\$\{USE_SECRETS:=(.+)\}"`)

for i, line := range lines {
if matches := regex.FindStringSubmatch(line); matches != nil {
if useSecrets {
lines[i] = strings.Replace(line, matches[1], "true", 1)
} else {
lines[i] = strings.Replace(line, matches[1], "false", 1)
}
break
}
}

output, err := os.Create(patternShPath)
if err != nil {
return err
}
defer output.Close()

for _, line := range lines {
_, err := output.WriteString(line + "\n")
if err != nil {
return err
}
}

return nil
}

// ModifyMakefileScript modifies the Makefile to set USE_SECRETS to the desired value.
func ModifyMakefileScript(makefilePath string, useSecrets bool) error {
file, err := os.Open(makefilePath)
if err != nil {
return err
}

var lines []string
scanner := bufio.NewScanner(file)
for scanner.Scan() {
lines = append(lines, scanner.Text())
}
file.Close()

if err := scanner.Err(); err != nil {
return err
}

// Regex to match the USE_SECRETS line in Makefile format
regex := regexp.MustCompile(`^USE_SECRETS\s*\?=\s*(.+)$`)

for i, line := range lines {
if matches := regex.FindStringSubmatch(line); matches != nil {
if useSecrets {
lines[i] = "USE_SECRETS ?= true"
} else {
lines[i] = "USE_SECRETS ?= false"
}
break
}
}

output, err := os.Create(makefilePath)
if err != nil {
return err
}
defer output.Close()

for _, line := range lines {
_, err := output.WriteString(line + "\n")
if err != nil {
return err
}
}

return nil
}

// HandleSecretsSetup handles the setup for secrets usage by copying the secrets template
// and modifying the pattern.sh script.
// HandleSecretsSetup handles the setup for secrets usage by copying the secrets template.
func HandleSecretsSetup(resourcesDir, repoRoot string) (err error) {
// Copy the values-secret.yaml.template file to the pattern root
secretsTemplateSrc := filepath.Join(resourcesDir, "values-secret.yaml.template")
Expand All @@ -155,10 +55,20 @@ func HandleSecretsSetup(resourcesDir, repoRoot string) (err error) {
return fmt.Errorf("error copying secrets template: %w", err)
}

// Modify the pattern.sh script to set USE_SECRETS=true
patternShPath := filepath.Join(repoRoot, "pattern.sh")
if err = ModifyPatternShScript(patternShPath, true); err != nil {
return fmt.Errorf("error modifying pattern.sh for secrets: %w", err)
return nil
}

// CreateIncludeMakefile creates a simple Makefile that includes Makefile-pattern.
func CreateIncludeMakefile(makefilePath string) error {
content := `# Generated by patternizer
# This Makefile includes the common pattern targets from Makefile-pattern
# You can add custom targets above or below the include line

include Makefile-pattern
`

if err := os.WriteFile(makefilePath, []byte(content), 0o644); err != nil {
return fmt.Errorf("failed to create Makefile: %w", err)
}

return nil
Expand Down
Loading