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
94 changes: 92 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,15 @@ patternizer init
# Initialize pattern files with secrets support
patternizer init --with-secrets

# Show help for the init command
# Update existing pattern to use patternizer workflow (with secrets by default)
patternizer update

# Update existing pattern without secrets
patternizer update --no-secrets

# Show help for specific commands
patternizer init help
patternizer update help
```

### Output Files
Expand All @@ -69,6 +76,13 @@ When using `--with-secrets`:
- `values-secret.yaml.template` - Template for secrets configuration
- Modified `pattern.sh` with `USE_SECRETS=true` as default

The `patternizer update` command modifies existing patterns:

- **Removes** `common/` directory (now handled by utility container)
- **Removes** existing `Makefile` (utility container provides its own)
- **Replaces** `pattern.sh` symlink/file with patternizer version
- **Sets** `USE_SECRETS=true` by default (use `--no-secrets` for `USE_SECRETS=false`)

---

## Container Usage
Expand Down Expand Up @@ -115,6 +129,75 @@ podman run --rm -it -v .:/repo:z quay.io/dminnear/patternizer init --with-secret

---

## Updating Existing Patterns

If you have an existing Validated Pattern that uses the traditional structure (with `common/` directory and symlinked `pattern.sh`), you can modernize it to use the patternizer workflow:

### Update Workflow

1. **Navigate to your existing pattern repository:**
```bash
cd /path/to/your/existing-pattern
```

2. **Update the pattern (with secrets by default):**
```bash
podman run --rm -it -v .:/repo:z quay.io/dminnear/patternizer update
```

3. **Or update without secrets:**
```bash
podman run --rm -it -v .:/repo:z quay.io/dminnear/patternizer update --no-secrets
```

4. **Verify the changes:**
```bash
# Check that old files are removed
ls -la common/ # Should not exist
ls -la Makefile # Should not exist

# Check that pattern.sh is now a real file
ls -la pattern.sh # Should be a regular file, not a symlink

# Verify USE_SECRETS setting
grep "USE_SECRETS" pattern.sh
```

### What Gets Updated

The `update` command modernizes your pattern by:
- ✅ **Removing** the `common/` directory (functionality moved to utility container)
- ✅ **Removing** the top-level `Makefile` (utility container provides its own)
- ✅ **Replacing** the symlinked `pattern.sh` with the patternizer version
- ✅ **Configuring** secrets support (`USE_SECRETS=true` by default)
- ✅ **Preserving** all your existing values files and custom configurations

### Example: Updating multicloud-gitops

```bash
# Clone an existing pattern
git clone https://github.com/validatedpatterns/multicloud-gitops.git
cd multicloud-gitops

# Before: traditional structure
ls -la common/ # Directory exists
ls -la Makefile # File exists
ls -la pattern.sh # Symlink to ./common/scripts/pattern-util.sh

# Update to patternizer workflow
podman run --rm -it -v .:/repo:z quay.io/dminnear/patternizer update

# After: modernized structure
ls -la common/ # Directory removed
ls -la Makefile # File removed
ls -la pattern.sh # Now a regular file with USE_SECRETS=true

# Use the updated pattern
./pattern.sh make install
```

---

## Development

### Prerequisites
Expand Down Expand Up @@ -216,7 +299,7 @@ The project includes comprehensive unit tests across multiple packages:

### Integration Tests

The integration test suite (`test/integration_test.sh`) validates the complete CLI workflow with four comprehensive test scenarios:
The integration test suite (`test/integration_test.sh`) validates the complete CLI workflow with five comprehensive test scenarios:

**Test 1: Basic Initialization (Without Secrets)**
- Clones the [trivial-pattern](https://github.com/dminnear-rh/trivial-pattern) repository
Expand All @@ -241,6 +324,13 @@ The integration test suite (`test/integration_test.sh`) validates the complete C
- Validates that the second command properly upgrades the configuration
- Ensures final state matches direct `--with-secrets` execution

**Test 5: Update Existing Pattern**
- Clones the [multicloud-gitops](https://github.com/validatedpatterns/multicloud-gitops) repository (real-world existing pattern)
- Verifies initial traditional pattern structure (`common/` directory, `Makefile`, symlinked `pattern.sh`)
- Runs `patternizer update` to modernize the pattern
- Validates complete cleanup: `common/` and `Makefile` removal, `pattern.sh` symlink replacement
- Ensures new `pattern.sh` has `USE_SECRETS=true` and executable permissions

Run integration tests locally:
```bash
# Run integration tests (automatically builds binary first)
Expand Down
19 changes: 19 additions & 0 deletions src/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
var withSecrets bool
var noSecrets bool

