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
1 change: 1 addition & 0 deletions Containerfile
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ WORKDIR ${PATTERNIZER_RESOURCES_DIR}

COPY pattern.sh .
COPY values-secret.yaml.template .
COPY Makefile-pattern .

ARG PATTERN_REPO_ROOT=/repo
WORKDIR ${PATTERN_REPO_ROOT}
Expand Down
171 changes: 171 additions & 0 deletions Makefile-pattern
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
NAME ?= $(shell basename "`pwd`")

ifneq ($(origin TARGET_SITE), undefined)
TARGET_SITE_OPT=--set main.clusterGroupName=$(TARGET_SITE)
endif

# Set this to true if you want to skip any origin validation
DISABLE_VALIDATE_ORIGIN ?= false
ifeq ($(DISABLE_VALIDATE_ORIGIN),true)
VALIDATE_ORIGIN :=
else
VALIDATE_ORIGIN := validate-origin
endif

# This variable can be set in order to pass additional helm arguments from the
# the command line. I.e. we can set things without having to tweak values files
EXTRA_HELM_OPTS ?=

# This variable can be set in order to pass additional ansible-playbook arguments from the
# the command line. I.e. we can set -vvv for more verbose logging
EXTRA_PLAYBOOK_OPTS ?=

# INDEX_IMAGES=registry-proxy.engineering.redhat.com/rh-osbs/iib:394248
# or
# INDEX_IMAGES=registry-proxy.engineering.redhat.com/rh-osbs/iib:394248,registry-proxy.engineering.redhat.com/rh-osbs/iib:394249
INDEX_IMAGES ?=

# git branch --show-current is also available as of git 2.22, but we will use this for compatibility
TARGET_BRANCH ?= $(shell git rev-parse --abbrev-ref HEAD)

# Default to the branch remote
TARGET_ORIGIN ?= $(shell git config branch.$(TARGET_BRANCH).remote)

# This is to ensure that whether we start with a git@ or https:// URL, we end up with an https:// URL
# This is because we expect to use tokens for repo authentication as opposed to SSH keys
TARGET_REPO=$(shell git ls-remote --get-url --symref $(TARGET_ORIGIN) | sed -e 's/.*URL:[[:space:]]*//' -e 's%^git@%%' -e 's%^https://%%' -e 's%:%/%' -e 's%^%https://%')

UUID_FILE ?= ~/.config/validated-patterns/pattern-uuid
UUID_HELM_OPTS ?=

# --set values always take precedence over the contents of -f
ifneq ("$(wildcard $(UUID_FILE))","")
UUID := $(shell cat $(UUID_FILE))
UUID_HELM_OPTS := --set main.analyticsUUID=$(UUID)
endif

# Set the secret name *and* its namespace when deploying from private repositories
# The format of said secret is documented here: https://argo-cd.readthedocs.io/en/stable/operator-manual/declarative-setup/#repositories
TOKEN_SECRET ?=
TOKEN_NAMESPACE ?=

ifeq ($(TOKEN_SECRET),)
HELM_OPTS=-f values-global.yaml --set main.git.repoURL="$(TARGET_REPO)" --set main.git.revision=$(TARGET_BRANCH) $(TARGET_SITE_OPT) $(UUID_HELM_OPTS) $(EXTRA_HELM_OPTS)
else
# When we are working with a private repository we do not escape the git URL as it might be using an ssh secret which does not use https://
TARGET_CLEAN_REPO=$(shell git ls-remote --get-url --symref $(TARGET_ORIGIN))
HELM_OPTS=-f values-global.yaml --set main.tokenSecret=$(TOKEN_SECRET) --set main.tokenSecretNamespace=$(TOKEN_NAMESPACE) --set main.git.repoURL="$(TARGET_CLEAN_REPO)" --set main.git.revision=$(TARGET_BRANCH) $(TARGET_SITE_OPT) $(UUID_HELM_OPTS) $(EXTRA_HELM_OPTS)
endif

