Page Menu
Home
DevCentral
Search
Configure Global Search
Log In
Files
F41711898
D4145.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
33 KB
Referenced Files
None
Subscribers
None
D4145.diff
View Options
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
Details
Attached
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)
Attached To
Mode
D4145: Validate salt-tower pillar files with JSON Schema
Attached
Detach File
Event Timeline
Log In to Comment