44from datetime import datetime
55from typing import TYPE_CHECKING , Optional , Tuple
66
7+ import boto3
8+ import click
79import requests
10+ from botocore .config import Config
811from getmac import get_mac_address
912from git .config import GitConfigParser
13+ from google .api_core .exceptions import NotFound
14+ from google .cloud import secretmanager
15+ from mypy_boto3_ssm .client import SSMClient
1016
1117from opta .constants import VERSION
18+ from opta .core .gcp import GCP
1219from opta .exceptions import UserErrors
1320from opta .module_processors .base import ModuleProcessor
1421from opta .utils import logger
@@ -27,9 +34,6 @@ class RunxProcessor(ModuleProcessor):
2734 def __init__ (self , module : "Module" , layer : "Layer" ):
2835 if module .data ["type" ] != "runx" :
2936 raise Exception (f"The module { module .name } was expected to be of type runx" )
30- if os .environ .get ("OPTA_API_KEY" ) is None :
31- raise UserErrors ("Need opta api key present for this to run" )
32- self .api_key = os .environ .get ("OPTA_API_KEY" )
3337 self .user_id = GitConfigParser ().get_value ("user" , "email" , "no_user" )
3438 self .device_id = get_mac_address ()
3539 self .os_name = os .name
@@ -38,10 +42,96 @@ def __init__(self, module: "Module", layer: "Layer"):
3842 super (RunxProcessor , self ).__init__ (module , layer )
3943
4044 def process (self , module_idx : int ) -> None :
41- self .fetch_jwt ()
45+ logger .info ("Checking for runx api key secret" )
46+ if self .fetch_secret () is None :
47+ self .set_secret ()
48+
49+ def fetch_secret (self ) -> Optional [str ]:
50+ if self .layer .cloud == "aws" :
51+ return self ._fetch_aws_secret ()
52+ elif self .layer .cloud == "google" :
53+ return self ._fetch_gcp_secret ()
54+ else :
55+ raise Exception ("Can not handle secrets of type" )
56+
57+ def _fetch_aws_secret (self ) -> Optional [str ]:
58+ providers = self .layer .gen_providers (0 )
59+ region = providers ["provider" ]["aws" ]["region" ]
60+ ssm_client : SSMClient = boto3 .client ("ssm" , config = Config (region_name = region ))
61+ try :
62+ parameter = ssm_client .get_parameter (
63+ Name = f"/opta-{ self .layer .get_env ()} /runx-api-key" , WithDecryption = True
64+ )
65+ return parameter ["Parameter" ]["Value" ]
66+ except ssm_client .exceptions .ParameterNotFound :
67+ return None
68+
69+ def _fetch_gcp_secret (self ) -> Optional [str ]:
70+ credentials , project_id = GCP .get_credentials ()
71+ sm_client = secretmanager .SecretManagerServiceClient (credentials = credentials )
72+ name = f"projects/{ project_id } /secrets/opta-{ self .layer .get_env ()} -runx-api-key/versions/1"
73+ try :
74+ # Access the secret version.
75+ response = sm_client .access_secret_version (
76+ request = secretmanager .AccessSecretVersionRequest ({"name" : name })
77+ )
78+ return response .payload .data .decode ("UTF-8" )
79+ except NotFound :
80+ return None
81+
82+ def set_secret (self ) -> None :
83+ while True :
84+ value = click .prompt ("Please enter your runx api key" , type = str ,)
85+ try :
86+ self .fetch_jwt (value )
87+ except UserErrors :
88+ logger .warn (
89+ "The api key which you passed was invalid, please provide a valid api key from runx"
90+ )
91+ else :
92+ break
93+ if self .layer .cloud == "aws" :
94+ return self ._set_aws_secret (value )
95+ elif self .layer .cloud == "google" :
96+ return self ._set_gcp_secret (value )
97+ else :
98+ raise Exception ("Can not handle secrets of type" )
99+
100+ def _set_aws_secret (self , secret : str ) -> None :
101+ providers = self .layer .gen_providers (0 )
102+ region = providers ["provider" ]["aws" ]["region" ]
103+ ssm_client : SSMClient = boto3 .client ("ssm" , config = Config (region_name = region ))
104+ ssm_client .put_parameter (
105+ Name = f"/opta-{ self .layer .get_env ()} /runx-api-key" ,
106+ Value = secret ,
107+ Type = "SecureString" ,
108+ )
109+
110+ def _set_gcp_secret (self , secret : str ) -> None :
111+ credentials , project_id = GCP .get_credentials ()
112+ sm_client = secretmanager .SecretManagerServiceClient (credentials = credentials )
113+ sm_secret = sm_client .create_secret (
114+ request = secretmanager .CreateSecretRequest (
115+ {
116+ "parent" : f"projects/{ project_id } " ,
117+ "secret_id" : f"opta-{ self .layer .get_env ()} -runx-api-key" ,
118+ "secret" : {"replication" : {"automatic" : {}}},
119+ }
120+ )
121+ )
122+ sm_client .add_secret_version (
123+ request = secretmanager .AddSecretVersionRequest (
124+ {"parent" : sm_secret .name , "payload" : {"data" : secret .encode ("utf-8" )}}
125+ )
126+ )
42127
43128 def post_hook (self , module_idx : int , exception : Optional [Exception ]) -> None :
44- validation_data , jwt = self .fetch_jwt ()
129+ api_key = self .fetch_secret ()
130+ if api_key is None :
131+ raise Exception (
132+ "The api key seems to have just disappeared from the secret storage"
133+ )
134+ validation_data , jwt = self .fetch_jwt (api_key )
45135 is_environment = self .layer .parent is None
46136 url_path = "/config/environments" if is_environment else "/config/services"
47137 body = {
@@ -73,9 +163,9 @@ def post_hook(self, module_idx: int, exception: Optional[Exception]) -> None:
73163 f"Invalid response when attempting to send data to backend: { resp .json ()} "
74164 )
75165
76- def fetch_jwt (self ) -> Tuple [dict , str ]:
166+ def fetch_jwt (self , api_key : str ) -> Tuple [dict , str ]:
77167 resp = requests .post (
78- f"https://{ OPTA_DOMAIN } /user/apikeys/validate" , json = {"api_key" : self . api_key }
168+ f"https://{ OPTA_DOMAIN } /user/apikeys/validate" , json = {"api_key" : api_key }
79169 )
80170 if resp .status_code == 404 :
81171 raise UserErrors (
0 commit comments