Completed
Pull Request — master (#2988)
by Edward
05:38
created

PackAPI.validate()   A

Complexity

Conditions 3

Size

Total Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
c 1
b 0
f 0
dl 0
loc 16
rs 9.4285
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.persistence.pack import ConfigSchema
29
from st2common.models.api.base import BaseAPI
30
from st2common.models.db.pack import PackDB
31
from st2common.models.db.pack import ConfigSchemaDB
32
from st2common.models.db.pack import ConfigDB
33
from st2common.exceptions.db import StackStormDBObjectNotFoundError
34
35
__all__ = [
36
    'PackAPI',
37
    'ConfigSchemaAPI',
38
    'ConfigAPI',
39
40
    'ConfigItemSetAPI',
41
42
    'PackInstallRequestAPI',
43
    'PackRegisterRequestAPI',
44
    'PackSearchRequestAPI',
45
    'PackAsyncAPI'
46
]
47
48
LOG = logging.getLogger(__name__)
49
50
51
class PackAPI(BaseAPI):
52
    model = PackDB
53
    schema = {
54
        'type': 'object',
55
        'description': 'Content pack schema.',
56
        'properties': {
57
            'id': {
58
                'type': 'string',
59
                'description': 'Unique identifier for the pack.',
60
                'default': None
61
            },
62
            'name': {
63
                'type': 'string',
64
                'description': 'Display name of the pack. If the name only contains lowercase'
65
                               'letters, digits and underscores, the "ref" field is not required.',
66
                'required': True
67
            },
68
            'ref': {
69
                'type': 'string',
70
                'description': 'Reference for the pack, used as an internal id.',
71
                'default': None,
72
                'pattern': PACK_REF_WHITELIST_REGEX
73
            },
74
            'uid': {
75
                'type': 'string'
76
            },
77
            'description': {
78
                'type': 'string',
79
                'description': 'Brief description of the pack and the service it integrates with.',
80
                'required': True
81
            },
82
            'keywords': {
83
                'type': 'array',
84
                'description': 'Keywords describing the pack.',
85
                'items': {'type': 'string'},
86
                'default': []
87
            },
88
            'version': {
89
                'type': 'string',
90
                'description': 'Pack version. Must follow the semver format '
91
                               '(for instance, "0.1.0").',
92
                'pattern': PACK_VERSION_REGEX,
93
                'required': True
94
            },
95
            'stackstorm_version': {
96
                'type': 'string',
97
                'description': 'Required StackStorm version. Examples: ">1.6.0", '
98
                               '">=1.8.0, <2.2.0"',
99
                'pattern': ST2_VERSION_REGEX,
100
            },
101
            'author': {
102
                'type': 'string',
103
                'description': 'Pack author or authors.',
104
                'required': True
105
            },
106
            'email': {
107
                'type': 'string',
108
                'description': 'E-mail of the pack author.',
109
                'format': 'email'
110
            },
111
            'files': {
112
                'type': 'array',
113
                'description': 'A list of files inside the pack.',
114
                'items': {'type': 'string'},
115
                'default': []
116
            },
117
            'dependencies': {
118
                'type': 'array',
119
                'description': 'A list of other StackStorm packs this pack depends upon. '
120
                               'The same format as in "st2 pack install" is used: '
121
                               '"<name or full URL>[=<version or git ref>]".',
122
                'items': {'type': 'string'},
123
                'default': []
124
            },
125
            'system': {
126
                'type': 'object',
127
                'description': 'Specification for the system components and packages '
128
                               'required for the pack.',
129
                'default': {}
130
            }
131
        }
132
    }
133
134
    def validate(self):
135
        # We wrap default validate() implementation and throw a more user-friendly exception in
136
        # case pack version doesn't follow a valid semver format
137
        try:
138
            super(PackAPI, self).validate()
139
        except jsonschema.ValidationError as e:
140
            msg = str(e)
141
142
            if "Failed validating 'pattern' in schema['properties']['version']" in msg:
143
                new_msg = ('Pack version "%s" doesn\'t follow a valid semver format. Valid '
144
                           'versions and formats include: 0.1.0, 0.2.1, 1.1.0, etc.' %
145
                           (self.version))
146
                new_msg += '\n\n' + msg
147
                raise jsonschema.ValidationError(new_msg)
148
149
            raise e
150
151
    @classmethod
152
    def to_model(cls, pack):
153
        ref = pack.ref
154
        name = pack.name
155
        description = pack.description
156
        keywords = getattr(pack, 'keywords', [])
157
        version = str(pack.version)
158
159
        stackstorm_version = getattr(pack, 'stackstorm_version', None)
160
        author = pack.author
161
        email = pack.email
162
        files = getattr(pack, 'files', [])
163
        dependencies = getattr(pack, 'dependencies', [])
164
        system = getattr(pack, 'system', {})
165
166
        model = cls.model(ref=ref, name=name, description=description, keywords=keywords,
167
                          version=version, author=author, email=email, files=files,
168
                          dependencies=dependencies, system=system,
169
                          stackstorm_version=stackstorm_version)
170
        return model
171
172
173
class ConfigSchemaAPI(BaseAPI):
174
    model = ConfigSchemaDB
175
    schema = {
176
        "title": "ConfigSchema",
177
        "description": "Pack config schema.",
178
        "type": "object",
179
        "properties": {
180
            "id": {
181
                "description": "The unique identifier for the config schema.",
182
                "type": "string"
183
            },
184
            "pack": {
185
                "description": "The content pack this config schema belongs to.",
186
                "type": "string"
187
            },
188
            "attributes": {
189
                "description": "Config schema attributes.",
190
                "type": "object",
191
                "patternProperties": {
192
                    "^\w+$": util_schema.get_action_parameters_schema()
0 ignored issues
show
Bug introduced by
A suspicious escape sequence \w was found. Did you maybe forget to add an r prefix?

Escape sequences in Python are generally interpreted according to rules similar to standard C. Only if strings are prefixed with r or R are they interpreted as regular expressions.

The escape sequence that was used indicates that you might have intended to write a regular expression.

Learn more about the available escape sequences. in the Python documentation.

Loading history...
193
                },
194
                'additionalProperties': False,
195
                "default": {}
196
            }
197
        },
198
        "additionalProperties": False
199
    }
200
201
    @classmethod
202
    def to_model(cls, config_schema):
203
        pack = config_schema.pack
204
        attributes = config_schema.attributes
205
206
        model = cls.model(pack=pack, attributes=attributes)
207
        return model
208
209
210
class ConfigAPI(BaseAPI):
211
    model = ConfigDB
212
    schema = {
213
        "title": "Config",
214
        "description": "Pack config.",
215
        "type": "object",
216
        "properties": {
217
            "id": {
218
                "description": "The unique identifier for the config.",
219
                "type": "string"
220
            },
221
            "pack": {
222
                "description": "The content pack this config belongs to.",
223
                "type": "string"
224
            },
225
            "values": {
226
                "description": "Config values.",
227
                "type": "object",
228
                "default": {}
229
            }
230
        },
231
        "additionalProperties": False
232
    }
233
234
    def validate(self, validate_against_schema=False):
0 ignored issues
show
Bug introduced by
Arguments number differs from overridden 'validate' method
Loading history...
235
        # Perform base API model validation against json schema
236
        result = super(ConfigAPI, self).validate()
237
238
        # Perform config values validation against the config values schema
239
        if validate_against_schema:
240
            cleaned_values = self._validate_config_values_against_schema()
241
            result.values = cleaned_values
242
243
        return result
244
245
    def _validate_config_values_against_schema(self):
246
        try:
247
            config_schema_db = ConfigSchema.get_by_pack(value=self.pack)
248
        except StackStormDBObjectNotFoundError:
249
            # Config schema is optional
250
            return
251
252
        # Note: We are doing optional validation so for now, we do allow additional properties
253
        instance = self.values or {}
254
        schema = config_schema_db.attributes
255
        schema = util_schema.get_schema_for_resource_parameters(parameters_schema=schema,
256
                                                                allow_additional_properties=True)
257
258
        try:
259
            cleaned = util_schema.validate(instance=instance, schema=schema,
260
                                           cls=util_schema.CustomValidator, use_default=True,
261
                                           allow_default_none=True)
262
        except jsonschema.ValidationError as e:
263
            attribute = getattr(e, 'path', [])
264
            attribute = '.'.join(attribute)
265
            configs_path = os.path.join(cfg.CONF.system.base_path, 'configs/')
266
            config_path = os.path.join(configs_path, '%s.yaml' % (self.pack))
267
268
            msg = ('Failed validating attribute "%s" in config for pack "%s" (%s): %s' %
269
                   (attribute, self.pack, config_path, str(e)))
270
            raise jsonschema.ValidationError(msg)
271
272
        return cleaned
273
274
    @classmethod
275
    def to_model(cls, config):
276
        pack = config.pack
277
        values = config.values
278
279
        model = cls.model(pack=pack, values=values)
280
        return model
281
282
283
class ConfigUpdateRequestAPI(BaseAPI):
284
    schema = {
285
        "type": "object"
286
    }
287
288
289
class ConfigItemSetAPI(BaseAPI):
290
    """
291
    API class used with the config set API endpoint.
292
    """
293
    model = None
294
    schema = {
295
        "title": "",
296
        "description": "",
297
        "type": "object",
298
        "properties": {
299
            "name": {
300
                "description": "Config item name (key)",
301
                "type": "string",
302
                "required": True
303
            },
304
            "value": {
305
                "description": "Config item value.",
306
                "type": ["string", "number", "boolean", "array", "object"],
307
                "required": True
308
            },
309
            "scope": {
310
                "description": "Config item scope (system / user)",
311
                "type": "string",
312
                "default": SYSTEM_SCOPE,
313
                "enum": [
314
                    SYSTEM_SCOPE,
315
                    USER_SCOPE
316
                ]
317
            },
318
            "user": {
319
                "description": "User for user-scoped items (only available to admins).",
320
                "type": "string",
321
                "required": False,
322
                "default": None
323
            }
324
        },
325
        "additionalProperties": False
326
    }
327
328
329
class PackInstallRequestAPI(BaseAPI):
330
    schema = {
331
        "type": "object",
332
        "properties": {
333
            "packs": {
334
                "type": "array"
335
            }
336
        }
337
    }
338
339
340
class PackRegisterRequestAPI(BaseAPI):
341
    schema = {
342
        "type": "object",
343
        "properties": {
344
            "types": {
345
                "type": "array",
346
                "items": {
347
                    "type": "string"
348
                }
349
            },
350
            "packs": {
351
                "type": "array",
352
                "items": {
353
                    "type": "string"
354
                }
355
            }
356
        }
357
    }
358
359
360
class PackSearchRequestAPI(BaseAPI):
361
    schema = {
362
        "type": "object",
363
        "oneOf": [
364
            {
365
                "properties": {
366
                    "query": {
367
                        "type": "string",
368
                        "required": True,
369
                    },
370
                },
371
                "additionalProperties": False,
372
            },
373
            {
374
                "properties": {
375
                    "pack": {
376
                        "type": "string",
377
                        "required": True,
378
                    },
379
                },
380
                "additionalProperties": False,
381
            },
382
        ]
383
    }
384
385
386
class PackAsyncAPI(BaseAPI):
387
    schema = {
388
        "type": "object",
389
        "properties": {
390
            "execution_id": {
391
                "type": "string",
392
                "required": True
393
            }
394
        },
395
        "additionalProperties": False
396
    }
397