diff --git a/README.md b/README.md index ac164bc..c465eb7 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 @@ -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 @@ -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 @@ -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) diff --git a/src/cmd/root.go b/src/cmd/root.go index 02d0045..1e72417 100644 --- a/src/cmd/root.go +++ b/src/cmd/root.go @@ -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", @@ -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) diff --git a/src/cmd/update.go b/src/cmd/update.go new file mode 100644 index 0000000..2baf9b3 --- /dev/null +++ b/src/cmd/update.go @@ -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 +} diff --git a/test/integration_test.sh b/test/integration_test.sh index c1820a3..aa230ac 100755 --- a/test/integration_test.sh +++ b/test/integration_test.sh @@ -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}" @@ -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") @@ -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) # @@ -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"