Store the policy JSON columns as TEXT for MySQL 5.6
The branch stored scopejson, desiredstate and params as sa.JSON, the only JSON columns in the product. MySQL 5.6 has no JSON type - it arrived in 5.7 - and the migration failed there with a bare syntax error near the word JSON. They are TEXT now, through a JSONText type decorator, so the seventeen call sites still assign and read dicts. The convention elsewhere is already TEXT: guides.blocksjson, geenforce.manifestjson. Spec regenerated: 331 paths, 477 operations across 22 surfaces.
This commit is contained in:
1609
docs/openapi.json
1609
docs/openapi.json
File diff suppressed because it is too large
Load Diff
@@ -22,6 +22,14 @@ branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
# TEXT rather than sa.JSON on purpose. MySQL 5.6 has no JSON type - it arrived
|
||||
# in 5.7 - and this migration failed there with a bare syntax error near the
|
||||
# word JSON. Every other JSON-bearing column in the product is TEXT for the same
|
||||
# reason (guides.blocksjson, geenforce.manifestjson); the model reads and writes
|
||||
# dicts through the JSONText type decorator, so nothing above the database
|
||||
# changes.
|
||||
|
||||
|
||||
def upgrade():
|
||||
bind = op.get_bind()
|
||||
insp = sa.inspect(bind)
|
||||
@@ -33,7 +41,7 @@ def upgrade():
|
||||
sa.Column('configurationpolicyid', sa.Integer(), nullable=False),
|
||||
sa.Column('name', sa.String(length=150), nullable=False),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.Column('scopejson', sa.JSON(), nullable=True),
|
||||
sa.Column('scopejson', sa.Text(), nullable=True),
|
||||
sa.Column('source', sa.String(length=50), nullable=False,
|
||||
server_default='local'),
|
||||
sa.Column('createddate', sa.DateTime(), nullable=False),
|
||||
@@ -57,8 +65,8 @@ def upgrade():
|
||||
sa.Column('action', sa.String(length=100), nullable=False),
|
||||
sa.Column('mode', sa.String(length=20), nullable=False,
|
||||
server_default='gate'),
|
||||
sa.Column('desiredstate', sa.JSON(), nullable=True),
|
||||
sa.Column('params', sa.JSON(), nullable=True),
|
||||
sa.Column('desiredstate', sa.Text(), nullable=True),
|
||||
sa.Column('params', sa.Text(), nullable=True),
|
||||
sa.Column('intervalminutes', sa.Integer(), nullable=True),
|
||||
sa.Column('sortorder', sa.Integer(), nullable=False,
|
||||
server_default='0'),
|
||||
|
||||
44
plugins/computers/models/jsontext.py
Normal file
44
plugins/computers/models/jsontext.py
Normal file
@@ -0,0 +1,44 @@
|
||||
"""JSON stored in a TEXT column.
|
||||
|
||||
MySQL 5.6 has no JSON type - it arrived in 5.7 - and a site is still running
|
||||
5.6, where `sa.JSON()` fails at migration time with a bare syntax error near the
|
||||
word JSON. Every other JSON-bearing column in this product is already TEXT for
|
||||
that reason: guides' `blocksjson`, GE-Enforce's `manifestjson`.
|
||||
|
||||
This keeps the Python side identical. A model attribute still holds a dict or a
|
||||
list, still assigns and reads like one; only the storage changes. The
|
||||
alternative - json.dumps and json.loads at seventeen call sites - is the same
|
||||
thing written out seventeen times, with seventeen chances to forget one.
|
||||
|
||||
Note the SQLite behaviour is deliberately the same as MySQL's. Tests run on
|
||||
SQLite and production does not, so a type that behaved differently across the
|
||||
two would hide exactly the failure this exists to prevent.
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
from sqlalchemy import Text
|
||||
from sqlalchemy.types import TypeDecorator
|
||||
|
||||
|
||||
class JSONText(TypeDecorator):
|
||||
"""A dict or list on the Python side, TEXT in the database."""
|
||||
|
||||
impl = Text
|
||||
cache_ok = True
|
||||
|
||||
def process_bind_param(self, value, dialect):
|
||||
if value is None:
|
||||
return None
|
||||
return json.dumps(value, separators=(',', ':'))
|
||||
|
||||
def process_result_value(self, value, dialect):
|
||||
if value is None or value == '':
|
||||
return None
|
||||
try:
|
||||
return json.loads(value)
|
||||
except (TypeError, ValueError):
|
||||
# A row written before this column was JSON, or hand-edited. Hand
|
||||
# back the raw text rather than raising: a policy that cannot be
|
||||
# parsed should be visible and fixable, not an exception on read.
|
||||
return value
|
||||
@@ -18,6 +18,7 @@ every table for a clean rollback.
|
||||
"""
|
||||
|
||||
from shopdb.api import db, BaseModel
|
||||
from .jsontext import JSONText
|
||||
|
||||
|
||||
# Provenance of a policy row.
|
||||
@@ -43,7 +44,7 @@ class ConfigurationPolicy(BaseModel):
|
||||
# Boolean target AST: {"type":"and","nodes":[{"type":"pctype","id":3},
|
||||
# {"type":"not","node":{"type":"app","id":12}}]}. Validated server-side
|
||||
# (known node types, depth cap) before it is stored - see policyscope.
|
||||
scopejson = db.Column(db.JSON, nullable=True)
|
||||
scopejson = db.Column(JSONText, nullable=True)
|
||||
|
||||
# GE-Enforce interop seam (see POLICY_SOURCE_LOCAL).
|
||||
source = db.Column(db.String(50), default=POLICY_SOURCE_LOCAL, nullable=False)
|
||||
@@ -101,11 +102,11 @@ class ConfigurationPolicyAction(BaseModel):
|
||||
|
||||
# Desired-state target for mode=desired, as JSON so different actions can
|
||||
# express different targets (e.g. {"running": true}). Null for gate mode.
|
||||
desiredstate = db.Column(db.JSON, nullable=True)
|
||||
desiredstate = db.Column(JSONText, nullable=True)
|
||||
|
||||
# Optional action parameters passed through to the command (JSON), and an
|
||||
# optional reconcile cadence in minutes (null = reconcile on every report).
|
||||
params = db.Column(db.JSON, nullable=True)
|
||||
params = db.Column(JSONText, nullable=True)
|
||||
intervalminutes = db.Column(db.Integer, nullable=True)
|
||||
|
||||
sortorder = db.Column(db.Integer, default=0, nullable=False)
|
||||
|
||||
Reference in New Issue
Block a user