Skip to content

Commit

Permalink
Merge pull request #174 from kislerdm/152-add-validation-for-module-p…
Browse files Browse the repository at this point in the history
…rovider-json

Add validation for generated json metafiles
  • Loading branch information
Yantrio authored Feb 2, 2024
2 parents 047a219 + dd9a483 commit 4b3824b
Show file tree
Hide file tree
Showing 9 changed files with 811 additions and 0 deletions.
45 changes: 45 additions & 0 deletions .github/workflows/validate-json-module.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: Validate Module JSON

on:
push:
paths:
- 'modules/**/*.json'
branches:
- 'module-**'

jobs:
validate-metadata-module:
runs-on: ubuntu-latest
environment:
name: ${{ inputs.environment }}

steps:
- uses: actions/checkout@v4
with:
fetch-depth: 2

- uses: actions/setup-go@v4
with:
go-version-file: './src/go.mod'

- name: Build validator
working-directory: ./src
run: |
mkdir -p /tmp/validate
go build -o /tmp/validate/run ./cmd/validate/main.go
chmod +x /tmp/validate/run
- name: List updated files
id: updated
uses: tj-actions/changed-files@v42
with:
files: modules/**/*.json

- name: Validate JSON
env:
CHANGED_FILES: ${{ steps.updated.outputs.all_changed_files }}
run: |
for path in "$CHANGED_FILES"
do
/tmp/validate/run module "$path"
done
45 changes: 45 additions & 0 deletions .github/workflows/validate-json-provider.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: Validate Provider JSON

on:
push:
paths:
- 'providers/**/*.json'
branches:
- 'provider-**'

jobs:
validate-metadata-provider:
runs-on: ubuntu-latest
environment:
name: ${{ inputs.environment }}

steps:
- uses: actions/checkout@v4
with:
fetch-depth: 2

- uses: actions/setup-go@v4
with:
go-version-file: './src/go.mod'

- name: Build validator
working-directory: ./src
run: |
mkdir -p /tmp/validate
go build -o /tmp/validate/run ./cmd/validate/main.go
chmod +x /tmp/validate/run
- name: List updated files
id: updated
uses: tj-actions/changed-files@v42
with:
files: providers/**/*.json

- name: Validate JSON
env:
CHANGED_FILES: ${{ steps.updated.outputs.all_changed_files }}
run: |
for path in "$CHANGED_FILES"
do
/tmp/validate/run provider "$path"
done
106 changes: 106 additions & 0 deletions src/cmd/validate/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package main

import (
"encoding/json"
"fmt"
"log/slog"
"os"

"github.com/opentofu/registry-stable/internal/module"
"github.com/opentofu/registry-stable/internal/provider"
"github.com/opentofu/registry-stable/internal/validate"
)

func main() {
var logger = slog.New(slog.NewJSONHandler(os.Stdout, nil))

const (
helpStr = `Run cmd/validate <CMD> PATH/TO/definition.json
CMD:
- module: validate module's registry JSON file.
- provider: validate provider's registry JSON file.
`
cliError = "command-line arguments don't match CLI signature"
)

args := os.Args[1:]

if len(args) < 1 {
logger.Error(cliError, slog.String("type", errorCLI))
os.Exit(1)
}

if args[0] == "help" {
fmt.Print(helpStr)
os.Exit(0)
}

if len(args) != 2 {
fmt.Print(helpStr)
logger.Error(cliError, slog.String("type", errorCLI))
os.Exit(1)
}

path := args[1]

var (
err error
errType string = errorJSONParsing
)
switch cmd := args[0]; cmd {
case "module":
var v module.Metadata
err = readJSONFile(path, &v)
if err == nil {
errType = errorValidation
err = module.Validate(v)
}

case "provider":
var v provider.Metadata
err = readJSONFile(path, &v)
if err == nil {
errType = errorValidation
err = provider.Validate(v)
}

default:
fmt.Print(helpStr)
logger.Error(fmt.Sprintf("%s command is not supported", cmd), slog.String("type", errorCLI))
os.Exit(1)
}

if err != nil {
args := []any{
slog.String("type", errType),
slog.String("path", path),
}
switch err.(type) {
case validate.Errors:
for _, e := range err.(validate.Errors) {
logger.Error(e.Error(), args...)
}
default:
logger.Error(err.Error(), args...)
}

os.Exit(1)
}
}

func readJSONFile(p string, v any) error {
f, err := os.Open(p)
if err != nil {
return err
}
defer f.Close()

return json.NewDecoder(f).Decode(v)
}