var rootCmd = &cobra.Command{
Use: "patternizer",
Expand All @@ -36,9 +37,27 @@ configures the pattern.sh script for secrets usage.`,
},
}

var updateCmd = &cobra.Command{
Use: "update",
Short: "Update existing Validated Pattern to use patternizer workflow",
Long: `Update existing Validated Pattern removes the common/ directory and replaces the pattern.sh script
with the patternizer version. By default, it configures the pattern to use secrets (USE_SECRETS=true).

Use --no-secrets flag to disable secrets usage (USE_SECRETS=false).`,
RunE: func(cmd *cobra.Command, args []string) error {
// Check if "help" is passed as an argument
if len(args) > 0 && args[0] == "help" {
return cmd.Help()
}
return runUpdate(noSecrets)
},
}

initCmd.Flags().BoolVar(&withSecrets, "with-secrets", false, "Include secrets template and configure pattern for secrets usage")
updateCmd.Flags().BoolVar(&noSecrets, "no-secrets", false, "Disable secrets usage (USE_SECRETS=false)")

rootCmd.AddCommand(initCmd)
rootCmd.AddCommand(updateCmd)

if err := rootCmd.Execute(); err != nil {
os.Exit(1)
Expand Down
79 changes: 79 additions & 0 deletions src/cmd/update.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package cmd

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

"github.com/dminnear-rh/patternizer/internal/fileutils"
"github.com/dminnear-rh/patternizer/internal/pattern"
)

// runUpdate handles the update logic for the update command.
func runUpdate(noSecrets bool) error {
// Get pattern name and repository root
patternName, repoRoot, err := pattern.GetPatternNameAndRepoRoot()
if err != nil {
return fmt.Errorf("error getting pattern information: %w", err)
}

// Remove the existing pattern.sh file if it exists (it might be a symlink to common/)
patternShDst := filepath.Join(repoRoot, "pattern.sh")
if _, err := os.Stat(patternShDst); err == nil {
fmt.Printf("Removing existing pattern.sh: %s\n", patternShDst)
if err := os.Remove(patternShDst); err != nil {
return fmt.Errorf("error removing existing pattern.sh: %w", err)
}
} else if !os.IsNotExist(err) {
return fmt.Errorf("error checking existing pattern.sh: %w", err)
}

// Remove the existing Makefile if it exists (utility container provides its own)
makefilePath := filepath.Join(repoRoot, "Makefile")
if _, err := os.Stat(makefilePath); err == nil {
fmt.Printf("Removing existing Makefile: %s\n", makefilePath)
if err := os.Remove(makefilePath); err != nil {
return fmt.Errorf("error removing existing Makefile: %w", err)
}
} else if !os.IsNotExist(err) {
return fmt.Errorf("error checking existing Makefile: %w", err)
}

// Delete the common/ directory if it exists
commonDir := filepath.Join(repoRoot, "common")
if _, err := os.Stat(commonDir); err == nil {
fmt.Printf("Removing common/ directory: %s\n", commonDir)
if err := os.RemoveAll(commonDir); err != nil {
return fmt.Errorf("error removing common/ directory: %w", err)
}
} else if !os.IsNotExist(err) {
return fmt.Errorf("error checking common/ directory: %w", err)
}

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

patternShSrc := filepath.Join(resourcesDir, "pattern.sh")
if err := fileutils.CopyFile(patternShSrc, patternShDst); err != nil {
return fmt.Errorf("error copying pattern.sh: %w", err)
}

// Set USE_SECRETS in pattern.sh based on the flag
// By default, update uses secrets (opposite of init)
useSecrets := !noSecrets
if err := fileutils.ModifyPatternShScript(patternShDst, useSecrets); err != nil {
return fmt.Errorf("error modifying pattern.sh: %w", err)
}

fmt.Printf("Successfully updated pattern '%s' in %s\n", patternName, repoRoot)
if useSecrets {
fmt.Println("Secrets configuration is enabled (USE_SECRETS=true).")
} else {
fmt.Println("Secrets configuration is disabled (USE_SECRETS=false).")
}

return nil
}
92 changes: 91 additions & 1 deletion test/integration_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@ NC='\033[0m' # No Color
# Test configuration
PATTERNIZER_BINARY="${PATTERNIZER_BINARY:-./src/patternizer}"
TEST_REPO_URL="https://github.com/dminnear-rh/trivial-pattern.git"
UPDATE_TEST_REPO_URL="https://github.com/validatedpatterns/multicloud-gitops.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_UPDATE="/tmp/patternizer-integration-test-update"

echo -e "${YELLOW}Starting patternizer integration tests...${NC}"

Expand All @@ -27,6 +29,9 @@ fi
if [ -d "$TEST_DIR_CUSTOM" ]; then
rm -rf "$TEST_DIR_CUSTOM"
fi
if [ -d "$TEST_DIR_UPDATE" ]; then
rm -rf "$TEST_DIR_UPDATE"
fi

# Convert PATTERNIZER_BINARY to absolute path before changing directories
PATTERNIZER_BINARY=$(realpath "$PATTERNIZER_BINARY")
Expand Down Expand Up @@ -128,6 +133,20 @@ check_file_exists() {
fi
}

# Function to check file/directory doesn't exist
check_not_exists() {
local path="$1"
local description="$2"

if [ ! -e "$path" ]; then
echo -e "${GREEN}PASS: $description${NC}"
return 0
else
echo -e "${RED}FAIL: $description - path still exists: $path${NC}"
return 1
fi
}

#
# Test 1: Basic initialization (without secrets)
#
Expand Down Expand Up @@ -285,8 +304,79 @@ check_file_exists "values-secret.yaml.template" "values-secret.yaml.template fil

echo -e "${GREEN}=== Test 4: Sequential execution PASSED ===${NC}"

#
# Test 5: Update existing pattern (multicloud-gitops)
#
echo -e "${YELLOW}=== Test 5: Update existing pattern (multicloud-gitops) ===${NC}"

cd "$REPO_ROOT" # Go back to repo root
echo -e "${YELLOW}Cloning multicloud-gitops repository for update test...${NC}"
git clone "$UPDATE_TEST_REPO_URL" "$TEST_DIR_UPDATE"
cd "$TEST_DIR_UPDATE"

echo -e "${YELLOW}Verifying initial state (before update)...${NC}"
# Verify the typical old pattern structure exists
if [ -d "common" ]; then
echo -e "${GREEN}INFO: common/ directory exists (as expected)${NC}"
else
echo -e "${YELLOW}WARNING: common/ directory not found - pattern may already be updated${NC}"
fi

if [ -f "Makefile" ]; then
echo -e "${GREEN}INFO: Makefile exists (as expected)${NC}"
else
echo -e "${YELLOW}WARNING: Makefile not found - pattern may already be updated${NC}"
fi

if [ -L "pattern.sh" ]; then
echo -e "${GREEN}INFO: pattern.sh is a symlink (as expected)${NC}"
echo -e "${GREEN}INFO: pattern.sh points to: $(readlink pattern.sh)${NC}"
elif [ -f "pattern.sh" ]; then
echo -e "${YELLOW}WARNING: pattern.sh exists but is not a symlink${NC}"
else
echo -e "${RED}ERROR: pattern.sh not found at all${NC}"
exit 1
fi

echo -e "${YELLOW}Running patternizer update...${NC}"
PATTERNIZER_RESOURCES_DIR="$REPO_ROOT" "$PATTERNIZER_BINARY" update

echo -e "${YELLOW}Running verification tests for update...${NC}"

# Test 5.1: Verify common/ directory was removed
check_not_exists "common" "common/ directory was removed"

# Test 5.2: Verify Makefile was removed
check_not_exists "Makefile" "Makefile was removed"

# Test 5.3: Verify pattern.sh exists and is a real file (not symlink)
if [ -f "pattern.sh" ] && [ ! -L "pattern.sh" ]; then
echo -e "${GREEN}PASS: pattern.sh exists and is a regular file (not symlink)${NC}"
else
echo -e "${RED}FAIL: pattern.sh is not a regular file${NC}"
if [ -L "pattern.sh" ]; then
echo "pattern.sh is still a symlink pointing to: $(readlink pattern.sh)"
elif [ ! -f "pattern.sh" ]; then
echo "pattern.sh does not exist"
fi
exit 1
fi

# Test 5.4: Check pattern.sh has USE_SECRETS=true (default for update)
check_file_content "pattern.sh" 'USE_SECRETS:=true' "pattern.sh contains USE_SECRETS=true (update default)"

# Test 5.5: Verify pattern.sh is executable
if [ -x "pattern.sh" ]; then
echo -e "${GREEN}PASS: pattern.sh is executable (update)${NC}"
else
echo -e "${RED}FAIL: pattern.sh is not executable (update)${NC}"
exit 1
fi

echo -e "${GREEN}=== Test 5: Update existing pattern PASSED ===${NC}"

echo -e "${GREEN}All integration tests passed!${NC}"

# Clean up
cd "$REPO_ROOT"
rm -rf "$TEST_DIR" "$TEST_DIR_SECRETS" "$TEST_DIR_CUSTOM" "$TEST_DIR_SEQUENTIAL"
rm -rf "$TEST_DIR" "$TEST_DIR_SECRETS" "$TEST_DIR_CUSTOM" "$TEST_DIR_SEQUENTIAL" "$TEST_DIR_UPDATE"