qubership-pipelines-common-library 0.2.6__py3-none-any.whl → 2.0.1__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.
Files changed (66) hide show
  1. qubership_pipelines_common_library/__init__.py +1 -1
  2. qubership_pipelines_common_library/v1/artifactory_client.py +1 -1
  3. qubership_pipelines_common_library/v1/execution/exec_command.py +63 -2
  4. qubership_pipelines_common_library/v1/execution/exec_context.py +6 -6
  5. qubership_pipelines_common_library/v1/execution/exec_context_file.py +1 -1
  6. qubership_pipelines_common_library/v1/execution/exec_info.py +4 -0
  7. qubership_pipelines_common_library/v1/execution/exec_logger.py +7 -5
  8. qubership_pipelines_common_library/v1/github_client.py +10 -1
  9. qubership_pipelines_common_library/v1/gitlab_client.py +175 -11
  10. qubership_pipelines_common_library/v1/jenkins_client.py +55 -18
  11. qubership_pipelines_common_library/v1/maven_client.py +2 -2
  12. qubership_pipelines_common_library/v1/minio_client.py +1 -1
  13. qubership_pipelines_common_library/v1/utils/rest.py +1 -1
  14. qubership_pipelines_common_library/v1/utils/utils.py +1 -1
  15. qubership_pipelines_common_library/v1/utils/utils_cli.py +43 -9
  16. qubership_pipelines_common_library/v1/utils/utils_dictionary.py +1 -1
  17. qubership_pipelines_common_library/v1/utils/utils_file.py +17 -0
  18. qubership_pipelines_common_library/v1/utils/utils_logging.py +53 -0
  19. qubership_pipelines_common_library/v2/__init__.py +0 -0
  20. qubership_pipelines_common_library/v2/artifacts_finder/__init__.py +0 -0
  21. qubership_pipelines_common_library/v2/artifacts_finder/artifact_finder.py +56 -0
  22. qubership_pipelines_common_library/v2/artifacts_finder/auth/__init__.py +0 -0
  23. qubership_pipelines_common_library/v2/artifacts_finder/auth/aws_credentials.py +106 -0
  24. qubership_pipelines_common_library/v2/artifacts_finder/auth/azure_credentials.py +72 -0
  25. qubership_pipelines_common_library/v2/artifacts_finder/auth/gcp_credentials.py +88 -0
  26. qubership_pipelines_common_library/v2/artifacts_finder/model/__init__.py +0 -0
  27. qubership_pipelines_common_library/v2/artifacts_finder/model/artifact.py +20 -0
  28. qubership_pipelines_common_library/v2/artifacts_finder/model/artifact_provider.py +35 -0
  29. qubership_pipelines_common_library/v2/artifacts_finder/model/credentials.py +16 -0
  30. qubership_pipelines_common_library/v2/artifacts_finder/model/credentials_provider.py +16 -0
  31. qubership_pipelines_common_library/v2/artifacts_finder/providers/__init__.py +0 -0
  32. qubership_pipelines_common_library/v2/artifacts_finder/providers/artifactory.py +52 -0
  33. qubership_pipelines_common_library/v2/artifacts_finder/providers/aws_code_artifact.py +79 -0
  34. qubership_pipelines_common_library/v2/artifacts_finder/providers/azure_artifacts.py +98 -0
  35. qubership_pipelines_common_library/v2/artifacts_finder/providers/gcp_artifact_registry.py +50 -0
  36. qubership_pipelines_common_library/v2/artifacts_finder/providers/nexus.py +41 -0
  37. qubership_pipelines_common_library/v2/extensions/pipeline_data_importer.py +24 -0
  38. qubership_pipelines_common_library/v2/github/__init__.py +0 -0
  39. qubership_pipelines_common_library/v2/github/github_client.py +5 -0
  40. qubership_pipelines_common_library/v2/github/github_pipeline_data_importer.py +21 -0
  41. qubership_pipelines_common_library/v2/github/github_run_pipeline_command.py +175 -0
  42. qubership_pipelines_common_library/v2/github/safe_github_client.py +24 -0
  43. qubership_pipelines_common_library/v2/gitlab/__init__.py +0 -0
  44. qubership_pipelines_common_library/v2/gitlab/custom_extensions.py +101 -0
  45. qubership_pipelines_common_library/v2/gitlab/gitlab_client.py +36 -0
  46. qubership_pipelines_common_library/v2/gitlab/gitlab_pipeline_data_importer.py +26 -0
  47. qubership_pipelines_common_library/v2/gitlab/gitlab_run_pipeline_command.py +195 -0
  48. qubership_pipelines_common_library/v2/gitlab/safe_gitlab_client.py +32 -0
  49. qubership_pipelines_common_library/v2/jenkins/__init__.py +0 -0
  50. qubership_pipelines_common_library/v2/jenkins/custom_extensions.py +63 -0
  51. qubership_pipelines_common_library/v2/jenkins/jenkins_client.py +5 -0
  52. qubership_pipelines_common_library/v2/jenkins/jenkins_pipeline_data_importer.py +31 -0
  53. qubership_pipelines_common_library/v2/jenkins/jenkins_run_pipeline_command.py +165 -0
  54. qubership_pipelines_common_library/v2/jenkins/safe_jenkins_client.py +14 -0
  55. qubership_pipelines_common_library/v2/podman/__init__.py +0 -0
  56. qubership_pipelines_common_library/v2/podman/podman_command.md +178 -0
  57. qubership_pipelines_common_library/v2/podman/podman_command.py +311 -0
  58. qubership_pipelines_common_library/v2/sops/sops_client.py +116 -0
  59. qubership_pipelines_common_library/v2/utils/crypto_utils.py +48 -0
  60. qubership_pipelines_common_library/v2/utils/extension_utils.py +22 -0
  61. qubership_pipelines_common_library/v2/utils/retry_decorator.py +93 -0
  62. {qubership_pipelines_common_library-0.2.6.dist-info → qubership_pipelines_common_library-2.0.1.dist-info}/METADATA +5 -3
  63. qubership_pipelines_common_library-2.0.1.dist-info/RECORD +76 -0
  64. qubership_pipelines_common_library-0.2.6.dist-info/RECORD +0 -32
  65. {qubership_pipelines_common_library-0.2.6.dist-info → qubership_pipelines_common_library-2.0.1.dist-info}/WHEEL +0 -0
  66. {qubership_pipelines_common_library-0.2.6.dist-info → qubership_pipelines_common_library-2.0.1.dist-info}/licenses/LICENSE +0 -0
