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.
- vgs_cli-0.0.1.dev0.data/data/vgscli/calm.yaml +16 -0
- vgs_cli-0.0.1.dev0.data/data/vgscli/checkout.yaml +21 -0
- vgs_cli-0.0.1.dev0.data/data/vgscli/http-route-template.yaml +61 -0
- vgs_cli-0.0.1.dev0.data/data/vgscli/mft-route-template.yaml +10 -0
- vgs_cli-0.0.1.dev0.data/data/vgscli/payments-admin.yaml +25 -0
- vgs_cli-0.0.1.dev0.data/data/vgscli/service-account-schema.yaml +54 -0
- vgs_cli-0.0.1.dev0.data/data/vgscli/sub-account-checkout.yaml +23 -0
- vgs_cli-0.0.1.dev0.data/data/vgscli/vault-resources.yaml +710 -0
- vgs_cli-0.0.1.dev0.data/data/vgscli/vault-schema.yaml +36 -0
- vgs_cli-0.0.1.dev0.data/data/vgscli/vault-template.yaml +12 -0
- vgs_cli-0.0.1.dev0.data/data/vgscli/vgs-cli.yaml +17 -0
- vgs_cli-0.0.1.dev0.dist-info/METADATA +139 -0
- vgs_cli-0.0.1.dev0.dist-info/RECORD +56 -0
- vgs_cli-0.0.1.dev0.dist-info/WHEEL +5 -0
- vgs_cli-0.0.1.dev0.dist-info/entry_points.txt +2 -0
- vgs_cli-0.0.1.dev0.dist-info/licenses/LICENSE +22 -0
- vgs_cli-0.0.1.dev0.dist-info/top_level.txt +1 -0
- vgscli/__init__.py +0 -0
- vgscli/_version.py +32 -0
- vgscli/access_logs.py +65 -0
- vgscli/audits_api.py +102 -0
- vgscli/auth.py +68 -0
- vgscli/auth_server.py +131 -0
- vgscli/auth_utils.py +24 -0
- vgscli/callback_server.py +41 -0
- vgscli/cert_manager_api.py +34 -0
- vgscli/cli/__init__.py +23 -0
- vgscli/cli/commands/__init__.py +3 -0
- vgscli/cli/commands/apply.py +307 -0
- vgscli/cli/commands/generate.py +134 -0
- vgscli/cli/commands/get.py +200 -0
- vgscli/cli/types/__init__.py +2 -0
- vgscli/cli/types/resource_id.py +39 -0
- vgscli/cli/types/variable.py +21 -0
- vgscli/cli_utils.py +132 -0
- vgscli/click_extensions.py +88 -0
- vgscli/config_file.py +58 -0
- vgscli/errors.py +263 -0
- vgscli/file_token_util.py +30 -0
- vgscli/id_generator.py +46 -0
- vgscli/keyring_token_util.py +128 -0
- vgscli/resource-templates/http-route-template.yaml +61 -0
- vgscli/resource-templates/mft-route-template.yaml +10 -0
- vgscli/resource-templates/service-account/calm.yaml +16 -0
- vgscli/resource-templates/service-account/checkout.yaml +21 -0
- vgscli/resource-templates/service-account/payments-admin.yaml +25 -0
- vgscli/resource-templates/service-account/sub-account-checkout.yaml +23 -0
- vgscli/resource-templates/service-account/vgs-cli.yaml +17 -0
- vgscli/resource-templates/vault-template.yaml +12 -0
- vgscli/testing.py +48 -0
- vgscli/text.py +9 -0
- vgscli/token_handler.py +11 -0
- vgscli/validation-schemas/service-account-schema.yaml +54 -0
- vgscli/validation-schemas/vault-resources.yaml +710 -0
- vgscli/validation-schemas/vault-schema.yaml +36 -0
- vgscli/vgs.py +249 -0
vgscli/auth_server.py
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import threading
|
|
3
|
+
import time
|
|
4
|
+
import webbrowser
|
|
5
|
+
from urllib.parse import urlencode
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
from vgs.sdk import auth_api
|
|
9
|
+
from vgs.sdk.utils import is_port_accessible
|
|
10
|
+
|
|
11
|
+
from vgscli.auth_utils import code_challenge, generate_code_verifier
|
|
12
|
+
from vgscli.callback_server import RequestServer
|
|
13
|
+
from vgscli.keyring_token_util import KeyringTokenUtil
|
|
14
|
+
from vgscli.token_handler import CodeHandler
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class AuthServer:
|
|
18
|
+
env_url = {
|
|
19
|
+
"dev": "https://auth.verygoodsecurity.io",
|
|
20
|
+
"prod": "https://auth.verygoodsecurity.com",
|
|
21
|
+
}
|
|
22
|
+
token_util = KeyringTokenUtil()
|
|
23
|
+
token_handler = CodeHandler()
|
|
24
|
+
|
|
25
|
+
# Api
|
|
26
|
+
CLIENT_ID = "vgs-cli-public"
|
|
27
|
+
SCOPES = "idp openid"
|
|
28
|
+
AUTH_URL = "{base_url}/auth/realms/vgs/protocol/openid-connect/auth"
|
|
29
|
+
CALLBACK_PATH = "/callback"
|
|
30
|
+
|
|
31
|
+
# AuthZ
|
|
32
|
+
code_verifier = generate_code_verifier()
|
|
33
|
+
code_method = "S256"
|
|
34
|
+
oauth_access_token = None
|
|
35
|
+
|
|
36
|
+
# Server constants.
|
|
37
|
+
# Ports have been chosen based on Unassigned port list: https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml?&page=111
|
|
38
|
+
ports = [7745, 8390, 9056]
|
|
39
|
+
host = os.getenv("AUTH_SERVER_BIND_IP", "127.0.0.1")
|
|
40
|
+
accessible_port = None
|
|
41
|
+
app = None
|
|
42
|
+
|
|
43
|
+
def __init__(self, environment):
|
|
44
|
+
self.accessible_port = next(
|
|
45
|
+
port for port in self.ports if is_port_accessible(self.host, port)
|
|
46
|
+
)
|
|
47
|
+
self.app = RequestServer(self.host, self.accessible_port)
|
|
48
|
+
self.environment = environment
|
|
49
|
+
self.auth_api = auth_api.create_api(environment)
|
|
50
|
+
|
|
51
|
+
def login(self, environment, **kwargs):
|
|
52
|
+
thread = self.ServerThread(self.app)
|
|
53
|
+
thread.daemon = True
|
|
54
|
+
thread.start()
|
|
55
|
+
|
|
56
|
+
query = {
|
|
57
|
+
"client_id": self.CLIENT_ID,
|
|
58
|
+
"code_challenge": code_challenge(self.code_verifier),
|
|
59
|
+
"code_challenge_method": self.code_method,
|
|
60
|
+
"redirect_uri": self.__get_host() + "/callback",
|
|
61
|
+
"response_type": "code",
|
|
62
|
+
"scope": self.SCOPES,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
idp = kwargs.get("idp")
|
|
66
|
+
if idp:
|
|
67
|
+
query["kc_idp_hint"] = idp
|
|
68
|
+
|
|
69
|
+
url = (
|
|
70
|
+
self.AUTH_URL.format(base_url=self.env_url[environment])
|
|
71
|
+
+ f"?{urlencode(query)}"
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
if kwargs.get("open_browser", True):
|
|
75
|
+
if not webbrowser.open(url, new=1, autoraise=True):
|
|
76
|
+
click.echo(
|
|
77
|
+
f"Could not open the default browser. Follow the link below to log in:\n{url}"
|
|
78
|
+
)
|
|
79
|
+
else:
|
|
80
|
+
click.echo(f"Follow the link below to log in:\n{url}")
|
|
81
|
+
|
|
82
|
+
while self.token_handler.get_code() is None:
|
|
83
|
+
time.sleep(1)
|
|
84
|
+
self.retrieve_access_token()
|
|
85
|
+
|
|
86
|
+
return self.token_util.get_access_token()
|
|
87
|
+
|
|
88
|
+
def logout(self):
|
|
89
|
+
auth_api.logout(
|
|
90
|
+
self.auth_api,
|
|
91
|
+
self.CLIENT_ID,
|
|
92
|
+
self.token_util.get_access_token(),
|
|
93
|
+
self.token_util.get_refresh_token(),
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
def refresh_authentication(self):
|
|
97
|
+
self.token_util.put_tokens(
|
|
98
|
+
auth_api.refresh_token(
|
|
99
|
+
self.auth_api, refresh_token=self.token_util.get_refresh_token()
|
|
100
|
+
).body
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
def retrieve_access_token(self):
|
|
104
|
+
callback_url = self.__get_host() + self.CALLBACK_PATH
|
|
105
|
+
response = auth_api.get_token(
|
|
106
|
+
self.auth_api,
|
|
107
|
+
self.token_handler.get_code(),
|
|
108
|
+
self.code_verifier,
|
|
109
|
+
callback_url,
|
|
110
|
+
)
|
|
111
|
+
self.set_access_token(response.body)
|
|
112
|
+
|
|
113
|
+
def set_access_token(self, token):
|
|
114
|
+
self.token_util.put_tokens(token)
|
|
115
|
+
|
|
116
|
+
def __get_host(self):
|
|
117
|
+
return "http://" + self.host + ":" + str(self.accessible_port)
|
|
118
|
+
|
|
119
|
+
def client_credentials_login(self, client_id, secret):
|
|
120
|
+
response = auth_api.get_auto_token(
|
|
121
|
+
self.auth_api, client_id=client_id, client_secret=secret
|
|
122
|
+
)
|
|
123
|
+
self.set_access_token(response.body)
|
|
124
|
+
|
|
125
|
+
class ServerThread(threading.Thread):
|
|
126
|
+
def __init__(self, app):
|
|
127
|
+
self.app = app
|
|
128
|
+
threading.Thread.__init__(self)
|
|
129
|
+
|
|
130
|
+
def run(self):
|
|
131
|
+
self.app.run()
|
vgscli/auth_utils.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import hashlib
|
|
3
|
+
import random
|
|
4
|
+
import string
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def generate_code_verifier(stringLength=64):
|
|
8
|
+
password_characters = string.ascii_letters + string.digits
|
|
9
|
+
return "".join(random.choice(password_characters) for i in range(stringLength))
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def code_challenge(code_verifier):
|
|
13
|
+
sha_signature = __sha256(code_verifier.encode())
|
|
14
|
+
return __base64_url_encode(sha_signature).decode("UTF-8").split("=")[0]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def __sha256(buffer):
|
|
18
|
+
m = hashlib.sha256()
|
|
19
|
+
m.update(buffer)
|
|
20
|
+
return m.digest()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def __base64_url_encode(random_bytes):
|
|
24
|
+
return base64.urlsafe_b64encode(random_bytes)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
2
|
+
from urllib import parse
|
|
3
|
+
|
|
4
|
+
from vgscli.keyring_token_util import KeyringTokenUtil
|
|
5
|
+
from vgscli.token_handler import CodeHandler
|
|
6
|
+
|
|
7
|
+
token_util = KeyringTokenUtil()
|
|
8
|
+
token_handler = CodeHandler()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class RequestHandler(BaseHTTPRequestHandler):
|
|
12
|
+
def do_GET(self):
|
|
13
|
+
if self.path.startswith("/callback"):
|
|
14
|
+
params = parse.parse_qs(self.path.split("?")[1])
|
|
15
|
+
token = params["code"][0]
|
|
16
|
+
token_handler.put_code(token)
|
|
17
|
+
self.send_response(200)
|
|
18
|
+
self.end_headers()
|
|
19
|
+
self.wfile.write("Go back to terminal".encode("utf-8"))
|
|
20
|
+
return
|
|
21
|
+
|
|
22
|
+
def log_message(self, format, *args):
|
|
23
|
+
return
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class RequestServer:
|
|
27
|
+
|
|
28
|
+
server = None
|
|
29
|
+
host = "localhost"
|
|
30
|
+
port = 8080
|
|
31
|
+
|
|
32
|
+
def __init__(self, host, port):
|
|
33
|
+
self.host = host
|
|
34
|
+
self.port = port
|
|
35
|
+
|
|
36
|
+
def run(self):
|
|
37
|
+
self.server = HTTPServer((self.host, self.port), RequestHandler)
|
|
38
|
+
self.server.serve_forever()
|
|
39
|
+
|
|
40
|
+
def close(self):
|
|
41
|
+
self.server.server_close()
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from simple_rest_client.api import API
|
|
2
|
+
from simple_rest_client.resource import Resource
|
|
3
|
+
|
|
4
|
+
from vgscli._version import __version__
|
|
5
|
+
|
|
6
|
+
CERT_MANAGER_URLS = {
|
|
7
|
+
"dev": "https://cert-manager-api.verygoodsecurity.io",
|
|
8
|
+
"prod": "https://cert-manager.apps.verygoodvault.com",
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CertificatesResource(Resource):
|
|
13
|
+
actions = {"list": {"method": "GET", "url": "api/certificates"}}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def create_cert_manager_api(vault_id, environment, token):
|
|
17
|
+
env = (environment or "prod").lower()
|
|
18
|
+
base_url = CERT_MANAGER_URLS.get(env, CERT_MANAGER_URLS["prod"])
|
|
19
|
+
api = API(
|
|
20
|
+
api_root_url=base_url,
|
|
21
|
+
params={},
|
|
22
|
+
headers={
|
|
23
|
+
"VGS-Tenant": vault_id,
|
|
24
|
+
"Content-Type": "application/json",
|
|
25
|
+
"Accept": "application/json",
|
|
26
|
+
"User-Agent": f"VGS CLI {__version__}",
|
|
27
|
+
"Authorization": f"Bearer {token}",
|
|
28
|
+
},
|
|
29
|
+
timeout=30,
|
|
30
|
+
append_slash=False,
|
|
31
|
+
json_encode_body=True,
|
|
32
|
+
)
|
|
33
|
+
api.add_resource(resource_name="certificates", resource_class=CertificatesResource)
|
|
34
|
+
return api
|
vgscli/cli/__init__.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import click
|
|
2
|
+
from vgs.sdk.account_mgmt import AccountMgmtAPI
|
|
3
|
+
from vgs.sdk.vault_mgmt import VaultMgmtAPI
|
|
4
|
+
|
|
5
|
+
from vgscli.auth import handshake, token_util
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def create_account_mgmt_api(ctx: click.Context) -> AccountMgmtAPI:
|
|
9
|
+
environment = ctx.obj.env
|
|
10
|
+
|
|
11
|
+
handshake(ctx, environment)
|
|
12
|
+
access_token = token_util.get_access_token()
|
|
13
|
+
|
|
14
|
+
return AccountMgmtAPI(access_token, environment)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def create_vault_mgmt_api(ctx: click.Context, root_url: str) -> VaultMgmtAPI:
|
|
18
|
+
environment = ctx.obj.env
|
|
19
|
+
|
|
20
|
+
handshake(ctx, environment)
|
|
21
|
+
access_token = token_util.get_access_token()
|
|
22
|
+
|
|
23
|
+
return VaultMgmtAPI(access_token, root_url)
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import time
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
from click_plugins import with_plugins
|
|
7
|
+
from simple_rest_client import exceptions
|
|
8
|
+
from simple_rest_client.exceptions import ClientError
|
|
9
|
+
from vgs.sdk.errors import RouteNotValidError
|
|
10
|
+
from vgs.sdk.routes import dump_yaml, normalize, sync_all_routes
|
|
11
|
+
from vgs.sdk.vaults_api import create_api as create_vaults_api
|
|
12
|
+
|
|
13
|
+
from vgscli.auth import handshake, token_util
|
|
14
|
+
from vgscli.cli import create_account_mgmt_api, create_vault_mgmt_api
|
|
15
|
+
from vgscli.cli.types import ResourceId, ResourceIdParamType
|
|
16
|
+
from vgscli.cli_utils import (
|
|
17
|
+
dump_camelized_yaml,
|
|
18
|
+
iter_entry_points,
|
|
19
|
+
validate_multi_yaml,
|
|
20
|
+
validate_yaml,
|
|
21
|
+
)
|
|
22
|
+
from vgscli.errors import ServiceClientCreationError, VgsCliError, handle_errors
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@with_plugins(iter_entry_points("vgs.apply.plugins"))
|
|
28
|
+
@click.group("apply")
|
|
29
|
+
def apply() -> None:
|
|
30
|
+
"""
|
|
31
|
+
Create or update a VGS resource.
|
|
32
|
+
"""
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@apply.command("service-account")
|
|
37
|
+
@click.option(
|
|
38
|
+
"-O",
|
|
39
|
+
"--organization",
|
|
40
|
+
"org_id",
|
|
41
|
+
type=ResourceIdParamType(prefix="AC"),
|
|
42
|
+
help="ID of the organization to associate the vault with.",
|
|
43
|
+
)
|
|
44
|
+
@click.option(
|
|
45
|
+
"--file",
|
|
46
|
+
"-f",
|
|
47
|
+
type=click.File(),
|
|
48
|
+
help="Configuration to apply.",
|
|
49
|
+
required=True,
|
|
50
|
+
)
|
|
51
|
+
@click.pass_context
|
|
52
|
+
@handle_errors()
|
|
53
|
+
def apply_service_account(ctx: click.Context, org_id: ResourceId, file) -> None:
|
|
54
|
+
"""
|
|
55
|
+
Create a Service Account client.
|
|
56
|
+
"""
|
|
57
|
+
data = validate_yaml(file, "validation-schemas/service-account-schema.yaml")["data"]
|
|
58
|
+
|
|
59
|
+
account_mgmt = create_account_mgmt_api(ctx)
|
|
60
|
+
try:
|
|
61
|
+
# noinspection PyUnresolvedReferences
|
|
62
|
+
response = account_mgmt.service_accounts.create(
|
|
63
|
+
org_id.base58,
|
|
64
|
+
body={
|
|
65
|
+
"data": {
|
|
66
|
+
"attributes": {
|
|
67
|
+
"name": data["name"],
|
|
68
|
+
"annotations": data.pop("annotations", {}),
|
|
69
|
+
"vaults": data.get("vaults", []),
|
|
70
|
+
"scopes": data["scopes"],
|
|
71
|
+
"access_token_lifespan": data.get("accessTokenLifespan", None),
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
)
|
|
76
|
+
except ClientError as cause:
|
|
77
|
+
raise ServiceClientCreationError(cause)
|
|
78
|
+
|
|
79
|
+
attributes = response.body["data"]["attributes"]
|
|
80
|
+
|
|
81
|
+
data["clientId"] = attributes["client_id"]
|
|
82
|
+
data["clientSecret"] = attributes["client_secret"]
|
|
83
|
+
|
|
84
|
+
# NOTE: Annotations are excluded from the output as they are undesirably camelized
|
|
85
|
+
# (e.g., "vgs.io/vault-id" becomes "vgs.io/vaultId")
|
|
86
|
+
|
|
87
|
+
click.echo(
|
|
88
|
+
dump_camelized_yaml(
|
|
89
|
+
{
|
|
90
|
+
"apiVersion": "1.0.0",
|
|
91
|
+
"kind": "ServiceAccount",
|
|
92
|
+
"data": data,
|
|
93
|
+
}
|
|
94
|
+
)
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@apply.command("vault")
|
|
99
|
+
@click.option(
|
|
100
|
+
"-O",
|
|
101
|
+
"--organization",
|
|
102
|
+
"org_id",
|
|
103
|
+
type=ResourceIdParamType(prefix="AC"),
|
|
104
|
+
help="ID of the organization to associate the vault with.",
|
|
105
|
+
)
|
|
106
|
+
@click.option(
|
|
107
|
+
"--file",
|
|
108
|
+
"-f",
|
|
109
|
+
type=click.File(),
|
|
110
|
+
help="Configuration to apply.",
|
|
111
|
+
required=True,
|
|
112
|
+
)
|
|
113
|
+
@click.pass_context
|
|
114
|
+
@handle_errors()
|
|
115
|
+
def apply_vault(ctx: click.Context, org_id: Optional[ResourceId], file) -> None:
|
|
116
|
+
"""
|
|
117
|
+
Create a new VGS vault.
|
|
118
|
+
"""
|
|
119
|
+
data = validate_yaml(file, "validation-schemas/vault-schema.yaml")["data"]
|
|
120
|
+
|
|
121
|
+
# kubectl behavior
|
|
122
|
+
if "organizationId" in data:
|
|
123
|
+
if org_id and org_id.base58 != data["organizationId"]:
|
|
124
|
+
raise VgsCliError(
|
|
125
|
+
f"Ambiguous organization ID. "
|
|
126
|
+
f"Run the command with '--organization={data['organizationId']}' to resolve."
|
|
127
|
+
)
|
|
128
|
+
else:
|
|
129
|
+
if not org_id:
|
|
130
|
+
raise VgsCliError(
|
|
131
|
+
"Missing organization ID. Pass the '--organization' option to resolve."
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
data["organizationId"] = org_id.base58
|
|
135
|
+
|
|
136
|
+
account_mgmt = create_account_mgmt_api(ctx)
|
|
137
|
+
|
|
138
|
+
# noinspection PyUnresolvedReferences
|
|
139
|
+
response = account_mgmt.vaults.create_or_update(
|
|
140
|
+
body={
|
|
141
|
+
"data": {
|
|
142
|
+
"attributes": {
|
|
143
|
+
"name": data["name"],
|
|
144
|
+
"environment": data["environment"],
|
|
145
|
+
},
|
|
146
|
+
"type": "vaults",
|
|
147
|
+
"relationships": {
|
|
148
|
+
"organization": {
|
|
149
|
+
"data": {"type": "organizations", "id": data["organizationId"]}
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
attributes = response.body["data"]["attributes"]
|
|
157
|
+
|
|
158
|
+
data["id"] = attributes["identifier"]
|
|
159
|
+
data["credentials"] = {
|
|
160
|
+
"username": attributes["credentials"]["key"],
|
|
161
|
+
"password": attributes["credentials"]["secret"],
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
vault_mgmt = create_vault_mgmt_api(
|
|
165
|
+
ctx, response.body["data"]["links"]["vault_management_api"]
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
while True:
|
|
169
|
+
# noinspection PyUnresolvedReferences
|
|
170
|
+
response = vault_mgmt.vaults.retrieve(
|
|
171
|
+
data["id"], headers={"VGS-Tenant": data["id"]}
|
|
172
|
+
)
|
|
173
|
+
if response.body["data"]["attributes"]["state"] == "PROVISIONED":
|
|
174
|
+
break
|
|
175
|
+
time.sleep(2)
|
|
176
|
+
|
|
177
|
+
click.echo(
|
|
178
|
+
dump_camelized_yaml(
|
|
179
|
+
{
|
|
180
|
+
"apiVersion": "1.0.0",
|
|
181
|
+
"kind": "Vault",
|
|
182
|
+
"data": data,
|
|
183
|
+
}
|
|
184
|
+
)
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def sync_http_route(payload, ctx, vault_id):
|
|
189
|
+
handshake(ctx, ctx.obj.env)
|
|
190
|
+
|
|
191
|
+
vault_management_api = create_vaults_api(
|
|
192
|
+
ctx, vault_id, ctx.obj.env, token_util.get_access_token()
|
|
193
|
+
)
|
|
194
|
+
route_id = payload["spec"]["id"]
|
|
195
|
+
try:
|
|
196
|
+
# api expects it to be wrapped in data attribute
|
|
197
|
+
response = vault_management_api.routes.update(
|
|
198
|
+
route_id, body={"data": payload["spec"]}
|
|
199
|
+
)
|
|
200
|
+
except exceptions.ClientError as e:
|
|
201
|
+
error_msg = "\n".join([error["detail"] for error in e.response.body["errors"]])
|
|
202
|
+
raise RouteNotValidError(error_msg)
|
|
203
|
+
if ctx.obj.debug:
|
|
204
|
+
click.echo(f"Received raw response {response}")
|
|
205
|
+
logger.debug(response)
|
|
206
|
+
# TODO: this flilth can be removed after the normalize_one function is exposed.
|
|
207
|
+
payload = normalize([response.body["data"]])[0]
|
|
208
|
+
if ctx.obj.debug:
|
|
209
|
+
click.echo(f"Normalized body {payload}")
|
|
210
|
+
payload = wrap_in_http_envelope(payload)
|
|
211
|
+
if ctx.obj.debug:
|
|
212
|
+
click.echo(f"Wrapped body {payload}")
|
|
213
|
+
click.echo(f"Route {route_id} processed")
|
|
214
|
+
return payload
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def wrap_in_http_envelope(payload):
|
|
218
|
+
# TODO: this either needs to come from the API or we should add support for versioning in the client (yuk)
|
|
219
|
+
envelope = {
|
|
220
|
+
"apiVersion": "vault.vgs.io/v1",
|
|
221
|
+
"kind": "HttpRoute",
|
|
222
|
+
"metadata": {"name": payload["id"]},
|
|
223
|
+
"spec": payload,
|
|
224
|
+
}
|
|
225
|
+
return envelope
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def sync_mft_route(payload, ctx, vault_id):
|
|
229
|
+
print("Sync MFT Route")
|
|
230
|
+
print(payload, ctx, vault_id)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def no_op(*_):
|
|
234
|
+
print("No Op")
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
HANDLERS = {
|
|
238
|
+
("vault.vgs.io/v1", "HttpRoute"): sync_http_route,
|
|
239
|
+
("mft.vgs.io/v1beta", "MftRoute"): sync_mft_route,
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
@apply.command("vault-resources")
|
|
244
|
+
@click.option("--vault", "-V", help="Vault ID", required=True)
|
|
245
|
+
@click.option(
|
|
246
|
+
"--file",
|
|
247
|
+
"-f",
|
|
248
|
+
type=click.File(),
|
|
249
|
+
help="Configuration to apply.",
|
|
250
|
+
required=True,
|
|
251
|
+
)
|
|
252
|
+
@click.option("--dry-run", default=False)
|
|
253
|
+
@click.pass_context
|
|
254
|
+
@handle_errors()
|
|
255
|
+
def all_vault_resources(
|
|
256
|
+
ctx: click.Context, vault: Optional[ResourceId], file, dry_run
|
|
257
|
+
) -> None:
|
|
258
|
+
"""
|
|
259
|
+
Apply all vault resources (routes, preferences, certificates) to a single vault.
|
|
260
|
+
"""
|
|
261
|
+
parsed_resources = validate_multi_yaml(
|
|
262
|
+
file, "validation-schemas/vault-resources.yaml"
|
|
263
|
+
)
|
|
264
|
+
for resource in parsed_resources:
|
|
265
|
+
if dry_run:
|
|
266
|
+
print(
|
|
267
|
+
f"Pretending to send {resource['apiVersion']} {resource['kind']} {resource['metadata']['name']} to server"
|
|
268
|
+
)
|
|
269
|
+
else:
|
|
270
|
+
# for each resource find handler.
|
|
271
|
+
logger.info(
|
|
272
|
+
f"Processing {resource['apiVersion']} {resource['kind']} {resource['metadata']['name']} to server"
|
|
273
|
+
)
|
|
274
|
+
response_payload = HANDLERS.get(
|
|
275
|
+
(resource["apiVersion"], resource["kind"]), no_op
|
|
276
|
+
)(resource, ctx, vault)
|
|
277
|
+
if response_payload:
|
|
278
|
+
print(dump_yaml(response_payload))
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
@apply.command("routes")
|
|
282
|
+
@click.option("--vault", "-V", help="Vault ID", required=True)
|
|
283
|
+
@click.option(
|
|
284
|
+
"--filename",
|
|
285
|
+
"-f",
|
|
286
|
+
help="Filename for the input data",
|
|
287
|
+
type=click.File("r"),
|
|
288
|
+
required=True,
|
|
289
|
+
)
|
|
290
|
+
@click.pass_context
|
|
291
|
+
@handle_errors()
|
|
292
|
+
def apply_routes(ctx, vault, filename):
|
|
293
|
+
"""
|
|
294
|
+
Create or update VGS routes.
|
|
295
|
+
"""
|
|
296
|
+
handshake(ctx, ctx.obj.env)
|
|
297
|
+
|
|
298
|
+
route_data = filename.read()
|
|
299
|
+
vault_management_api = create_vaults_api(
|
|
300
|
+
ctx, vault, ctx.obj.env, token_util.get_access_token()
|
|
301
|
+
)
|
|
302
|
+
sync_all_routes(
|
|
303
|
+
vault_management_api,
|
|
304
|
+
route_data,
|
|
305
|
+
lambda route_id: click.echo(f"Route {route_id} processed"),
|
|
306
|
+
)
|
|
307
|
+
click.echo(f"Routes updated successfully for vault {vault}")
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import click
|
|
2
|
+
from click_plugins import with_plugins
|
|
3
|
+
from jinja2 import Environment, PackageLoader, StrictUndefined, UndefinedError
|
|
4
|
+
|
|
5
|
+
from vgscli.cli import create_account_mgmt_api, create_vault_mgmt_api
|
|
6
|
+
from vgscli.cli.types import Variable, VariableParamType
|
|
7
|
+
from vgscli.cli_utils import dump_camelized_yaml, iter_entry_points, read_file
|
|
8
|
+
from vgscli.errors import handle_errors
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@with_plugins(iter_entry_points("vgs.generate.plugins"))
|
|
12
|
+
@click.group("generate")
|
|
13
|
+
def generate() -> None:
|
|
14
|
+
"""
|
|
15
|
+
Output a VGS resource template. Edited templates can be applied with a
|
|
16
|
+
corresponding command.
|
|
17
|
+
"""
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@generate.command("vault")
|
|
22
|
+
@handle_errors()
|
|
23
|
+
def generate_vault() -> None:
|
|
24
|
+
"""
|
|
25
|
+
Output a vault template.
|
|
26
|
+
"""
|
|
27
|
+
click.echo(read_file("resource-templates/vault-template.yaml"), nl=False)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@generate.command("access-credentials")
|
|
31
|
+
@click.option("--vault", "-V", help="Vault ID", required=True)
|
|
32
|
+
@click.pass_context
|
|
33
|
+
@handle_errors()
|
|
34
|
+
def generate_access_credentials(ctx, vault):
|
|
35
|
+
"""
|
|
36
|
+
Generate a VGS access-credential
|
|
37
|
+
"""
|
|
38
|
+
account_mgmt = create_account_mgmt_api(ctx)
|
|
39
|
+
|
|
40
|
+
response = account_mgmt.vaults.get_by_id(vault)
|
|
41
|
+
|
|
42
|
+
vault_mgmt = create_vault_mgmt_api(
|
|
43
|
+
ctx, response.body["data"][0]["links"]["vault_management_api"]
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
response = vault_mgmt.credentials.create(headers={"VGS-Tenant": vault})
|
|
47
|
+
|
|
48
|
+
click.echo(
|
|
49
|
+
dump_camelized_yaml(
|
|
50
|
+
{
|
|
51
|
+
"apiVersion": "1.0.0",
|
|
52
|
+
"kind": "AccessCredentials",
|
|
53
|
+
"data": response.body["data"],
|
|
54
|
+
}
|
|
55
|
+
)
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@generate.command("http-route")
|
|
60
|
+
@handle_errors()
|
|
61
|
+
def generate_route():
|
|
62
|
+
"""
|
|
63
|
+
Generate a VGS HTTP Route
|
|
64
|
+
"""
|
|
65
|
+
click.echo(read_file("resource-templates/http-route-template.yaml"), nl=False)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@generate.command("mft-route")
|
|
69
|
+
@handle_errors()
|
|
70
|
+
def generate_route():
|
|
71
|
+
"""
|
|
72
|
+
Generate a VGS MFT Route
|
|
73
|
+
"""
|
|
74
|
+
click.echo(read_file("resource-templates/mft-route-template.yaml"), nl=False)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@generate.command("service-account")
|
|
78
|
+
@click.option(
|
|
79
|
+
"--template",
|
|
80
|
+
"-t",
|
|
81
|
+
type=click.Choice(
|
|
82
|
+
["vgs-cli", "calm", "checkout", "sub-account-checkout", "payments-admin"]
|
|
83
|
+
),
|
|
84
|
+
help="Predefined service account template configuration",
|
|
85
|
+
required=True,
|
|
86
|
+
)
|
|
87
|
+
@click.option(
|
|
88
|
+
"--var",
|
|
89
|
+
"variables",
|
|
90
|
+
type=VariableParamType(),
|
|
91
|
+
multiple=True,
|
|
92
|
+
help="Template variables.",
|
|
93
|
+
)
|
|
94
|
+
@click.option(
|
|
95
|
+
"--vault",
|
|
96
|
+
"vaults",
|
|
97
|
+
type=click.STRING,
|
|
98
|
+
multiple=True,
|
|
99
|
+
help="Service Account accessible vaults",
|
|
100
|
+
)
|
|
101
|
+
@click.pass_context
|
|
102
|
+
@handle_errors()
|
|
103
|
+
def generate_service_account(ctx, template, variables, vaults):
|
|
104
|
+
"""
|
|
105
|
+
Output a Service Account template.
|
|
106
|
+
"""
|
|
107
|
+
environment = Environment(
|
|
108
|
+
loader=PackageLoader(
|
|
109
|
+
package_name="vgscli", package_path="resource-templates/service-account"
|
|
110
|
+
),
|
|
111
|
+
undefined=StrictUndefined,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
def cli_warn(msg):
|
|
115
|
+
click.echo(click.style("Warning!", fg="yellow") + " " + msg, err=True)
|
|
116
|
+
|
|
117
|
+
def cli_fail(msg):
|
|
118
|
+
click.echo(click.style("Error!", fg="red") + " " + msg, err=True)
|
|
119
|
+
ctx.exit(1)
|
|
120
|
+
|
|
121
|
+
environment.globals["cli_warn"] = cli_warn
|
|
122
|
+
environment.globals["cli_fail"] = cli_fail
|
|
123
|
+
|
|
124
|
+
variables = variables + (Variable("vaults", vaults),)
|
|
125
|
+
try:
|
|
126
|
+
template = environment.get_template(f"{template}.yaml")
|
|
127
|
+
click.echo(template.render(**{var.name: var.value for var in variables}))
|
|
128
|
+
except UndefinedError as error:
|
|
129
|
+
click.echo(
|
|
130
|
+
click.style("Error!", fg="red") + f" Could not render service "
|
|
131
|
+
f"account template: {error}. "
|
|
132
|
+
f"Please use '--var variable=value' to pass required variable."
|
|
133
|
+
)
|
|
134
|
+
ctx.exit(1)
|