vgs-cli 0.0.1.dev0__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.
Files changed (56) hide show
  1. vgs_cli-0.0.1.dev0.data/data/vgscli/calm.yaml +16 -0
  2. vgs_cli-0.0.1.dev0.data/data/vgscli/checkout.yaml +21 -0
  3. vgs_cli-0.0.1.dev0.data/data/vgscli/http-route-template.yaml +61 -0
  4. vgs_cli-0.0.1.dev0.data/data/vgscli/mft-route-template.yaml +10 -0
  5. vgs_cli-0.0.1.dev0.data/data/vgscli/payments-admin.yaml +25 -0
  6. vgs_cli-0.0.1.dev0.data/data/vgscli/service-account-schema.yaml +54 -0
  7. vgs_cli-0.0.1.dev0.data/data/vgscli/sub-account-checkout.yaml +23 -0
  8. vgs_cli-0.0.1.dev0.data/data/vgscli/vault-resources.yaml +710 -0
  9. vgs_cli-0.0.1.dev0.data/data/vgscli/vault-schema.yaml +36 -0
  10. vgs_cli-0.0.1.dev0.data/data/vgscli/vault-template.yaml +12 -0
  11. vgs_cli-0.0.1.dev0.data/data/vgscli/vgs-cli.yaml +17 -0
  12. vgs_cli-0.0.1.dev0.dist-info/METADATA +139 -0
  13. vgs_cli-0.0.1.dev0.dist-info/RECORD +56 -0
  14. vgs_cli-0.0.1.dev0.dist-info/WHEEL +5 -0
  15. vgs_cli-0.0.1.dev0.dist-info/entry_points.txt +2 -0
  16. vgs_cli-0.0.1.dev0.dist-info/licenses/LICENSE +22 -0
  17. vgs_cli-0.0.1.dev0.dist-info/top_level.txt +1 -0
  18. vgscli/__init__.py +0 -0
  19. vgscli/_version.py +32 -0
  20. vgscli/access_logs.py +65 -0
  21. vgscli/audits_api.py +102 -0
  22. vgscli/auth.py +68 -0
  23. vgscli/auth_server.py +131 -0
  24. vgscli/auth_utils.py +24 -0
  25. vgscli/callback_server.py +41 -0
  26. vgscli/cert_manager_api.py +34 -0
  27. vgscli/cli/__init__.py +23 -0
  28. vgscli/cli/commands/__init__.py +3 -0
  29. vgscli/cli/commands/apply.py +307 -0
  30. vgscli/cli/commands/generate.py +134 -0
  31. vgscli/cli/commands/get.py +200 -0
  32. vgscli/cli/types/__init__.py +2 -0
  33. vgscli/cli/types/resource_id.py +39 -0
  34. vgscli/cli/types/variable.py +21 -0
  35. vgscli/cli_utils.py +132 -0
  36. vgscli/click_extensions.py +88 -0
  37. vgscli/config_file.py +58 -0
  38. vgscli/errors.py +263 -0
  39. vgscli/file_token_util.py +30 -0
  40. vgscli/id_generator.py +46 -0
  41. vgscli/keyring_token_util.py +128 -0
  42. vgscli/resource-templates/http-route-template.yaml +61 -0
  43. vgscli/resource-templates/mft-route-template.yaml +10 -0
  44. vgscli/resource-templates/service-account/calm.yaml +16 -0
  45. vgscli/resource-templates/service-account/checkout.yaml +21 -0
  46. vgscli/resource-templates/service-account/payments-admin.yaml +25 -0
  47. vgscli/resource-templates/service-account/sub-account-checkout.yaml +23 -0
  48. vgscli/resource-templates/service-account/vgs-cli.yaml +17 -0
  49. vgscli/resource-templates/vault-template.yaml +12 -0
  50. vgscli/testing.py +48 -0
  51. vgscli/text.py +9 -0
  52. vgscli/token_handler.py +11 -0
  53. vgscli/validation-schemas/service-account-schema.yaml +54 -0
  54. vgscli/validation-schemas/vault-resources.yaml +710 -0
  55. vgscli/validation-schemas/vault-schema.yaml +36 -0
  56. vgscli/vgs.py +249 -0
