Skip to content

Commit 9908819

Browse files
committed
wip w/o packages robusta.core, robusta.integrations, hikaru, kubernetes
1 parent df52b5c commit 9908819

14 files changed

Lines changed: 2483 additions & 2 deletions

poetry.lock

Lines changed: 602 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,18 @@ readme = "README.md"
77

88
[tool.poetry.dependencies]
99
python = "^3.8, <3.13"
10-
requests = "^2.32.2"
1110
typer = "^0.12.3"
1211
pyyaml = "^6.0.1"
1312
click-spinner = "^0.1.10"
14-
jwt = "^1.3.1"
13+
cryptography = "^42.0.7"
14+
dpath = "^2.0.5"
15+
pydantic = "1.8.1"
16+
slack-sdk = "^3"
17+
pyjwt = "^2.4.0"
18+
requests = "^2.32.2"
19+
certifi = "^2024.2.2"
20+
types-toml = "^0.10.2"
21+
toml = "^0.10.2"
1522

1623

1724
[build-system]

robusta_cli/cli/__init__.py

Whitespace-only changes.

robusta_cli/cli/auth.py

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import base64
2+
import re
3+
import subprocess
4+
import traceback
5+
import uuid
6+
from typing import Optional
7+
8+
import click_spinner
9+
import requests
10+
import typer
11+
import yaml
12+
from cryptography.hazmat.primitives.asymmetric import rsa
13+
from cryptography.hazmat.primitives.serialization import Encoding, NoEncryption, PrivateFormat, PublicFormat
14+
from dpath.util import get
15+
from pydantic import BaseModel
16+
17+
from backend_profile import backend_profile
18+
from playbooks_cmd import NAMESPACE_EXPLANATION, get_playbooks_config
19+
from utils import exec_in_robusta_runner_output, namespace_to_kubectl
20+
21+
AUTH_SECRET_NAME = "robusta-auth-config-secret"
22+
app = typer.Typer(add_completion=False)
23+
24+
25+
class RSAKeyPair(BaseModel):
26+
prv: str = None
27+
pub: str = None
28+
private: str = None
29+
public: str = None
30+
31+
32+
def gen_rsa_pair() -> RSAKeyPair:
33+
# generate private/public key pair
34+
key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
35+
36+
# get public key in OpenSSH format
37+
public_key = key.public_key().public_bytes(Encoding.PEM, PublicFormat.SubjectPublicKeyInfo)
38+
39+
# get private key in PEM container format
40+
pem = key.private_bytes(
41+
encoding=Encoding.PEM, format=PrivateFormat.TraditionalOpenSSL, encryption_algorithm=NoEncryption()
42+
)
43+
44+
return RSAKeyPair(public=base64.b64encode(public_key), private=base64.b64encode(pem))
45+
46+
47+
def get_existing_auth_config(namespace: str) -> Optional[RSAKeyPair]:
48+
try:
49+
secret_content = subprocess.check_output(
50+
f"kubectl get secret {namespace_to_kubectl(namespace)} {AUTH_SECRET_NAME} -o yaml",
51+
shell=True,
52+
)
53+
except Exception:
54+
return None
55+
56+
auth_secret = yaml.safe_load(secret_content)
57+
return RSAKeyPair(
58+
prv=base64.b64decode(auth_secret["data"]["prv"]).decode(),
59+
pub=base64.b64decode(auth_secret["data"]["pub"]).decode(),
60+
)
61+
62+
63+
class TokenDetails(BaseModel):
64+
pub: str
65+
account_id: str
66+
user_id: str
67+
session_token: str
68+
enc_key: str
69+
key_id: str
70+
71+
72+
def store_server_token(token_details: TokenDetails, debug: bool = False) -> bool:
73+
try:
74+
response = requests.post(backend_profile.robusta_store_token_url, json=token_details.dict())
75+
if debug and response.status_code != 201:
76+
typer.secho(f"Failed to store server token. status-code {response.status_code} text {response.text}")
77+
78+
return response.status_code == 201
79+
except Exception:
80+
if debug:
81+
typer.secho(f"Error trying to store server token. {traceback.format_exc()}")
82+
return False
83+
84+
85+
def _get_signing_key_from_env_variable(namespace: Optional[str], env_var_name: str) -> str:
86+
return str(exec_in_robusta_runner_output(f'echo "${env_var_name}"', namespace), "utf-8").strip()
87+
88+
89+
@app.command(name="web-connect")
90+
@app.command()
91+
def gen_token(
92+
account_id: str = typer.Option(
93+
...,
94+
help="Robusta account id",
95+
),
96+
user_id: str = typer.Option(..., help="User id for which the token is created"),
97+
session_token: str = typer.Option(
98+
...,
99+
help="User session token. Created for an authenticated user via the Robusta UI",
100+
),
101+
namespace: str = typer.Option(
102+
None,
103+
help=NAMESPACE_EXPLANATION,
104+
),
105+
debug: bool = typer.Option(False),
106+
):
107+
"""Generate token required to run actions manually in Robusta UI"""
108+
typer.echo("connecting to cluster...")
109+
with click_spinner.spinner():
110+
auth_config = get_existing_auth_config(namespace)
111+
112+
if not auth_config:
113+
typer.secho(
114+
"\nRSA auth isn't configured. "
115+
"Please update Robusta and run `robusta update-config` to configure it. Aborting!",
116+
fg="red",
117+
)
118+
return
119+
120+
playbooks_config = get_playbooks_config(namespace)
121+
active_playbooks_file = playbooks_config["data"]["active_playbooks.yaml"]
122+
playbooks_config_yaml = yaml.safe_load(active_playbooks_file)
123+
signing_key = get(playbooks_config_yaml, "global_config/signing_key", default=None)
124+
if not signing_key:
125+
typer.secho("signing_key is not defined. Please update Robusta and run `robusta update-config`", fg="red")
126+
return
127+
128+
try:
129+
env_match = re.fullmatch(r"\{\{\s*env\.(\S+)\s*}}", signing_key)
130+
if env_match:
131+
env_var_name = env_match.group(1)
132+
typer.secho(f"Fetching secret key from an env var named: {env_var_name}")
133+
signing_key = _get_signing_key_from_env_variable(namespace, env_var_name)
134+
if not signing_key:
135+
typer.secho(f"Could not find an env var named {env_var_name}", fg="red")
136+
return
137+
signing_key_uuid = uuid.UUID(signing_key)
138+
except Exception:
139+
typer.secho(
140+
"Bad format for signing_key. Please run `robusta update-config` to generate a new valid"
141+
" signing_key for your account.",
142+
fg="red",
143+
)
144+
return
145+
146+
client_enc_key = uuid.uuid4()
147+
server_enc_key = uuid.UUID(int=(signing_key_uuid.int ^ client_enc_key.int))
148+
key_id = str(uuid.uuid4())
149+
150+
token_response = TokenDetails(
151+
pub=auth_config.pub,
152+
account_id=account_id,
153+
user_id=user_id,
154+
session_token=session_token,
155+
enc_key=str(server_enc_key),
156+
key_id=key_id,
157+
)
158+
if not store_server_token(token_response, debug):
159+
typer.secho("Failed to store server token. Aborting!", fg="red")
160+
return
161+
162+
# client response is the same, only with a different enc_key
163+
token_response.enc_key = str(client_enc_key)
164+
165+
typer.secho("Token created successfully. Submit it in the Robusta UI", fg="green")
166+
typer.secho(base64.b64encode(token_response.json(exclude={"session_token"}).encode("utf-8")).decode())