@@ -22,7 +22,7 @@ class Artifact:
22
22
  def from_string(artifact_str: str):
23
23
  parts = artifact_str.split(":")
24
24
  if len(parts) == 3:
25
- group, artifact, version = parts[0], parts[1], parts[-1]
25
+ artifact, version = parts[1], parts[-1]
26
26
  return Artifact(artifact, version)
27
27
 
28
28
 
@@ -67,7 +67,7 @@ class MavenArtifactSearcher:
67
67
  if not artifact:
68
68
  artifact = Artifact(artifact_id=artifact_id, version=version, extension=extension)
69
69
  if not artifact.artifact_id or not artifact.version:
70
- raise Exception(f"Artifact 'artifact_id' and 'version' must be specified!")
70
+ raise Exception("Artifact 'artifact_id' and 'version' must be specified!")
71
71
  logging.debug(f"Searching for '{artifact.artifact_id}' in {self.registry_url}...")
72
72
  return self._search_func(artifact=artifact)
73
73
 
@@ -68,7 +68,7 @@ class MinioClient:
68
68
 
69
69
  def put_file(self, bucket_name: str, path: str, local_path: str):
70
70
  """"""
71
- result = self.minio.fput_object(bucket_name, path, local_path)
71
+ self.minio.fput_object(bucket_name, path, local_path)
72
72
 
73
73
  def get_text_file_content(self, bucket_name: str, file_path: str):
74
74
  """"""
@@ -77,4 +77,4 @@ class RestClient:
77
77
  return False
78
78
  except requests.RequestException as e:
79
79
  logging.error(f"Error: {e}")
80
- return False
80
+ return False
@@ -47,4 +47,4 @@ def recursive_merge(source: dict, target: dict):
47
47
  source[key] = recursive_merge(source[key], value)
48
48
  else:
49
49
  source[key] = value
50
- return source
50
+ return source
@@ -1,14 +1,21 @@
1
+ import logging
1
2
  import re