# Helm does the right thing and fetches all the tags and detects the newest one
PATTERN_INSTALL_CHART ?= oci://quay.io/hybridcloudpatterns/pattern-install

.PHONY: default
default: help

.PHONY: help
help: ## This help message
@echo "Pattern: $(NAME)"
@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; \
fi
@echo "Installed"

.PHONY: operator-deploy
operator-deploy operator-upgrade: validate-prereq $(VALIDATE_ORIGIN) validate-cluster
@echo -n "Installing pattern: "
@RUNS=10; \
WAIT=15; \
for i in $$(seq 1 $$RUNS); do \
exec 3>&1 4>&2; \
OUT=$$( { helm template --include-crds --name-template $(NAME) $(PATTERN_INSTALL_CHART) $(HELM_OPTS) 2>&4 | oc apply -f- 2>&4 1>&3; } 4>&1 3>&1); \
ret=$$?; \
exec 3>&- 4>&-; \
if [ $$ret -eq 0 ]; then \
break; \
else \
echo -n "."; \
sleep "$$WAIT"; \
fi; \
done; \
if [ $$i -eq $$RUNS ]; then \
echo "Installation failed [$$i/$$RUNS]. Error:"; \
echo "$$OUT"; \
exit 1; \
fi
@echo "Done"

.PHONY: validate-prereq
validate-prereq:
$(eval GLOBAL_PATTERN := $(shell yq -r .global.pattern values-global.yaml))
@if [ $(NAME) != $(GLOBAL_PATTERN) ]; then\
echo "";\
echo "WARNING: folder directory is \"$(NAME)\" and global.pattern is set to \"$(GLOBAL_PATTERN)\"";\
echo "this can create problems. Please make sure they are the same!";\
echo "";\
fi
@if [ ! -f /run/.containerenv ]; then\
echo "Checking prerequisites:";\
echo -n " Check for python-kubernetes: ";\
if ! ansible -m ansible.builtin.command -a "{{ ansible_python_interpreter }} -c 'import kubernetes'" localhost > /dev/null 2>&1; then echo "Not found"; exit 1; fi;\
echo "OK";\
echo -n " Check for kubernetes.core collection: ";\
if ! ansible-galaxy collection list | grep kubernetes.core > /dev/null 2>&1; then echo "Not found"; exit 1; fi;\
echo "OK";\
else\
if [ -f values-global.yaml ]; then\
OUT=`yq -r '.main.multiSourceConfig.enabled // (.main.multiSourceConfig.enabled = "false")' values-global.yaml`;\
if [ "$${OUT,,}" = "false" ]; then\
echo "You must set \".main.multiSourceConfig.enabled: true\" in your 'values-global.yaml' file";\
echo "because your common subfolder is the slimmed down version with no helm charts in it";\
exit 1;\
fi;\
fi;\
fi

.PHONY: validate-origin
validate-origin:
@echo "Checking repository:"
$(eval UPSTREAMURL := $(shell yq -r '.main.git.repoUpstreamURL // (.main.git.repoUpstreamURL = "")' values-global.yaml))
@if [ -z "$(UPSTREAMURL)" ]; then\
echo -n " $(TARGET_REPO) - branch '$(TARGET_BRANCH)': ";\
git ls-remote --exit-code --heads $(TARGET_REPO) $(TARGET_BRANCH) >/dev/null &&\
echo "OK" || (echo "NOT FOUND"; exit 1);\
else\
echo "Upstream URL set to: $(UPSTREAMURL)";\
echo -n " $(UPSTREAMURL) - branch '$(TARGET_BRANCH)': ";\
git ls-remote --exit-code --heads $(UPSTREAMURL) $(TARGET_BRANCH) >/dev/null &&\
echo "OK" || (echo "NOT FOUND"; exit 1);\
fi

