opentf-toolkit-nightly 0.62.0.dev1347__py3-none-any.whl → 0.62.0.dev1373__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -27,8 +27,6 @@ import jwt
27
27
 
28
28
  from flask import Flask, current_app, make_response, request, g, Response
29
29
 
30
- from toposort import toposort, CircularDependencyError
31
-
32
30
  from .auth import (
33
31
  initialize_authn_authz,
34
32
  get_user_accessible_namespaces,
@@ -220,6 +218,10 @@ def _get_contextparameter_spec(app: Flask, name: str) -> Optional[Dict[str, Any]
220
218
  parameters = app.config[PARAMETERS_KEY]
221
219
  try:
222
220
  spec = get_named(name, parameters)
221
+ if spec.get('type') == 'int':
222
+ spec['type'] = 'number'
223
+ if spec.get('type') == 'bool':
224
+ spec['type'] = 'boolean'
223
225
  except ValueError:
224
226
  spec = None
225
227
  return spec
@@ -429,7 +431,7 @@ def make_app(
429
431
 
430
432
  `CONFIG` is a dictionary, the complete config file. `CONTEXT` is a
431
433
  subset of `CONFIG`, the current entry in `CONFIG['context']`. It is
432
- also a dictionary. `DESCRIPTOR` is the service descriptor.
434
+ also a dictionary. `DESCRIPTOR` is the service descriptors.
433
435
 
434
436
  # Raised Exception
435
437
 
@@ -466,6 +468,12 @@ def make_app(
466
468
  app.config['DESCRIPTOR'] = (
467
469
  descriptor if isinstance(descriptor, list) else [descriptor]
468
470
  )
471
+ try:
472
+ validate_descriptors(app.config['DESCRIPTOR'])
473
+ except ValueError as err:
474
+ app.logger.error('Invalid descriptor: %s', err)
475
+ sys.exit(2)
476
+
469
477
  app.route('/health', methods=['GET'])(lambda: 'OK')
470
478
  app.before_request(_make_authenticator(context))
471
479
  app.after_request(_add_securityheaders)
@@ -566,11 +574,10 @@ def get_context_parameter(
566
574
  )
567
575
 
568
576
  if spec:
569
- if spec.get('type') == 'int':
570
- try:
571
- val = int(val)
572
- except ValueError as err:
573
- _fatal(f'Context parameter "{name}" not an integer: {err}.')
577
+ try:
578
+ val = validate_value(spec, val)
579
+ except ValueError as err:
580
+ _fatal(f'Context parameter "{name}": {err}')
574
581
  desc = spec['descriptiveName'][0].upper() + spec['descriptiveName'][1:]
575
582
  if 'minValue' in spec and val < spec['minValue']:
576
583
  _fatal(f'{desc} must be greater than {spec["minValue"]-1}.')
@@ -746,173 +753,3 @@ def annotate_response(
746
753
  if seen := {k for k in processed if request.args.get(k) is not None}:
747
754
  response.headers['X-Processed-Query'] = ','.join(seen)
748
755
  return response
749
-
750
-
751
- ########################################################################
752
- # Pipelines Helpers
753
-
754
-
755
- def validate_pipeline(
756
- workflow: Dict[str, Any],
757
- ) -> Tuple[bool, Union[str, List[List[str]]]]:
758
- """Validate workflow jobs, looking for circular dependencies.
759
-
760
- # Required parameters
761
-
762
- - workflow: a dictionary
763
-
764
- # Returned value
765
-
766
- A (`bool`, extra) pair.
767
-
768
- If there is a dependency on an non-existing job, returns
769
- `(False, description (a string))`.
770
-
771
- If there are circular dependencies in the workflow jobs, returns
772
- `(False, description (a string))`.
773
-
774
- If there are no circular dependencies, returns `(True, jobs)` where
775
- `jobs` is an ordered list of job names lists. Each item in the
776
- returned list is a set of jobs that can run in parallel.
777
- """
778
- try:
779
- jobs = {}
780
- for job_name, job_definition in workflow['jobs'].items():
781
- if needs := job_definition.get('needs'):
782
- if isinstance(needs, list):
783
- jobs[job_name] = set(needs)
784
- else:
785
- jobs[job_name] = {needs}
786
- else:
787
- jobs[job_name] = set()
788
- for src, dependencies in jobs.items():
789
- for dep in dependencies:
790
- if dep not in jobs:
791
- return (
792
- False,
793
- f"Job '{src}' has a dependency on job '{dep}' which does not exist.",
794
- )
795
-
796
- return True, [list(items) for items in toposort(jobs)]
797
- except CircularDependencyError as err:
798
- return False, str(err)
799
-
800
-
801
- def _normalize_inputs(inputs: Dict[str, Any]) -> None:
802
- """Normalize inputs.
803
-
804
- The 'normalized' form for inputs is with `-` separators, not `_`.
805
-
806
- Non-normalized inputs are removed from the dictionary.
807
-
808
- # Raised exceptions
809
-
810
- A _ValueError_ exception is raised if an input is provided twice, in
811
- a normalized as well as a non-normalized form.
812
- """
813
- for key in inputs.copy():
814
- if '_' in key:
815
- normalized = key.replace('_', '-')
816
- if normalized in inputs:
817
- raise ValueError(
818
- f'Both "{key}" and "{normalized}" specified in inputs.'
819
- )
820
- inputs[normalized] = inputs.pop(key)
821
-
822
-
823
- def _set_default(inputs: Dict[str, Any], key: str, definition: Dict[str, Any]) -> None:
824
- if (default := definition.get('default')) is not None:
825
- inputs[key] = default
826
- elif type_ := definition.get('type'):
827
- if type_ == 'string':
828
- inputs[key] = ''
829
- elif type_ == 'number':
830
- inputs[key] = 0
831
- elif type_ == 'boolean':
832
- inputs[key] = False
833
-
834
-
835
- def validate_inputs(
836
- declaration: Dict[str, Dict[str, Any]],
837
- inputs: Dict[str, Any],
838
- additional_inputs: bool = False,
839
- normalize: bool = True,
840
- ) -> None:
841
- """Validate inputs.
842
-
843
- Default values are filled in `inputs` as appropriate.
844
-
845
- Input names are normalized to use hyphens instead of underscores
846
- by default (declaration is expected to be normalized).
847
-
848
- If `normalize` is set, non-normalized inputs are removed from the
849
- dictionary.
850
-
851
- Choices values are validated.
852
-
853
- Types are enforced if declared as `boolean`, `number`, or `string`.
854
- Conversion is performed if needed.
855
-
856
- # Required parameters
857
-
858
- - declaration: a dictionary
859
- - inputs: a dictionary
860
-
861
- # Optional parameters
862
-
863
- - additional_inputs: a boolean (False by default)
864
- - normalize: a boolean (True by default)
865
-
866
- # Raised exceptions
867
-
868
- A _ValueError_ exception is raised if inputs do not match
869
- declaration.
870
- """
871
- if normalize and not any(key.startswith('{') for key in declaration):
872
- _normalize_inputs(inputs)
873
-
874
- for key, definition in declaration.items():
875
- if key.startswith('{'):
876
- continue
877
- if key not in inputs:
878
- if definition.get('required', False):
879
- raise ValueError(f'Mandatory input "{key}" not provided.')
880
- _set_default(inputs, key, definition)
881
- continue
882
- val = inputs.get(key)
883
- type_ = definition.get('type')
884
- if type_ == 'choice':
885
- if val not in definition['options']:
886
- allowed = '", "'.join(sorted(definition['options']))
887
- raise ValueError(
888
- f'Invalid value "{val}" for input "{key}". Allowed values: "{allowed}".'
889
- )
890
- elif type_ == 'boolean' and not isinstance(val, bool):
891
- if isinstance(val, str) and val.lower() in ('true', 'false'):
892
- inputs[key] = val.lower() == 'true'
893
- continue
894
- raise ValueError(
895
- f'Invalid value "{val}" for input "{key}". Allowed values: "true", "false".'
896
- )
897
- elif type_ == 'number' and not isinstance(val, (int, float)):
898
- try:
899
- inputs[key] = int(val)
900
- except ValueError:
901
- try:
902
- inputs[key] = float(val)
903
- except ValueError:
904
- raise ValueError(
905
- f'Invalid value "{val}" for input "{key}". Expected a number.'
906
- )
907
- elif type_ == 'string' and not isinstance(val, str):
908
- inputs[key] = str(val)
909
-
910
- if additional_inputs:
911
- return
912
-
913
- if unexpected := set(inputs) - set(declaration):
914
- allowed = '", "'.join(sorted(declaration))
915
- unexpected = '", "'.join(sorted(unexpected))
916
- raise ValueError(
917
- f'Unexpected inputs "{unexpected}" found. Allowed inputs: "{allowed}".'
918
- )
opentf/commons/config.py CHANGED
@@ -220,7 +220,7 @@ def read_config(
220
220
  else:
221
221
  configfile = altconfig or configfile
222
222
  try:
223
- config = read_and_validate(configfile, schema or SERVICECONFIG)
223
+ config = read_and_validate(schema or SERVICECONFIG, configfile)
224
224
  except (ValueError, OSError) as err:
225
225
  raise ConfigError(f'Could not read configfile "{configfile}": {err}.')
226
226
 
opentf/commons/schemas.py CHANGED
@@ -12,17 +12,19 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
- """Helpers for the OpenTestFactory schemas."""
15
+ """Helpers for the OpenTestFactory schemas and validation."""
16
16
 
17
- from typing import Any, Dict, Optional, Tuple
17
+ from typing import Any, Dict, Iterable, List, Optional, Tuple, Union
18
18
 
19
19
  import json
20
20
  import logging
21
21
  import os
22
22
 
23
23
  from jsonschema import Draft201909Validator, ValidationError
24
+ from toposort import toposort, CircularDependencyError
24
25
  from yaml import safe_load
25
26
 
27
+
26
28
  import opentf.schemas
27
29
 
28
30
 
@@ -65,6 +67,8 @@ RETENTION_POLICY = 'opentestfactory.org/v1alpha1/RetentionPolicy'
65
67
  TRACKER_PUBLISHER = 'opentestfactory.org/v1alpha1/TrackerPublisher'
66
68
  INSIGHT_COLLECTOR = 'opentestfactory.org/v1alpha1/InsightCollector'
67
69
 
70
+ PLUGIN_DESCRIPTOR = 'config.opentestfactory.org/v1/Descriptor'
71
+
68
72
 
69
73
  ########################################################################
70
74
  # JSON Schema Helpers
@@ -133,17 +137,17 @@ def validate_schema(
133
137
  return True, None
134
138
 
135
139
 
136
- def read_and_validate(filename: str, schema: str) -> Dict[str, Any]:
140
+ def read_and_validate(schema: str, filename: str) -> Dict[str, Any]:
137
141
  """Read and validate a JSON or YAML file.
138
142
 
139
143
  # Required parameters
140
144
 
145
+ - schema: a string, the schema name (its kind)
141
146
  - filename: a string, the file name
142
- - schema: a string, the schema to validate the file content
143
147
 
144
148
  # Returned value
145
149
 
146
- A dictionary, the definition.
150
+ A dictionary, the valid content.
147
151
 
148
152
  # Raised exceptions
149
153
 
@@ -161,3 +165,290 @@ def read_and_validate(filename: str, schema: str) -> Dict[str, Any]:
161
165
  if not valid:
162
166
  raise ValueError(f'Invalid content: {extra}.')
163
167
  return config
168
+
169
+
170
+ ########################################################################
171
+ # Pipelines Helpers
172
+
173
+
174
+ def validate_pipeline(
175
+ workflow: Dict[str, Any],
176
+ ) -> Tuple[bool, Union[str, List[List[str]]]]:
177
+ """Validate workflow jobs, looking for circular dependencies.
178
+
179
+ # Required parameters
180
+
181
+ - workflow: a dictionary
182
+
183
+ # Returned value
184
+
185
+ A (`bool`, extra) pair.
186
+
187
+ If there is a dependency on an non-existing job, returns
188
+ `(False, description (a string))`.
189
+
190
+ If there are circular dependencies in the workflow jobs, returns
191
+ `(False, description (a string))`.
192
+
193
+ If there are no circular dependencies, returns `(True, jobs)` where
194
+ `jobs` is an ordered list of job names lists. Each item in the
195
+ returned list is a set of jobs that can run in parallel.
196
+ """
197
+ jobs = {}
198
+ for job_name, job_definition in workflow['jobs'].items():
199
+ if needs := job_definition.get('needs'):
200
+ if isinstance(needs, list):
201
+ jobs[job_name] = set(needs)
202
+ else:
203
+ jobs[job_name] = {needs}
204
+ else:
205
+ jobs[job_name] = set()
206
+ for src, dependencies in jobs.items():
207
+ for dep in dependencies:
208
+ if dep not in jobs:
209
+ return (
210
+ False,
211
+ f'Job "{src}" has a dependency on job "{dep}" which does not exist.',
212
+ )
213
+ try:
214
+ return True, [list(items) for items in toposort(jobs)]
215
+ except CircularDependencyError as err:
216
+ return False, str(err)
217
+
218
+
219
+ def validate_workflow(workflow: Dict[str, Any]) -> List[List[str]]:
220
+ """Validate workflow.
221
+
222
+ # Required parameters
223
+
224
+ - workflow: a dictionary
225
+
226
+ # Returned value
227
+
228
+ An ordered list of job names lists. Each item in the returned list
229
+ is a set of jobs that can run in parallel.
230
+
231
+ # Raised exceptions
232
+
233
+ A _ValueError_ exception is raised if the workflow is not a valid.
234
+ """
235
+ valid, extra = validate_schema(WORKFLOW, workflow)
236
+ if not valid:
237
+ raise ValueError(extra)
238
+
239
+ if declaration := workflow.get('inputs'):
240
+ try:
241
+ _validate_defaults(declaration)
242
+ except ValueError as err:
243
+ raise ValueError(
244
+ f'Invalid "inputs" section, default value for {err}'
245
+ ) from None
246
+
247
+ valid, extra = validate_pipeline(workflow)
248
+ if not valid:
249
+ raise ValueError(extra)
250
+
251
+ return extra
252
+
253
+
254
+ # Inputs Helpers
255
+
256
+ INPUTSTYPE_VALIDATION = {'string': (str,), 'boolean': (bool,), 'number': (int, float)}
257
+
258
+
259
+ def _normalize_inputs(inputs: Dict[str, Any]) -> None:
260
+ """Normalize inputs.
261
+
262
+ The 'normalized' form for inputs is with `-` separators, not `_`.
263
+
264
+ Non-normalized inputs are removed from the dictionary.
265
+
266
+ # Raised exceptions
267
+
268
+ A _ValueError_ exception is raised if an input is provided twice, in
269
+ a normalized as well as a non-normalized form.
270
+ """
271
+ for key in inputs.copy():
272
+ if '_' not in key:
273
+ continue
274
+ normalized = key.replace('_', '-')
275
+ if normalized in inputs:
276
+ raise ValueError(f'Both "{key}" and "{normalized}" specified in inputs.')
277
+ inputs[normalized] = inputs.pop(key)
278
+
279
+
280
+ def _set_default(inputs: Dict[str, Any], key: str, spec: Dict[str, Any]) -> None:
281
+ if (default := spec.get('default')) is not None:
282
+ inputs[key] = default
283
+ elif type_ := spec.get('type'):
284
+ if type_ == 'string':
285
+ inputs[key] = ''
286
+ elif type_ == 'number':
287
+ inputs[key] = 0
288
+ elif type_ == 'boolean':
289
+ inputs[key] = False
290
+
291
+
292
+ def validate_value(spec: Dict[str, Any], val: Any) -> Any:
293
+ """Validate a value.
294
+
295
+ # Required parameters
296
+
297
+ - spec: a dictionary
298
+ - val: the value to validate
299
+
300
+ # Returned value
301
+
302
+ The validated value, converted to the expected type if appropriate.
303
+
304
+ # Raised exceptions
305
+
306
+ A _ValueError_ exception is raised if the validation fails.
307
+ """
308
+ type_ = spec.get('type')
309
+
310
+ if type_ == 'choice':
311
+ if val in spec['options']:
312
+ return val
313
+ allowed = '", "'.join(sorted(spec['options']))
314
+ raise ValueError(f'Invalid value "{val}". Allowed values: "{allowed}".')
315
+
316
+ if isinstance(val, INPUTSTYPE_VALIDATION.get(type_, object)):
317
+ return val
318
+
319
+ if type_ == 'boolean':
320
+ if isinstance(val, str) and val.lower() in ('true', 'false'):
321
+ return val.lower() == 'true'
322
+ raise ValueError(f'Invalid value "{val}". Allowed values: "true", "false".')
323
+ if type_ == 'number':
324
+ for cast in (int, float):
325
+ try:
326
+ return cast(val)
327
+ except (ValueError, TypeError):
328
+ pass
329
+ raise ValueError(f'Invalid value "{val}". Expected a number.')
330
+ if type_ == 'string':
331
+ return str(val)
332
+
333
+ raise ValueError(f'Invalid value "{val}". Expected a {type_}.')
334
+
335
+
336
+ def validate_inputs(
337
+ declaration: Dict[str, Dict[str, Any]],
338
+ inputs: Dict[str, Any],
339
+ additional_inputs: bool = False,
340
+ normalize: bool = True,
341
+ ) -> None:
342
+ """Validate inputs.
343
+
344
+ Default values are filled in `inputs` as appropriate.
345
+
346
+ Input names are normalized to use hyphens instead of underscores
347
+ by default (declaration is expected to be normalized in this case).
348
+
349
+ If `normalize` is set, non-normalized inputs are removed from the
350
+ dictionary.
351
+
352
+ If declaration contains a pattern (a key starting with '{'),
353
+ normalization is ignored.
354
+
355
+ Choices values are validated.
356
+
357
+ Types are enforced if declared as `boolean`, `number`, or `string`.
358
+ Conversion is performed if needed.
359
+
360
+ # Required parameters
361
+
362
+ - declaration: a dictionary
363
+ - inputs: a dictionary
364
+
365
+ # Optional parameters
366
+
367
+ - additional_inputs: a boolean (False by default)
368
+ - normalize: a boolean (True by default)
369
+
370
+ # Raised exceptions
371
+
372
+ A _ValueError_ exception is raised if inputs do not match
373
+ declaration.
374
+ """
375
+ if normalize and not any(key.startswith('{') for key in declaration):
376
+ _normalize_inputs(inputs)
377
+
378
+ for key, spec in declaration.items():
379
+ if key.startswith('{'):
380
+ continue
381
+ if key not in inputs:
382
+ if spec.get('required'):
383
+ raise ValueError(f'Mandatory input "{key}" not provided.')
384
+ _set_default(inputs, key, spec)
385
+ continue
386
+ try:
387
+ inputs[key] = validate_value(spec, inputs[key])
388
+ except ValueError as err:
389
+ raise ValueError(f'Input "{key}": {err}') from None
390
+
391
+ if additional_inputs:
392
+ return
393
+
394
+ if unexpected := set(inputs) - set(declaration):
395
+ allowed = '", "'.join(sorted(declaration))
396
+ unexpected = '", "'.join(sorted(unexpected))
397
+ raise ValueError(
398
+ f'Unexpected inputs "{unexpected}" found. Allowed inputs: "{allowed}".'
399
+ )
400
+
401
+
402
+ def _validate_defaults(declaration: Dict[str, Any]) -> None:
403
+ """Validate defaults.
404
+
405
+ Ensure `default`, if specified, is of the correct type and range.
406
+ """
407
+ for key, spec in declaration.items():
408
+ type_ = spec.get('type')
409
+ if not (type_ and 'default' in spec):
410
+ continue
411
+ default = spec['default']
412
+ if expected := INPUTSTYPE_VALIDATION.get(type_):
413
+ if not isinstance(default, expected):
414
+ raise ValueError(f'"{key}" must be a {type_}, got {repr(default)}.')
415
+ continue
416
+ if type_ == 'choice' and default not in spec.get('options'):
417
+ allowed = '", "'.join(sorted(spec.get('options')))
418
+ raise ValueError(
419
+ f'"{key}" must be one of "{allowed}", got {repr(default)}.'
420
+ )
421
+
422
+
423
+ def validate_descriptors(descriptors: Iterable[Dict[str, Any]]) -> None:
424
+ """Validate descriptors.
425
+
426
+ Validate descriptors against PLUGIN_DESCRIPTOR schema for plugins.
427
+
428
+ If applicable, `default` values are checked against the input's
429
+ `type`.
430
+
431
+ # Required parameters
432
+
433
+ - descriptors: a series of manifests
434
+
435
+ # Raised exceptions
436
+
437
+ A _ValueError_ exception is raised if a descriptor is invalid.
438
+ """
439
+ for manifest in descriptors:
440
+ try:
441
+ metadata = manifest['metadata']
442
+ what = f'{metadata["name"].lower()} {metadata.get("action", "")}'.strip()
443
+ except Exception as err:
444
+ raise ValueError(f'Missing "metadata.name" section: {err}.') from None
445
+ valid, extra = validate_schema(PLUGIN_DESCRIPTOR, manifest)
446
+ if not valid:
447
+ raise ValueError(f'"{what}": {extra}')
448
+ if declaration := manifest.get('inputs'):
449
+ try:
450
+ _validate_defaults(declaration)
451
+ except ValueError as err:
452
+ raise ValueError(
453
+ f'"inputs" section for "{what}", default value for {err}'
454
+ ) from None
@@ -0,0 +1,323 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2019-09/schema#",
3
+ "title": "JSON SCHEMA for opentestfactory inputs",
4
+ "description": "An events/inputs/outputs part in a plugin descriptor.",
5
+ "type": "object",
6
+ "properties": {
7
+ "apiVersion": {
8
+ "type": "string"
9
+ },
10
+ "kind": {
11
+ "type": "string"
12
+ },
13
+ "metadata": {
14
+ "type": "object",
15
+ "properties": {
16
+ "name": {
17
+ "type": "string"
18
+ },
19
+ "title": {
20
+ "type": "string"
21
+ },
22
+ "action": {
23
+ "type": "string"
24
+ },
25
+ "description": {
26
+ "type": "string"
27
+ },
28
+ "note": {
29
+ "type": "string"
30
+ },
31
+ "license": {
32
+ "type": "string"
33
+ },
34
+ "hooks": {
35
+ "const": "default"
36
+ },
37
+ "variables": {
38
+ "const": "default"
39
+ }
40
+ },
41
+ "required": [
42
+ "name"
43
+ ],
44
+ "additionalProperties": true
45
+ },
46
+ "cmd": {
47
+ "type": "string"
48
+ },
49
+ "spec": {
50
+ "type": "object",
51
+ "properties": {
52
+ "contextParameters": {
53
+ "$ref": "#/definitions/contextparameters"
54
+ },
55
+ "variables": {
56
+ "$ref": "#/definitions/variables"
57
+ }
58
+ },
59
+ "patternProperties": {
60
+ "^.*\\..*$": {
61
+ "type": [
62
+ "string",
63
+ "array",
64
+ "object"
65
+ ]
66
+ }
67
+ },
68
+ "required": [],
69
+ "additionalProperties": false
70
+ },
71
+ "events": {
72
+ "type": "array",
73
+ "minItems": 1
74
+ },
75
+ "inputs": {
76
+ "$ref": "#/definitions/inputs"
77
+ },
78
+ "additionalInputs": {
79
+ "type": "boolean"
80
+ },
81
+ "outputs": {
82
+ "$ref": "#/definitions/outputs"
83
+ },
84
+ "reports": {
85
+ "type": "object"
86
+ },
87
+ "branding": {
88
+ "type": "object"
89
+ }
90
+ },
91
+ "required": [
92
+ "metadata"
93
+ ],
94
+ "allOf": [
95
+ {
96
+ "if": {
97
+ "properties": {
98
+ "metadata": {
99
+ "properties": {
100
+ "action": {
101
+ "type": "string"
102
+ }
103
+ },
104
+ "required": [
105
+ "action"
106
+ ]
107
+ }
108
+ }
109
+ },
110
+ "then": {
111
+ "required": [
112
+ "events"
113
+ ]
114
+ }
115
+ },
116
+ {
117
+ "if": {
118
+ "properties": {
119
+ "additionalInputs": {
120
+ }
121
+ },
122
+ "required": [
123
+ "additionalInputs"
124
+ ]
125
+ },
126
+ "then": {
127
+ "required": [
128
+ "inputs"
129
+ ]
130
+ }
131
+ }
132
+ ],
133
+ "additionalProperties": false,
134
+ "definitions": {
135
+ "contextparameters": {
136
+ "type": "array",
137
+ "minItems": 1,
138
+ "items": {
139
+ "type": "object",
140
+ "properties": {
141
+ "name": {
142
+ "type": "string"
143
+ },
144
+ "descriptiveName": {
145
+ "type": "string"
146
+ },
147
+ "description": {
148
+ "type": "string"
149
+ },
150
+ "deprecatedNames": {
151
+ "type": "array",
152
+ "minItems": 1,
153
+ "items": {
154
+ "type": "string"
155
+ }
156
+ },
157
+ "type": {
158
+ "enum": [
159
+ "string",
160
+ "number",
161
+ "boolean",
162
+ "choice",
163
+ "int",
164
+ "bool"
165
+ ]
166
+ },
167
+ "options": {
168
+ "type": "array",
169
+ "minItems": 1,
170
+ "items": {
171
+ "type": "string"
172
+ }
173
+ },
174
+ "default": {},
175
+ "minValue": { "type": "number" },
176
+ "maxValue": { "type": "number" },
177
+ "shared": {
178
+ "type": "boolean"
179
+ }
180
+ },
181
+ "required": [
182
+ "name",
183
+ "descriptiveName"
184
+ ],
185
+ "allOf": [
186
+ {
187
+ "$ref": "#/definitions/choice"
188
+ }
189
+ ],
190
+ "additionalProperties": false
191
+ }
192
+ },
193
+ "variables": {
194
+ "type": "object",
195
+ "minProperties": 1,
196
+ "patternProperties": {
197
+ "^[a-zA-Z0-9_]+$": {
198
+ "type": "object",
199
+ "properties": {
200
+ "descriptiveName": {
201
+ "type": "string"
202
+ },
203
+ "description": {
204
+ "type": "string"
205
+ },
206
+ "default": {
207
+ "type": "string"
208
+ },
209
+ "required": {
210
+ "type": "boolean"
211
+ },
212
+ "masked": {
213
+ "type": "boolean"
214
+ }
215
+ },
216
+ "required": [
217
+ "descriptiveName"
218
+ ],
219
+ "additionalProperties": false
220
+ }
221
+ }
222
+ },
223
+ "inputs": {
224
+ "type": "object",
225
+ "minProperties": 0,
226
+ "patternProperties": {
227
+ "^[a-zA-Z-][a-zA-Z0-9-]*$|^{.*": {
228
+ "type": "object",
229
+ "properties": {
230
+ "description": {
231
+ "type": "string"
232
+ },
233
+ "required": {
234
+ "type": "boolean"
235
+ },
236
+ "default": {},
237
+ "type": {
238
+ "enum": [
239
+ "string",
240
+ "number",
241
+ "boolean",
242
+ "choice"
243
+ ]
244
+ },
245
+ "options": {
246
+ "type": "array",
247
+ "minItems": 1,
248
+ "items": {
249
+ "type": "string"
250
+ }
251
+ },
252
+ "depreciationMessage": {
253
+ "type": "string"
254
+ }
255
+ },
256
+ "required": [
257
+ "description"
258
+ ],
259
+ "allOf": [
260
+ {
261
+ "$ref": "#/definitions/choice"
262
+ }
263
+ ],
264
+ "additionalProperties": false
265
+ }
266
+ },
267
+ "additionalProperties": false
268
+ },
269
+ "outputs": {
270
+ "type": "object",
271
+ "minProperties": 1,
272
+ "patternProperties": {
273
+ "^[a-zA-Z_][a-zA-Z0-9_-]*$": {
274
+ "oneOf": [
275
+ {
276
+ "type": "object",
277
+ "properties": {
278
+ "description": {
279
+ "type": "string"
280
+ },
281
+ "value": {
282
+ "type": "string"
283
+ }
284
+ },
285
+ "required": [
286
+ "value"
287
+ ],
288
+ "additionalProperties": false
289
+ },
290
+ {
291
+ "type": "string"
292
+ }
293
+ ]
294
+ }
295
+ },
296
+ "additionalProperties": false
297
+ },
298
+ "choice": {
299
+ "if": {
300
+ "properties": {
301
+ "type": {
302
+ "const": "choice"
303
+ }
304
+ },
305
+ "required": [
306
+ "type"
307
+ ]
308
+ },
309
+ "then": {
310
+ "required": [
311
+ "options"
312
+ ]
313
+ },
314
+ "else": {
315
+ "not": {
316
+ "required": [
317
+ "options"
318
+ ]
319
+ }
320
+ }
321
+ }
322
+ }
323
+ }
@@ -552,7 +552,6 @@
552
552
  "type": "boolean"
553
553
  },
554
554
  "default": {
555
- "type": "string"
556
555
  },
557
556
  "type": {
558
557
  "enum": [
@@ -221,6 +221,7 @@
221
221
  "IDLE",
222
222
  "PENDING",
223
223
  "UNREACHABLE",
224
+ "TERMINATING",
224
225
  null
225
226
  ]
226
227
  },
@@ -1,4 +1,4 @@
1
- # Copyright (c) Henix, Henix.fr
1
+ # Copyright (c) 2021 Henix, Henix.fr
2
2
  #
3
3
  # Licensed under the Apache License, Version 2.0 (the "License");
4
4
  # you may not use this file except in compliance with the License.
@@ -14,7 +14,7 @@
14
14
 
15
15
  """A toolkit for creating OpenTestFactory plugins."""
16
16
 
17
- from typing import Any, Callable, Dict, Optional
17
+ from typing import Any, Callable, Dict, List, Optional
18
18
 
19
19
  import os
20
20
  import threading
@@ -34,13 +34,16 @@ from opentf.commons import (
34
34
  subscribe,
35
35
  unsubscribe,
36
36
  EXECUTIONCOMMAND,
37
+ EXECUTIONRESULT,
37
38
  PROVIDERCOMMAND,
38
39
  PROVIDERCONFIG,
39
40
  GENERATORCOMMAND,
40
41
  SERVICECONFIG,
41
42
  CHANNEL_HOOKS,
43
+ PLUGIN_DESCRIPTOR,
42
44
  make_dispatchqueue,
43
45
  make_status_response,
46
+ validate_descriptors,
44
47
  validate_inputs,
45
48
  validate_schema,
46
49
  )
@@ -465,23 +468,10 @@ def _subscribe(
465
468
  manifest.get('inputs', {}),
466
469
  manifest.get('additionalInputs'),
467
470
  )
468
- try:
469
- context[OUTPUTS_KEY][(cat_prefix, cat, cat_version)] = {
470
- k: v['value'] if isinstance(v, dict) else v
471
- for k, v in manifest.get('outputs', {}).items()
472
- }
473
- except KeyError as err:
474
- plugin.logger.error(
475
- 'Invalid descriptor "outputs" section, could not find key %s in: %s.',
476
- err,
477
- manifest.get('outputs', {}),
478
- )
479
- sys.exit(2)
480
- except Exception as err:
481
- plugin.logger.error(
482
- 'Invalid descriptor "outputs" section, got %s while parsing outputs.', err
483
- )
484
- sys.exit(2)
471
+ context[OUTPUTS_KEY][(cat_prefix, cat, cat_version)] = {
472
+ k: v['value'] if isinstance(v, dict) else v
473
+ for k, v in manifest.get('outputs', {}).items()
474
+ }
485
475
  return subscribe(kind=kind, target='inbox', app=plugin, labels=labels)
486
476
 
487
477
 
@@ -574,14 +564,14 @@ def make_plugin(
574
564
  - configfile: a string or None (None by default)
575
565
  - args: a list or None (None by default)
576
566
 
567
+ # Returned value
568
+
569
+ A plugin service (not started).
570
+
577
571
  # Raised exceptions
578
572
 
579
573
  A _ValueError_ exception is raised if the provided parameters are
580
574
  invalid.
581
-
582
- # Returned value
583
-
584
- A plugin service (not started).
585
575
  """
586
576
 
587
577
  def process_inbox():
@@ -600,7 +590,7 @@ def make_plugin(
600
590
  except KeyError:
601
591
  return make_status_response(
602
592
  'BadRequest',
603
- f'Not a valid {kind} request: Missing metadata section',
593
+ f'Not a valid {kind} request: Missing "metadata" section',
604
594
  )
605
595
 
606
596
  valid, extra = validate_schema(kind, body)
@@ -643,11 +633,6 @@ def make_plugin(
643
633
  '"args" is required for channel plugins and must be a list of one element.'
644
634
  )
645
635
 
646
- kind = (
647
- EXECUTIONCOMMAND
648
- if channel
649
- else GENERATORCOMMAND if generator else PROVIDERCOMMAND
650
- )
651
636
  if not schema:
652
637
  schema = SERVICECONFIG if generator else PROVIDERCONFIG
653
638
 
@@ -658,8 +643,18 @@ def make_plugin(
658
643
  schema=schema,
659
644
  descriptor=descriptor if descriptor is not None else 'plugin.yaml',
660
645
  )
661
- plugin.route('/inbox', methods=['POST'])(process_inbox)
646
+
647
+ if channel:
648
+ kind = EXECUTIONCOMMAND
649
+ elif generator:
650
+ kind = GENERATORCOMMAND
651
+ elif publisher:
652
+ kind = EXECUTIONRESULT
653
+ else:
654
+ kind = PROVIDERCOMMAND
655
+
662
656
  plugin.config['CONTEXT'][KIND_KEY] = kind
657
+ plugin.route('/inbox', methods=['POST'])(process_inbox)
663
658
 
664
659
  if kind == PROVIDERCOMMAND:
665
660
  _maybe_add_hook_watcher(plugin, schema)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: opentf-toolkit-nightly
3
- Version: 0.62.0.dev1347
3
+ Version: 0.62.0.dev1373
4
4
  Summary: OpenTestFactory Orchestrator Toolkit
5
5
  Home-page: https://gitlab.com/henixdevelopment/open-source/opentestfactory/python-toolkit
6
6
  Author: Martin Lafaix
@@ -1,21 +1,22 @@
1
- opentf/commons/__init__.py,sha256=BVz2VMgMa0oCwCBXSLxL8ZTDeAn-CBzcK4rYiHEtVSw,28393
1
+ opentf/commons/__init__.py,sha256=N8GrBU1oL4sDUB4iVF5kPVB__uBlhe4FwD8E5ITV2pI,23011
2
2
  opentf/commons/auth.py,sha256=yUmAoZPk9Aru2UVT5xSjH96u9DOKPk17AeL1_12mjBM,16399
3
- opentf/commons/config.py,sha256=gcge4zou7wMgu3GTyM5vVZudTyEpuyi3JEY8cdCBf5A,10454
3
+ opentf/commons/config.py,sha256=WiXoxuWQUebHrWAIY1fkxpwKb17uCnG29_8cZ2CaCcE,10454
4
4
  opentf/commons/exceptions.py,sha256=7dhUXO8iyAbqVwlUKxZhgRzGqVcb7LkG39hFlm-VxIA,2407
5
5
  opentf/commons/expressions.py,sha256=jM_YKXVOFhvOE2aE2IuacuvxhIsOYTFs2oQkpcbWR6g,19645
6
6
  opentf/commons/meta.py,sha256=ygSO3mE2d-Ux62abzK1wYk86noT4R5Tumd90nyZo0MU,3322
7
7
  opentf/commons/pubsub.py,sha256=M0bvajR9raUP-xe5mfRjdrweZyHQw1_Qsy56gS-Sck4,7676
8
- opentf/commons/schemas.py,sha256=MbUt4XLiFKdyL6g-gKwgnKVUs7f5CXrY89vuqT0ehRM,5128
8
+ opentf/commons/schemas.py,sha256=LT8SlkUcJGtqbnUUDT0U1KsQqTEXjl-ShMny332DkMQ,14042
9
9
  opentf/commons/selectors.py,sha256=2mmnvfZ13KizBQLsIvHXPU0Qtf6hkIvJpYdejNRszUs,7203
10
10
  opentf/schemas/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
11
  opentf/schemas/abac.opentestfactory.org/v1alpha1/Policy.json,sha256=JXsfNAPSEYggeyaDutSQBeG38o4Bmcr70dPLWWeqIh8,2105
12
+ opentf/schemas/config.opentestfactory.org/v1/Descriptor.json,sha256=V0ttnb2V9Yz5vWcVbtrSm0yylYp9eTyzEicuxRxjZu0,9541
12
13
  opentf/schemas/opentestfactory.org/v1/ExecutionCommand.json,sha256=UtmAlTaYdPSZDeQnx-ocX8IonyEPWoNmgDkHwQwQxWE,4126
13
14
  opentf/schemas/opentestfactory.org/v1/ExecutionResult.json,sha256=u0n-WYXNX9d0cEyu2D4tegU35k2Gf3eA6PGs9Uz4QDg,3275
14
15
  opentf/schemas/opentestfactory.org/v1/GeneratorResult.json,sha256=sOwieyDWi6UZR7X29R9IjOR87ruSRQ4U6CM2_yT81Zk,16607
15
16
  opentf/schemas/opentestfactory.org/v1/ProviderCommand.json,sha256=EU4kvEKxlqpH2IkLZ1HdBealuG6C0YzPkAtI7dNrfHg,4427
16
17
  opentf/schemas/opentestfactory.org/v1/ProviderResult.json,sha256=Ej4zhCE3rCqCGKcaeAoIHwSJTV_7fw-rAxhJ52qA-Gs,9641
17
18
  opentf/schemas/opentestfactory.org/v1/Subscription.json,sha256=hNaCUW3qSqqa1LY01Ao7kQ8FJhkjkwMrrPf4xihgSt4,7150
18
- opentf/schemas/opentestfactory.org/v1/Workflow.json,sha256=17EE3VgrjSe_nOufJ2UZbgWbOSnK8Yu4TxIoJjGEkF4,24229
19
+ opentf/schemas/opentestfactory.org/v1/Workflow.json,sha256=5SoyzTHvCxx7ygSK0A8y66VJwNfk2tQkoFuxbvook6Y,24184
19
20
  opentf/schemas/opentestfactory.org/v1/WorkflowCanceled.json,sha256=BCWxnm3rt4VWh9GXkIhdNY9JYohFWxi9Wg3JjXIavaU,1694
20
21
  opentf/schemas/opentestfactory.org/v1/WorkflowCancellation.json,sha256=HAtc8xjrr-WwXwIck9B86BrHELkCb1oPYhTsRzHMMOE,1993
21
22
  opentf/schemas/opentestfactory.org/v1/WorkflowCompleted.json,sha256=KtC9-oeaBNM3D60SFpmYnLMtqewUAGRoOJfVzb7CdHg,1635
@@ -29,7 +30,7 @@ opentf/schemas/opentestfactory.org/v1alpha1/ExecutionResult.json,sha256=UeWc4TfR
29
30
  opentf/schemas/opentestfactory.org/v1alpha1/GeneratorCommand.json,sha256=uxbqDhP4newgz-85TnGKbchx448QEQ8WB5PXpcJy2ME,1754
30
31
  opentf/schemas/opentestfactory.org/v1alpha1/GeneratorResult.json,sha256=LkHLGt2uam1Q5Ux0zP_O9oFgxBMCjD3Th3BsfsXxd1g,6633
31
32
  opentf/schemas/opentestfactory.org/v1alpha1/InsightCollector.json,sha256=mPYt6vuRlW2nq_hOHP1ssk1vXiaOKugzMwRiPm3FzTw,17940
32
- opentf/schemas/opentestfactory.org/v1alpha1/Notification.json,sha256=yx2_JJSTSAP9NVe_dxwc-0Y1StMcEqcqAJohztG5PsQ,8437
33
+ opentf/schemas/opentestfactory.org/v1alpha1/Notification.json,sha256=V-Yd7yQR6r8135cDrnh0W-ugQvtSvKpHQiNRoMP1N9g,8496
33
34
  opentf/schemas/opentestfactory.org/v1alpha1/PluginMetadata.json,sha256=BLklO7CObT4OpAEsQT60WJ1ttOcG71hIYzgN-e7Ch9k,2803
34
35
  opentf/schemas/opentestfactory.org/v1alpha1/ProviderCommand.json,sha256=soe0imdnnq1mfGEpcLJvF3JVUIrF-7FFECc7CzNzobI,2875
35
36
  opentf/schemas/opentestfactory.org/v1alpha1/ProviderConfig.json,sha256=HT0bgPJ5fNytQJr-wxU21oApp4RrjogurgRP-zj_eDs,3878
@@ -56,11 +57,11 @@ opentf/schemas/opentestfactory.org/v1beta1/Workflow.json,sha256=QZ8mM9PhzsI9gTmw
56
57
  opentf/schemas/opentestfactory.org/v1beta2/ServiceConfig.json,sha256=rEvK2YWL5lG94_qYgR_GnLWNsaQhaQ-2kuZdWJr5NnY,3517
57
58
  opentf/scripts/launch_java_service.sh,sha256=S0jAaCuv2sZy0Gf2NGBuPX-eD531rcM-b0fNyhmzSjw,2423
58
59
  opentf/scripts/startup.py,sha256=AcVXU2auPvqMb_6OpGzkVqrpgYV6vz7x_Rnv8YbAEkk,23114
59
- opentf/toolkit/__init__.py,sha256=7MGAfKb5V9ckzWE8ozfj0PcC7g0a-0VfdyrClkWTk38,22319
60
+ opentf/toolkit/__init__.py,sha256=EBTZJ3srbzrLDd4fCmEN5C4NnoZ2kKhpE1aj6oMPtPs,22041
60
61
  opentf/toolkit/channels.py,sha256=6qKSsAgq_oJpuDRiKqVUz-EAjdfikcCG3SFAGmKZdhQ,25551
61
62
  opentf/toolkit/core.py,sha256=8REPRzLWrN50B2VMz8yO-AClgOOt8MsqYdafDw2my48,9608
62
- opentf_toolkit_nightly-0.62.0.dev1347.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
63
- opentf_toolkit_nightly-0.62.0.dev1347.dist-info/METADATA,sha256=9iyT0PpbkoJPaIPZiVj867iz0wm6QV2YRAbUQstQyIA,2214
64
- opentf_toolkit_nightly-0.62.0.dev1347.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
65
- opentf_toolkit_nightly-0.62.0.dev1347.dist-info/top_level.txt,sha256=_gPuE6GTT6UNXy1DjtmQSfCcZb_qYA2vWmjg7a30AGk,7
66
- opentf_toolkit_nightly-0.62.0.dev1347.dist-info/RECORD,,
63
+ opentf_toolkit_nightly-0.62.0.dev1373.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
64
+ opentf_toolkit_nightly-0.62.0.dev1373.dist-info/METADATA,sha256=__vhe-US9DtGBJloCMN9oggHCE8nK9HNRXdxwRhip0I,2214
65
+ opentf_toolkit_nightly-0.62.0.dev1373.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
66
+ opentf_toolkit_nightly-0.62.0.dev1373.dist-info/top_level.txt,sha256=_gPuE6GTT6UNXy1DjtmQSfCcZb_qYA2vWmjg7a30AGk,7
67
+ opentf_toolkit_nightly-0.62.0.dev1373.dist-info/RECORD,,