3
+
2
4
  import click
3
- import logging
4
- import sys, os
5
+ from rich import box
6
+ from rich.logging import RichHandler
7
+ from rich.panel import Panel
8
+
5
9
  from qubership_pipelines_common_library.v1.execution.exec_logger import ExecutionLogger
10
+ from qubership_pipelines_common_library.v1.utils.utils_logging import rich_console, ExtendedReprHighlighter, \
11
+ LevelColorFilter
6
12
 
7
13
  DEFAULT_CONTEXT_FILE_PATH = 'context.yaml'
8
14
 
9
15
 
10
16
  def utils_cli(func):
11
17
  """Decorator to add CLI options for logging level, context path and custom input params."""
18
+
12
19
  @click.option('--log-level', default='INFO', show_default=True,
13
20
  type=click.Choice(['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'], case_sensitive=False),
14
21
  help="Set the logging level")
@@ -20,23 +27,50 @@ def utils_cli(func):
20
27
  @click.pass_context
21
28
  def wrapper(ctx, *args, log_level, **kwargs):
22
29
  ExecutionLogger.EXECUTION_LOG_LEVEL = getattr(logging, log_level.upper(), logging.INFO)
23
- _configure_global_logger(logging.getLogger(), log_level, ExecutionLogger.DEFAULT_FORMAT)
30
+ _configure_global_logger(logging.getLogger(), log_level)
31
+ _print_command_name()
24
32
  _transform_kwargs(kwargs)
25
33
  return ctx.invoke(func, *args, **kwargs)
34
+
26
35
  return wrapper
27
36
 
28
37
 
29
- def _configure_global_logger(global_logger: logging.Logger, log_level: str, formatter_str: str):
38
+ def _configure_global_logger(global_logger: logging.Logger, log_level: str):
30
39
  """Configure the global logger with a specific log level and formatter."""
31
- log_level_value = getattr(logging, log_level.upper(), logging.INFO)
32
40
  global_logger.setLevel(logging.DEBUG)
33
41
  if global_logger.hasHandlers():
34
42
  global_logger.handlers.clear()
35
43
  global_logger.propagate = True
36
- stdout_handler = logging.StreamHandler(sys.stdout)
37
- stdout_handler.setLevel(log_level_value)
38
- stdout_handler.setFormatter(logging.Formatter(formatter_str))
39
- global_logger.addHandler(stdout_handler)
44
+ rich_handler = RichHandler(
45
+ console=rich_console,
46
+ show_time=False,
47
+ show_level=False,
48
+ show_path=False,
49
+ enable_link_path=False,
50
+ rich_tracebacks=True,
51
+ tracebacks_show_locals=False,
52
+ markup=True,
53
+ highlighter=ExtendedReprHighlighter(),
54
+ )
55
+ rich_handler.addFilter(LevelColorFilter())
56
+ rich_handler.setFormatter(logging.Formatter(ExecutionLogger.LEVELNAME_COLORED_FORMAT))
57
+ log_level_value = getattr(logging, log_level.upper(), logging.INFO)
58
+ rich_handler.setLevel(log_level_value)
59
+ global_logger.addHandler(rich_handler)
60
+
61
+
62
+ def _print_command_name():
63
+ try:
64
+ click_context = click.get_current_context()
65
+ command_name = click_context.command.name or click_context.info_name
66
+ except RuntimeError:
67
+ logging.getLogger().warning("Can't find command name.")
68
+ command_name = ""
69
+
70
+ command_panel = Panel(f"command_name = {command_name}", expand=False, padding=(0, 1), box=box.ROUNDED)
71
+ rich_console.print()
72
+ rich_console.print(command_panel)
73
+ rich_console.print()
40
74
 
41
75
 
42
76
  def _transform_kwargs(kwargs):
@@ -39,4 +39,4 @@ class UtilsDictionary:
39
39
  elif key not in curr:
40
40
  curr[key] = {}
41
41
  curr = curr[key]
42
- return input_dict
42
+ return input_dict
@@ -66,3 +66,20 @@ class UtilsFile:
66
66
  func(path)
67
67
  else:
68
68
  raise