@@ -0,0 +1,200 @@
1
+ import click
2
+ from click_plugins import with_plugins
3
+ from simple_rest_client.exceptions import ClientError, ServerError
4
+ from vgs.sdk.routes import dump_all_routes, normalize
5
+ from vgs.sdk.serializers import yaml # this one correctly outputs blocks
6
+ from vgs.sdk.vaults_api import create_api as create_vault_mgmt_api_routes
7
+
8
+ from vgscli.auth import handshake, token_util
9
+ from vgscli.cert_manager_api import create_cert_manager_api
10
+ from vgscli.cli import create_account_mgmt_api, create_vault_mgmt_api
11
+ from vgscli.cli.types import ResourceId, ResourceIdParamType
12
+ from vgscli.cli_utils import dump_camelized_yaml, iter_entry_points
13
+ from vgscli.errors import ServiceClientListingError, handle_errors
14
+
15
+ from .apply import wrap_in_http_envelope
16
+
17
+
18
+ @with_plugins(iter_entry_points("vgs.get.plugins"))
19
+ @click.group("get")
20
+ def get() -> None:
21
+ """
22
+ Get VGS resource.
23
+ """
24
+ pass
25
+
26
+
27
+ @get.command("service-accounts")
28
+ @click.option(
29
+ "-O",
30
+ "--organization",
31
+ "org_id",
32
+ type=ResourceIdParamType(prefix="AC"),
33
+ help="Organization ID which service accounts will be listed",
34
+ )
35
+ @click.pass_context
36
+ @handle_errors()
37
+ def get_service_accounts(ctx: click.Context, org_id: ResourceId):
38
+ """
39
+ Get service accounts from the organization.
40
+ """
41
+
42
+ account_mgmt = create_account_mgmt_api(ctx)
43
+ try:
44
+ # noinspection PyUnresolvedReferences
45
+ response = account_mgmt.service_accounts.get(org_id.base58)
46
+ except (ClientError, ServerError) as e:
47
+ raise ServiceClientListingError(e)
48
+
49
+ accounts = response.body["data"]["attributes"]["service_accounts"]
50
+
51
+ for account in accounts:
52
+ click.echo("---")
53
+ click.echo(
54
+ dump_camelized_yaml(
55
+ {
56
+ "apiVersion": "1.0.0",
57
+ "kind": "ServiceAccount",
58
+ "data": account,
59
+ }
60
+ )
61
+ )
62
+
63
+
64
+ @get.command("access-credentials")
65
+ @click.option("--vault", "-V", help="Vault ID", required=True)
66
+ @click.pass_context
67
+ @handle_errors()
68
+ def get_access_credentials(ctx, vault):
69
+ """
70
+ Get access-credentials
71
+ """
72
+
73
+ account_mgmt = create_account_mgmt_api(ctx)
74
+
75
+ response = account_mgmt.vaults.get_by_id(vault)
76
+
77
+ vault_mgmt = create_vault_mgmt_api(
78
+ ctx, response.body["data"][0]["links"]["vault_management_api"]
79
+ )
80
+
81
+ response = vault_mgmt.credentials.list(headers={"VGS-Tenant": vault})
82
+
83
+ click.echo(
84
+ dump_camelized_yaml(
85
+ {
86
+ "apiVersion": "1.0.0",
87
+ "kind": "AccessCredentials",
88
+ "data": response.body["data"],
89
+ }
90
+ )
91
+ )
92
+
93
+
94
+ @get.command("organizations")
95
+ @click.pass_context
96
+ @handle_errors()
97
+ def get_organizations(ctx):
98
+ """
99
+ Get organizations
100
+ """
101
+
102
+ account_mgmt = create_account_mgmt_api(ctx)
103
+
104
+ response = account_mgmt.organizations.list()
105
+
106
+ click.echo(
107
+ dump_camelized_yaml(
108
+ {
109
+ "apiVersion": "1.0.0",
110
+ "kind": "Organizations",
111
+ "data": response.body["data"],
112
+ }
113
+ )
114
+ )
115
+
116
+
117
+ @get.command("vaults")
118
+ @click.pass_context
119
+ @handle_errors()
120
+ def get_vaults(ctx):
121
+ """
122
+ Get vaults
123
+ """
124
+
125
+ account_mgmt = create_account_mgmt_api(ctx)
126
+
127
+ response = account_mgmt.vaults.list()
128
+
129
+ click.echo(
130
+ dump_camelized_yaml(
131
+ {
132
+ "apiVersion": "1.0.0",
133
+ "kind": "Vaults",
134
+ "data": response.body["data"],
135
+ }
136
+ )
137
+ )
138
+
139
+
140
+ @get.command("http-routes")
141
+ @click.option("--vault", "-V", help="Vault ID", required=True)
142
+ @click.pass_context
143
+ @handle_errors()
144
+ def get_http_routes(ctx, vault):
145
+ """
146
+ Get HTTP routes
147
+ """
148
+
149
+ handshake(ctx, ctx.obj.env)
150
+
151
+ vault_management_api = create_vault_mgmt_api_routes(
152
+ ctx, vault, ctx.obj.env, token_util.get_access_token()
153
+ )
154
+
155
+ response = vault_management_api.routes.list()
156
+ routes = normalize(response.body["data"])
157
+
158
+ # for each route, wrap in envelope and return
159
+ wrapped = [wrap_in_http_envelope(route) for route in routes]
160
+
161
+ click.echo(yaml.dump_all(wrapped, indent=2))
162
+
163
+
164
+ @get.command("routes")
165
+ @click.option("--vault", "-V", help="Vault ID", required=True)
166
+ @click.pass_context
167
+ @handle_errors()
168
+ def get_routes(ctx, vault):
169
+ """
170
+ Get routes
171
+ """
172
+ handshake(ctx, ctx.obj.env)
173
+
174
+ routes_api = create_vault_mgmt_api_routes(
175
+ ctx, vault, ctx.obj.env, token_util.get_access_token()
176
+ )
177
+ dump = dump_all_routes(routes_api)
178
+ click.echo(dump)
179
+
180
+
181
+ @get.command("certificates")
182
+ @click.option("--vault", "-V", help="Vault ID", required=True)
183
+ @click.pass_context
184
+ @handle_errors()
185
+ def get_certificates(ctx, vault):
186
+ """
187
+ Get certificates for a vault.
188
+ """
189
+ token = token_util.get_access_token()
190
+ cert_api = create_cert_manager_api(vault, ctx.obj.env, token)
191
+ response = cert_api.certificates.list(params={"vault_identifier": vault})
192
+ click.echo(
193
+ dump_camelized_yaml(
194
+ {
195
+ "apiVersion": "1.0.0",
196
+ "kind": "Certificates",
197
+ "data": response.body,
198
+ }
199
+ )
200
+ )
@@ -0,0 +1,2 @@
1
+ from .resource_id import ResourceId, ResourceIdParamType
2
+ from .variable import Variable, VariableParamType
@@ -0,0 +1,39 @@
1
+ import click
2
+
3
+ from vgscli.id_generator import base58_to_uuid, uuid_to_base58
4
+
5
+
6
+ class ResourceId:
7
+ """
8
+ ID of a VGS resource as shown on the dashboard (e.g., ACtELxZxTcXrAMk2Gp8Qp5yh).
9
+ """
10
+
11
+ def __init__(self, prefix: str, uuid_string: str):
12
+ self.uuid = uuid_string
13
+ self._base58 = uuid_to_base58(uuid_string, prefix)
14
+
15
+ @property
16
+ def base58(self) -> str:
17
+ return self._base58
18
+
19
+ @staticmethod
20
+ def decode_base58(prefix: str, base58_string: str) -> "ResourceId":
21
+ uuid_string = base58_to_uuid(base58_string)
22
+ return ResourceId(prefix, uuid_string)
23
+
24
+
25
+ class ResourceIdParamType(click.ParamType):
26
+ name = "resource_id"
27
+
28
+ def __init__(self, **kwargs):
29
+ self.prefix = kwargs.get("prefix", "")
30
+
31
+ def convert(self, value: str, param, ctx) -> ResourceId:
32
+ try:
33
+ if value.startswith(self.prefix):
34
+ base58_string = value[len(self.prefix) :]
35
+ return ResourceId.decode_base58(self.prefix, base58_string)
36
+
37
+ return ResourceId(self.prefix, value)
38
+ except ValueError:
39
+ self.fail(f"{value!r} is not a valid organization ID", param, ctx)
@@ -0,0 +1,21 @@
1
+ from typing import Any
2
+
3
+ import click
4
+
5
+
6
+ class Variable:
7
+ """
8
+ A parsed "name=value" pair.
9
+ """
10
+
11
+ def __init__(self, name: str, value: Any):
12
+ self.name = name
13
+ self.value = value
14
+
15
+
16
+ class VariableParamType(click.ParamType):
17
+ name = "name=value"
18
+
19
+ def convert(self, value: str, param, ctx) -> Variable:
20
+ tokens = value.split("=", 2)
21
+ return Variable(tokens[0], tokens[1])
vgscli/cli_utils.py ADDED
@@ -0,0 +1,132 @@
1
+ import logging
2
+ import os
3
+ import uuid
4
+ from collections import OrderedDict
5
+ from importlib.metadata import entry_points
6
+
7
+ import humps
8
+ import jsonschema
9
+ import yaml
10
+ from vgs.sdk.serializers import dump_yaml
11
+ from vgs.sdk.utils import read_file
12
+
13
+ from vgscli.errors import NoSuchFileOrDirectoryError, SchemaValidationError
14
+ from vgscli.id_generator import uuid_to_base58
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ def dump_camelized_yaml(payload):
20
+ """
21
+ Transform snake_case to camelCase and dump as a yaml document.
22
+ """
23
+ return dump_yaml(OrderedDict(humps.camelize(payload))).rstrip()
24
+
25
+
26
+ def validate_yaml(file, schema_path, schema_root=os.path.dirname(__file__)):
27
+ """
28
+ Validates the file against the schema.
29
+
30
+ Parameters
31
+ ----------
32
+ file: is the buffered content of the file
33
+ schema_path: is the path relative to the working directory
34
+ schema_root: was added to enable validating from different working directories like vgs-admin-cli-plugin
35
+ """
36
+ try:
37
+ schema = read_file(schema_path, schema_root)
38
+ file_content = yaml.full_load(file.read())
39
+
40
+ jsonschema.validate(file_content, yaml.full_load(schema))
41
+
42
+ return file_content
43
+ except jsonschema.exceptions.ValidationError as e:
44
+ raise SchemaValidationError(str(e))
45
+
46
+
47
+ def validate_multi_yaml(file_to_validate, path_to_schema):
48
+ schema_file = read_file(path_to_schema, os.path.dirname(__file__))
49
+ file_content = yaml.load_all(file_to_validate.read(), Loader=yaml.FullLoader)
50
+ schemas = list(yaml.load_all(schema_file, Loader=yaml.FullLoader))
51
+ for file in file_content:
52
+ if not file:
53
+ logger.debug("Skipping blank file")
54
+ continue
55
+ failures = [
56
+ x for x in [fails_validation(file, schema) for schema in schemas] if x
57
+ ]
58
+ if len(failures) == len(schemas):
59
+ for failure in failures:
60
+ logger.exception(failure)
61
+ raise SchemaValidationError(
62
+ f"Failed to validate {file['apiVersion']} {file['kind']} {file['metadata']['name']} against any known schema."
63
+ )
64
+ logger.debug(
65
+ f"Validated {file['apiVersion']} {file['kind']} {file['metadata']['name']}"
66
+ )
67
+ yield file
68
+
69
+
70
+ def fails_validation(file, schema):
71
+ logger.info(
72
+ f"Validating version={file['apiVersion']} kind={file['kind']} name={file['metadata']['name']}\nagainst\n\t{schema}"
73
+ )
74
+ try:
75
+ jsonschema.validate(file, schema)
76
+ except jsonschema.exceptions.ValidationError as ex:
77
+ return ex
78
+ return None
79
+
80
+
81
+ def read_file(file_path, file_root=os.path.dirname(__file__)):
82
+ full_path = os.path.join(file_root, file_path)
83
+ try:
84
+ with open(full_path, "r") as f:
85
+ schema = f.read()
86
+ f.close()
87
+ return schema
88
+ except FileNotFoundError:
89
+ raise NoSuchFileOrDirectoryError(full_path)
90
+
91
+
92
+ def is_valid_uuid(uuid_to_test, version=4):
93
+ """
94
+ Check if uuid_to_test is a valid UUID.
95
+
96
+ Parameters
97
+ ----------
98
+ uuid_to_test : str
99
+ version : {1, 2, 3, 4}
100
+
101
+ Returns
102
+ -------
103
+ `True` if uuid_to_test is a valid UUID, otherwise `False`.
104
+
105
+ Examples
106
+ --------
107
+ >>> is_valid_uuid('c9bf9e57-1685-4c89-bafb-ff5af830be8a')
108
+ True
109
+ >>> is_valid_uuid('c9bf9e58')
110
+ False
111
+ """
112
+ # noinspection PyBroadException
113
+ try:
114
+ uuid_obj = uuid.UUID(uuid_to_test, version=version)
115
+ except Exception:
116
+ return False
117
+
118
+ return str(uuid_obj) == uuid_to_test
119
+
120
+
121
+ def format_org_id(org_id):
122
+ if is_valid_uuid(org_id):
123
+ org_id = uuid_to_base58(org_id, "AC")
124
+ return org_id
125
+
126
+
127
+ def iter_entry_points(group):
128
+ eps = entry_points()
129
+ if hasattr(eps, "select"):
130
+ return eps.select(group=group)
131
+ else:
132
+ return [ep for ep in eps.get(group, [])]
@@ -0,0 +1,88 @@
1
+ from datetime import datetime, timedelta
2
+
3
+ import click
4
+
5
+
6
+ class Config(object):
7
+ def __init__(self, debug, env):
8
+ self.debug = debug
9
+ self.env = env
10
+
11
+
12
+ # https://gist.github.com/jacobtolar/fb80d5552a9a9dfc32b12a829fa21c0c#file-click_mutually_exclusive_argument-py-L4
13
+ class MutuallyExclusiveOption(click.Option):
14
+ def __init__(self, *args, **kwargs):
15
+ self.mutually_exclusive = set(kwargs.pop("mutually_exclusive", []))
16
+ help_cmd = kwargs.get("help", "")
17
+ if self.mutually_exclusive:
18
+ ex_str = ", ".join(self.mutually_exclusive)
19
+ kwargs["help"] = help_cmd + (
20
+ " NOTE: This argument is mutually exclusive with "
21
+ " arguments: [" + ex_str + "]."
22
+ )
23
+ super(MutuallyExclusiveOption, self).__init__(*args, **kwargs)
24
+
25
+ def handle_parse_result(self, ctx, opts, args):
26
+ if self.mutually_exclusive.intersection(opts) and self.name in opts:
27
+ raise click.UsageError(
28
+ "Illegal usage: `{}` is mutually exclusive with "
29
+ "arguments `{}`.".format(self.name, ", ".join(self.mutually_exclusive))
30
+ )
31
+
32
+ return super(MutuallyExclusiveOption, self).handle_parse_result(ctx, opts, args)
33
+
34
+
35
+ class Duration(click.ParamType):
36
+ name = "duration"
37
+
38
+ def convert(self, value, param, ctx):
39
+ try:
40
+ delta_unites = {"h": "hours", "m": "minutes", "s": "seconds"}
41
+ unit = delta_unites[value[-1:].lower()]
42
+ amount = int(value[:-1])
43
+ delta_options = {unit: amount}
44
+ return (datetime.utcnow() - timedelta(**delta_options)).strftime(
45
+ "%Y-%m-%dT%H:%M:%S"
46
+ )
47
+ except TypeError:
48
+ self.fail(
49
+ "expected string for int() conversion, got "
50
+ f"{value!r} of type {type(value).__name__}",
51
+ param,
52
+ ctx,
53
+ )
54
+ except ValueError:
55
+ self.fail(f"{value!r} is not a valid integer", param, ctx)
56
+ except KeyError:
57
+ self.fail("Only integers that end with h, m or s are allowed", param, ctx)
58
+
59
+
60
+ DURATION = Duration()
61
+
62
+
63
+ class DateTimeDuration(click.ParamType):
64
+ name = "dateduration"
65
+
66
+ def __init__(self, formats=None):
67
+ self.formats = formats or ["%Y-%m-%d", "%Y-%m-%dT%H:%M:%S", "%Y-%m-%d %H:%M:%S"]
68
+
69
+ @staticmethod
70
+ def _try_to_parse_date(value, pattern):
71
+ try:
72
+ return datetime.strptime(value, pattern)
73
+ except ValueError:
74
+ return None
75
+
76
+ def convert(self, value, param, ctx):
77
+ for pattern in self.formats:
78
+ if self._try_to_parse_date(value, pattern):
79
+ return value
80
+
81
+ if len(str(value)) > 7:
82
+ return self.fail(
83
+ "invalid date format: {}. (choose from {})".format(
84
+ value, ", ".join(self.formats)
85
+ )
86
+ )
87
+
88
+ return DURATION.convert(value, param, ctx)
vgscli/config_file.py ADDED
@@ -0,0 +1,58 @@
1
+ import functools
2
+ import os
3
+
4
+ import click
5
+ import configobj
6
+
7
+ APP_DIR = click.get_app_dir("vgs", force_posix=True)
8
+ OPTION_NAME = "--config"
9
+
10
+
11
+ class ConfigObjProvider:
12
+ """
13
+ When invoked, reads a config file and returns its content as a dictionary.
14
+ """
15
+
16
+ def __init__(self, section=None):
17
+ self.section = section
18
+
19
+ def __call__(self, *args, **kwargs):
20
+ config = configobj.ConfigObj(args[0])
21
+ if self.section:
22
+ config = config[self.section] if self.section in config else {}
23
+ return config
24
+
25
+
26
+ # noinspection PyUnusedLocal
27
+ def callback(provider, ctx: click.Context, param: click.Parameter, value: str):
28
+ """
29
+ Change the execution flow to populate the default map first.
30
+ """
31
+ ctx.default_map = ctx.default_map or {}
32
+
33
+ if value:
34
+ try:
35
+ config = provider(value)
36
+ except Exception as cause:
37
+ raise click.BadOptionUsage(
38
+ OPTION_NAME, f"Failed to read configuration file: {cause}", ctx
39
+ )
40
+ ctx.default_map.update(config)
41
+
42
+ return value
43
+
44
+
45
+ def configuration_option(section=None):
46
+ def decorator(f):
47
+ return click.option(
48
+ OPTION_NAME,
49
+ callback=functools.partial(callback, ConfigObjProvider(section)),
50
+ default=os.path.join(APP_DIR, "config"),
51
+ envvar="VGS_CONFIG_FILE",
52
+ expose_value=False,
53
+ help="Read configuration from FILE.",
54
+ is_eager=True,
55
+ type=click.Path(dir_okay=False),
56
+ )(f)
57
+
58
+ return decorator