.PHONY: validate-cluster
validate-cluster:
@echo "Checking cluster:"
@echo -n " cluster-info: "
@oc cluster-info >/dev/null && echo "OK" || (echo "Error"; exit 1)
@echo -n " storageclass: "
@if [ `oc get storageclass -o go-template='{{printf "%d\n" (len .items)}}'` -eq 0 ]; then\
echo "WARNING: No storageclass found";\
else\
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); \
PATTERN_DIR=$$(pwd); \
BACKEND=$$(yq '.global.secretStore.backend' "$$PATTERN_DIR/values-global.yaml" 2>/dev/null); \
if [ -z "$$BACKEND" -o "$$BACKEND" = "null" ]; then \
BACKEND="vault"; \
fi; \
ansible-playbook -e pattern_name="$$PATTERN_NAME" -e pattern_dir="$$PATTERN_DIR" -e secrets_backing_store="$$BACKEND" $(EXTRA_PLAYBOOK_OPTS) "rhvp.cluster_utils.process_secrets"
98 changes: 8 additions & 90 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,15 +53,8 @@ patternizer init
# Initialize pattern files with secrets support
patternizer init --with-secrets

# 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 @@ -71,17 +64,14 @@ The `patternizer init` command generates:
- `values-global.yaml` - Global pattern configuration
- `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

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

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`)
Both `pattern.sh` and `Makefile` provide equivalent functionality for pattern installation and management, with the Makefile offering a more traditional build tool approach.

---

Expand Down Expand Up @@ -129,75 +119,6 @@ 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 @@ -299,37 +220,34 @@ 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 five comprehensive test scenarios:
The integration test suite (`test/integration_test.sh`) validates the complete CLI workflow with four comprehensive test scenarios:

**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

**Test 2: Initialization with Secrets**
- Tests `patternizer init --with-secrets` functionality
- 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

**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

**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

**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
- Verifies both `pattern.sh` and `Makefile` are updated correctly in the sequential workflow

Run integration tests locally:
```bash
Expand Down
8 changes: 4 additions & 4 deletions pattern.sh
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/usr/bin/env bash
#!/bin/bash

function is_available {
command -v $1 >/dev/null 2>&1 || { echo >&2 "$1 is required but it's not installed. Aborting."; exit 1; }
Expand All @@ -9,7 +9,7 @@ function version {
}

if [ -z "$PATTERN_UTILITY_CONTAINER" ]; then
PATTERN_UTILITY_CONTAINER="quay.io/dminnear/common-utility-container"
PATTERN_UTILITY_CONTAINER="quay.io/hybridcloudpatterns/utility-container"
fi
# If PATTERN_DISCONNECTED_HOME is set it will be used to populate both PATTERN_UTILITY_CONTAINER
# and PATTERN_INSTALL_CHART automatically
Expand Down Expand Up @@ -107,10 +107,10 @@ podman run -it --rm --pull=newer \
-e K8S_AUTH_TOKEN \
-e USE_SECRETS="$USE_SECRETS" \
${PKI_HOST_MOUNT_ARGS} \
-v "$(pwd)":/pattern-repo \
-v "${HOME}":"${HOME}" \
-v "${HOME}":/pattern-home \
${PODMAN_ARGS} \
${EXTRA_ARGS} \
-w "$(pwd)" \
"$PATTERN_UTILITY_CONTAINER" \
bash -c 'cp -r /pattern-repo/. /pattern && exec "$@"' _ "$@"
$@
14 changes: 13 additions & 1 deletion src/cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ func runInit(withSecrets bool) error {
return fmt.Errorf("error processing cluster group values: %w", err)
}

// Copy pattern.sh from resources
// Copy pattern.sh and Makefile from resources
resourcesDir, err := fileutils.GetResourcePath()
if err != nil {
return fmt.Errorf("error getting resource path: %w", err)
Expand All @@ -51,6 +51,18 @@ func runInit(withSecrets bool) error {
return fmt.Errorf("error modifying pattern.sh: %w", err)
}

// Copy and modify Makefile
makefileSrc := filepath.Join(resourcesDir, "Makefile-pattern")
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)
}

// Handle secrets setup if requested
if withSecrets {
if err := fileutils.HandleSecretsSetup(resourcesDir, repoRoot); err != nil {
Expand Down
Loading