69
+
70
+ @staticmethod
71
+ def create_parent_dirs(filepath):
72
+ if directory := os.path.dirname(filepath):
73
+ os.makedirs(directory, exist_ok=True)
74
+
75
+ @staticmethod
76
+ def create_exec_dir(execution_folder_path: str | Path, exists_ok: bool = False) -> Path:
77
+ import shutil
78
+ exec_dir = Path(execution_folder_path)
79
+ if exec_dir.exists() and not exists_ok:
80
+ if exec_dir.is_dir():
81
+ shutil.rmtree(exec_dir)
82
+ else:
83
+ raise FileExistsError(f"Path '{execution_folder_path}' exists and is a file, not a directory.")
84
+ exec_dir.mkdir(parents=True, exist_ok=exists_ok)
85
+ return exec_dir
@@ -0,0 +1,53 @@
1
+ import logging
2
+
3
+ from rich.console import Console
4
+ from rich.highlighter import ReprHighlighter
5
+ from rich.theme import Theme
6
+
7
+ soft_theme = Theme({
8
+ "repr.number": "rgb(180,200,255)",
9
+ "repr.bool_true": "rgb(140,230,140)",
10
+ "repr.bool_false": "rgb(230,140,140)",
11
+ "repr.none": "rgb(200,200,200) italic",
12
+ "repr.path": "rgb(190,220,160)",
13
+ "repr.filename": "rgb(160,210,190)",
14
+ "repr.url": "rgb(130,180,255) underline",
15
+ "repr.uuid": "rgb(200,180,220)",
16
+ "repr.attrib_name": "rgb(220,200,130)",
17
+ "repr.attrib_value": "rgb(170,190,220)",
18
+ "repr.str": "rgb(140,180,140)",
19
+ "repr.tag_name": "rgb(200,170,220)",
20
+ "repr.tag_value": "rgb(170,200,220)",
21
+
22
+ "repr.time": "rgb(160,190,220) italic",
23
+ })
24
+
25
+ rich_console = Console(
26
+ theme=soft_theme,
27
+ force_terminal=True,
28
+ no_color=False,
29
+ highlight=True,
30
+ width=150,
31
+ )
32
+
33
+ level_colors = {
34
+ logging.DEBUG: "steel_blue",
35
+ logging.INFO: "light_sea_green",
36
+ logging.WARNING: "orange3",
37
+ logging.ERROR: "indian_red",
38
+ logging.CRITICAL: "bold medium_violet_red",
39
+ }
40
+
41
+
42
+ class LevelColorFilter(logging.Filter):
43
+ def filter(self, record):
44
+ color = level_colors.get(record.levelno, "default")
45
+ record.levelname_color_open_tag = f"[{color}]"
46
+ record.levelname_color_close_tag = "[/]"
47
+ return True
48
+
49
+
50
+ class ExtendedReprHighlighter(ReprHighlighter):
51
+ highlights = ReprHighlighter.highlights + [
52
+ r"(?P<time>\b([01]?[0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9](?:[.,]\d{1,9})?\b)",
53
+ ]
File without changes
@@ -0,0 +1,56 @@
1
+ import logging
2
+ from pathlib import Path
3
+
4
+ from qubership_pipelines_common_library.v2.artifacts_finder.model.artifact import Artifact
5
+ from qubership_pipelines_common_library.v2.artifacts_finder.model.artifact_provider import ArtifactProvider
6
+
7
+
8
+ class ArtifactFinder:
9
+ """
10
+ Allows searching for specific descriptor artifacts in different repositories without knowing full coordinates
11
+ (e.g. knowing only `artifact_id` and `version`, but not its `group_id`)
12
+
13
+ Supports different repository providers: Artifactory, Nexus, AWS, GCP, Azure
14
+
15
+ Provides different auth methods for Cloud Providers, implementing `CloudCredentialsProvider` interface
16
+
17
+ Start by initializing this client with one of implementations:
18
+ ``finder = ArtifactFinder(artifact_provider=ArtifactoryProvider(registry_url="https://our_url", username="user", password="password"))``
19
+
20
+ Then find your artifacts using
21
+ ``resource_urls = finder.find_artifact_urls(artifact_id='art_id', version='1.0.0', extension='json')``
22
+
23
+ Additionally, perform filtering of returned results (if you expect to find more than one artifact), and then download necessary artifacts with
24
+ ``finder.download_artifact(one_of_the_returned_resource_urls, './my_artifact.json')``
25
+
26
+ For more complex providers (e.g. AWS Code Artifact), you need to use specific Credential Providers
27
+ As an example:
28
+ ```
29
+ aws_creds = AwsCredentialsProvider().with_assume_role(...all the required params...).get_credentials()
30
+ aws_code_artifact_provider = AwsCodeArtifactProvider(creds=creds, domain='our_domain', project='our_project')
31
+ finder = ArtifactFinder(artifact_provider=aws_code_artifact_provider)
32
+ ```
33
+ """
34
+
35
+ def __init__(self, artifact_provider: ArtifactProvider, **kwargs):
36
+ if not artifact_provider:
37
+ raise Exception("Initialize ArtifactFinder with one of registry artifact providers first!")
38
+ self.provider = artifact_provider
39
+
40
+ def find_artifact_urls(self, artifact_id: str = None, version: str = None, group_id: str = None,
41
+ extension: str = "jar", artifact: Artifact = None) -> list[str]:
42
+ if not artifact:
43
+ artifact = Artifact(group_id=group_id, artifact_id=artifact_id, version=version, extension=extension)
44
+ if not artifact.artifact_id or not artifact.version:
45
+ raise Exception("Artifact 'artifact_id' and 'version' must be specified!")
46
+ logging.debug(f"Searching for '{artifact.artifact_id}:{artifact.version}' in {self.provider.get_provider_name()}...")
47
+ return self.provider.search_artifacts(artifact=artifact)
48
+
49
+ def download_artifact(self, resource_url: str, local_path: str | Path, artifact: Artifact = None):
50
+ from qubership_pipelines_common_library.v1.utils.utils_file import UtilsFile
51
+ download_path = Path(local_path)
52
+ if artifact:
53
+ download_path = download_path.joinpath(artifact.get_filename())
54
+ UtilsFile.create_parent_dirs(download_path)
55
+ logging.debug(f"Downloading artifact from '{resource_url}' to '{download_path}'...")
56
+ return self.provider.download_artifact(resource_url=resource_url, local_path=download_path)
@@ -0,0 +1,106 @@
1
+ import boto3
2
+
3
+ from enum import StrEnum
4
+ from botocore.config import Config
5
+ from qubership_pipelines_common_library.v2.artifacts_finder.model.credentials import Credentials
6
+ from qubership_pipelines_common_library.v2.artifacts_finder.model.credentials_provider import CloudCredentialsProvider
7
+
8
+
9
+ class AwsCredentialsProvider(CloudCredentialsProvider):
10
+
11
+ DEFAULT_ROLE_SESSION_NAME = "codeartifact-session"
12
+
13
+ access_key: str
14
+ secret_key: str
15
+ session_token: str
16
+ region_name: str
17
+ role_arn: str
18
+ role_session_name: str
19
+ _auth_type = None
20
+
21
+ class AuthType(StrEnum):
22
+ DIRECT = 'DIRECT'
23
+ ASSUME_ROLE = 'ASSUME_ROLE'
24
+
25
+ def with_direct_credentials(self,
26
+ access_key: str,
27
+ secret_key: str,
28
+ region_name: str,
29
+ session_token: str | None = None,
30
+ ):
31
+ self.access_key = access_key
32
+ self.secret_key = secret_key
33
+ self.session_token = session_token
34
+ self.region_name = region_name
35
+ self.validate_mandatory_attrs(["access_key", "secret_key", "region_name"])
36
+ self._auth_type = self.AuthType.DIRECT
37
+ return self
38
+
39
+ def with_assume_role(self,
40
+ access_key: str,
41
+ secret_key: str,
42
+ region_name: str,
43
+ role_arn: str,
44
+ role_session_name: str | None = DEFAULT_ROLE_SESSION_NAME,
45
+ ):
46
+ self.access_key = access_key
47
+ self.secret_key = secret_key
48
+ self.region_name = region_name
49
+ self.role_arn = role_arn
50
+ self.role_session_name = role_session_name
51
+ self.validate_mandatory_attrs(["access_key", "secret_key", "region_name", "role_arn", "role_session_name"])
52
+ self._auth_type = self.AuthType.ASSUME_ROLE
53
+ return self
54
+
55
+ def get_credentials(self) -> Credentials:
56
+ if self._auth_type is self.AuthType.DIRECT:
57
+ return self._get_direct_credentials()
58
+ elif self._auth_type is self.AuthType.ASSUME_ROLE:
59
+ return self._get_assume_role_credentials()
60
+ else:
61
+ raise ValueError("Need to initialize this provider with AuthType via .with_*auth_type* method first!")
62
+
63
+ def get_ecr_authorization_token(self) -> str:
64
+ creds = self.get_credentials()
65
+ ecr_client = boto3.client(
66
+ service_name="ecr",
67
+ config=Config(region_name=self.region_name),
68
+ aws_access_key_id=creds["access_key"],
69
+ aws_secret_access_key=creds["secret_key"],
70
+ aws_session_token=creds["session_token"],
71
+ )
72
+ ecr_authorization_token = ecr_client.get_authorization_token()
73
+ ecr_authorization_data = ecr_authorization_token["authorizationData"][0]
74
+ return ecr_authorization_data["authorizationToken"]
75
+
76
+ def _get_direct_credentials(self) -> Credentials:
77
+ return Credentials(
78
+ access_key=self.access_key,
79
+ secret_key=self.secret_key,
80
+ session_token=self.session_token,
81
+ region_name=self.region_name,
82
+ )
83
+
84
+ def _get_assume_role_credentials(self) -> Credentials:
85
+ creds = self._assume_role()
86
+ return Credentials(
87
+ access_key=creds["AccessKeyId"],
88
+ secret_key=creds["SecretAccessKey"],
89
+ session_token=creds["SessionToken"],
90
+ role_arn=self.role_arn,
91
+ role_session_name=self.role_session_name,
92
+ region_name=self.region_name,
93
+ )
94
+
95
+ def _assume_role(self) -> dict:
96
+ sts_client = boto3.client(
97
+ service_name="sts",
98
+ config=Config(region_name=self.region_name),
99
+ aws_access_key_id=self.access_key,
100
+ aws_secret_access_key=self.secret_key,
101
+ )
102
+ assumed = sts_client.assume_role(
103
+ RoleArn=self.role_arn,
104
+ RoleSessionName=self.role_session_name,
105
+ )
106
+ return assumed["Credentials"]
@@ -0,0 +1,72 @@
1
+ import requests
2
+
3
+ from enum import StrEnum
4
+ from qubership_pipelines_common_library.v2.artifacts_finder.model.credentials import Credentials
5
+ from qubership_pipelines_common_library.v2.artifacts_finder.model.credentials_provider import CloudCredentialsProvider
6
+
7
+
8
+ class AzureCredentialsProvider(CloudCredentialsProvider):
9
+
10
+ tenant_id: str
11
+ client_id: str
12
+ client_secret: str
13
+ target_resource: str
14
+ _auth_data: dict
15
+ _auth_type = None
16
+
17
+ class AuthType(StrEnum):
18
+ OAUTH2 = 'OAUTH2'
19
+
20
+ def with_oauth2(self,
21
+ tenant_id: str,
22
+ client_id: str,
23
+ client_secret: str,
24
+ target_resource: str,
25
+ ):
26
+ self.tenant_id = tenant_id
27
+ self.client_id = client_id
28
+ self.client_secret = client_secret
29
+ self.target_resource = target_resource
30
+ self.validate_mandatory_attrs(["tenant_id", "client_id", "client_secret", "target_resource"])
31
+ self._auth_data = {
32
+ "grant_type": "client_credentials",
33
+ "client_id": client_id,
34
+ "client_secret": client_secret,
35
+ "scope": f"{target_resource}/.default"
36
+ }
37
+ self._auth_type = self.AuthType.OAUTH2
38
+ return self
39
+
40
+ def with_oauth2_custom_data(self,
41
+ tenant_id: str,
42
+ custom_auth_data: dict,
43
+ ):
44
+ self.tenant_id = tenant_id
45
+ self._auth_data = custom_auth_data
46
+ self.validate_mandatory_attrs(["tenant_id", "_auth_data"])
47
+ self._auth_type = self.AuthType.OAUTH2
48
+ return self
49
+
50
+ def get_credentials(self) -> Credentials:
51
+ if self._auth_type is self.AuthType.OAUTH2:
52
+ return self._get_oauth2_credentials()
53
+ else:
54
+ raise ValueError("Need to initialize this provider with AuthType via .with_*auth_type* method first!")
55
+
56
+ def _get_oauth2_credentials(self) -> Credentials:
57
+ token_url = f"https://login.microsoftonline.com/{self.tenant_id}/oauth2/v2.0/token"
58
+ response = requests.post(
59
+ token_url,
60
+ data=self._auth_data,
61
+ headers={"Content-Type": "application/x-www-form-urlencoded"},
62
+ )
63
+ response.raise_for_status()
64
+ response_json = response.json()
65
+ access_token = response_json.get("access_token")
66
+ if not access_token:
67
+ raise Exception(f"Failed to get access token from {token_url}: {response_json}")
68
+
69
+ return Credentials(
70
+ access_token=access_token,
71
+ tenant_id=self.tenant_id,
72
+ )
@@ -0,0 +1,88 @@
1
+ import json
2
+ from enum import StrEnum
3
+ from pathlib import Path
4
+
5
+ from google.auth.transport.requests import AuthorizedSession, Request
6
+
7
+ from qubership_pipelines_common_library.v2.artifacts_finder.model.credentials import Credentials
8
+ from qubership_pipelines_common_library.v2.artifacts_finder.model.credentials_provider import CloudCredentialsProvider
9
+
10
+
11
+ class GcpCredentialsProvider(CloudCredentialsProvider):
12
+
13
+ DEFAULT_SCOPES = ["https://www.googleapis.com/auth/cloud-platform"]
14
+ DEFAULT_SUBJECT_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:jwt"
15
+
16
+ service_account_key_content: str
17
+ oidc_credential_source: dict
18
+ audience: str
19
+ scopes: list[str]
20
+ subject_token_type: str
21
+ _auth_type = None
22
+
23
+ class AuthType(StrEnum):
24
+ SA_KEY = 'SA_KEY'
25
+ OIDC_CREDS = 'OIDC_CREDS'
26
+
27
+ def with_service_account_key(self,
28
+ service_account_key_content: str | None = None,
29
+ service_account_key_path: str | Path | None = None,
30
+ scopes: list[str] = None,
31
+ ):
32
+ if scopes is None:
33
+ scopes = self.DEFAULT_SCOPES
34
+ self.scopes = scopes
35
+ self.service_account_key_content = service_account_key_content
36
+ if service_account_key_path:
37
+ with open(service_account_key_path, 'r') as key_file:
38
+ self.service_account_key_content = key_file.read()
39
+ self.validate_mandatory_attrs(["service_account_key_content"])
40
+ self._auth_type = self.AuthType.SA_KEY
41
+ return self
42
+
43
+ def with_oidc_creds(self,
44
+ oidc_credential_source: dict,
45
+ audience: str,
46
+ oidc_token_file_path: str = None,
47
+ subject_token_type: str = DEFAULT_SUBJECT_TOKEN_TYPE,
48
+ scopes: list[str] = None,
49
+ ):
50
+ if scopes is None:
51
+ scopes = self.DEFAULT_SCOPES
52
+ self.scopes = scopes
53
+ self.oidc_credential_source = oidc_credential_source
54
+ if oidc_token_file_path:
55
+ self.oidc_credential_source = {"file": oidc_token_file_path}
56
+ self.audience = audience
57
+ self.subject_token_type = subject_token_type
58
+ self.validate_mandatory_attrs(["oidc_credential_source", "audience"])
59
+ self._auth_type = self.AuthType.OIDC_CREDS
60
+ return self
61
+
62
+ def get_credentials(self) -> Credentials:
63
+ if self._auth_type == self.AuthType.SA_KEY:
64
+ from google.oauth2 import service_account
65
+ google_creds = service_account.Credentials.from_service_account_info(
66
+ info=json.loads(self.service_account_key_content),
67
+ scopes=self.scopes,
68
+ )
69
+ elif self._auth_type == self.AuthType.OIDC_CREDS:
70
+ from google.auth import identity_pool
71
+ google_creds = identity_pool.Credentials(
72
+ audience=self.audience,
73
+ subject_token_type=self.subject_token_type,
74
+ credential_source=self.oidc_credential_source,
75
+ scopes=self.scopes,
76
+ )
77
+ else:
78
+ raise ValueError("Need to initialize this provider with AuthType via .with_*auth_type* method first!")
79
+
80
+ google_creds.refresh(Request())
81
+ creds = Credentials(
82
+ google_credentials_object=google_creds,
83
+ gcp_authorization_token=google_creds.token, # It can be used in Basic Authorization (in some Execution Commands)
84
+ authorized_session=AuthorizedSession(credentials=google_creds),
85
+ )
86
+ if self._auth_type == self.AuthType.SA_KEY:
87
+ creds.service_account_key_content = self.service_account_key_content
88
+ return creds
@@ -0,0 +1,20 @@
1
+ class Artifact:
2
+ def __init__(self, group_id = None, artifact_id = None, version = None, extension='jar'):
3
+ self.group_id = group_id
4
+ self.artifact_id = artifact_id
5
+ self.version = version
6
+ self.extension = "jar" if not extension else extension
7
+
8
+ def is_snapshot(self):
9
+ return self.version and self.version.endswith("SNAPSHOT")
10
+
11
+ def get_filename(self) -> str:
12
+ return f"{self.artifact_id}-{self.version}.{self.extension}"
13
+
14
+ @staticmethod
15
+ def from_string(artifact_str: str):
16
+ parts = artifact_str.split(":")
17
+ if len(parts) == 3:
18
+ group, artifact, version = parts[0], parts[1], parts[-1]
19
+ return Artifact(group, artifact, version)
20
+ raise Exception("Invalid artifact string")
@@ -0,0 +1,35 @@
1
+ import requests
2
+
3
+ from pathlib import Path
4
+ from abc import ABC, abstractmethod
5
+ from qubership_pipelines_common_library.v2.artifacts_finder.model.artifact import Artifact
6
+
7
+
8
+ class ArtifactProvider(ABC):
9
+ """Base class for all artifact providers"""
10
+
11
+ TIMESTAMP_VERSION_PATTERN = "^(.*-)?([0-9]{8}\\.[0-9]{6}-[0-9]+)$"
12
+
13
+ def __init__(self, params: dict = None, **kwargs):
14
+ self.params = params if params else {}
15
+ self._session = requests.Session()
16
+ self._session.verify = self.params.get('verify', True)
17
+ self.timeout = self.params.get('timeout', None)
18
+
19
+ def generic_download(self, resource_url: str, local_path: str | Path):
20
+ response = self._session.get(url=resource_url, timeout=self.timeout)
21
+ response.raise_for_status()
22
+ with open(local_path, 'wb') as file:
23
+ file.write(response.content)
24
+
25
+ @abstractmethod
26
+ def download_artifact(self, resource_url: str, local_path: str | Path, **kwargs) -> None:
27
+ pass
28
+
29
+ @abstractmethod
30
+ def search_artifacts(self, artifact: Artifact, **kwargs) -> list[str]:
31
+ pass
32
+
33
+ @abstractmethod
34
+ def get_provider_name(self) -> str:
35
+ pass
@@ -0,0 +1,16 @@
1
+ class Credentials(dict):
2
+ def __init__(self, *args, **kwargs):
3
+ super().__init__(*args, **kwargs)
4
+ self.__dict__ = self # allow attribute access
5
+
6
+ def __getitem__(self, key):
7
+ return self.get(key)
8
+
9
+ def __getattr__(self, key):
10
+ try:
11
+ return self[key]
12
+ except KeyError:
13
+ return None
14
+
15
+ def __setattr__(self, key, value):
16
+ self[key] = value
@@ -0,0 +1,16 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+ from qubership_pipelines_common_library.v2.artifacts_finder.model.credentials import Credentials
4
+
5
+
6
+ class CloudCredentialsProvider(ABC):
7
+ """Base class for all cloud credentials"""
8
+
9
+ @abstractmethod
10
+ def get_credentials(self) -> Credentials:
11
+ pass
12
+
13
+ def validate_mandatory_attrs(self, attrs_names) -> None:
14
+ missing_attrs = [attr for attr in attrs_names if getattr(self, attr, None) is None]
15
+ if missing_attrs:
16
+ raise ValueError(f"The following mandatory attributes are not set: {', '.join(missing_attrs)}")