kubernetes-watch 0.1.4__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.
@@ -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,166 +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
- new_secret_response = vault_client.auth.approle.generate_secret_id(
112
- role_name=role_name,
113
- mount_point=f'approle/{vault_path}'
114
- )
115
-
116
- return { env_var_name : new_secret_response['data']['secret_id'] }
117
-
118
-
119
-
120
- def delete_secret_id(vault_client, role_name, secret_id, vault_path):
121
- """
122
- Delete (revoke) a secret ID associated with a role in Vault.
123
-
124
- Parameters:
125
- vault_client (hvac.Client): An authenticated Vault client.
126
- role_name (str): The name of the role the secret ID is associated with.
127
- secret_id (str): The secret ID to be deleted.
128
- vault_path (str): The path where the AppRole is enabled.
129
- """
130
- try:
131
- vault_client.auth.approle.destroy_secret_id(
132
- mount_point=f"approle/{vault_path}",
133
- role_name=role_name,
134
- secret_id=secret_id
135
- )
136
-
137
- logger.info("Secret ID successfully revoked.")
138
- except hvac.exceptions.InvalidRequest as e:
139
- logger.error("Failed to revoke the secret ID: %s", str(e))
140
- raise RuntimeError("Failed to delete the secret ID.") from e
141
-
142
-
143
- def clean_secret_ids(vault_client, role_name, secret_id_env, vault_path, has_kube_secret_updated):
144
- """
145
- This function removes all idle secret-ids from `role_name`, except the
146
- inputted `secret_id_env`.
147
-
148
- Note: secret_id_env is a dictionary. The key, VAULT_SECRET_ID, has the secret_id value.
149
- """
150
- secret_id = secret_id_env.get("VAULT_SECRET_ID")
151
- if has_kube_secret_updated:
152
- secret_ids_path = f'auth/approle/{vault_path}/role/{role_name}/secret-id'
153
- try:
154
- response = vault_client.list(secret_ids_path)
155
- if 'data' in response:
156
- secret_ids = response['data']['keys']
157
- for idx in secret_ids:
158
- if idx != secret_id:
159
- delete_secret_id(vault_client, role_name, secret_id, vault_path)
160
- logger.info(f"Revoking idle secret id for role: {role_name}")
161
- else:
162
- logger.info("No secrets found at this path.")
163
- except hvac.exceptions.Forbidden:
164
- logger.error("Access denied. Ensure your token has the correct policies to read this path.")
165
- except Exception as e:
166
- 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}")