Add validation for Terraform module source URLs
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 53b912b..eb3cf8b 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -63,8 +63,8 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: "1.23.2" - - name: Validate contributors + go-version: "1.25.0" + - name: Validate Reademde run: go build ./cmd/readmevalidation && ./readmevalidation - name: Remove build file artifact run: rm ./readmevalidation Signed-off-by: Muhammad Atif Ali <me@matifali.dev>
This commit is contained in:
parent
9452763f7d
commit
72d7ee418b
4
.github/workflows/ci.yaml
vendored
4
.github/workflows/ci.yaml
vendored
@ -63,8 +63,8 @@ jobs:
|
|||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: "1.23.2"
|
go-version: "1.25.0"
|
||||||
- name: Validate contributors
|
- name: Validate Reademde
|
||||||
run: go build ./cmd/readmevalidation && ./readmevalidation
|
run: go build ./cmd/readmevalidation && ./readmevalidation
|
||||||
- name: Remove build file artifact
|
- name: Remove build file artifact
|
||||||
run: rm ./readmevalidation
|
run: rm ./readmevalidation
|
||||||
|
|||||||
@ -3,11 +3,87 @@ package main
|
|||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"context"
|
"context"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"golang.org/x/xerrors"
|
"golang.org/x/xerrors"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
terraformSourceRe = regexp.MustCompile(`^\s*source\s*=\s*"([^"]+)"`)
|
||||||
|
)
|
||||||
|
|
||||||
|
func normalizeModuleName(name string) string {
|
||||||
|
// Normalize module names by replacing hyphens with underscores for comparison
|
||||||
|
// since Terraform allows both but directory names typically use hyphens
|
||||||
|
return strings.ReplaceAll(name, "-", "_")
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractNamespaceAndModuleFromPath(filePath string) (string, string, error) {
|
||||||
|
// Expected path format: registry/<namespace>/modules/<module-name>/README.md
|
||||||
|
parts := strings.Split(filepath.Clean(filePath), string(filepath.Separator))
|
||||||
|
if len(parts) < 5 || parts[0] != "registry" || parts[2] != "modules" || parts[4] != "README.md" {
|
||||||
|
return "", "", xerrors.Errorf("invalid module path format: %s", filePath)
|
||||||
|
}
|
||||||
|
namespace := parts[1]
|
||||||
|
moduleName := parts[3]
|
||||||
|
return namespace, moduleName, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateModuleSourceURL(body string, filePath string) []error {
|
||||||
|
var errs []error
|
||||||
|
|
||||||
|
namespace, moduleName, err := extractNamespaceAndModuleFromPath(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return []error{err}
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedSource := "registry.coder.com/" + namespace + "/" + moduleName + "/coder"
|
||||||
|
|
||||||
|
trimmed := strings.TrimSpace(body)
|
||||||
|
foundCorrectSource := false
|
||||||
|
isInsideTerraform := false
|
||||||
|
firstTerraformBlock := true
|
||||||
|
|
||||||
|
lineScanner := bufio.NewScanner(strings.NewReader(trimmed))
|
||||||
|
for lineScanner.Scan() {
|
||||||
|
nextLine := lineScanner.Text()
|
||||||
|
|
||||||
|
if strings.HasPrefix(nextLine, "```") {
|
||||||
|
if strings.HasPrefix(nextLine, "```tf") && firstTerraformBlock {
|
||||||
|
isInsideTerraform = true
|
||||||
|
firstTerraformBlock = false
|
||||||
|
} else if isInsideTerraform {
|
||||||
|
// End of first terraform block
|
||||||
|
break
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if isInsideTerraform {
|
||||||
|
// Check for any source line in the first terraform block
|
||||||
|
if matches := terraformSourceRe.FindStringSubmatch(nextLine); matches != nil {
|
||||||
|
actualSource := matches[1]
|
||||||
|
if actualSource == expectedSource {
|
||||||
|
foundCorrectSource = true
|
||||||
|
break
|
||||||
|
} else if strings.HasPrefix(actualSource, "registry.coder.com/") && strings.Contains(actualSource, "/"+moduleName+"/coder") {
|
||||||
|
// Found source for this module but with wrong namespace/format
|
||||||
|
errs = append(errs, xerrors.Errorf("incorrect source URL format: found %q, expected %q", actualSource, expectedSource))
|
||||||
|
return errs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !foundCorrectSource {
|
||||||
|
errs = append(errs, xerrors.Errorf("did not find correct source URL %q in first Terraform code block", expectedSource))
|
||||||
|
}
|
||||||
|
|
||||||
|
return errs
|
||||||
|
}
|
||||||
|
|
||||||
func validateCoderModuleReadmeBody(body string) []error {
|
func validateCoderModuleReadmeBody(body string) []error {
|
||||||
var errs []error
|
var errs []error
|
||||||
|
|
||||||
@ -94,6 +170,9 @@ func validateCoderModuleReadme(rm coderResourceReadme) []error {
|
|||||||
for _, err := range validateCoderModuleReadmeBody(rm.body) {
|
for _, err := range validateCoderModuleReadmeBody(rm.body) {
|
||||||
errs = append(errs, addFilePathToError(rm.filePath, err))
|
errs = append(errs, addFilePathToError(rm.filePath, err))
|
||||||
}
|
}
|
||||||
|
for _, err := range validateModuleSourceURL(rm.body, rm.filePath) {
|
||||||
|
errs = append(errs, addFilePathToError(rm.filePath, err))
|
||||||
|
}
|
||||||
for _, err := range validateResourceGfmAlerts(rm.body) {
|
for _, err := range validateResourceGfmAlerts(rm.body) {
|
||||||
errs = append(errs, addFilePathToError(rm.filePath, err))
|
errs = append(errs, addFilePathToError(rm.filePath, err))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,3 +20,86 @@ func TestValidateCoderResourceReadmeBody(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestValidateModuleSourceURL(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
t.Run("Valid source URL format", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
body := "# Test Module\n\n```tf\nmodule \"test-module\" {\n source = \"registry.coder.com/test-namespace/test-module/coder\"\n version = \"1.0.0\"\n agent_id = coder_agent.example.id\n}\n```\n"
|
||||||
|
filePath := "registry/test-namespace/modules/test-module/README.md"
|
||||||
|
errs := validateModuleSourceURL(body, filePath)
|
||||||
|
if len(errs) != 0 {
|
||||||
|
t.Errorf("Expected no errors, got: %v", errs)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Invalid source URL format - wrong namespace", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
body := "# Test Module\n\n```tf\nmodule \"test-module\" {\n source = \"registry.coder.com/wrong-namespace/test-module/coder\"\n version = \"1.0.0\"\n agent_id = coder_agent.example.id\n}\n```\n"
|
||||||
|
filePath := "registry/test-namespace/modules/test-module/README.md"
|
||||||
|
errs := validateModuleSourceURL(body, filePath)
|
||||||
|
if len(errs) != 1 {
|
||||||
|
t.Errorf("Expected 1 error, got %d: %v", len(errs), errs)
|
||||||
|
}
|
||||||
|
if len(errs) > 0 && !contains(errs[0].Error(), "incorrect source URL format") {
|
||||||
|
t.Errorf("Expected source URL format error, got: %s", errs[0].Error())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Missing source URL", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
body := "# Test Module\n\n```tf\nmodule \"other-module\" {\n source = \"registry.coder.com/other/other-module/coder\"\n version = \"1.0.0\"\n agent_id = coder_agent.example.id\n}\n```\n"
|
||||||
|
filePath := "registry/test-namespace/modules/test-module/README.md"
|
||||||
|
errs := validateModuleSourceURL(body, filePath)
|
||||||
|
if len(errs) != 1 {
|
||||||
|
t.Errorf("Expected 1 error, got %d: %v", len(errs), errs)
|
||||||
|
}
|
||||||
|
if len(errs) > 0 && !contains(errs[0].Error(), "did not find correct source URL") {
|
||||||
|
t.Errorf("Expected missing source URL error, got: %s", errs[0].Error())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Module name with hyphens vs underscores", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
body := "# Test Module\n\n```tf\nmodule \"test_module\" {\n source = \"registry.coder.com/test-namespace/test-module/coder\"\n version = \"1.0.0\"\n agent_id = coder_agent.example.id\n}\n```\n"
|
||||||
|
filePath := "registry/test-namespace/modules/test-module/README.md"
|
||||||
|
errs := validateModuleSourceURL(body, filePath)
|
||||||
|
if len(errs) != 0 {
|
||||||
|
t.Errorf("Expected no errors for hyphen/underscore variation, got: %v", errs)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Invalid file path format", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
body := "# Test Module"
|
||||||
|
filePath := "invalid/path/format"
|
||||||
|
errs := validateModuleSourceURL(body, filePath)
|
||||||
|
if len(errs) != 1 {
|
||||||
|
t.Errorf("Expected 1 error, got %d: %v", len(errs), errs)
|
||||||
|
}
|
||||||
|
if len(errs) > 0 && !contains(errs[0].Error(), "invalid module path format") {
|
||||||
|
t.Errorf("Expected path format error, got: %s", errs[0].Error())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func contains(s, substr string) bool {
|
||||||
|
return len(s) >= len(substr) && (s == substr || (len(s) > len(substr) &&
|
||||||
|
(s[:len(substr)] == substr || s[len(s)-len(substr):] == substr ||
|
||||||
|
indexOfSubstring(s, substr) >= 0)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func indexOfSubstring(s, substr string) int {
|
||||||
|
for i := 0; i <= len(s)-len(substr); i++ {
|
||||||
|
if s[i:i+len(substr)] == substr {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|||||||
@ -5,7 +5,8 @@
|
|||||||
"fmt:ci": "bun x prettier --check . && terraform fmt -check -recursive -diff",
|
"fmt:ci": "bun x prettier --check . && terraform fmt -check -recursive -diff",
|
||||||
"terraform-validate": "./scripts/terraform_validate.sh",
|
"terraform-validate": "./scripts/terraform_validate.sh",
|
||||||
"test": "./scripts/terraform_test_all.sh",
|
"test": "./scripts/terraform_test_all.sh",
|
||||||
"update-version": "./update-version.sh"
|
"update-version": "./update-version.sh",
|
||||||
|
"validate-readme": "go build ./cmd/readmevalidation && ./readmevalidation"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "^1.2.21",
|
"@types/bun": "^1.2.21",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user