robusta_cli/cli/backend_profile.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import os
2+
import sys
3+
4+
import typer
5+
from pydantic.main import BaseModel
6+
7+
from utils import host_for_params
8+
9+
ROBUSTA_BACKEND_PROFILE = os.environ.get("ROBUSTA_BACKEND_PROFILE", "")
10+
11+
12+
class BackendProfile(BaseModel):
13+
robusta_cloud_api_host: str = ""
14+
robusta_ui_domain: str = ""
15+
robusta_relay_ws_address: str = ""
16+
robusta_relay_external_actions_url: str = ""
17+
robusta_telemetry_endpoint: str = ""
18+
robusta_store_token_url: str = ""
19+
custom_profile: bool = False
20+
21+
@classmethod
22+
def fromDomainProvider(
23+
cls, domain: str, api_endpoint_prefix: str, platform_endpoint_prefix: str, relay_endpoint_prefix: str
24+
):
25+
return cls(
26+
robusta_cloud_api_host=host_for_params(api_endpoint_prefix, domain),
27+
robusta_ui_domain=host_for_params(platform_endpoint_prefix, domain),
28+
robusta_relay_ws_address=host_for_params(relay_endpoint_prefix, domain, "wss"),
29+
robusta_relay_external_actions_url=f"{host_for_params(api_endpoint_prefix, domain)}/integrations/generic/actions",
30+
robusta_telemetry_endpoint=f"{host_for_params(api_endpoint_prefix, domain)}/telemetry",
31+
robusta_store_token_url=f"{host_for_params(api_endpoint_prefix, domain)}/auth/server/tokens",
32+
)
33+
34+
35+
# default values
36+
backend_profile = BackendProfile(
37+
robusta_cloud_api_host="https://api.robusta.dev",
38+
robusta_ui_domain="https://platform.robusta.dev",
39+
robusta_store_token_url="https://api.robusta.dev/auth/server/tokens",
40+
)
41+
42+
if ROBUSTA_BACKEND_PROFILE:
43+
typer.secho(
44+
f"Using Robusta backend profile: {ROBUSTA_BACKEND_PROFILE}",
45+
color="blue",
46+
)
47+
backend_profile = BackendProfile.parse_file(ROBUSTA_BACKEND_PROFILE)
48+
backend_profile.custom_profile = True
49+
50+
profile_dict = backend_profile.dict()
51+
for attribute, val in profile_dict.items():
52+
if not profile_dict.get(attribute):
53+
typer.secho(f"Illegal profile. Missing {attribute}. Aborting!")
54+
sys.exit(1)

robusta_cli/cli/demo_alert.py

Whitespace-only changes.

robusta_cli/cli/eula.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import requests
2+
import typer
3+
4+
from backend_profile import backend_profile
5+
6+
7+
def handle_eula(account_id, robusta_api_key, cloud_routing_enabled):
8+
require_eula = robusta_api_key or cloud_routing_enabled
9+
if not require_eula:
10+
return
11+
12+
while True:
13+
eula_url = f"{backend_profile.robusta_cloud_api_host}/eula.html"
14+
typer.echo(f"Please read and approve our End User License Agreement: {eula_url}")
15+
eula_approved = typer.confirm("Do you accept our End User License Agreement?")
16+
17+
if eula_approved:
18+
try:
19+
requests.get(f"{eula_url}?account_id={account_id}")
20+
except Exception:
21+
typer.echo(f"\nEula approval failed: {eula_url}")
22+
return
23+
24+
typer.secho(
25+
"End User License Agreement rejected. Sorry, you must either accept or restart the installation and disable the UI and cloud features",
26+
fg="red",
27+
)

0 commit comments

Comments
 (0)