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

COPY resources/* .

ARG PATTERN_REPO_ROOT=/repo
WORKDIR ${PATTERN_REPO_ROOT}

ENV PATTERNIZER_RESOURCES_DIR=${PATTERNIZER_RESOURCES_DIR}

ENTRYPOINT ["patternizer"]
Expand Down
16 changes: 8 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Patternizer

![Version: 0.2.0](https://img.shields.io/badge/Version-0.2.0-informational?style=flat-square)
![Version: 0.2.1](https://img.shields.io/badge/Version-0.2.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)

Expand Down Expand Up @@ -50,7 +50,7 @@ Navigate to your repository's root directory and run the initialization command:

```bash
# In the root of your pattern-repo
podman run -v "$PWD:/repo:z" quay.io/validatedpatterns/patternizer init
podman run -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.
Expand All @@ -68,7 +68,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:/repo:z" quay.io/validatedpatterns/patternizer init
podman run -v "$PWD:$PWD:z" -w "$PWD" quay.io/validatedpatterns/patternizer init
```

3. **Review, commit, and push the generated files:**
Expand All @@ -90,18 +90,18 @@ 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:/repo:z"` 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. The `-v "$PWD:$PWD:z" -w "$PWD"` flag mounts your current directory into the container's `/repo` workspace.

#### **Initialize without secrets:**

```bash
podman run -v "$PWD:/repo:z" quay.io/validatedpatterns/patternizer init
podman run -v "$PWD:$PWD:z" -w "$PWD" quay.io/validatedpatterns/patternizer init
```

#### **Initialize with secrets support:**

```bash
podman run -v "$PWD:/repo:z" quay.io/validatedpatterns/patternizer init --with-secrets
podman run -v "$PWD:$PWD:z" -w "$PWD" quay.io/validatedpatterns/patternizer init --with-secrets
```

#### **Upgrade an existing pattern repository:**
Expand All @@ -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:/repo:z" quay.io/validatedpatterns/patternizer upgrade
podman run -v "$PWD:$PWD:z" -w "$PWD" quay.io/validatedpatterns/patternizer upgrade

# Replace your Makefile with the default from Patternizer
podman run -v "$PWD:/repo:z" quay.io/validatedpatterns/patternizer upgrade --replace-makefile
podman run -v "$PWD:$PWD:z" -w "$PWD" quay.io/validatedpatterns/patternizer upgrade --replace-makefile
```

What upgrade does:
Expand Down
5 changes: 3 additions & 2 deletions resources/pattern.sh
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ 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 -q | wc -l)
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
# 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
Expand Down Expand Up @@ -107,10 +107,11 @@ podman run -it --rm --pull=newer \
-e UUID_FILE \
-e VALUES_SECRET \
${PKI_HOST_MOUNT_ARGS} \
-v "$(pwd -P)":"$(pwd -P)" \
-v "${HOME}":"${HOME}" \
-v "${HOME}":/pattern-home \
${PODMAN_ARGS} \
${EXTRA_ARGS} \
-w "$(pwd)" \
-w "$(pwd -P)" \
"$PATTERN_UTILITY_CONTAINER" \
$@
29 changes: 29 additions & 0 deletions src/internal/fileutils/fileutils.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
"os"
"path/filepath"
"strings"

"gopkg.in/yaml.v3"
)

// CopyFile copies a file from src to dst. If dst already exists, it will be overwritten.
Expand Down Expand Up @@ -130,3 +132,30 @@ func PrependLineToFile(filePath, line string) error {
newContents := []byte(line + "\n" + string(data))
return os.WriteFile(filePath, newContents, mode)
}

// WriteYAMLWithIndent marshals the given data structure to YAML and writes it to a file
// with 2-space indentation. This ensures consistency with prettier formatting.
func WriteYAMLWithIndent(data interface{}, filePath string) error {
file, err := os.Create(filePath)
if err != nil {
return fmt.Errorf("failed to create file %s: %w", filePath, err)
}
defer file.Close()

encoder := yaml.NewEncoder(file)
defer encoder.Close()

// Set indentation to 2 spaces instead of the default 4
encoder.SetIndent(2)

if err := encoder.Encode(data); err != nil {
return fmt.Errorf("failed to encode YAML to %s: %w", filePath, err)
}

// Set file permissions to 0644
if err := os.Chmod(filePath, 0o644); err != nil {
return fmt.Errorf("failed to set permissions on %s: %w", filePath, err)
}

return nil
}
77 changes: 77 additions & 0 deletions src/internal/fileutils/fileutils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
"runtime"
"strings"
"testing"

"gopkg.in/yaml.v3"
)

func writeFileWithMode(t *testing.T, path, content string, mode os.FileMode) {
Expand Down Expand Up @@ -221,3 +223,78 @@ func TestPrependLineToFile_PrependsAndPreservesMode(t *testing.T) {
t.Fatalf("mode not preserved: got %v want %v", info.Mode().Perm(), mode.Perm())
}
}

func TestWriteYAMLWithIndent_Uses2SpaceIndentation(t *testing.T) {
dir := t.TempDir()
p := filepath.Join(dir, "test.yaml")

// Create a nested structure to verify indentation
type NestedStruct struct {
Field1 string `yaml:"field1"`
Field2 int `yaml:"field2"`
}
type TestStruct struct {
Name string `yaml:"name"`
Nested NestedStruct `yaml:"nested"`
Items []string `yaml:"items"`
}

data := TestStruct{
Name: "test",
Nested: NestedStruct{
Field1: "value1",
Field2: 42,
},
Items: []string{"item1", "item2", "item3"},
}

// Write the YAML with our function
if err := WriteYAMLWithIndent(data, p); err != nil {
t.Fatalf("WriteYAMLWithIndent failed: %v", err)
}

// Read the file and verify indentation
content, err := os.ReadFile(p)
if err != nil {
t.Fatalf("read failed: %v", err)
}

// Verify 2-space indentation by checking the content
// The nested fields should be indented with 2 spaces, not 4
contentStr := string(content)
if !strings.Contains(contentStr, " field1: value1") {
t.Fatalf("expected 2-space indentation for nested.field1, got:\n%s", contentStr)
}
if !strings.Contains(contentStr, " field2: 42") {
t.Fatalf("expected 2-space indentation for nested.field2, got:\n%s", contentStr)
}
// Verify it's not 4-space indentation
if strings.Contains(contentStr, " field1") || strings.Contains(contentStr, " field2") {
t.Fatalf("unexpected 4-space indentation found:\n%s", contentStr)
}

// Verify the file can be unmarshaled back correctly
var decoded TestStruct
if err := yaml.Unmarshal(content, &decoded); err != nil {
t.Fatalf("failed to unmarshal written YAML: %v", err)
}
if decoded.Name != data.Name {
t.Fatalf("name mismatch: got %q want %q", decoded.Name, data.Name)
}
if decoded.Nested.Field1 != data.Nested.Field1 {
t.Fatalf("nested.field1 mismatch: got %q want %q", decoded.Nested.Field1, data.Nested.Field1)
}
if decoded.Nested.Field2 != data.Nested.Field2 {
t.Fatalf("nested.field2 mismatch: got %d want %d", decoded.Nested.Field2, data.Nested.Field2)
}

// Verify file permissions
info, err := os.Stat(p)
if err != nil {
t.Fatalf("stat failed: %v", err)
}
expectedMode := os.FileMode(0o644)
if info.Mode().Perm() != expectedMode.Perm() {
t.Fatalf("mode mismatch: got %v want %v", info.Mode().Perm(), expectedMode.Perm())
}
}
47 changes: 5 additions & 42 deletions src/internal/pattern/pattern.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import (
"fmt"
"os"
"path/filepath"
"strings"

"gopkg.in/yaml.v3"

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

Expand All @@ -26,35 +26,6 @@ func GetPatternNameAndRepoRoot() (patternName, repoRoot string, err error) {
return patternName, repoRoot, nil
}

// extractPatternNameFromURL extracts the pattern name from a Git repository URL.
// Returns an error if the URL format is not recognized.
func extractPatternNameFromURL(url string) (string, error) {
// Handle SSH URLs: git@github.com:user/repo.git
if strings.HasPrefix(url, "git@") {
parts := strings.Split(url, ":")
if len(parts) >= 2 {
repoPath := parts[1]
repoName := filepath.Base(repoPath)
return strings.TrimSuffix(repoName, ".git"), nil
}
return "", fmt.Errorf("invalid SSH URL format")
}

// Handle HTTPS URLs: https://github.com/user/repo.git
if strings.HasPrefix(url, "https://") {
repoName := filepath.Base(url)
return strings.TrimSuffix(repoName, ".git"), nil
}

// Handle HTTP URLs: http://github.com/user/repo.git
if strings.HasPrefix(url, "http://") {
repoName := filepath.Base(url)
return strings.TrimSuffix(repoName, ".git"), nil
}

return "", fmt.Errorf("unsupported URL format: expected git@host:user/repo.git, https://host/user/repo.git, or http://host/user/repo.git")
}

// ProcessGlobalValues processes the global values YAML file.
// It returns the pattern name and cluster group name that should be used (from the file if they exist, or the detected/default names).
func ProcessGlobalValues(patternName, repoRoot string, withSecrets bool) (actualPatternName, clusterGroupName string, err error) {
Expand Down Expand Up @@ -84,12 +55,8 @@ func ProcessGlobalValues(patternName, repoRoot string, withSecrets bool) (actual
// If withSecrets is false, we want secretLoader to be disabled (disabled = true)
values.Global.SecretLoader.Disabled = !withSecrets

// Write back the merged values
finalYamlBytes, err := yaml.Marshal(values)
if err != nil {
return "", "", fmt.Errorf("failed to marshal global values: %w", err)
}
if err = os.WriteFile(globalValuesPath, finalYamlBytes, 0o644); err != nil {
// Write back the merged values with 2-space indentation
if err = fileutils.WriteYAMLWithIndent(values, globalValuesPath); err != nil {
return "", "", fmt.Errorf("failed to write to %s: %w", globalValuesPath, err)
}

Expand Down Expand Up @@ -118,12 +85,8 @@ func ProcessClusterGroupValues(patternName, clusterGroupName, repoRoot string, c
mergeClusterGroupValues(values, &existingValues)
}

// Write back the merged values
finalYamlBytes, err := yaml.Marshal(values)
if err != nil {
return fmt.Errorf("failed to marshal cluster group values: %w", err)
}
if err = os.WriteFile(clusterGroupValuesPath, finalYamlBytes, 0o644); err != nil {
// Write back the merged values with 2-space indentation
if err = fileutils.WriteYAMLWithIndent(values, clusterGroupValuesPath); err != nil {
return fmt.Errorf("failed to write to %s: %w", clusterGroupValuesPath, err)
}

Expand Down
88 changes: 0 additions & 88 deletions src/internal/pattern/pattern_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,94 +10,6 @@ import (
"github.com/dminnear-rh/patternizer/internal/types"
)

// TestExtractPatternNameFromURL tests URL parsing for different Git URL formats.
func TestExtractPatternNameFromURL(t *testing.T) {
tests := []struct {
name string
url string
expected string
expectError bool
}{
{
name: "SSH URL with .git suffix",
url: "git@github.com:user/my-pattern.git",
expected: "my-pattern",
},
{
name: "SSH URL without .git suffix",
url: "git@github.com:user/my-pattern",
expected: "my-pattern",
},
{
name: "HTTPS URL with .git suffix",
url: "https://github.com/user/my-pattern.git",
expected: "my-pattern",
},
{
name: "HTTPS URL without .git suffix",
url: "https://github.com/user/my-pattern",
expected: "my-pattern",
},
{
name: "HTTP URL with .git suffix",
url: "http://github.com/user/my-pattern.git",
expected: "my-pattern",
},
{
name: "HTTP URL without .git suffix",
url: "http://github.com/user/my-pattern",
expected: "my-pattern",
},
{
name: "GitLab SSH URL",
url: "git@gitlab.com:group/subgroup/my-pattern.git",
expected: "my-pattern",
},
{
name: "GitLab HTTPS URL",
url: "https://gitlab.com/group/subgroup/my-pattern.git",
expected: "my-pattern",
},
{
name: "Invalid SSH URL format",
url: "git@github.com",
expectError: true,
},
{
name: "Unsupported protocol",
url: "ftp://github.com/user/repo.git",
expectError: true,
},
{
name: "Empty URL",
url: "",
expectError: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := extractPatternNameFromURL(tt.url)

if tt.expectError {
if err == nil {
t.Errorf("Expected error for URL '%s', but got none", tt.url)
}
return
}

if err != nil {
t.Errorf("Unexpected error for URL '%s': %v", tt.url, err)
return
}

if result != tt.expected {
t.Errorf("extractPatternNameFromURL('%s') = '%s', expected '%s'", tt.url, result, tt.expected)
}
})
}
}

// TestProcessGlobalValuesPreservesFields tests that ProcessGlobalValues preserves existing user fields.
func TestProcessGlobalValuesPreservesFields(t *testing.T) {
tempDir, err := os.MkdirTemp("", "pattern-test-*")
Expand Down
Loading