Completed
Pull Request — master (#3092)
by Dmitri
03:42
created

ConfigSchemaAPI   A

Complexity

Total Complexity 1

Size/Duplication

Total Lines 35
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 1
dl 0
loc 35
rs 10
c 0
b 0
f 0
1
# Licensed to the StackStorm, Inc ('StackStorm') under one or more
2
# contributor license agreements.  See the NOTICE file distributed with
3
# this work for additional information regarding copyright ownership.
4
# The ASF licenses this file to You under the Apache License, Version 2.0
5
# (the "License"); you may not use this file except in compliance with
6
# the License.  You may obtain a copy of the License at
7
#
8
#     http://www.apache.org/licenses/LICENSE-2.0
9
#
10
# Unless required by applicable law or agreed to in writing, software
11
# distributed under the License is distributed on an "AS IS" BASIS,
12
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
# See the License for the specific language governing permissions and
14
# limitations under the License.
15
16
import os
17
18
import jsonschema
19
from oslo_config import cfg
20
21
from st2common import log as logging
22
from st2common.util import schema as util_schema
23
from st2common.constants.keyvalue import SYSTEM_SCOPE
24
from st2common.constants.keyvalue import USER_SCOPE
25
from st2common.constants.pack import PACK_REF_WHITELIST_REGEX
26
from st2common.constants.pack import PACK_VERSION_REGEX
27
from st2common.constants.pack import ST2_VERSION_REGEX
28
from st2common.constants.pack import NORMALIZE_PACK_VERSION
29
from st2common.persistence.pack import ConfigSchema
30
from st2common.models.api.base import BaseAPI
31
from st2common.models.db.pack import PackDB
32
from st2common.models.db.pack import ConfigSchemaDB
33
from st2common.models.db.pack import ConfigDB
34
from st2common.exceptions.db import StackStormDBObjectNotFoundError
35
from st2common.util.pack import validate_config_against_schema
36
from st2common.util.pack import normalize_pack_version
37
38
__all__ = [
39
    'PackAPI',
40
    'ConfigSchemaAPI',
41
    'ConfigAPI',
42
43
    'ConfigItemSetAPI',
44
45
    'PackInstallRequestAPI',
46
    'PackRegisterRequestAPI',
47
    'PackSearchRequestAPI',
48
    'PackAsyncAPI'
49
]
50
51
LOG = logging.getLogger(__name__)
52
53
54
class PackAPI(BaseAPI):
55
    model = PackDB
56
    schema = {
57
        'type': 'object',
58
        'description': 'Content pack schema.',
59
        'properties': {
60
            'id': {
61
                'type': 'string',
62
                'description': 'Unique identifier for the pack.',
63
                'default': None
64
            },
65
            'name': {
66
                'type': 'string',
67
                'description': 'Display name of the pack. If the name only contains lowercase'
68
                               'letters, digits and underscores, the "ref" field is not required.',
69
                'required': True
70
            },
71
            'ref': {
72
                'type': 'string',
73
                'description': 'Reference for the pack, used as an internal id.',
74
                'default': None,
75
                'pattern': PACK_REF_WHITELIST_REGEX
76
            },
77
            'uid': {
78
                'type': 'string'
79
            },
80
            'description': {
81
                'type': 'string',
82
                'description': 'Brief description of the pack and the service it integrates with.',
83
                'required': True
84
            },
85
            'keywords': {
86
                'type': 'array',
87
                'description': 'Keywords describing the pack.',
88
                'items': {'type': 'string'},
89
                'default': []
90
            },
91
            'version': {
92
                'type': 'string',
93
                'description': 'Pack version. Must follow the semver format '
94
                               '(for instance, "0.1.0").',
95
                'pattern': PACK_VERSION_REGEX,
96
                'required': True
97
            },
98
            'stackstorm_version': {
99
                'type': 'string',
100
                'description': 'Required StackStorm version. Examples: ">1.6.0", '
101
                               '">=1.8.0, <2.2.0"',
102
                'pattern': ST2_VERSION_REGEX,
103
            },
104
            'author': {
105
                'type': 'string',
106
                'description': 'Pack author or authors.',
107
                'required': True
108
            },
109
            'email': {
110
                'type': 'string',
111
                'description': 'E-mail of the pack author.',
112
                'format': 'email'
113
            },
114
            'contributors': {
115
                'type': 'array',
116
                'items': {
117
                    'type': 'string',
118
                    'maxLength': 100
119
                },
120
                'description': ('A list of people who have contributed to the pack. Format is: '
121
                                'Name <email address> e.g. Tomaz Muraus <[email protected]>.')
122
            },
123
            'files': {
124
                'type': 'array',
125
                'description': 'A list of files inside the pack.',
126
                'items': {'type': 'string'},
127
                'default': []
128
            },
129
            'dependencies': {
130
                'type': 'array',
131
                'description': 'A list of other StackStorm packs this pack depends upon. '
132
                               'The same format as in "st2 pack install" is used: '
133
                               '"<name or full URL>[=<version or git ref>]".',
134
                'items': {'type': 'string'},
135
                'default': []
136
            },
137
            'system': {
138
                'type': 'object',
139
                'description': 'Specification for the system components and packages '
140
                               'required for the pack.',
141
                'default': {}
142
            }
143
        }
144
    }
145
146
    def __init__(self, **values):
147
        name = values.get('name', None)
148
149
        # Note: If some version values are not explicitly surrounded by quotes they are recognized
150
        # as numbers so we cast them to string
151
        if values.get('version', None):
152
            values['version'] = str(values['version'])
153
154
        # Special case for old version which didn't follow semver format (e.g. 0.1, 1.0, etc.)
155
        # In case the version doesn't match that format, we simply append ".0" to the end (e.g.
156
        # 0.1 -> 0.1.0, 1.0, -> 1.0.0, etc.)
157
        if NORMALIZE_PACK_VERSION:
158
            new_version = normalize_pack_version(version=values['version'])
159
            if new_version != values['version']:
160
                LOG.warning('Pack "%s" contains invalid semver version specifer, casting it to a '
161
                            'full semver version specifier (%s -> %s).\n'
162
                            'Short versions will become INVALID in StackStorm 2.2, and the pack '
163
                            'will stop working. Update the pack version in "pack.yaml".'
164
                            % (name, values['version'], new_version))
165
           values['version'] = new_version
166
167
        super(PackAPI, self).__init__(**values)
168
169
    def validate(self):
170
        # We wrap default validate() implementation and throw a more user-friendly exception in
171
        # case pack version doesn't follow a valid semver format
172
        try:
173
            super(PackAPI, self).validate()
174
        except jsonschema.ValidationError as e:
175
            msg = str(e)
176
177
            if "Failed validating 'pattern' in schema['properties']['version']" in msg:
178
                new_msg = ('Pack version "%s" doesn\'t follow a valid semver format. Valid '
179
                           'versions and formats include: 0.1.0, 0.2.1, 1.1.0, etc.' %
180
                           (self.version))
181
                new_msg += '\n\n' + msg
182
                raise jsonschema.ValidationError(new_msg)
183
184
            raise e
185
186
    @classmethod
187
    def to_model(cls, pack):
188
        ref = pack.ref
189
        name = pack.name
190
        description = pack.description
191
        keywords = getattr(pack, 'keywords', [])
192
        version = str(pack.version)
193
194
        stackstorm_version = getattr(pack, 'stackstorm_version', None)
195
        author = pack.author
196
        email = pack.email
197
        contributors = getattr(pack, 'contributors', [])
198
        files = getattr(pack, 'files', [])
199
        dependencies = getattr(pack, 'dependencies', [])
200
        system = getattr(pack, 'system', {})
201
202
        model = cls.model(ref=ref, name=name, description=description, keywords=keywords,
203
                          version=version, author=author, email=email, contributors=contributors,
204
                          files=files, dependencies=dependencies, system=system,
205
                          stackstorm_version=stackstorm_version)
206
        return model
207
208
209
class ConfigSchemaAPI(BaseAPI):
210
    model = ConfigSchemaDB
211
    schema = {
212
        "title": "ConfigSchema",
213
        "description": "Pack config schema.",
214
        "type": "object",
215
        "properties": {
216
            "id": {
217
                "description": "The unique identifier for the config schema.",
218
                "type": "string"
219
            },
220
            "pack": {
221
                "description": "The content pack this config schema belongs to.",
222
                "type": "string"
223
            },
224
            "attributes": {
225
                "description": "Config schema attributes.",
226
                "type": "object",
227
                "patternProperties": {
228
                    "^\w+$": util_schema.get_action_parameters_schema()
229
                },
230
                'additionalProperties': False,
231
                "default": {}
232
            }
233
        },
234
        "additionalProperties": False
235
    }
236
237
    @classmethod
238
    def to_model(cls, config_schema):
239
        pack = config_schema.pack
240
        attributes = config_schema.attributes
241
242
        model = cls.model(pack=pack, attributes=attributes)
243
        return model
244
245
246
class ConfigAPI(BaseAPI):
247
    model = ConfigDB
248
    schema = {
249
        "title": "Config",
250
        "description": "Pack config.",
251
        "type": "object",
252
        "properties": {
253
            "id": {
254
                "description": "The unique identifier for the config.",
255
                "type": "string"
256
            },
257
            "pack": {
258
                "description": "The content pack this config belongs to.",
259
                "type": "string"
260
            },
261
            "values": {
262
                "description": "Config values.",
263
                "type": "object",
264
                "default": {}
265
            }
266
        },
267
        "additionalProperties": False
268
    }
269
270
    def validate(self, validate_against_schema=False):
271
        # Perform base API model validation against json schema
272
        result = super(ConfigAPI, self).validate()
273
274
        # Perform config values validation against the config values schema
275
        if validate_against_schema:
276
            cleaned_values = self._validate_config_values_against_schema()
277
            result.values = cleaned_values
278
279
        return result
280
281
    def _validate_config_values_against_schema(self):
282
        try:
283
            config_schema_db = ConfigSchema.get_by_pack(value=self.pack)
284
        except StackStormDBObjectNotFoundError:
285
            # Config schema is optional
286
            return
287
288
        # Note: We are doing optional validation so for now, we do allow additional properties
289
        instance = self.values or {}
290
        schema = config_schema_db.attributes
291
292
        configs_path = os.path.join(cfg.CONF.system.base_path, 'configs/')
293
        config_path = os.path.join(configs_path, '%s.yaml' % (self.pack))
294
295
        cleaned = validate_config_against_schema(config_schema=schema,
296
                                                 config_object=instance,
297
                                                 config_path=config_path,
298
                                                 pack_name=self.pack)
299
300
        return cleaned
301
302
    @classmethod
303
    def to_model(cls, config):
304
        pack = config.pack
305
        values = config.values
306
307
        model = cls.model(pack=pack, values=values)
308
        return model
309
310
311
class ConfigUpdateRequestAPI(BaseAPI):
312
    schema = {
313
        "type": "object"
314
    }
315
316
317
class ConfigItemSetAPI(BaseAPI):
318
    """
319
    API class used with the config set API endpoint.
320
    """
321
    model = None
322
    schema = {
323
        "title": "",
324
        "description": "",
325
        "type": "object",
326
        "properties": {
327
            "name": {
328
                "description": "Config item name (key)",
329
                "type": "string",
330
                "required": True
331
            },
332
            "value": {
333
                "description": "Config item value.",
334
                "type": ["string", "number", "boolean", "array", "object"],
335
                "required": True
336
            },
337
            "scope": {
338
                "description": "Config item scope (system / user)",
339
                "type": "string",
340
                "default": SYSTEM_SCOPE,
341
                "enum": [
342
                    SYSTEM_SCOPE,
343
                    USER_SCOPE
344
                ]
345
            },
346
            "user": {
347
                "description": "User for user-scoped items (only available to admins).",
348
                "type": "string",
349
                "required": False,
350
                "default": None
351
            }
352
        },
353
        "additionalProperties": False
354
    }
355
356
357
class PackInstallRequestAPI(BaseAPI):
358
    schema = {
359
        "type": "object",
360
        "properties": {
361
            "packs": {
362
                "type": "array"
363
            },
364
            "force": {
365
                "type": "boolean",
366
                "description": "Force pack installation",
367
                "default": False
368
            }
369
        }
370
    }
371
372
373
class PackRegisterRequestAPI(BaseAPI):
374
    schema = {
375
        "type": "object",
376
        "properties": {
377
            "types": {
378
                "type": "array",
379
                "items": {
380
                    "type": "string"
381
                }
382
            },
383
            "packs": {
384
                "type": "array",
385
                "items": {
386
                    "type": "string"
387
                }
388
            }
389
        }
390
    }
391
392
393
class PackSearchRequestAPI(BaseAPI):
394
    schema = {
395
        "type": "object",
396
        "oneOf": [
397
            {
398
                "properties": {
399
                    "query": {
400
                        "type": "string",
401
                        "required": True,
402
                    },
403
                },
404
                "additionalProperties": False,
405
            },
406
            {
407
                "properties": {
408
                    "pack": {
409
                        "type": "string",
410
                        "required": True,
411
                    },
412
                },
413
                "additionalProperties": False,
414
            },
415
        ]
416
    }
417
418
419
class PackAsyncAPI(BaseAPI):
420
    schema = {
421
        "type": "object",
422
        "properties": {
423
            "execution_id": {
424
                "type": "string",
425
                "required": True
426
            }
427
        },
428
        "additionalProperties": False
429
    }
430