diff --git a/.github/workflows/cue.yml b/.github/workflows/cue.yml index 2058218..42b65d3 100644 --- a/.github/workflows/cue.yml +++ b/.github/workflows/cue.yml @@ -51,6 +51,8 @@ jobs: cue_version: "v0.15.4" - name: check that CUE schemas evaluate correctly run: make cue-eval + - name: run the unit tests for CUE schemas + run: make cue-test publish-module: name: "publish module to Central Registry" diff --git a/Makefile b/Makefile index 6d53a47..38d0fb5 100644 --- a/Makefile +++ b/Makefile @@ -37,3 +37,8 @@ cue-gen: done cp -r cue.mod/gen/github.com/perses/spec/go/* cue/ && rm -r cue.mod/gen find cue/ -name "*.cue" -exec sed -i 's/\"github.com\/perses\/spec\/go/\"github.com\/perses\/spec\/cue/g' {} \; + +.PHONY: cue-test +cue-test: + @echo ">> Run the unit tests for CUE schemas" + $(GO) run ./scripts/test-cue/test-cue.go diff --git a/cue-test/datasource/http.cue b/cue-test/datasource/http.cue new file mode 100644 index 0000000..a72864a --- /dev/null +++ b/cue-test/datasource/http.cue @@ -0,0 +1,41 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package datasource + +import ( + "github.com/perses/spec/cue/datasource/proxy/http" +) + +myDirectSpec: #HTTPDatasourceSpec & { + directUrl: "http://localhost:8080" +} + +myProxySpec: #HTTPDatasourceSpec & { + proxy: http.#Proxy & { + kind: "HTTPProxy" + spec: { + url: "https://prometheus.demo.prometheus.io" + allowedEndpoints: [ + { + endpointPattern: "/api/v1/labels" + method: "POST" + }, + { + endpointPattern: "/api/v1/series" + method: "POST" + }, + ] + } + } +} diff --git a/cue-test/datasource/sql.cue b/cue-test/datasource/sql.cue new file mode 100644 index 0000000..04b4701 --- /dev/null +++ b/cue-test/datasource/sql.cue @@ -0,0 +1,29 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package datasource + +import ( + "github.com/perses/spec/cue/datasource/proxy/sql" +) + +mySQLProxySpec: #SQLDatasourceSpec & { + proxy: sql.#Proxy & { + kind: "SQLProxy" + spec: { + driver: "postgres" + host: "localhost:5432" + database: "mydb" + } + } +} diff --git a/cue/datasource/datasource_patch.cue b/cue/datasource/datasource_patch.cue index 6629751..8d4125c 100644 --- a/cue/datasource/datasource_patch.cue +++ b/cue/datasource/datasource_patch.cue @@ -21,4 +21,4 @@ import ( #SQLDatasourceSpec: { proxy: sql.#Proxy } -#HTTPDatasourceSpec: { directUrl: common.#url } | { proxy: http.#Proxy } +#HTTPDatasourceSpec: { directUrl: common.#URL } | { proxy: http.#Proxy } diff --git a/cue/datasource/proxy/http/http_patch.cue b/cue/datasource/proxy/http/http_patch.cue index 5bb4fca..ff35ea9 100644 --- a/cue/datasource/proxy/http/http_patch.cue +++ b/cue/datasource/proxy/http/http_patch.cue @@ -24,7 +24,7 @@ package http import ( "github.com/perses/spec/cue/common" "github.com/perses/spec/cue/datasource/proxy" - ) +) #AllowedEndpoint: { endpointPattern: string @go(EndpointPattern) @@ -47,4 +47,5 @@ import ( #Proxy: proxy.#Proxy & { kind: "HTTPProxy" @go(Kind) + spec: #Config @go(Spec) } diff --git a/cue/datasource/proxy/sql/sql_patch.cue b/cue/datasource/proxy/sql/sql_patch.cue index 29a3ea1..ab5f7e8 100644 --- a/cue/datasource/proxy/sql/sql_patch.cue +++ b/cue/datasource/proxy/sql/sql_patch.cue @@ -57,4 +57,5 @@ import ( #Proxy: proxy.#Proxy & { kind: "SQLProxy" @go(Kind) + spec: #Config @go(Spec) } diff --git a/go.mod b/go.mod index 839850e..3756e18 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/perses/spec go 1.26.0 require ( + github.com/sirupsen/logrus v1.9.4 github.com/stretchr/testify v1.11.1 gopkg.in/yaml.v3 v3.0.1 ) @@ -10,4 +11,5 @@ require ( require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + golang.org/x/sys v0.13.0 // indirect ) diff --git a/go.sum b/go.sum index c4c1710..bc810fc 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,12 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= +github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/scripts/test-cue/test-cue.go b/scripts/test-cue/test-cue.go new file mode 100644 index 0000000..087cc9e --- /dev/null +++ b/scripts/test-cue/test-cue.go @@ -0,0 +1,162 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + + "github.com/sirupsen/logrus" +) + +// This script validates CUE schema packages against their corresponding test packages. +// It merges all .cue files within each package directory to properly handle imports and +// package-level definitions. + +const ( + schemasDir = "cue" + testDir = "cue-test" +) + +// dirsInScope specifies which subdirectories under cue/ to validate +var dirsInScope = []string{"datasource"} + +// NB: this function assume 1 dirInScope = 1 package. CUE allows multiple packages per dirInScope, but this is not used here. +func findPackages(basePath string, dirInScope string) ([]string, error) { + var packages []string + dirPath := filepath.Join(basePath, dirInScope) + + // Include the root directory itself + packages = append(packages, dirInScope) + + err := filepath.Walk(dirPath, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + if info.IsDir() && path != dirPath { + relPath, err := filepath.Rel(basePath, path) + if err != nil { + return err + } + packages = append(packages, relPath) + } + + return nil + }) + + return packages, err +} + +// vetPackage validates CUE files in schemaDir against test files in testDir. +// It collects all .cue files from both directories and runs `cue vet` on them together, +// allowing CUE to merge files in the same package and resolve imports properly. +// The command runs from schemasDir to ensure cue.mod/module.cue is accessible for imports. +func vetPackage(schemaDir, testDir string) error { + logrus.Debugf("Validating package %s against %s", schemaDir, testDir) + + // Get list of all .cue files in both directories + schemaFiles, err := filepath.Glob(filepath.Join(schemaDir, "*.cue")) + if err != nil { + return fmt.Errorf("failed to glob schema files: %w", err) + } + testFiles, err := filepath.Glob(filepath.Join(testDir, "*.cue")) + if err != nil { + return fmt.Errorf("failed to glob test files: %w", err) + } + + // Build command args with paths relative to schemasDir (cue/) + args := []string{"vet"} + for _, f := range schemaFiles { + rel, err := filepath.Rel(schemasDir, f) + if err != nil { + return fmt.Errorf("failed to get relative path for %s: %w", f, err) + } + args = append(args, rel) + } + for _, f := range testFiles { + // testFiles are in ../cue-test relative to schemasDir + rel, err := filepath.Rel(schemasDir, f) + if err != nil { + return fmt.Errorf("failed to get relative path for %s: %w", f, err) + } + args = append(args, rel) + } + + cmd := exec.Command("cue", args...) + cmd.Dir = schemasDir + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + if err := cmd.Run(); err != nil { + return fmt.Errorf("failed to validate %s: %w", schemaDir, err) + } + + return nil +} + +func validateCueSchemas() error { + logrus.Debugf("Starting CUE files validation") + + // Check if cue command is available + if _, err := exec.LookPath("cue"); err != nil { + return fmt.Errorf("cue command not found in PATH: %w", err) + } + + validatedCount := 0 + skippedCount := 0 + errCount := 0 + + for _, dirInScope := range dirsInScope { + logrus.Debugf("Processing directory: %s", dirInScope) + packageDirs, err := findPackages(schemasDir, dirInScope) + if err != nil { + return fmt.Errorf("failed to find directories in %s/%s: %w", schemasDir, dirInScope, err) + } + + for _, packageDir := range packageDirs { + schemaDir := filepath.Join(schemasDir, packageDir) + testDir := filepath.Join(testDir, packageDir) + + // Check if corresponding test directory exists + if _, err := os.Stat(testDir); os.IsNotExist(err) { + logrus.Debugf("Skipping %s: test directory %s not found", schemaDir, testDir) + skippedCount++ + continue + } + + logrus.Infof("Validating package %s with test package %s", schemaDir, testDir) + if err := vetPackage(schemaDir, testDir); err != nil { + logrus.Errorf("Validation failed for %s: %v", schemaDir, err) + errCount++ + } + + validatedCount++ + } + } + if errCount > 0 { + return fmt.Errorf("validation failed for %d file(s)", errCount) + } + + logrus.Infof("CUE files validation completed: %d validated, %d skipped", validatedCount, skippedCount) + return nil +} + +func main() { + if err := validateCueSchemas(); err != nil { + logrus.Fatal(err) + } +}