const (
errorValidation = "validation"
errorJSONParsing = "parsing"
errorCLI = "CLI"
)
27 changes: 27 additions & 0 deletions src/internal/module/validate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package module

import (
"fmt"

"github.com/opentofu/registry-stable/internal/validate"
)

// Validate validates module's metadata.
func Validate(v Metadata) error {
var errs = make([]error, 0)
if len(v.Versions) < 1 {
errs = append(errs, validate.ErrorEmptyList)
}

for _, ver := range v.Versions {
if !validate.IsValidVersion(ver.Version) {
errs = append(errs, fmt.Errorf("found semver-incompatible version: %s", ver.Version))
}
}

if len(errs) == 0 {
return nil
}

return validate.Errors(errs)
}
50 changes: 50 additions & 0 deletions src/internal/module/validate_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package module

import (
"testing"
)

func TestValidate(t *testing.T) {
type TestCase struct {
name string
input Metadata
wantErrStr string
}

tests := []TestCase{
{
name: "valid",
input: Metadata{
Versions: []Version{{"0.0.2"}, {"0.0.1"}},
},
},
{
name: "invalid-version",
input: Metadata{
Versions: []Version{{"0.0.2"}, {"foo"}},
},
wantErrStr: "found semver-incompatible version: foo\n",
},
{
name: "empty-versions-list",
input: Metadata{},
wantErrStr: "found empty list of versions\n",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := Validate(tt.input)
switch tt.wantErrStr != "" {
case true:
if err == nil || tt.wantErrStr != err.Error() {
t.Fatalf("unexpected error message, want = %s, got = %v", tt.wantErrStr, err)
}
default:
if err != nil {
t.Fatalf("unexpected error message: %v", err)
}
}
})
}
}
101 changes: 101 additions & 0 deletions src/internal/provider/validate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package provider

import (
"fmt"
"slices"
"strings"

"github.com/opentofu/registry-stable/internal/validate"
)

// Validate validates provider's metadata.
func Validate(v Metadata) error {
var errs = make([]error, 0)

if len(v.Versions) < 1 {
errs = append(errs, validate.ErrorEmptyList)
}

for _, ver := range v.Versions {
for _, err := range validateProviderVersion(ver) {
errs = append(errs, fmt.Errorf("v%s: %w", ver.Version, err))
}
}

if len(errs) == 0 {
return nil
}

return validate.Errors(errs)
}

func validateProviderVersion(ver Version) []error {
var errs = make([]error, 0)

if !validate.IsValidVersion(ver.Version) {
errs = append(errs, fmt.Errorf("found semver-incompatible version: %s", ver.Version))
}

// validates provider's protocols:
if len(ver.Protocols) == 0 {
errs = append(errs, fmt.Errorf("empty protocols list"))
} else {
for _, protocol := range ver.Protocols {
if !isValidProviderProtocol(protocol) {
errs = append(errs, fmt.Errorf("unsupported protocol found: %s", protocol))
}
}
}

// validates provider's targets:
if len(ver.Targets) == 0 {
errs = append(errs, fmt.Errorf("empty targets list"))
} else {
for _, verTarget := range ver.Targets {
if err := validateProviderVersionTarget(verTarget); err != nil {
errs = append(errs, err...)
}
}
}

return errs
}

// isValidProviderProtocol validates the protocol version.
// It's based on the providers which are currently available in the registry.
func isValidProviderProtocol(s string) bool {
switch s {
case "1.0",
"1.0.0",
"4.0",
"5.0",
"6.0":
return true
default:
return false
}
}

func validateProviderVersionTarget(v Target) []error {
var errs = make([]error, 0)

if !slices.Contains(goos, v.OS) {
errs = append(errs, fmt.Errorf("target %s-%s: unsupported OS: %s", v.OS, v.Arch, v.OS))
}

if !slices.Contains(goarch, v.Arch) {
errs = append(errs, fmt.Errorf("target %s-%s: unsupported ARCH: %s", v.OS, v.Arch, v.Arch))
}

// check if the filename matches the url
if !strings.HasSuffix(v.DownloadURL, v.Filename) {
errs = append(errs, fmt.Errorf("target %s-%s: 'filename' is not consistent with 'download_url'", v.OS, v.Arch))
}

// check if the SHA sum was modified
if len(v.SHASum) != 64 {
errs = append(errs, fmt.Errorf("target %s-%s: SHASum length is wrong", v.OS, v.Arch))
}

return errs
}
Loading

0 comments on commit 4b3824b

Please sign in to comment.