kubernetes-watch 0.1.5__py3-none-any.whl → 0.1.8__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.
- kube_watch/enums/kube.py +5 -5
- kube_watch/enums/logic.py +8 -8
- kube_watch/enums/providers.py +12 -12
- kube_watch/enums/workflow.py +17 -17
- kube_watch/models/common.py +16 -16
- kube_watch/models/workflow.py +60 -60
- kube_watch/modules/clusters/kube.py +185 -185
- kube_watch/modules/database/__init__.py +0 -0
- kube_watch/modules/database/model.py +12 -0
- kube_watch/modules/database/postgre.py +271 -0
- kube_watch/modules/logic/actions.py +55 -55
- kube_watch/modules/logic/checks.py +7 -7
- kube_watch/modules/logic/load.py +23 -23
- kube_watch/modules/logic/merge.py +31 -31
- kube_watch/modules/logic/scheduler.py +74 -74
- kube_watch/modules/mock/mock_generator.py +53 -53
- kube_watch/modules/providers/aws.py +210 -210
- kube_watch/modules/providers/git.py +32 -32
- kube_watch/modules/providers/github.py +126 -126
- kube_watch/modules/providers/vault.py +188 -188
- kube_watch/standalone/metarecogen/ckan_to_gn.py +132 -132
- kube_watch/watch/__init__.py +1 -1
- kube_watch/watch/helpers.py +170 -170
- kube_watch/watch/workflow.py +232 -100
- {kubernetes_watch-0.1.5.dist-info → kubernetes_watch-0.1.8.dist-info}/LICENSE +21 -21
- {kubernetes_watch-0.1.5.dist-info → kubernetes_watch-0.1.8.dist-info}/METADATA +5 -3
- kubernetes_watch-0.1.8.dist-info/RECORD +36 -0
- kubernetes_watch-0.1.5.dist-info/RECORD +0 -33
- {kubernetes_watch-0.1.5.dist-info → kubernetes_watch-0.1.8.dist-info}/WHEEL +0 -0
|
@@ -1,126 +1,126 @@
|
|
|
1
|
-
import requests
|
|
2
|
-
from datetime import datetime, timedelta
|
|
3
|
-
import pytz
|
|
4
|
-
|
|
5
|
-
from prefect import get_run_logger
|
|
6
|
-
|
|
7
|
-
logger = get_run_logger()
|
|
8
|
-
|
|
9
|
-
def parse_datetime(dt_str):
|
|
10
|
-
"""Parse a datetime string into a datetime object."""
|
|
11
|
-
return datetime.strptime(dt_str, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=pytz.UTC)
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def add_version_dependency(versions):
|
|
15
|
-
"""
|
|
16
|
-
Finds untagged versions that were created within 2 minutes of any tagged version.
|
|
17
|
-
|
|
18
|
-
Args:
|
|
19
|
-
versions (list of dict): List of dictionaries, each containing 'created_at' and possibly 'tags'.
|
|
20
|
-
|
|
21
|
-
Returns:
|
|
22
|
-
list: A list of untagged versions that meet the criteria.
|
|
23
|
-
"""
|
|
24
|
-
tagged_versions = [v for v in versions if v['metadata']['container']['tags']]
|
|
25
|
-
untagged_versions = [v for v in versions if not v['metadata']['container']['tags']]
|
|
26
|
-
|
|
27
|
-
# Convert all creation times to datetime objects
|
|
28
|
-
for v in versions:
|
|
29
|
-
v['created_datetime'] = parse_datetime(v['created_at'])
|
|
30
|
-
|
|
31
|
-
# Check each untagged version against all tagged versions
|
|
32
|
-
for v in versions:
|
|
33
|
-
if v in untagged_versions:
|
|
34
|
-
for tagged in tagged_versions:
|
|
35
|
-
time_diff = abs(tagged['created_datetime'] - v['created_datetime'])
|
|
36
|
-
if time_diff < timedelta(minutes=2):
|
|
37
|
-
v['tag'] = tagged['tag']
|
|
38
|
-
break # Stop checking once a close tagged version is found
|
|
39
|
-
|
|
40
|
-
return versions
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def get_github_package_versions(token, organization, package_type, package_name):
|
|
44
|
-
"""
|
|
45
|
-
This function returns all available versions in a github package registry `ghcr`.
|
|
46
|
-
|
|
47
|
-
:param: token: GitHub token with proper permissions
|
|
48
|
-
:param: organization: GitHub organization name
|
|
49
|
-
:param: package_type: GitHub package type (e.g. container, npm)
|
|
50
|
-
:param: package_name: GitHub package name
|
|
51
|
-
"""
|
|
52
|
-
base_url = f"https://api.github.com/orgs/{organization}/packages/{package_type}/{package_name}/versions"
|
|
53
|
-
headers = {
|
|
54
|
-
'Authorization': f'token {token}',
|
|
55
|
-
'Accept': 'application/vnd.github.v3+json'
|
|
56
|
-
}
|
|
57
|
-
versions = []
|
|
58
|
-
url = base_url
|
|
59
|
-
|
|
60
|
-
while url:
|
|
61
|
-
logger.info(f"Requesting: {url}") # Debug output to check the URL being requested
|
|
62
|
-
response = requests.get(url, headers=headers)
|
|
63
|
-
if response.status_code == 200:
|
|
64
|
-
page_versions = response.json()
|
|
65
|
-
versions.extend(page_versions)
|
|
66
|
-
link_header = response.headers.get('Link', None)
|
|
67
|
-
if link_header:
|
|
68
|
-
links = {rel.split('; ')[1][5:-1]: rel.split('; ')[0][1:-1] for rel in link_header.split(', ')}
|
|
69
|
-
url = links.get("next", None) # Get the URL for the next page
|
|
70
|
-
if url:
|
|
71
|
-
logger.info(f"Next page link found: {url}") # Debug output to check the next page link
|
|
72
|
-
else:
|
|
73
|
-
logger.info("No next page link found in header.") # End of pagination
|
|
74
|
-
else:
|
|
75
|
-
logger.info("No 'Link' header present, likely the last page.") # If no 'Link' header, it's the last page
|
|
76
|
-
url = None
|
|
77
|
-
else:
|
|
78
|
-
logger.error(f"Failed to retrieve package versions: {response.status_code}, {response.text}")
|
|
79
|
-
url = None
|
|
80
|
-
|
|
81
|
-
for item in versions:
|
|
82
|
-
tags = item['metadata']['container']['tags']
|
|
83
|
-
item['tag'] = tags[0] if len(tags) > 0 else None
|
|
84
|
-
|
|
85
|
-
return versions
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
def delete_versions(versions, token, organization, package_type, package_name):
|
|
89
|
-
"""
|
|
90
|
-
:param: versions: list of versions to be deleted
|
|
91
|
-
:param: token: GitHub token with proper permissions
|
|
92
|
-
:param: organization: GitHub organization name
|
|
93
|
-
:param: package_type: GitHub package type (e.g. container, npm)
|
|
94
|
-
:param: package_name: GitHub package name
|
|
95
|
-
"""
|
|
96
|
-
headers = {
|
|
97
|
-
'Authorization': f'token {token}',
|
|
98
|
-
'Accept': 'application/vnd.github.v3+json'
|
|
99
|
-
}
|
|
100
|
-
for version in versions:
|
|
101
|
-
delete_url = f"https://api.github.com/orgs/{organization}/packages/{package_type}/{package_name}/versions/{version['id']}"
|
|
102
|
-
response = requests.delete(delete_url, headers=headers)
|
|
103
|
-
if response.status_code == 204:
|
|
104
|
-
logger.info(f"Successfully deleted version: {version['metadata']['container']['tags']} (ID: {version['id']})")
|
|
105
|
-
else:
|
|
106
|
-
logger.info(f"Failed to delete version: {version['metadata']['container']['tags']} (ID: {version['id']}), {response.status_code}, {response.text}")
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
def delete_untaged_versions(versions, token, organization, package_type, package_name):
|
|
111
|
-
# Identifying untagged versions that are related to a tagged version
|
|
112
|
-
untag_test = list(filter(lambda ver: ver['tag'] is None, versions))
|
|
113
|
-
logger.info(f"UNTAGGED BEFORE: {len(untag_test)}")
|
|
114
|
-
versions = add_version_dependency(versions)
|
|
115
|
-
untag_vers = list(filter(lambda ver: ver['tag'] is None, versions))
|
|
116
|
-
logger.info(f"UNTAGGED BEFORE: {len(untag_vers)}")
|
|
117
|
-
|
|
118
|
-
delete_versions(untag_vers, token, organization, package_type, package_name)
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
def task_get_latest_image_digest(versions, tag_name):
|
|
122
|
-
lst = list(filter(lambda ver: ver['tag'] == tag_name, versions))
|
|
123
|
-
if len(lst) == 0:
|
|
124
|
-
raise ValueError(f"Provided tag: {tag_name} was not found.")
|
|
125
|
-
|
|
126
|
-
return lst[0]['name']
|
|
1
|
+
import requests
|
|
2
|
+
from datetime import datetime, timedelta
|
|
3
|
+
import pytz
|
|
4
|
+
|
|
5
|
+
from prefect import get_run_logger
|
|
6
|
+
|
|
7
|
+
logger = get_run_logger()
|
|
8
|
+
|
|
9
|
+
def parse_datetime(dt_str):
|
|
10
|
+
"""Parse a datetime string into a datetime object."""
|
|
11
|
+
return datetime.strptime(dt_str, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=pytz.UTC)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def add_version_dependency(versions):
|
|
15
|
+
"""
|
|
16
|
+
Finds untagged versions that were created within 2 minutes of any tagged version.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
versions (list of dict): List of dictionaries, each containing 'created_at' and possibly 'tags'.
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
list: A list of untagged versions that meet the criteria.
|
|
23
|
+
"""
|
|
24
|
+
tagged_versions = [v for v in versions if v['metadata']['container']['tags']]
|
|
25
|
+
untagged_versions = [v for v in versions if not v['metadata']['container']['tags']]
|
|
26
|
+
|
|
27
|
+
# Convert all creation times to datetime objects
|
|
28
|
+
for v in versions:
|
|
29
|
+
v['created_datetime'] = parse_datetime(v['created_at'])
|
|
30
|
+
|
|
31
|
+
# Check each untagged version against all tagged versions
|
|
32
|
+
for v in versions:
|
|
33
|
+
if v in untagged_versions:
|
|
34
|
+
for tagged in tagged_versions:
|
|
35
|
+
time_diff = abs(tagged['created_datetime'] - v['created_datetime'])
|
|
36
|
+
if time_diff < timedelta(minutes=2):
|
|
37
|
+
v['tag'] = tagged['tag']
|
|
38
|
+
break # Stop checking once a close tagged version is found
|
|
39
|
+
|
|
40
|
+
return versions
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def get_github_package_versions(token, organization, package_type, package_name):
|
|
44
|
+
"""
|
|
45
|
+
This function returns all available versions in a github package registry `ghcr`.
|
|
46
|
+
|
|
47
|
+
:param: token: GitHub token with proper permissions
|
|
48
|
+
:param: organization: GitHub organization name
|
|
49
|
+
:param: package_type: GitHub package type (e.g. container, npm)
|
|
50
|
+
:param: package_name: GitHub package name
|
|
51
|
+
"""
|
|
52
|
+
base_url = f"https://api.github.com/orgs/{organization}/packages/{package_type}/{package_name}/versions"
|
|
53
|
+
headers = {
|
|
54
|
+
'Authorization': f'token {token}',
|
|
55
|
+
'Accept': 'application/vnd.github.v3+json'
|
|
56
|
+
}
|
|
57
|
+
versions = []
|
|
58
|
+
url = base_url
|
|
59
|
+
|
|
60
|
+
while url:
|
|
61
|
+
logger.info(f"Requesting: {url}") # Debug output to check the URL being requested
|
|
62
|
+
response = requests.get(url, headers=headers)
|
|
63
|
+
if response.status_code == 200:
|
|
64
|
+
page_versions = response.json()
|
|
65
|
+
versions.extend(page_versions)
|
|
66
|
+
link_header = response.headers.get('Link', None)
|
|
67
|
+
if link_header:
|
|
68
|
+
links = {rel.split('; ')[1][5:-1]: rel.split('; ')[0][1:-1] for rel in link_header.split(', ')}
|
|
69
|
+
url = links.get("next", None) # Get the URL for the next page
|
|
70
|
+
if url:
|
|
71
|
+
logger.info(f"Next page link found: {url}") # Debug output to check the next page link
|
|
72
|
+
else:
|
|
73
|
+
logger.info("No next page link found in header.") # End of pagination
|
|
74
|
+
else:
|
|
75
|
+
logger.info("No 'Link' header present, likely the last page.") # If no 'Link' header, it's the last page
|
|
76
|
+
url = None
|
|
77
|
+
else:
|
|
78
|
+
logger.error(f"Failed to retrieve package versions: {response.status_code}, {response.text}")
|
|
79
|
+
url = None
|
|
80
|
+
|
|
81
|
+
for item in versions:
|
|
82
|
+
tags = item['metadata']['container']['tags']
|
|
83
|
+
item['tag'] = tags[0] if len(tags) > 0 else None
|
|
84
|
+
|
|
85
|
+
return versions
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def delete_versions(versions, token, organization, package_type, package_name):
|
|
89
|
+
"""
|
|
90
|
+
:param: versions: list of versions to be deleted
|
|
91
|
+
:param: token: GitHub token with proper permissions
|
|
92
|
+
:param: organization: GitHub organization name
|
|
93
|
+
:param: package_type: GitHub package type (e.g. container, npm)
|
|
94
|
+
:param: package_name: GitHub package name
|
|
95
|
+
"""
|
|
96
|
+
headers = {
|
|
97
|
+
'Authorization': f'token {token}',
|
|
98
|
+
'Accept': 'application/vnd.github.v3+json'
|
|
99
|
+
}
|
|
100
|
+
for version in versions:
|
|
101
|
+
delete_url = f"https://api.github.com/orgs/{organization}/packages/{package_type}/{package_name}/versions/{version['id']}"
|
|
102
|
+
response = requests.delete(delete_url, headers=headers)
|
|
103
|
+
if response.status_code == 204:
|
|
104
|
+
logger.info(f"Successfully deleted version: {version['metadata']['container']['tags']} (ID: {version['id']})")
|
|
105
|
+
else:
|
|
106
|
+
logger.info(f"Failed to delete version: {version['metadata']['container']['tags']} (ID: {version['id']}), {response.status_code}, {response.text}")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def delete_untaged_versions(versions, token, organization, package_type, package_name):
|
|
111
|
+
# Identifying untagged versions that are related to a tagged version
|
|
112
|
+
untag_test = list(filter(lambda ver: ver['tag'] is None, versions))
|
|
113
|
+
logger.info(f"UNTAGGED BEFORE: {len(untag_test)}")
|
|
114
|
+
versions = add_version_dependency(versions)
|
|
115
|
+
untag_vers = list(filter(lambda ver: ver['tag'] is None, versions))
|
|
116
|
+
logger.info(f"UNTAGGED BEFORE: {len(untag_vers)}")
|
|
117
|
+
|
|
118
|
+
delete_versions(untag_vers, token, organization, package_type, package_name)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def task_get_latest_image_digest(versions, tag_name):
|
|
122
|
+
lst = list(filter(lambda ver: ver['tag'] == tag_name, versions))
|
|
123
|
+
if len(lst) == 0:
|
|
124
|
+
raise ValueError(f"Provided tag: {tag_name} was not found.")
|
|
125
|
+
|
|
126
|
+
return lst[0]['name']
|
|
@@ -1,188 +1,188 @@
|
|
|
1
|
-
import hvac
|
|
2
|
-
import os
|
|
3
|
-
from prefect import get_run_logger
|
|
4
|
-
|
|
5
|
-
from kube_watch.enums.providers import Providers
|
|
6
|
-
|
|
7
|
-
logger = get_run_logger()
|
|
8
|
-
|
|
9
|
-
def login(url, app_role_id, secret_id, path):
|
|
10
|
-
"""
|
|
11
|
-
Login to Vault, using an existing token if available, or via AppRole otherwise.
|
|
12
|
-
|
|
13
|
-
Parameters:
|
|
14
|
-
url (str): Vault server URL.
|
|
15
|
-
app_role_id (str): AppRole ID.
|
|
16
|
-
secret_id (str): AppRole Secret ID.
|
|
17
|
-
path (str): Path where the AppRole is enabled.
|
|
18
|
-
|
|
19
|
-
Returns:
|
|
20
|
-
dict: Dictionary containing the initialized vault_client.
|
|
21
|
-
"""
|
|
22
|
-
vault_client = hvac.Client(url=url)
|
|
23
|
-
|
|
24
|
-
# Attempt to use an existing token from environment variables
|
|
25
|
-
vault_token = os.getenv('VAULT_TOKEN', None)
|
|
26
|
-
if vault_token:
|
|
27
|
-
vault_client.token = vault_token
|
|
28
|
-
# Verify if the current token is still valid
|
|
29
|
-
try:
|
|
30
|
-
if vault_client.is_authenticated():
|
|
31
|
-
logger.info("Authenticated with existing token.")
|
|
32
|
-
return vault_client
|
|
33
|
-
except hvac.exceptions.InvalidRequest as e:
|
|
34
|
-
logger.warning(f"Failed to authenticate with the existing token: {str(e)}")
|
|
35
|
-
|
|
36
|
-
# If token is not valid or not present, authenticate with AppRole
|
|
37
|
-
try:
|
|
38
|
-
vault_client.auth.approle.login(
|
|
39
|
-
role_id=app_role_id,
|
|
40
|
-
secret_id=secret_id,
|
|
41
|
-
mount_point=f'approle/{path}'
|
|
42
|
-
)
|
|
43
|
-
|
|
44
|
-
# Store the new token in environment variables for subsequent use
|
|
45
|
-
os.environ['VAULT_TOKEN'] = vault_client.token
|
|
46
|
-
logger.info("Authenticated with new token and stored in environment variable.")
|
|
47
|
-
|
|
48
|
-
return vault_client
|
|
49
|
-
except hvac.exceptions.InvalidRequest as e:
|
|
50
|
-
logger.error(f"Authentication failed with provided secret_id: {str(e)}")
|
|
51
|
-
raise RuntimeError("Authentication failed: unable to log in with the provided credentials.") from e
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def get_secret(vault_client, secret_path, vault_mount_point):
|
|
56
|
-
"""
|
|
57
|
-
Retrieve a secret from Vault
|
|
58
|
-
"""
|
|
59
|
-
res = vault_client.secrets.kv.v2.read_secret_version(
|
|
60
|
-
path=secret_path,
|
|
61
|
-
mount_point=vault_mount_point,
|
|
62
|
-
raise_on_deleted_version=True
|
|
63
|
-
)
|
|
64
|
-
return res.get('data', {}).get('data')
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
def update_secret(vault_client, secret_path, secret_data, vault_mount_point):
|
|
68
|
-
"""
|
|
69
|
-
Update or create a secret in Vault at the specified path.
|
|
70
|
-
|
|
71
|
-
Args:
|
|
72
|
-
vault_client: The authenticated Vault client instance.
|
|
73
|
-
secret_path (str): The path where the secret will be stored or updated in Vault.
|
|
74
|
-
secret_data (dict): The secret data to store as a dictionary.
|
|
75
|
-
vault_mount_point (str): The mount point for the KV store.
|
|
76
|
-
|
|
77
|
-
Returns:
|
|
78
|
-
bool: True if the operation was successful, False otherwise.
|
|
79
|
-
"""
|
|
80
|
-
try:
|
|
81
|
-
# Writing the secret data to Vault at the specified path
|
|
82
|
-
vault_client.secrets.kv.v2.create_or_update_secret(
|
|
83
|
-
path=secret_path,
|
|
84
|
-
secret=secret_data,
|
|
85
|
-
mount_point=vault_mount_point
|
|
86
|
-
)
|
|
87
|
-
print("Secret updated successfully.")
|
|
88
|
-
return True
|
|
89
|
-
except Exception as e:
|
|
90
|
-
print(f"Failed to update secret: {e}")
|
|
91
|
-
return False
|
|
92
|
-
|
|
93
|
-
def generate_provider_creds(vault_client, provider, backend_path, role_name):
|
|
94
|
-
"""
|
|
95
|
-
Generate credentials for a specified provider
|
|
96
|
-
"""
|
|
97
|
-
if provider == Providers.AWS:
|
|
98
|
-
backend_path = backend_path
|
|
99
|
-
role_name = role_name
|
|
100
|
-
creds_path = f"{backend_path}/creds/{role_name}"
|
|
101
|
-
return vault_client.read(creds_path)
|
|
102
|
-
|
|
103
|
-
raise ValueError("Unknown provider")
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
def generate_new_secret_id(vault_client, role_name, vault_path, env_var_name):
|
|
108
|
-
"""
|
|
109
|
-
Generates new secret_id. Note an admin role is required for this.
|
|
110
|
-
"""
|
|
111
|
-
try:
|
|
112
|
-
# Write directly to the Vault endpoint to create the secret ID with num_uses
|
|
113
|
-
# response = vault_client.write(
|
|
114
|
-
# f"auth/approle/{vault_path}/role/{role_name}/secret-id",
|
|
115
|
-
# )
|
|
116
|
-
response = vault_client.auth.approle.generate_secret_id(
|
|
117
|
-
role_name=role_name,
|
|
118
|
-
mount_point=f'approle/{vault_path}'
|
|
119
|
-
)
|
|
120
|
-
# Check if the response contains the secret ID
|
|
121
|
-
if response and 'data' in response:
|
|
122
|
-
secret_id = response['data']['secret_id']
|
|
123
|
-
secret_id_accessor = response['data']['secret_id_accessor']
|
|
124
|
-
logger.info("Generated a new secret ID with usage buffer.")
|
|
125
|
-
return {env_var_name: secret_id, f"{env_var_name}_ACCESSOR": secret_id_accessor}
|
|
126
|
-
else:
|
|
127
|
-
logger.error("No secret ID returned in the response.")
|
|
128
|
-
raise RuntimeError("Failed to generate new secret ID: No content returned.")
|
|
129
|
-
except hvac.exceptions.InvalidRequest as e:
|
|
130
|
-
logger.error("Error generating new secret ID: %s", str(e))
|
|
131
|
-
raise RuntimeError("Failed to generate new secret ID.") from e
|
|
132
|
-
# new_secret_response = vault_client.auth.approle.generate_secret_id(
|
|
133
|
-
# role_name=role_name,
|
|
134
|
-
# mount_point=f'approle/{vault_path}'
|
|
135
|
-
# )
|
|
136
|
-
|
|
137
|
-
# return { env_var_name : new_secret_response['data']['secret_id'] }
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
def delete_secret_id(vault_client, role_name, secret_id, vault_path):
|
|
142
|
-
"""
|
|
143
|
-
Delete (revoke) a secret ID associated with a role in Vault.
|
|
144
|
-
|
|
145
|
-
Parameters:
|
|
146
|
-
vault_client (hvac.Client): An authenticated Vault client.
|
|
147
|
-
role_name (str): The name of the role the secret ID is associated with.
|
|
148
|
-
secret_id (str): The secret ID to be deleted.
|
|
149
|
-
vault_path (str): The path where the AppRole is enabled.
|
|
150
|
-
"""
|
|
151
|
-
try:
|
|
152
|
-
vault_client.auth.approle.destroy_secret_id(
|
|
153
|
-
mount_point=f"approle/{vault_path}",
|
|
154
|
-
role_name=role_name,
|
|
155
|
-
secret_id=secret_id
|
|
156
|
-
)
|
|
157
|
-
|
|
158
|
-
logger.info("Secret ID successfully revoked.")
|
|
159
|
-
except hvac.exceptions.InvalidRequest as e:
|
|
160
|
-
logger.error("Failed to revoke the secret ID: %s", str(e))
|
|
161
|
-
raise RuntimeError("Failed to delete the secret ID.") from e
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
def clean_secret_ids(vault_client, role_name, secret_id_env, vault_path, has_kube_secret_updated):
|
|
165
|
-
"""
|
|
166
|
-
This function removes all idle secret-ids from `role_name`, except the
|
|
167
|
-
inputted `secret_id_env`.
|
|
168
|
-
|
|
169
|
-
Note: secret_id_env is a dictionary. The key, VAULT_SECRET_ID, has the secret_id value.
|
|
170
|
-
"""
|
|
171
|
-
secret_id = secret_id_env.get("VAULT_SECRET_ID_ACCESSOR")
|
|
172
|
-
|
|
173
|
-
if has_kube_secret_updated:
|
|
174
|
-
secret_ids_path = f'auth/approle/{vault_path}/role/{role_name}/secret-id'
|
|
175
|
-
try:
|
|
176
|
-
response = vault_client.list(secret_ids_path)
|
|
177
|
-
if 'data' in response:
|
|
178
|
-
secret_ids = response['data']['keys']
|
|
179
|
-
for idx in secret_ids:
|
|
180
|
-
if idx != secret_id:
|
|
181
|
-
delete_secret_id(vault_client, role_name, secret_id, vault_path)
|
|
182
|
-
logger.info(f"Revoking idle secret id for role: {role_name}")
|
|
183
|
-
else:
|
|
184
|
-
logger.info("No secrets found at this path.")
|
|
185
|
-
except hvac.exceptions.Forbidden:
|
|
186
|
-
logger.error("Access denied. Ensure your token has the correct policies to read this path.")
|
|
187
|
-
except Exception as e:
|
|
188
|
-
logger.error(f"An error occurred: {e}")
|
|
1
|
+
import hvac
|
|
2
|
+
import os
|
|
3
|
+
from prefect import get_run_logger
|
|
4
|
+
|
|
5
|
+
from kube_watch.enums.providers import Providers
|
|
6
|
+
|
|
7
|
+
logger = get_run_logger()
|
|
8
|
+
|
|
9
|
+
def login(url, app_role_id, secret_id, path):
|
|
10
|
+
"""
|
|
11
|
+
Login to Vault, using an existing token if available, or via AppRole otherwise.
|
|
12
|
+
|
|
13
|
+
Parameters:
|
|
14
|
+
url (str): Vault server URL.
|
|
15
|
+
app_role_id (str): AppRole ID.
|
|
16
|
+
secret_id (str): AppRole Secret ID.
|
|
17
|
+
path (str): Path where the AppRole is enabled.
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
dict: Dictionary containing the initialized vault_client.
|
|
21
|
+
"""
|
|
22
|
+
vault_client = hvac.Client(url=url)
|
|
23
|
+
|
|
24
|
+
# Attempt to use an existing token from environment variables
|
|
25
|
+
vault_token = os.getenv('VAULT_TOKEN', None)
|
|
26
|
+
if vault_token:
|
|
27
|
+
vault_client.token = vault_token
|
|
28
|
+
# Verify if the current token is still valid
|
|
29
|
+
try:
|
|
30
|
+
if vault_client.is_authenticated():
|
|
31
|
+
logger.info("Authenticated with existing token.")
|
|
32
|
+
return vault_client
|
|
33
|
+
except hvac.exceptions.InvalidRequest as e:
|
|
34
|
+
logger.warning(f"Failed to authenticate with the existing token: {str(e)}")
|
|
35
|
+
|
|
36
|
+
# If token is not valid or not present, authenticate with AppRole
|
|
37
|
+
try:
|
|
38
|
+
vault_client.auth.approle.login(
|
|
39
|
+
role_id=app_role_id,
|
|
40
|
+
secret_id=secret_id,
|
|
41
|
+
mount_point=f'approle/{path}'
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# Store the new token in environment variables for subsequent use
|
|
45
|
+
os.environ['VAULT_TOKEN'] = vault_client.token
|
|
46
|
+
logger.info("Authenticated with new token and stored in environment variable.")
|
|
47
|
+
|
|
48
|
+
return vault_client
|
|
49
|
+
except hvac.exceptions.InvalidRequest as e:
|
|
50
|
+
logger.error(f"Authentication failed with provided secret_id: {str(e)}")
|
|
51
|
+
raise RuntimeError("Authentication failed: unable to log in with the provided credentials.") from e
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def get_secret(vault_client, secret_path, vault_mount_point):
|
|
56
|
+
"""
|
|
57
|
+
Retrieve a secret from Vault
|
|
58
|
+
"""
|
|
59
|
+
res = vault_client.secrets.kv.v2.read_secret_version(
|
|
60
|
+
path=secret_path,
|
|
61
|
+
mount_point=vault_mount_point,
|
|
62
|
+
raise_on_deleted_version=True
|
|
63
|
+
)
|
|
64
|
+
return res.get('data', {}).get('data')
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def update_secret(vault_client, secret_path, secret_data, vault_mount_point):
|
|
68
|
+
"""
|
|
69
|
+
Update or create a secret in Vault at the specified path.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
vault_client: The authenticated Vault client instance.
|
|
73
|
+
secret_path (str): The path where the secret will be stored or updated in Vault.
|
|
74
|
+
secret_data (dict): The secret data to store as a dictionary.
|
|
75
|
+
vault_mount_point (str): The mount point for the KV store.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
bool: True if the operation was successful, False otherwise.
|
|
79
|
+
"""
|
|
80
|
+
try:
|
|
81
|
+
# Writing the secret data to Vault at the specified path
|
|
82
|
+
vault_client.secrets.kv.v2.create_or_update_secret(
|
|
83
|
+
path=secret_path,
|
|
84
|
+
secret=secret_data,
|
|
85
|
+
mount_point=vault_mount_point
|
|
86
|
+
)
|
|
87
|
+
print("Secret updated successfully.")
|
|
88
|
+
return True
|
|
89
|
+
except Exception as e:
|
|
90
|
+
print(f"Failed to update secret: {e}")
|
|
91
|
+
return False
|
|
92
|
+
|
|
93
|
+
def generate_provider_creds(vault_client, provider, backend_path, role_name):
|
|
94
|
+
"""
|
|
95
|
+
Generate credentials for a specified provider
|
|
96
|
+
"""
|
|
97
|
+
if provider == Providers.AWS:
|
|
98
|
+
backend_path = backend_path
|
|
99
|
+
role_name = role_name
|
|
100
|
+
creds_path = f"{backend_path}/creds/{role_name}"
|
|
101
|
+
return vault_client.read(creds_path)
|
|
102
|
+
|
|
103
|
+
raise ValueError("Unknown provider")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def generate_new_secret_id(vault_client, role_name, vault_path, env_var_name):
|
|
108
|
+
"""
|
|
109
|
+
Generates new secret_id. Note an admin role is required for this.
|
|
110
|
+
"""
|
|
111
|
+
try:
|
|
112
|
+
# Write directly to the Vault endpoint to create the secret ID with num_uses
|
|
113
|
+
# response = vault_client.write(
|
|
114
|
+
# f"auth/approle/{vault_path}/role/{role_name}/secret-id",
|
|
115
|
+
# )
|
|
116
|
+
response = vault_client.auth.approle.generate_secret_id(
|
|
117
|
+
role_name=role_name,
|
|
118
|
+
mount_point=f'approle/{vault_path}'
|
|
119
|
+
)
|
|
120
|
+
# Check if the response contains the secret ID
|
|
121
|
+
if response and 'data' in response:
|
|
122
|
+
secret_id = response['data']['secret_id']
|
|
123
|
+
secret_id_accessor = response['data']['secret_id_accessor']
|
|
124
|
+
logger.info("Generated a new secret ID with usage buffer.")
|
|
125
|
+
return {env_var_name: secret_id, f"{env_var_name}_ACCESSOR": secret_id_accessor}
|
|
126
|
+
else:
|
|
127
|
+
logger.error("No secret ID returned in the response.")
|
|
128
|
+
raise RuntimeError("Failed to generate new secret ID: No content returned.")
|
|
129
|
+
except hvac.exceptions.InvalidRequest as e:
|
|
130
|
+
logger.error("Error generating new secret ID: %s", str(e))
|
|
131
|
+
raise RuntimeError("Failed to generate new secret ID.") from e
|
|
132
|
+
# new_secret_response = vault_client.auth.approle.generate_secret_id(
|
|
133
|
+
# role_name=role_name,
|
|
134
|
+
# mount_point=f'approle/{vault_path}'
|
|
135
|
+
# )
|
|
136
|
+
|
|
137
|
+
# return { env_var_name : new_secret_response['data']['secret_id'] }
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def delete_secret_id(vault_client, role_name, secret_id, vault_path):
|
|
142
|
+
"""
|
|
143
|
+
Delete (revoke) a secret ID associated with a role in Vault.
|
|
144
|
+
|
|
145
|
+
Parameters:
|
|
146
|
+
vault_client (hvac.Client): An authenticated Vault client.
|
|
147
|
+
role_name (str): The name of the role the secret ID is associated with.
|
|
148
|
+
secret_id (str): The secret ID to be deleted.
|
|
149
|
+
vault_path (str): The path where the AppRole is enabled.
|
|
150
|
+
"""
|
|
151
|
+
try:
|
|
152
|
+
vault_client.auth.approle.destroy_secret_id(
|
|
153
|
+
mount_point=f"approle/{vault_path}",
|
|
154
|
+
role_name=role_name,
|
|
155
|
+
secret_id=secret_id
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
logger.info("Secret ID successfully revoked.")
|
|
159
|
+
except hvac.exceptions.InvalidRequest as e:
|
|
160
|
+
logger.error("Failed to revoke the secret ID: %s", str(e))
|
|
161
|
+
raise RuntimeError("Failed to delete the secret ID.") from e
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def clean_secret_ids(vault_client, role_name, secret_id_env, vault_path, has_kube_secret_updated):
|
|
165
|
+
"""
|
|
166
|
+
This function removes all idle secret-ids from `role_name`, except the
|
|
167
|
+
inputted `secret_id_env`.
|
|
168
|
+
|
|
169
|
+
Note: secret_id_env is a dictionary. The key, VAULT_SECRET_ID, has the secret_id value.
|
|
170
|
+
"""
|
|
171
|
+
secret_id = secret_id_env.get("VAULT_SECRET_ID_ACCESSOR")
|
|
172
|
+
|
|
173
|
+
if has_kube_secret_updated:
|
|
174
|
+
secret_ids_path = f'auth/approle/{vault_path}/role/{role_name}/secret-id'
|
|
175
|
+
try:
|
|
176
|
+
response = vault_client.list(secret_ids_path)
|
|
177
|
+
if 'data' in response:
|
|
178
|
+
secret_ids = response['data']['keys']
|
|
179
|
+
for idx in secret_ids:
|
|
180
|
+
if idx != secret_id:
|
|
181
|
+
delete_secret_id(vault_client, role_name, secret_id, vault_path)
|
|
182
|
+
logger.info(f"Revoking idle secret id for role: {role_name}")
|
|
183
|
+
else:
|
|
184
|
+
logger.info("No secrets found at this path.")
|
|
185
|
+
except hvac.exceptions.Forbidden:
|
|
186
|
+
logger.error("Access denied. Ensure your token has the correct policies to read this path.")
|
|
187
|
+
except Exception as e:
|
|
188
|
+
logger.error(f"An error occurred: {e}")
|