Page MenuHomeDevCentral

D4145.diff
No OneTemporary

D4145.diff

diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,25 @@
+{
+ "yaml.schemas": {
+ "./_resources/schemas/pillar/certificates.schema.json": [
+ "pillar/core/certificates/*.sls"
+ ],
+ "./_resources/schemas/pillar/dbserver.schema.json": [
+ "pillar/dbserver/*.sls"
+ ],
+ "./_resources/schemas/pillar/dns.schema.json": [
+ "pillar/dns/*/*.sls"
+ ],
+ "./_resources/schemas/pillar/nextcloud.schema.json": [
+ "pillar/saas/nextcloud/*/*.sls"
+ ],
+ "./_resources/schemas/pillar/paas-alkane.schema.json": [
+ "pillar/paas/alkane/*/*.sls"
+ ],
+ "./_resources/schemas/pillar/paas-docker.schema.json": [
+ "pillar/paas/docker/*/*.sls"
+ ],
+ "./_resources/schemas/pillar/webserver.schema.json": [
+ "pillar/webserver/*/*.sls"
+ ]
+ }
+}
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -127,6 +127,8 @@
Resources used by automated tasks to generate repository files are stored
in `_resources/`, for example, templates for new services.
+It also contains JSON schemas to validate key pillar data.
+
### G. Utilities
Scripts and other utilities to be run by Nasqueron Operations SIG members
diff --git a/_resources/schemas/pillar/certificates.schema.json b/_resources/schemas/pillar/certificates.schema.json
new file mode 100644
--- /dev/null
+++ b/_resources/schemas/pillar/certificates.schema.json
@@ -0,0 +1,43 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://operations.nasqueron.org/schemas/pillar/certificates.schema.json",
+ "title": "Nasqueron certificates pillar",
+ "type": "object",
+ "required": ["certificates"],
+ "additionalProperties": false,
+ "properties": {
+ "certificates": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "minLength": 1
+ },
+ "uniqueItems": true
+ },
+ "certificates_default_options": {
+ "$ref": "#/$defs/certificateOptions"
+ },
+ "certificates_options": {
+ "type": "object",
+ "additionalProperties": {
+ "$ref": "#/$defs/certificateOptions"
+ }
+ }
+ },
+ "$defs": {
+ "certificateOptions": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "reload": {
+ "type": "string",
+ "minLength": 1
+ },
+ "shared_group": {
+ "type": "string",
+ "minLength": 1
+ }
+ }
+ }
+ }
+}
diff --git a/_resources/schemas/pillar/dbserver.schema.json b/_resources/schemas/pillar/dbserver.schema.json
new file mode 100644
--- /dev/null
+++ b/_resources/schemas/pillar/dbserver.schema.json
@@ -0,0 +1,306 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://operations.nasqueron.org/schemas/pillar/dbserver.schema.json",
+ "title": "Nasqueron dbserver pillar",
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "dbserver_mysql": {
+ "$ref": "#/$defs/mysql"
+ },
+ "dbserver_mysql_aliases": {
+ "type": "object"
+ },
+ "dbserver_mysql_privileges_templates": {
+ "type": "object"
+ },
+ "dbserver_postgresql": {
+ "$ref": "#/$defs/postgresql"
+ },
+ "x-mariadb-encodings": {
+ "type": "object"
+ }
+ },
+ "anyOf": [
+ {
+ "required": ["dbserver_mysql"]
+ },
+ {
+ "required": ["dbserver_postgresql"]
+ }
+ ],
+ "$defs": {
+ "mysql": {
+ "type": "object",
+ "required": ["server"],
+ "additionalProperties": false,
+ "properties": {
+ "databases": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "collation": {
+ "type": "string",
+ "minLength": 1
+ },
+ "encoding": {
+ "type": "string",
+ "minLength": 1
+ }
+ }
+ }
+ },
+ "server": {
+ "type": "object",
+ "required": ["salt"],
+ "additionalProperties": false,
+ "properties": {
+ "salt": {
+ "type": "object",
+ "required": ["credentials"],
+ "additionalProperties": false,
+ "properties": {
+ "credentials": {
+ "type": "string",
+ "minLength": 1
+ }
+ }
+ }
+ }
+ },
+ "users": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "object",
+ "required": ["host", "password"],
+ "additionalProperties": false,
+ "properties": {
+ "host": {
+ "type": ["string", "integer"],
+ "minLength": 1
+ },
+ "password": {
+ "type": "string",
+ "minLength": 1
+ },
+ "privileges": {
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/mysqlPrivilege"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "mysqlPrivilege": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "database": {
+ "type": "string",
+ "minLength": 1
+ },
+ "privileges": {
+ "type": "string",
+ "minLength": 1
+ },
+ "scope": {
+ "enum": ["database", "table"]
+ },
+ "tables": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "minLength": 1
+ }
+ }
+ },
+ "oneOf": [
+ {
+ "required": ["database", "scope"],
+ "properties": {
+ "scope": {
+ "const": "database"
+ }
+ }
+ },
+ {
+ "required": ["database", "privileges", "scope", "tables"],
+ "properties": {
+ "scope": {
+ "const": "table"
+ }
+ }
+ }
+ ]
+ },
+ "postgresql": {
+ "type": "object",
+ "required": ["connections", "databases", "server", "users"],
+ "additionalProperties": false,
+ "properties": {
+ "connections": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": ["db", "ips", "user"],
+ "additionalProperties": false,
+ "properties": {
+ "db": {
+ "type": "string",
+ "minLength": 1
+ },
+ "ips": {
+ "type": "string",
+ "pattern": "^.+/.+$"
+ },
+ "method": {
+ "type": "string",
+ "minLength": 1
+ },
+ "user": {
+ "type": "string",
+ "minLength": 1
+ }
+ }
+ }
+ },
+ "databases": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "object",
+ "required": ["owner"],
+ "additionalProperties": false,
+ "properties": {
+ "collation": {
+ "type": "string",
+ "minLength": 1
+ },
+ "ctype": {
+ "type": "string",
+ "minLength": 1
+ },
+ "encoding": {
+ "type": "string",
+ "minLength": 1
+ },
+ "extensions": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "minLength": 1
+ }
+ },
+ "owner": {
+ "type": "string",
+ "minLength": 1
+ }
+ }
+ }
+ },
+ "server": {
+ "type": "object",
+ "required": ["cluster", "listen_addresses", "version"],
+ "additionalProperties": false,
+ "properties": {
+ "cluster": {
+ "type": ["string", "integer"],
+ "minLength": 1
+ },
+ "listen_addresses": {
+ "type": "string",
+ "minLength": 1
+ },
+ "version": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "with_contrib": {
+ "type": "boolean"
+ }
+ }
+ },
+ "users": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "object",
+ "required": ["password"],
+ "additionalProperties": false,
+ "properties": {
+ "password": {
+ "type": "string",
+ "minLength": 1
+ },
+ "privileges": {
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/postgresqlPrivilege"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "postgresqlPrivilege": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "database": {
+ "type": "string",
+ "minLength": 1
+ },
+ "privileges": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "minLength": 1
+ }
+ },
+ "schema": {
+ "type": "string",
+ "minLength": 1
+ },
+ "schemas": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "minLength": 1
+ }
+ },
+ "scope": {
+ "enum": ["schema", "table"]
+ },
+ "tables": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "minLength": 1
+ }
+ }
+ },
+ "oneOf": [
+ {
+ "required": ["database", "privileges", "scope"],
+ "properties": {
+ "scope": {
+ "const": "schema"
+ }
+ }
+ },
+ {
+ "required": ["database", "privileges", "schema", "scope", "tables"],
+ "properties": {
+ "scope": {
+ "const": "table"
+ }
+ }
+ }
+ ]
+ }
+ }
+}
diff --git a/_resources/schemas/pillar/dns.schema.json b/_resources/schemas/pillar/dns.schema.json
new file mode 100644
--- /dev/null
+++ b/_resources/schemas/pillar/dns.schema.json
@@ -0,0 +1,34 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://operations.nasqueron.org/schemas/pillar/dns.schema.json",
+ "title": "Nasqueron DNS pillar",
+ "type": "object",
+ "required": ["dns_identity", "dns_zones"],
+ "additionalProperties": false,
+ "properties": {
+ "dns_identity": {
+ "type": "string",
+ "minLength": 1
+ },
+ "dns_zones": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "minLength": 1
+ },
+ "uniqueItems": true
+ },
+ "dns_zone_variables": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "object",
+ "additionalProperties": {
+ "type": ["string", "integer", "number", "boolean", "null"]
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/_resources/schemas/pillar/nextcloud.schema.json b/_resources/schemas/pillar/nextcloud.schema.json
new file mode 100644
--- /dev/null
+++ b/_resources/schemas/pillar/nextcloud.schema.json
@@ -0,0 +1,21 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://operations.nasqueron.org/schemas/pillar/nextcloud.schema.json",
+ "title": "Nasqueron Nextcloud pillar",
+ "type": "object",
+ "required": ["nextcloud"],
+ "additionalProperties": false,
+ "properties": {
+ "nextcloud": {
+ "type": "object",
+ "required": ["user"],
+ "additionalProperties": false,
+ "properties": {
+ "user": {
+ "type": "string",
+ "minLength": 1
+ }
+ }
+ }
+ }
+}
diff --git a/_resources/schemas/pillar/paas-alkane.schema.json b/_resources/schemas/pillar/paas-alkane.schema.json
new file mode 100644
--- /dev/null
+++ b/_resources/schemas/pillar/paas-alkane.schema.json
@@ -0,0 +1,220 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://operations.nasqueron.org/schemas/pillar/paas-alkane.schema.json",
+ "title": "Nasqueron PaaS Alkane pillar",
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "alkane_recipes": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "string",
+ "minLength": 1
+ }
+ }
+ },
+ "nginx_vhosts": {
+ "$ref": "#/$defs/stringListByName"
+ },
+ "php_fpm_instances": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "object",
+ "required": ["command"],
+ "additionalProperties": false,
+ "properties": {
+ "command": {
+ "type": "string",
+ "minLength": 1
+ }
+ }
+ }
+ },
+ "web_aliases": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/scalar"
+ }
+ }
+ },
+ "web_domains": {
+ "$ref": "#/$defs/stringListByName"
+ },
+ "web_php_sites": {
+ "type": "object",
+ "additionalProperties": {
+ "$ref": "#/$defs/phpSite"
+ }
+ },
+ "web_static_sites": {
+ "$ref": "#/$defs/stringListByName"
+ },
+ "webserver_content_dotenv": {
+ "type": "object",
+ "additionalProperties": {
+ "$ref": "#/$defs/dotenvFile"
+ }
+ },
+ "wwwroot_identities": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "object",
+ "required": ["secret", "path"],
+ "additionalProperties": false,
+ "properties": {
+ "secret": {
+ "type": "string",
+ "minLength": 1
+ },
+ "path": {
+ "type": "string",
+ "minLength": 1
+ }
+ }
+ }
+ }
+ },
+ "$defs": {
+ "scalar": {
+ "type": ["string", "integer", "number", "boolean", "null"]
+ },
+ "stringListByName": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "minLength": 1
+ },
+ "uniqueItems": true
+ }
+ },
+ "phpSite": {
+ "type": "object",
+ "required": ["domain", "subdomain", "user", "php-fpm"],
+ "dependentRequired": {
+ "target": ["source"]
+ },
+ "additionalProperties": false,
+ "properties": {
+ "capabilities": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "minLength": 1
+ }
+ },
+ "display_errors": {
+ "type": "string",
+ "minLength": 1
+ },
+ "domain": {
+ "type": "string",
+ "minLength": 1
+ },
+ "env": {
+ "type": "object",
+ "additionalProperties": {
+ "$ref": "#/$defs/scalar"
+ }
+ },
+ "php_flags": {
+ "type": "object",
+ "additionalProperties": {
+ "$ref": "#/$defs/scalar"
+ }
+ },
+ "php_values": {
+ "type": "object",
+ "additionalProperties": {
+ "$ref": "#/$defs/scalar"
+ }
+ },
+ "php-fpm": {
+ "type": "string",
+ "minLength": 1
+ },
+ "skipCreateUser": {
+ "type": "boolean"
+ },
+ "slow_delay": {
+ "type": "string",
+ "minLength": 1
+ },
+ "source": {
+ "type": "string",
+ "minLength": 1
+ },
+ "subdomain": {
+ "type": "string",
+ "minLength": 1
+ },
+ "target": {
+ "type": "string",
+ "minLength": 1
+ },
+ "uid": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "user": {
+ "type": "string",
+ "minLength": 1
+ }
+ }
+ },
+ "dotenvFile": {
+ "type": "object",
+ "required": ["user"],
+ "additionalProperties": false,
+ "properties": {
+ "databases": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": ["service", "credentials"],
+ "additionalProperties": false,
+ "properties": {
+ "credentials": {
+ "type": "string",
+ "minLength": 1
+ },
+ "prefix": {
+ "type": "string"
+ },
+ "service": {
+ "type": "string",
+ "minLength": 1
+ }
+ }
+ }
+ },
+ "extra_credentials": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "string",
+ "minLength": 1
+ }
+ },
+ "extra_values": {
+ "type": "object",
+ "additionalProperties": {
+ "$ref": "#/$defs/scalar"
+ }
+ },
+ "user": {
+ "type": "string",
+ "minLength": 1
+ },
+ "vault": {
+ "type": "string",
+ "minLength": 1
+ }
+ }
+ }
+ }
+}
diff --git a/_resources/schemas/pillar/paas-docker.schema.json b/_resources/schemas/pillar/paas-docker.schema.json
new file mode 100644
--- /dev/null
+++ b/_resources/schemas/pillar/paas-docker.schema.json
@@ -0,0 +1,149 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://operations.nasqueron.org/schemas/pillar/paas-docker.schema.json",
+ "title": "Nasqueron PaaS Docker pillar",
+ "type": "object",
+ "additionalProperties": true,
+ "properties": {
+ "docker_aliases": {
+ "type": "array"
+ },
+ "docker_daemon": {
+ "type": "object",
+ "additionalProperties": {
+ "$ref": "#/$defs/scalar"
+ }
+ },
+ "docker_images": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "minLength": 1
+ },
+ "uniqueItems": true
+ },
+ "docker_networks": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "object",
+ "required": ["subnet"],
+ "additionalProperties": {
+ "$ref": "#/$defs/scalar"
+ },
+ "properties": {
+ "subnet": {
+ "type": "string",
+ "pattern": "^.+/.+$"
+ }
+ }
+ }
+ },
+ "docker_containers": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "object",
+ "additionalProperties": {
+ "$ref": "#/$defs/container"
+ }
+ }
+ },
+ "docker_containers_monitoring": {
+ "type": "object",
+ "additionalProperties": {
+ "$ref": "#/$defs/monitoringNode"
+ }
+ }
+ },
+ "$defs": {
+ "scalar": {
+ "type": ["string", "integer", "number", "boolean", "null"]
+ },
+ "scalarOrList": {
+ "type": ["string", "integer", "number", "boolean", "null", "array"]
+ },
+ "monitoringNode": {
+ "type": "object",
+ "additionalProperties": {
+ "anyOf": [
+ {
+ "$ref": "#/$defs/scalarOrList"
+ },
+ {
+ "$ref": "#/$defs/monitoringNode"
+ }
+ ]
+ }
+ },
+ "container": {
+ "type": "object",
+ "additionalProperties": true,
+ "properties": {
+ "app_port": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 65535
+ },
+ "command_port": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 65535
+ },
+ "host": {
+ "type": "string",
+ "minLength": 1
+ },
+ "aliases": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "minLength": 1
+ }
+ },
+ "allowed_ips": {
+ "type": "array",
+ "items": {
+ "type": ["string", "integer"]
+ }
+ },
+ "credentials": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "string",
+ "minLength": 1
+ }
+ },
+ "credential": {
+ "type": "string",
+ "minLength": 1
+ },
+ "ip": {
+ "type": "string",
+ "minLength": 1
+ },
+ "network": {
+ "type": "string",
+ "minLength": 1
+ },
+ "ports": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 65535
+ }
+ },
+ "realm": {
+ "type": "string",
+ "minLength": 1
+ },
+ "skip_container": {
+ "type": "boolean"
+ },
+ "version": {
+ "type": ["string", "integer", "number"],
+ "minLength": 1
+ }
+ }
+ }
+ }
+}
diff --git a/_resources/schemas/pillar/webserver.schema.json b/_resources/schemas/pillar/webserver.schema.json
new file mode 100644
--- /dev/null
+++ b/_resources/schemas/pillar/webserver.schema.json
@@ -0,0 +1,18 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://operations.nasqueron.org/schemas/pillar/webserver.schema.json",
+ "title": "Nasqueron webserver content pillar",
+ "type": "object",
+ "required": ["web_content_sls"],
+ "additionalProperties": false,
+ "properties": {
+ "web_content_sls": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "minLength": 1
+ },
+ "uniqueItems": true
+ }
+ }
+}
diff --git a/_tests/pillar/core/test_certificates_schema.py b/_tests/pillar/core/test_certificates_schema.py
new file mode 100755
--- /dev/null
+++ b/_tests/pillar/core/test_certificates_schema.py
@@ -0,0 +1,24 @@
+#!/usr/bin/env python3
+
+import unittest
+from unittest_data_provider import data_provider
+
+from pillar.schema_helpers import PILLAR_ROOT, assert_matches_schema, find_sls_files
+
+PILLAR_PATH = PILLAR_ROOT / "core" / "certificates"
+SCHEMA = "certificates.schema.json"
+
+
+class Testinstance(unittest.TestCase):
+ @staticmethod
+ def provide_pillar_files():
+ for pillar_file_path in find_sls_files(PILLAR_PATH):
+ yield (pillar_file_path,)
+
+ @data_provider(provide_pillar_files)
+ def test_pillar_matches_schema(self, pillar_file_path):
+ assert_matches_schema(self, pillar_file_path, SCHEMA)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/_tests/pillar/dbserver/test_postgresql.py b/_tests/pillar/dbserver/test_postgresql.py
--- a/_tests/pillar/dbserver/test_postgresql.py
+++ b/_tests/pillar/dbserver/test_postgresql.py
@@ -13,6 +13,7 @@
from unittest_data_provider import data_provider
from helpers import load_pillars
+from pillar.schema_helpers import PILLAR_ROOT, assert_matches_schema, find_sls_files
# -------------------------------------------------------------
# Connection keys
@@ -38,9 +39,16 @@
pillars = load_pillars("../pillar/dbserver/")
+PILLAR_PATH = PILLAR_ROOT / "dbserver"
+SCHEMA = "dbserver.schema.json"
class Testinstance(unittest.TestCase):
+ @staticmethod
+ def provide_pillar_files():
+ for pillar_file_path in find_sls_files(PILLAR_PATH):
+ yield (pillar_file_path,)
+
@staticmethod
def provide_pillars():
for file_path, pillar in pillars.items():
@@ -69,3 +77,7 @@
connection["ips"],
f"Connection IP range should be in CIDR notation in {pillar_file_path}.",
)
+
+ @data_provider(provide_pillar_files)
+ def test_pillar_matches_schema(self, pillar_file_path):
+ assert_matches_schema(self, pillar_file_path, SCHEMA)
diff --git a/_tests/pillar/dns/__init__.py b/_tests/pillar/dns/__init__.py
new file mode 100644
--- /dev/null
+++ b/_tests/pillar/dns/__init__.py
@@ -0,0 +1 @@
+"""DNS pillar tests."""
diff --git a/_tests/pillar/dns/test_schema.py b/_tests/pillar/dns/test_schema.py
new file mode 100755
--- /dev/null
+++ b/_tests/pillar/dns/test_schema.py
@@ -0,0 +1,24 @@
+#!/usr/bin/env python3
+
+import unittest
+from unittest_data_provider import data_provider
+
+from pillar.schema_helpers import PILLAR_ROOT, assert_matches_schema, find_sls_files
+
+PILLAR_PATH = PILLAR_ROOT / "dns"
+SCHEMA = "dns.schema.json"
+
+
+class Testinstance(unittest.TestCase):
+ @staticmethod
+ def provide_pillar_files():
+ for pillar_file_path in find_sls_files(PILLAR_PATH):
+ yield (pillar_file_path,)
+
+ @data_provider(provide_pillar_files)
+ def test_pillar_matches_schema(self, pillar_file_path):
+ assert_matches_schema(self, pillar_file_path, SCHEMA)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/_tests/pillar/paas/test_alkane_schema.py b/_tests/pillar/paas/test_alkane_schema.py
new file mode 100755
--- /dev/null
+++ b/_tests/pillar/paas/test_alkane_schema.py
@@ -0,0 +1,24 @@
+#!/usr/bin/env python3
+
+import unittest
+from unittest_data_provider import data_provider
+
+from pillar.schema_helpers import PILLAR_ROOT, assert_matches_schema, find_sls_files
+
+PILLAR_PATH = PILLAR_ROOT / "paas" / "alkane"
+SCHEMA = "paas-alkane.schema.json"
+
+
+class Testinstance(unittest.TestCase):
+ @staticmethod
+ def provide_pillar_files():
+ for pillar_file_path in find_sls_files(PILLAR_PATH):
+ yield (pillar_file_path,)
+
+ @data_provider(provide_pillar_files)
+ def test_pillar_matches_schema(self, pillar_file_path):
+ assert_matches_schema(self, pillar_file_path, SCHEMA)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/_tests/pillar/paas/test_docker.py b/_tests/pillar/paas/test_docker.py
--- a/_tests/pillar/paas/test_docker.py
+++ b/_tests/pillar/paas/test_docker.py
@@ -5,7 +5,10 @@
import unittest
import yaml
+from pillar.schema_helpers import assert_matches_schema
+
PILLAR_PATH = "../pillar/paas/docker/"
+SCHEMA = "paas-docker.schema.json"
class Testinstance(unittest.TestCase):
@@ -28,6 +31,10 @@
entry = ":".join(["docker_containers", pillar_file, service, instance])
self.assertIn("app_port", container, entry + ": app_port missing")
+ @data_provider(pillar_files)
+ def test_pillar_matches_schema(self, pillar_file):
+ assert_matches_schema(self, pillar_file, SCHEMA)
+
def find_sls_files(path):
sls_files = []
diff --git a/_tests/pillar/saas/__init__.py b/_tests/pillar/saas/__init__.py
new file mode 100644
--- /dev/null
+++ b/_tests/pillar/saas/__init__.py
@@ -0,0 +1 @@
+"""SaaS pillar tests."""
diff --git a/_tests/pillar/saas/test_nextcloud_schema.py b/_tests/pillar/saas/test_nextcloud_schema.py
new file mode 100755
--- /dev/null
+++ b/_tests/pillar/saas/test_nextcloud_schema.py
@@ -0,0 +1,24 @@
+#!/usr/bin/env python3
+
+import unittest
+from unittest_data_provider import data_provider
+
+from pillar.schema_helpers import PILLAR_ROOT, assert_matches_schema, find_sls_files
+
+PILLAR_PATH = PILLAR_ROOT / "saas" / "nextcloud"
+SCHEMA = "nextcloud.schema.json"
+
+
+class Testinstance(unittest.TestCase):
+ @staticmethod
+ def provide_pillar_files():
+ for pillar_file_path in find_sls_files(PILLAR_PATH):
+ yield (pillar_file_path,)
+
+ @data_provider(provide_pillar_files)
+ def test_pillar_matches_schema(self, pillar_file_path):
+ assert_matches_schema(self, pillar_file_path, SCHEMA)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/_tests/pillar/schema_helpers.py b/_tests/pillar/schema_helpers.py
new file mode 100644
--- /dev/null
+++ b/_tests/pillar/schema_helpers.py
@@ -0,0 +1,137 @@
+# -------------------------------------------------------------
+# Helper utilities for tests suite
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Project: Nasqueron
+# Description: Helper methods to help to JSON Schema
+# License: BSD-2-Clause
+# -------------------------------------------------------------
+
+import re
+from pathlib import Path
+from typing import Any
+
+from jsonschema import Draft202012Validator
+from jsonschema.exceptions import SchemaError, ValidationError
+import yaml
+
+REPOSITORY_ROOT = Path(__file__).resolve().parents[2]
+PILLAR_ROOT = REPOSITORY_ROOT / "pillar"
+SCHEMAS_ROOT = REPOSITORY_ROOT / "_resources" / "schemas" / "pillar"
+
+JSONPATH_IDENTIFIER = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
+SCHEMA_SUFFIXES = (".schema.json", ".json", ".yaml", ".yml")
+
+
+def load_yaml_file(file_path: Path) -> Any:
+ with file_path.open(encoding="utf-8") as fd:
+ data = yaml.load(fd, Loader=yaml.SafeLoader)
+
+ return {} if data is None else data
+
+
+def resolve_schema_path(schema_name: str) -> Path:
+ schema_path = SCHEMAS_ROOT / schema_name
+
+ if schema_path.suffix:
+ if not schema_path.is_file():
+ raise FileNotFoundError(
+ f"Schema {schema_name!r} not found at "
+ f"{schema_path.relative_to(REPOSITORY_ROOT)}"
+ )
+ return schema_path
+
+ candidates = []
+ for suffix in SCHEMA_SUFFIXES:
+ candidate = schema_path.with_name(schema_path.name + suffix)
+ candidates.append(candidate)
+
+ if candidate.is_file():
+ return candidate
+
+ tried_paths = ", ".join(
+ str(candidate.relative_to(REPOSITORY_ROOT)) for candidate in candidates
+ )
+ raise FileNotFoundError(f"Schema {schema_name!r} not found. Tried: {tried_paths}")
+
+
+def load_schema(schema_name: str) -> Any:
+ return load_yaml_file(resolve_schema_path(schema_name))
+
+
+def find_sls_files(path: Path) -> list[Path]:
+ return sorted(p for p in path.rglob("*.sls") if p.is_file())
+
+
+def format_json_path(error: ValidationError) -> str:
+ if not error.absolute_path:
+ return "$"
+
+ path = "$"
+
+ for segment in error.absolute_path:
+ if isinstance(segment, int):
+ path += f"[{segment}]"
+ elif JSONPATH_IDENTIFIER.match(str(segment)):
+ path += f".{segment}"
+ else:
+ escaped_segment = str(segment).replace("\\", "\\\\").replace("'", "\\'")
+ path += f"['{escaped_segment}']"
+
+ return path
+
+
+def sort_path_segments(error: ValidationError) -> list[tuple[int, Any]]:
+ """
+ Build a type-aware sort key for a validation error path.
+
+ Integers are sorted numerically, strings lexically, and the leading type
+ rank avoids direct int/str comparisons.
+ """
+ return [
+ (0, segment) if isinstance(segment, int) else (1, str(segment))
+ for segment in error.absolute_path
+ ]
+
+
+def assert_matches_schema(
+ test_case: Any, pillar_file_path: Path, schema_name: str
+) -> None:
+ pillar_file_path = Path(pillar_file_path).resolve()
+ schema_path = resolve_schema_path(schema_name)
+ schema = load_yaml_file(schema_path)
+
+ relative_schema = schema_path.relative_to(REPOSITORY_ROOT)
+
+ try:
+ Draft202012Validator.check_schema(schema)
+ except SchemaError as error:
+ test_case.fail(
+ f"{relative_schema} is not a valid JSON Schema: " f"{error.message}"
+ )
+
+ validator = Draft202012Validator(schema)
+ pillar = load_yaml_file(pillar_file_path)
+
+ errors = sorted(
+ validator.iter_errors(pillar),
+ key=sort_path_segments,
+ )
+
+ if not errors:
+ return
+
+ relative_pillar = pillar_file_path.relative_to(REPOSITORY_ROOT)
+
+ formatted_errors = [
+ f"{relative_pillar}: {format_json_path(error)}: {error.message}"
+ for error in errors
+ ]
+
+ test_case.fail(
+ "\n".join(
+ [
+ f"{relative_pillar} doesn't match {relative_schema}:",
+ *formatted_errors,
+ ]
+ )
+ )
diff --git a/_tests/pillar/webserver/__init__.py b/_tests/pillar/webserver/__init__.py
new file mode 100644
--- /dev/null
+++ b/_tests/pillar/webserver/__init__.py
@@ -0,0 +1 @@
+"""Webserver pillar tests."""
diff --git a/_tests/pillar/webserver/test_schema.py b/_tests/pillar/webserver/test_schema.py
new file mode 100755
--- /dev/null
+++ b/_tests/pillar/webserver/test_schema.py
@@ -0,0 +1,27 @@
+#!/usr/bin/env python3
+
+import unittest
+from unittest_data_provider import data_provider
+
+from pillar.schema_helpers import PILLAR_ROOT, assert_matches_schema, find_sls_files
+
+PILLAR_PATH = PILLAR_ROOT / "webserver"
+SCHEMA = "webserver.schema.json"
+
+
+class Testinstance(unittest.TestCase):
+ @staticmethod
+ def provide_pillar_files():
+ for pillar_file_path in find_sls_files(PILLAR_PATH):
+ if pillar_file_path.parent == PILLAR_PATH:
+ continue
+
+ yield (pillar_file_path,)
+
+ @data_provider(provide_pillar_files)
+ def test_pillar_matches_schema(self, pillar_file_path):
+ assert_matches_schema(self, pillar_file_path, SCHEMA)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/requirements.txt b/requirements.txt
--- a/requirements.txt
+++ b/requirements.txt
@@ -9,6 +9,7 @@
# Tests
Jinja2>=3.1.6,<4.0
+jsonschema>=4.23.0,<5.0
mock>=5.1.0,<6.0
pre-commit>=3.7.1,<4.0
pre_commit_hooks>=4.6.0,<5.0

File Metadata

Mime Type
text/plain
Expires
Thu, Aug 6, 07:21 (13 h, 27 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3973717
Default Alt Text
D4145.diff (33 KB)

Event Timeline