diff --git a/saagieapi/apps/apps.py b/saagieapi/apps/apps.py index 05cfe79..882cd6a 100644 --- a/saagieapi/apps/apps.py +++ b/saagieapi/apps/apps.py @@ -668,6 +668,7 @@ def edit( emails: List = None, logins: List = None, status_list: List = None, + resources: Dict = None, ) -> Dict: """Edit an app. \ Each optional parameter can be set to change the value of the corresponding field. @@ -694,6 +695,8 @@ def edit( Receive an email when the app status change to a specific status. Each item of the list should be one of these following values: "STARTING","STARTED", "ROLLING_BACK","UPGRADING","RECOVERING","RESTARTING","STOPPING","STOPPED","FAILED" + resources : dict, optional + Resources CPU, RAM & GPU limited and guaranteed for the app. Returns ------- @@ -728,6 +731,9 @@ def edit( if emails or logins or status_list: params["alerting"] = self.check_alerting(emails, logins, status_list) + if resources: + params["resources"] = resources + params = {"app": params} result = self.saagie_api.client.execute(query=gql(GQL_EDIT_APP), variable_values=params) logging.info("✅ App [%s] successfully edited", app_id) @@ -1458,3 +1464,87 @@ def count_history_statuses(self, history_id, version_number, start_time): } return self.saagie_api.client.execute(query=gql(GQL_COUNT_HISTORY_APP_STATUS), variable_values=params) + + def get_logs( + self, + app_id: str, + app_execution_id: str, + limit: int = None, + skip: int = None, + log_stream: str = None, + start_at: str = None, + ): + """Get logs of the app + + Parameters + ---------- + app_id : str + UUID of your app + app_execution_id : str + UUID of the execution of your app + limit : int, optional + Number of log lines to retrieve + skip : int, optional + Number of log lines to skip + log_stream: str, optional + Stream of logs to follow. Values accepted : + [ENVVARS_STDOUT, ENVVARS_STDERR, ORCHESTRATION_STDOUT, ORCHESTRATION_STDERR, STDERR, STDOUT] + By default, all the streams are retrieved + start_at: str, optional + Get logs since a specific datetime. + Following formats accepted : "2024-04-09 10:00:00" and "2024-04-09T10:00:00" + + Returns + ------- + dict + Logs of the app + + Examples + -------- + >>> saagieapi.apps.get_logs( + ... app_id="70e85ade-d6cc-4a90-8d7d-639adbd25e5d", + ... app_execution_id="e3e31074-4a12-450e-96e4-0eae7801dfca", + ... limit=2, + ... skip=5, + ... start_at="2024-04-09 10:00:00" + ... ) + { + "appLogs": { + "count": 25, + "content": [ + { + "index": 5, + "value": "[I 2024-04-09 13:38:36.982 ServerApp] jupyterlab_git | extension was successfully linked.", + "containerId": "d7104fa7371c5ed6ef540fa8b0620a654a0e02c57136e29f0fcc03d16e36d74f", + "stream": "STDERR", + "recordAt": "2024-04-09T13:38:36.982473892Z" + }, + { + "index": 6, + "value": "[W 2024-04-09 13:38:36.987 NotebookApp] 'ip' has moved from NotebookApp to ServerApp. This config will be passed to ServerApp. Be sure to update your config before our next release.", + "containerId": "d7104fa7371c5ed6ef540fa8b0620a654a0e02c57136e29f0fcc03d16e36d74f", + "stream": "STDERR", + "recordAt": "2024-04-09T13:38:36.987400105Z" + } + ] + } + } + """ + params = { + "appId": app_id, + "appExecutionId": app_execution_id, + } + + if limit: + params["limit"] = limit + + if skip: + params["skip"] = skip + + if log_stream: + params["stream"] = log_stream + + if start_at: + params["recordAt"] = start_at + + return self.saagie_api.client.execute(query=gql(GQL_GET_APP_LOG), variable_values=params) diff --git a/saagieapi/apps/gql_queries.py b/saagieapi/apps/gql_queries.py index 3435f9c..f8a6770 100644 --- a/saagieapi/apps/gql_queries.py +++ b/saagieapi/apps/gql_queries.py @@ -452,3 +452,19 @@ countAppHistoryStatuses(appHistoryId: $appHistoryId, versionNumber: $versionNumber, startTime: $startTime) } """ + +GQL_GET_APP_LOG = """ +query appLogs($appId: UUID!, $appExecutionId: UUID!, $limit: Int, $skip: Int, $stream: LogStream, $recordAt: String) { + appLogs(appId: $appId, appExecutionId: $appExecutionId, limit: $limit, skip: $skip, stream: $stream, recordAt: $recordAt) + { + count + content { + index + value + containerId + stream + recordAt + } + } +} +""" diff --git a/saagieapi/env_vars/env_vars.py b/saagieapi/env_vars/env_vars.py index 4847756..9653cb3 100644 --- a/saagieapi/env_vars/env_vars.py +++ b/saagieapi/env_vars/env_vars.py @@ -16,9 +16,9 @@ def handle_error(msg, project_id): return False -def check_scope(scope, project_id, pipeline_id): - if scope not in ("GLOBAL", "PROJECT", "PIPELINE"): - raise ValueError("❌ 'scope' must be one of GLOBAL, PROJECT or PIPELINE") +def check_scope(scope, project_id, pipeline_id, app_id): + if scope not in ("GLOBAL", "PROJECT", "PIPELINE", "APP"): + raise ValueError("❌ 'scope' must be one of GLOBAL, PROJECT, PIPELINE or APP") if scope == "PROJECT" and project_id is None: raise ValueError("❌ 'project_id' must be provided for scope PROJECT") @@ -26,6 +26,9 @@ def check_scope(scope, project_id, pipeline_id): if scope == "PIPELINE" and pipeline_id is None: raise ValueError("❌ 'pipeline_id' must be provided for scope PIPELINE") + if scope == "APP" and app_id is None: + raise ValueError("❌ 'app_id' must be provided for scope APP") + class EnvVars: # pylint: disable=singleton-comparison @@ -977,6 +980,7 @@ def list( scope: str, project_id: str = None, pipeline_id: str = None, + app_id: str = None, scope_only: bool = False, pprint_result: Optional[bool] = None, ) -> Dict: @@ -992,6 +996,8 @@ def list( UUID of your project (see README on how to find it) pipeline_id : str, optional UUID of your pipeline (see README on how to find it) + app_id : str, optional + UUID of your app (see README on how to find it) scope_only : bool, optional Whether to return only the environment variables of the given scope pprint_result : bool, optional @@ -1031,7 +1037,7 @@ def list( } ] """ - check_scope(scope, project_id, pipeline_id) + check_scope(scope, project_id, pipeline_id, app_id) if scope == "GLOBAL": res = self.saagie_api.client.execute(query=gql(GQL_LIST_GLOBAL_ENV_VARS), pprint_result=pprint_result)[ @@ -1049,6 +1055,12 @@ def list( variable_values={"pipelineId": pipeline_id}, pprint_result=pprint_result, )["pipelineEnvironmentVariables"] + elif scope == "APP": + res = self.saagie_api.client.execute( + query=gql(GQL_LIST_APP_ENV_VARS), + variable_values={"appId": app_id}, + pprint_result=pprint_result, + )["appEnvironmentVariables"] return [env for env in res if env["scope"] == scope] if scope_only else res @@ -1058,6 +1070,7 @@ def get( name: str, project_id: str = None, pipeline_id: str = None, + app_id: str = None, scope_only: bool = False, pprint_result: Optional[bool] = None, ) -> Dict: @@ -1076,6 +1089,8 @@ def get( UUID of your project (see README on how to find it) pipeline_id : str, optional UUID of your pipeline (see README on how to find it) + app_id : str, optional + UUID of your app (see README on how to find it) scope_only : bool, optional Whether to return only the environment variables of the given scope pprint_result : bool, optional @@ -1101,9 +1116,9 @@ def get( 'invalidReasons': None } """ - check_scope(scope, project_id, pipeline_id) + check_scope(scope, project_id, pipeline_id, app_id) - env_vars = self.list(scope, project_id, pipeline_id, scope_only, pprint_result) + env_vars = self.list(scope, project_id, pipeline_id, app_id, scope_only, pprint_result) return next((d for d in env_vars if d["name"] == name), None) @@ -1116,6 +1131,7 @@ def create( is_password: bool = False, project_id: str = None, pipeline_id: str = None, + app_id: str = None, ) -> Dict: """Create an environment variable in a given scope @@ -1135,6 +1151,8 @@ def create( UUID of your project (see README on how to find it) pipeline_id : str, optional UUID of your pipeline (see README on how to find it) + app_id : str, optional + UUID of your app (see README on how to find it) Returns ------- @@ -1158,7 +1176,7 @@ def create( } """ - check_scope(scope, project_id, pipeline_id) + check_scope(scope, project_id, pipeline_id, app_id) params = { "envVar": { @@ -1170,10 +1188,12 @@ def create( }, } - if scope == "PIPELINE": - params["entityId"] = pipeline_id - elif scope == "PROJECT": + if scope == "PROJECT": params["entityId"] = project_id + elif scope == "PIPELINE": + params["entityId"] = pipeline_id + elif scope == "APP": + params["entityId"] = app_id result = self.saagie_api.client.execute(query=gql(GQL_CREATE_ENV_VAR), variable_values=params) logging.info("✅ Environment variable [%s] successfully created", name) @@ -1189,6 +1209,7 @@ def update( is_password: bool = None, project_id: str = None, pipeline_id: str = None, + app_id: str = None, ) -> Dict: """ Update environment variable with provided function variables if it exists @@ -1211,6 +1232,8 @@ def update( UUID of your project (see README on how to find it) pipeline_id : str, optional UUID of your pipeline (see README on how to find it) + app_id : str, optional + UUID of your app (see README on how to find it) Returns ------- @@ -1239,9 +1262,9 @@ def update( } """ - check_scope(scope, project_id, pipeline_id) + check_scope(scope, project_id, pipeline_id, app_id) - existing_env_var = self.list(scope, project_id, pipeline_id, scope_only=True, pprint_result=False) + existing_env_var = self.list(scope, project_id, pipeline_id, app_id, scope_only=True, pprint_result=False) var = next((d for d in existing_env_var if d["name"] == name), None) if var is None: @@ -1255,6 +1278,8 @@ def update( params["entityId"] = project_id elif scope == "PIPELINE": params["entityId"] = pipeline_id + elif scope == "APP": + params["entityId"] = app_id params["envVar"].pop("isValid", None) params["envVar"].pop("overriddenValues", None) @@ -1282,6 +1307,7 @@ def create_or_update( is_password: bool = None, project_id: str = None, pipeline_id: str = None, + app_id: str = None, ) -> Dict: """ Create a new environment variable or update it if it already exists @@ -1302,6 +1328,8 @@ def create_or_update( UUID of your project (see README on how to find it) pipeline_id : str, optional UUID of your pipeline (see README on how to find it) + app_id : str, optional + UUID of your app (see README on how to find it) Returns ------- @@ -1324,9 +1352,9 @@ def create_or_update( } """ - check_scope(scope, project_id, pipeline_id) + check_scope(scope, project_id, pipeline_id, app_id) - existing_env_var = self.list(scope, project_id, pipeline_id, scope_only=True, pprint_result=False) + existing_env_var = self.list(scope, project_id, pipeline_id, app_id, scope_only=True, pprint_result=False) var = next((d for d in existing_env_var if d["name"] == name), None) @@ -1340,6 +1368,7 @@ def create_or_update( "is_password": is_password, "project_id": project_id, "pipeline_id": pipeline_id, + "app_id": app_id, } args2 = {k: v for k, v in args.items() if v is not None} return self.create(**args2) @@ -1352,6 +1381,7 @@ def create_or_update( is_password=is_password, project_id=project_id, pipeline_id=pipeline_id, + app_id=app_id, ) def delete( @@ -1360,6 +1390,7 @@ def delete( name: str, project_id: str = None, pipeline_id: str = None, + app_id: str = None, ) -> Dict: """Delete a given environment variable inside a given scope @@ -1373,6 +1404,8 @@ def delete( UUID of your project (see README on how to find it) pipeline_id : str, optional UUID of your pipeline (see README on how to find it) + app_id : str, optional + UUID of your app (see README on how to find it) Returns ------- @@ -1392,9 +1425,9 @@ def delete( "deleteEnvironmentVariable": True } """ - check_scope(scope, project_id, pipeline_id) + check_scope(scope, project_id, pipeline_id, app_id) - existing_env_var = self.list(scope, project_id, pipeline_id, scope_only=True, pprint_result=False) + existing_env_var = self.list(scope, project_id, pipeline_id, app_id, scope_only=True, pprint_result=False) var = next((d for d in existing_env_var if d["name"] == name), None) if var is None: diff --git a/saagieapi/env_vars/gql_queries.py b/saagieapi/env_vars/gql_queries.py index a51ea34..8a0481f 100644 --- a/saagieapi/env_vars/gql_queries.py +++ b/saagieapi/env_vars/gql_queries.py @@ -103,6 +103,35 @@ } """ +GQL_LIST_APP_ENV_VARS = """ +query appEnvironmentVariablesQuery($appId: UUID!, + $scopeFilter: EnvVarScope) { + appEnvironmentVariables(appId: $appId, + scope: $scopeFilter + ) { + id + scope + name + value + description + isPassword + isValid + overriddenValues { + id + scope + value + description + isPassword + } + invalidReasons{ + type + concernedProperty + message + } + } +} +""" + GQL_CREATE_PIPELINE_ENV_VAR = """ mutation replaceEnvironmentVariablesByRawForScope($entityId: UUID, $scope: EnvVarScope!, diff --git a/saagieapi/jobs/jobs.py b/saagieapi/jobs/jobs.py index 070d2e9..d38b86d 100644 --- a/saagieapi/jobs/jobs.py +++ b/saagieapi/jobs/jobs.py @@ -728,7 +728,7 @@ def create( params["resources"] = resources if source_url: - params["source_url"] = source_url + params["sourceUrl"] = source_url result = self.__launch_request(file, GQL_CREATE_JOB, params) logging.info("✅ Job [%s] successfully created", job_name) diff --git a/saagieapi/pipelines/gql_queries.py b/saagieapi/pipelines/gql_queries.py index c10ba86..d549f60 100644 --- a/saagieapi/pipelines/gql_queries.py +++ b/saagieapi/pipelines/gql_queries.py @@ -87,6 +87,7 @@ creator isCurrent isMajor + sourceUrl } creationDate creator @@ -178,6 +179,7 @@ creator isCurrent isMajor + sourceUrl } creationDate creator @@ -272,6 +274,102 @@ creator isCurrent isMajor + sourceUrl + } + creationDate + creator + isScheduled + cronScheduling + scheduleStatus + scheduleTimezone + isLegacyPipeline + hasExecutionVariablesEnabled + } +} +""" + +GQL_GET_PIPELINE_BY_ALIAS = """ +query graphPipelineByAliasQuery( + $projectId: UUID!, + $pipelineAlias: String!, + $instancesLimit: Int, + $versionsLimit: Int, + $versionsOnlyCurrent: Boolean +) { + graphPipelineByAlias(projectId: $projectId, alias: $pipelineAlias) { + id + name + alias + description + alerting{ + emails + loginEmails{ + login + email + } + statusList + } + pipelineInstanceCount + instances(limit: $instancesLimit){ + id + status + startTime + endTime + runWithExecutionVariables + initialExecutionVariables{ + key + value + isPassword + } + jobsInstance{ + id + jobId + number + startTime + endTime + } + conditionsInstance{ + id + conditionNodeId + isSuccess + startTime + endTime + } + } + versions(limit: $versionsLimit, onlyCurrent: $versionsOnlyCurrent) { + number + releaseNote + graph{ + jobNodes{ + id + job{ + id + name + } + position{ + x + y + } + nextNodes + } + conditionNodes{ + id + position{ + x + y + } + nextNodesSuccess + nextNodesFailure + condition { + toString + } + } + } + creationDate + creator + isCurrent + isMajor + sourceUrl } creationDate creator @@ -388,7 +486,8 @@ $scheduleTimezone:TimeZone, $jobNodes: [JobNodeInput!], $conditionNodes: [ConditionNodeInput!], - $alias: String!) { + $alias: String!, + $sourceUrl: String) { createGraphPipeline( pipeline: { name: $name @@ -404,6 +503,7 @@ conditionNodes: $conditionNodes } alias: $alias + sourceUrl: $sourceUrl } ) { id @@ -423,14 +523,16 @@ mutation($id: UUID!, $jobNodes: [JobNodeInput!], $conditionNodes: [ConditionNodeInput!], - $releaseNote: String){ + $releaseNote: String, + $sourceUrl: String){ addGraphPipelineVersion( pipelineId: $id, graph: { jobNodes: $jobNodes, conditionNodes: $conditionNodes }, - releaseNote: $releaseNote + releaseNote: $releaseNote, + sourceUrl: $sourceUrl ){ number, releaseNote, diff --git a/saagieapi/pipelines/pipelines.py b/saagieapi/pipelines/pipelines.py index 5f50090..d698b80 100644 --- a/saagieapi/pipelines/pipelines.py +++ b/saagieapi/pipelines/pipelines.py @@ -660,6 +660,170 @@ def get_info_by_name( query=gql(GQL_GET_PIPELINE_BY_NAME), variable_values=params, pprint_result=pprint_result ) + def get_info_by_alias( + self, + project_id: str, + pipeline_alias: str, + instances_limit: Optional[int] = None, + versions_limit: Optional[int] = None, + versions_only_current: bool = False, + pprint_result: Optional[bool] = None, + ) -> Dict: + """Get a given pipeline information by giving its alias + + Parameters + ---------- + project_id : str + UUID of your pipeline (see README on how to find it) + pipeline_alias : str + Alias of your pipeline + instances_limit : int, optional + Maximum limit of instances to fetch per job. Fetch from most recent + to the oldest + versions_limit : int, optional + Maximum limit of versions to fetch per pipeline. Fetch from most recent + to the oldest + versions_only_current : bool, optional + Whether to only fetch the current version of each pipeline + pprint_result : bool, optional + Whether to pretty print the result of the query, default to + saagie_api.pprint_global + + Returns + ------- + dict + Dict of pipeline's information + + Examples + -------- + >>> saagieapi.pipelines.get_info_by_alias( + ... project_id=""8321e13c-892a-4481-8552-5be4d6cc5df4", + ... pipeline_id="Pipeline A" + ... ) + { + "graphPipelineByAlias": { + "id": "5d1999f5-fa70-47d9-9f41-55ad48333629", + "name": "Pipeline A", + "description": "My Pipeline A", + "alerting": "NULL", + "pipelineInstanceCount": 0, + "instances": [ + { + "id": "cc11c32a-66c5-43ad-b176-444cee7079ff", + "status": "SUCCEEDED", + "startTime": "2022-03-15T11:42:07.559Z", + "endTime": "2022-03-15T11:43:17.716Z", + "runWithExecutionVariables": True, + "initialExecutionVariables": [ + { + "key": "TEST_PASSWORD", + "value": None, + "isPassword": True + }, + { + "key": "TEST_PROJECT", + "value": "TEST_PROJECT", + "isPassword": False + } + ], + "jobsInstance": [ + { + "id": "f8e77fc3-9c4d-450b-8efd-9d3080b38edb", + "jobId": "9a71afa4-aed4-4061-87d2-b279a3adf8c3", + "number": 80, + "startTime": "2022-03-15T11:42:07.559Z", + "endTime": "2022-03-15T11:43:17.716Z" + } + ], + "conditionsInstance": [ + { + "id": "2292a535-affb-4b1c-973d-690c185d949e", + "conditionNodeId": "c2f23720-e361-11ed-894d-6b696861cc8f", + "isSuccess": true, + "startTime": "2022-03-15T11:42:30.559Z", + "endTime": "2022-03-15T11:42:45.559Z" + } + ], + }, + { + "id": "d7aba110-3bd9-4505-b70c-84c4d212345", + "status": "SUCCEEDED", + "startTime": "2022-02-04T00:00:00.062Z", + "endTime": "2022-02-04T00:00:27.249Z", + "runWithExecutionVariables": False, + "initialExecutionVariables": [], + "jobsInstance": [], + "conditionsInstance": [], + } + ], + "versions": [ + { + "number": 1, + "releaseNote": None, + "graph": { + "jobNodes": [ + { + "id": "00000000-0000-0000-0000-000000000000", + "job": { + "id": "6f56e714-37e4-4596-ae20-7016a1d954e9", + "name": "Spark 2.4 java" + }, + "position": None, + "nextNodes": ["00000000-0000-0000-0000-000000000001"] + }, + { + "id": "00000000-0000-0000-0000-000000000001", + "job": { + "id": "6ea1b022-db8b-4af7-885b-56ddc9ba764a", "name": "bash" + }, + "position": None, + "nextNodes": [] + } + ], + "conditionNodes": [ + { + "id": "00000000-0000-0000-0000-000000000001", + "position": { + "x": 310.00092, + "y": 75 + }, + "nextNodesSuccess": [ + "00000000-0000-0000-0000-000000000002" + ], + "nextNodesFailure": [], + "condition": { + "toString": "ConditionExpression(expression=\"tube_name.contains(\"Tube\") || double(diameter) > 1.0\")" + } + } + ] + }, + "creationDate": "2022-01-31T10:36:42.327Z", + "creator": "john.doe", + "isCurrent": True, + "isMajor": False + } + ], + "creationDate": "2022-01-31T10:36:42.327Z", + "creator": "john.doe", + "isScheduled": False, + "cronScheduling": None, + "scheduleStatus": None, + "scheduleTimezone": "UTC", + "isLegacyPipeline": False + } + } + """ # pylint: disable=line-too-long + params = { + "projectId": project_id, + "pipelineAlias": pipeline_alias, + "instancesLimit": instances_limit, + "versionsLimit": versions_limit, + "versionsOnlyCurrent": versions_only_current, + } + return self.saagie_api.client.execute( + query=gql(GQL_GET_PIPELINE_BY_ALIAS), variable_values=params, pprint_result=pprint_result + ) + def get_instance(self, pipeline_instance_id: str, pprint_result: Optional[bool] = None) -> Dict: """ Get the information of a given pipeline instance id @@ -739,6 +903,7 @@ def create_graph( cron_scheduling: str = None, schedule_timezone: str = "UTC", has_execution_variables_enabled: bool = None, + source_url: str = "", ) -> Dict: """ Create a pipeline in a given project @@ -764,9 +929,9 @@ def create_graph( release_note: str, optional Release note of the pipeline emails: List[String], optional - Emails to receive alerts for the job, each item should be a valid email, + Emails to receive alerts for the pipeline, each item should be a valid email, status_list: List[String], optional - Receive an email when the job status change to a specific status + Receive an email when the pipeline status change to a specific status Each item of the list should be one of these following values: "REQUESTED", "QUEUED", "RUNNING", "FAILED", "KILLED", "KILLING", "SUCCEEDED", "UNKNOWN", "AWAITING", "SKIPPED" cron_scheduling : str, optional @@ -775,6 +940,8 @@ def create_graph( Timezone of the scheduling has_execution_variables_enabled: bool, optional Boolean to activate or desactivate the execution variables + source_url: str, optional + URL of the source code used for the pipeline (link to the commit for example) Returns ------- @@ -845,6 +1012,9 @@ def create_graph( if has_execution_variables_enabled: params["hasExecutionVariablesEnabled"] = has_execution_variables_enabled + if source_url: + params["sourceUrl"] = source_url + result = self.saagie_api.client.execute(query=gql(GQL_CREATE_GRAPH_PIPELINE), variable_values=params) logging.info("✅ Pipeline [%s] successfully created", name) return result @@ -874,16 +1044,21 @@ def delete(self, pipeline_id: str) -> Dict: logging.info("✅ Pipeline [%s] successfully deleted", pipeline_id) return result - def upgrade(self, pipeline_id: str, graph_pipeline: GraphPipeline, release_note: str = "") -> Dict: + def upgrade( + self, pipeline_id: str, graph_pipeline: GraphPipeline, release_note: str = "", source_url: str = "" + ) -> Dict: """ Upgrade a pipeline in a given project Parameters ---------- - pipeline_id: str, ID of pipeline + pipeline_id: str, + UUID of pipeline graph_pipeline : GraphPipeline release_note: str, optional Release note of the pipeline + source_url: str, optional + URL of the source code used for the pipeline (link to the commit for example) Returns ------- @@ -957,6 +1132,9 @@ def upgrade(self, pipeline_id: str, graph_pipeline: GraphPipeline, release_note: "releaseNote": release_note, } + if source_url: + params["sourceUrl"] = source_url + result = self.saagie_api.client.execute(query=gql(GQL_UPGRADE_PIPELINE), variable_values=params) logging.info("✅ Pipeline [%s] successfully upgraded", pipeline_id) return result @@ -1090,6 +1268,7 @@ def create_or_upgrade( cron_scheduling: str = None, schedule_timezone: str = "UTC", has_execution_variables_enabled: bool = None, + source_url: str = None, ) -> Dict: """Create or upgrade a pipeline in a given project @@ -1115,11 +1294,11 @@ def create_or_upgrade( release_note: str, optional Release note of the pipeline emails: List[String], optional - Emails to receive alerts for the job, each item should be a valid email, + Emails to receive alerts for the pipeline, each item should be a valid email, If you want to remove alerting, please set emails to [] or list() if not filled, defaults to current value status_list: List[String], optional - Receive an email when the job status change to a specific status + Receive an email when the pipeline status change to a specific status Each item of the list should be one of these following values: "REQUESTED", "QUEUED", "RUNNING", "FAILED", "KILLED", "KILLING", "SUCCEEDED", "UNKNOWN", "AWAITING", "SKIPPED" is_scheduled : bool, optional @@ -1134,6 +1313,8 @@ def create_or_upgrade( Example: "UTC", "Pacific/Pago_Pago" has_execution_variables_enabled: bool, optional Boolean to activate or desactivate the execution variables + source_url: str, optional + URL of the source code used for the pipeline (link to the commit for example) Returns ------- @@ -1187,7 +1368,7 @@ def create_or_upgrade( has_execution_variables_enabled=has_execution_variables_enabled, )["editPipeline"] } - responses["addGraphPipelineVersion"] = self.upgrade(pipeline_id, graph_pipeline, release_note)[ + responses["addGraphPipelineVersion"] = self.upgrade(pipeline_id, graph_pipeline, release_note, source_url)[ "addGraphPipelineVersion" ] @@ -1207,6 +1388,7 @@ def create_or_upgrade( "cron_scheduling": cron_scheduling, "schedule_timezone": schedule_timezone, "has_execution_variables_enabled": has_execution_variables_enabled, + "source_url": source_url, }.items() if v is not None # Remove None values from the dict } diff --git a/tests/integration/apps_integration_test.py b/tests/integration/apps_integration_test.py index 45ce680..2c9f935 100644 --- a/tests/integration/apps_integration_test.py +++ b/tests/integration/apps_integration_test.py @@ -347,3 +347,18 @@ def test_count_history_statuses(create_global_project, create_then_delete_app_fr ) assert "countAppHistoryStatuses" in result + + @staticmethod + def test_get_app_logs(create_global_project, create_then_delete_app_from_scratch): + conf = create_global_project + app_id = create_then_delete_app_from_scratch + + app_info = conf.saagie_api.apps.run(app_id=app_id) + + app_info = conf.saagie_api.apps.get_info(app_id=app_id) + + app_execution_id = app_info["app"]["history"]["currentExecutionId"] + + result = conf.saagie_api.apps.get_logs(app_id=app_id, app_execution_id=app_execution_id) + + assert "appLogs" in result diff --git a/tests/integration/pipelines_integration_test.py b/tests/integration/pipelines_integration_test.py index 397d177..2c09898 100644 --- a/tests/integration/pipelines_integration_test.py +++ b/tests/integration/pipelines_integration_test.py @@ -454,3 +454,14 @@ def test_duplicate_pipeline(create_global_project, create_then_delete_graph_pipe conf.saagie_api.pipelines.delete(pipeline_id=result["duplicatePipeline"]["id"]) assert "duplicatePipeline" in result + + @staticmethod + def test_get_info_by_alias(create_global_project, create_then_delete_graph_pipeline): + conf = create_global_project + pipeline_id, _ = create_then_delete_graph_pipeline + + result = conf.saagie_api.pipelines.get_info_by_alias( + pipeline_alias="ALIAS_TEST_VIA_API", project_id=conf.project_id + ) + + assert result["graphPipelineByAlias"]["id"] == pipeline_id diff --git a/tests/unit/apps_unit_test.py b/tests/unit/apps_unit_test.py index ebf9466..f6423b3 100644 --- a/tests/unit/apps_unit_test.py +++ b/tests/unit/apps_unit_test.py @@ -252,6 +252,7 @@ def test_edit_app(self, saagie_api_mock): description = "new_description" emails = ["test@email.com"] status = ["RECOVERING", "FAILED"] + resources = {"cpu": {"request": 1.5, "limit": 2.2}, "memory": {"request": 2.0}} params = { "app": { @@ -259,6 +260,7 @@ def test_edit_app(self, saagie_api_mock): "name": name, "description": description, "alerting": {"statusList": status, "emails": emails}, + "resources": resources, } } @@ -267,7 +269,14 @@ def test_edit_app(self, saagie_api_mock): with patch.object(app, "get_info") as info, patch.object(app, "check_alerting") as chk_alert: info.return_value = {"app": {"alerting": {"statusList": status, "emails": emails}}} chk_alert.return_value = {"statusList": status, "emails": emails} - app.edit(app_id=app_id, app_name=name, description=description, emails=emails, status_list=status) + app.edit( + app_id=app_id, + app_name=name, + description=description, + emails=emails, + status_list=status, + resources=resources, + ) saagie_api_mock.client.execute.assert_called_with(query=expected_query, variable_values=params) @@ -776,3 +785,39 @@ def test_count_history_app_statuses(self, saagie_api_mock): app.count_history_statuses(history_id=history_id, version_number=1, start_time="2024-04-10T14:26:27.073Z") saagie_api_mock.client.execute.assert_called_with(query=expected_query, variable_values=params) + + def test_get_app_logs_gql(self): + query = gql(GQL_GET_APP_LOG) + self.client.validate(query) + + def test_get_app_logs(self, saagie_api_mock): + app = Apps(saagie_api_mock) + + app_id = "70e85ade-d6cc-4a90-8d7d-639adbd25e5d" + app_execution_id = "e3e31074-4a12-450e-96e4-0eae7801dfca" + limit = 2 + skip = 5 + stream = "STDERR" + start_at = "2024-04-09 10:00:00" + + params = { + "appId": app_id, + "appExecutionId": app_execution_id, + "limit": limit, + "skip": skip, + "stream": stream, + "recordAt": start_at, + } + + expected_query = gql(GQL_GET_APP_LOG) + + app.get_logs( + app_id=app_id, + app_execution_id=app_execution_id, + limit=limit, + skip=skip, + log_stream=stream, + start_at=start_at, + ) + + saagie_api_mock.client.execute.assert_called_with(query=expected_query, variable_values=params) diff --git a/tests/unit/env_vars_unit_test.py b/tests/unit/env_vars_unit_test.py index 092edb2..e0ada2e 100644 --- a/tests/unit/env_vars_unit_test.py +++ b/tests/unit/env_vars_unit_test.py @@ -20,133 +20,147 @@ class TestEnvVars: def setup_method(self): self.client = create_gql_client() - def test_list_global_env_vars_gql(self): - query = gql(GQL_LIST_GLOBAL_ENV_VARS) - self.client.validate(query) - - def test_create_env_var_gql(self): - query = gql(GQL_CREATE_ENV_VAR) - self.client.validate(query) - - def test_update_env_var_gql(self): - query = gql(GQL_UPDATE_ENV_VAR) - self.client.validate(query) - - def test_delete_env_var_gql(self): - query = gql(GQL_DELETE_ENV_VAR) - self.client.validate(query) - - def test_list_project_env_vars_gql(self): - query = gql(GQL_LIST_PROJECT_ENV_VARS) - self.client.validate(query) - - def test_list_pipeline_env_vars_gql(self): - query = gql(GQL_LIST_PIPELINE_ENV_VARS) - self.client.validate(query) - - def test_create_pipeline_env_vars_gql(self): - query = gql(GQL_CREATE_PIPELINE_ENV_VAR) - self.client.validate(query) - @pytest.fixture def saagie_api_mock(self): saagie_api_mock = Mock() saagie_api_mock.client.execute = MagicMock() return saagie_api_mock - # Test of check_scope def test_check_scope_global(self): - check_scope(scope="GLOBAL", project_id=None, pipeline_id=None) + check_scope(scope="GLOBAL", project_id=None, pipeline_id=None, app_id=None) def test_check_scope_error(self): with pytest.raises(ValueError): - check_scope(scope="WRONG", project_id=None, pipeline_id=None) + check_scope(scope="WRONG", project_id=None, pipeline_id=None, app_id=None) def test_check_scope_project_success(self): - check_scope(scope="PROJECT", project_id="project_id", pipeline_id=None) + check_scope(scope="PROJECT", project_id="project_id", pipeline_id=None, app_id=None) def test_check_scope_project_error(self): with pytest.raises(ValueError): - check_scope(scope="PROJECT", project_id=None, pipeline_id=None) + check_scope(scope="PROJECT", project_id=None, pipeline_id=None, app_id=None) def test_check_scope_pipeline_success(self): - check_scope(scope="PIPELINE", project_id=None, pipeline_id="pipeline_id") + check_scope(scope="PIPELINE", project_id=None, pipeline_id="pipeline_id", app_id=None) def test_check_scope_pipeline_error(self): with pytest.raises(ValueError): - check_scope(scope="PIPELINE", project_id=None, pipeline_id=None) + check_scope(scope="PIPELINE", project_id=None, pipeline_id=None, app_id=None) + + def test_list_global_env_vars_gql(self): + query = gql(GQL_LIST_GLOBAL_ENV_VARS) + self.client.validate(query) - # Test the `list` method def test_list_global_vars(self, saagie_api_mock): - # Create an instance of EnvVars with the mock saagie_api env_vars = EnvVars(saagie_api_mock) - # Set up the expected parameters scope = "GLOBAL" - project_id = None - pipeline_id = None scope_only = False pprint_result = True - # Define the expected query expected_query = gql(GQL_LIST_GLOBAL_ENV_VARS) - # Call the function under test - env_vars.list(scope, project_id, pipeline_id, scope_only, pprint_result) + env_vars.list(scope=scope, scope_only=scope_only, pprint_result=pprint_result) - # Assert that the query was executed with the expected parameters saagie_api_mock.client.execute.assert_called_with(query=expected_query, pprint_result=pprint_result) + def test_list_project_env_vars_gql(self): + query = gql(GQL_LIST_PROJECT_ENV_VARS) + self.client.validate(query) + def test_list_project_vars(self, saagie_api_mock): - # Create an instance of EnvVars with the mock saagie_api env_vars = EnvVars(saagie_api_mock) - # Set up the expected parameters scope = "PROJECT" project_id = "MY_PROJECT_ID" - pipeline_id = None scope_only = False pprint_result = True - # Define the expected query expected_query = gql(GQL_LIST_PROJECT_ENV_VARS) - # Call the function under test - env_vars.list(scope, project_id, pipeline_id, scope_only, pprint_result) + env_vars.list(scope=scope, project_id=project_id, scope_only=scope_only, pprint_result=pprint_result) - # Assert that the query was executed with the expected parameters saagie_api_mock.client.execute.assert_called_with( query=expected_query, variable_values={"projectId": project_id}, pprint_result=pprint_result ) + def test_list_pipeline_env_vars_gql(self): + query = gql(GQL_LIST_PIPELINE_ENV_VARS) + self.client.validate(query) + def test_list_pipeline_vars(self, saagie_api_mock): - # Create an instance of EnvVars with the mock saagie_api env_vars = EnvVars(saagie_api_mock) - # Set up the expected parameters scope = "PIPELINE" - project_id = "MY_PROJECT_ID" pipeline_id = "MY_PIPELINE_ID" scope_only = False pprint_result = True - # Define the expected query expected_query = gql(GQL_LIST_PIPELINE_ENV_VARS) - # Call the function under test - env_vars.list(scope, project_id, pipeline_id, scope_only, pprint_result) + env_vars.list(scope=scope, pipeline_id=pipeline_id, scope_only=scope_only, pprint_result=pprint_result) - # Assert that the query was executed with the expected parameters saagie_api_mock.client.execute.assert_called_with( query=expected_query, variable_values={"pipelineId": pipeline_id}, pprint_result=pprint_result ) - # Test the `create` method - @patch("saagieapi.env_vars.env_vars.check_scope", return_value=True) - def test_create_global_variable(self, check_scope_mock, saagie_api_mock): + def test_list_app_env_vars_gql(self): + query = gql(GQL_LIST_APP_ENV_VARS) + self.client.validate(query) + + def test_list_app_vars(self, saagie_api_mock): + env_vars = EnvVars(saagie_api_mock) + + scope = "APP" + app_id = "MY_APP_ID" + scope_only = False + pprint_result = True + + expected_query = gql(GQL_LIST_APP_ENV_VARS) + + env_vars.list(scope=scope, app_id=app_id, scope_only=scope_only, pprint_result=pprint_result) + + saagie_api_mock.client.execute.assert_called_with( + query=expected_query, variable_values={"appId": app_id}, pprint_result=pprint_result + ) + + def test_get_valid_env_var(self, saagie_api_mock): + saagie_api_mock.env_vars.env_vars.check_scope.return_value = True + instance = EnvVars(saagie_api_mock) + + scope = "GLOBAL" + name = "TEST_VARIABLE" + + with patch.object(instance, "list") as list_env: + list_env.return_value = [ + {"id": "env_var_id", "name": "TEST_VARIABLE", "value": "value1", "scope": "GLOBAL"} + ] + result = instance.get(scope, name) + + assert result["name"] == name + + def test_get_invalid_env_var(self, saagie_api_mock): + saagie_api_mock.env_vars.env_vars.check_scope.return_value = True + instance = EnvVars(saagie_api_mock) + + scope = "GLOBAL" + name = "INVALID_ENV_VAR" + + with patch.object(instance, "list") as list_env: + list_env.return_value = [ + {"id": "env_var_id", "name": "TEST_VARIABLE", "value": "value1", "scope": "GLOBAL"} + ] + result = instance.get(scope, name) + + assert result is None + + def test_create_env_var_gql(self): + query = gql(GQL_CREATE_ENV_VAR) + self.client.validate(query) + + def test_create_global_variable(self, saagie_api_mock): + saagie_api_mock.env_vars.env_vars.check_scope.return_value = True instance = EnvVars(saagie_api_mock) - mock_env_vars = {"saveEnvironmentVariable": {"id": "env_var_id"}} - saagie_api_mock.client.execute.return_value = mock_env_vars name = "env_var1" value = "value1" @@ -154,23 +168,26 @@ def test_create_global_variable(self, check_scope_mock, saagie_api_mock): is_password = False scope = "GLOBAL" - result = instance.create(scope=scope, name=name, value=value, description=description, is_password=is_password) + params = { + "envVar": { + "name": name, + "value": value, + "description": description, + "isPassword": is_password, + "scope": scope, + }, + } - # Assertions - check_scope_mock.assert_called_with(scope, None, None) - arg1 = saagie_api_mock.client.execute.call_args.kwargs["query"] - assert arg1 == gql(GQL_CREATE_ENV_VAR) - assert "saveEnvironmentVariable" in result - assert "id" in result["saveEnvironmentVariable"] - assert isinstance(result["saveEnvironmentVariable"]["id"], str) + expected_query = gql(GQL_CREATE_ENV_VAR) - @patch("saagieapi.env_vars.env_vars.check_scope", return_value=True) - def test_create_project_variable(self, check_scope_mock, saagie_api_mock): + instance.create(scope=scope, name=name, value=value, description=description, is_password=is_password) + + saagie_api_mock.client.execute.assert_called_with(query=expected_query, variable_values=params) + + def test_create_project_variable(self, saagie_api_mock): + saagie_api_mock.env_vars.env_vars.check_scope.return_value = True instance = EnvVars(saagie_api_mock) - mock_env_vars = {"saveEnvironmentVariable": {"id": "env_var_id"}} - saagie_api_mock.client.execute.return_value = mock_env_vars - # Set up the expected parameters name = "env_var1" value = "value1" description = "description1" @@ -178,441 +195,430 @@ def test_create_project_variable(self, check_scope_mock, saagie_api_mock): scope = "PROJECT" project_id = "MY_PROJECT_ID" - result = instance.create( + params = { + "envVar": { + "name": name, + "value": value, + "description": description, + "isPassword": is_password, + "scope": scope, + }, + "entityId": project_id, + } + + expected_query = gql(GQL_CREATE_ENV_VAR) + + instance.create( scope=scope, name=name, value=value, description=description, project_id=project_id, is_password=is_password ) - # Assertions - check_scope_mock.assert_called_with(scope, project_id, None) - arg1 = saagie_api_mock.client.execute.call_args.kwargs["query"] - assert arg1 == gql(GQL_CREATE_ENV_VAR) - assert "saveEnvironmentVariable" in result - assert "id" in result["saveEnvironmentVariable"] - assert isinstance(result["saveEnvironmentVariable"]["id"], str) + saagie_api_mock.client.execute.assert_called_with(query=expected_query, variable_values=params) - @patch("saagieapi.env_vars.env_vars.check_scope", return_value=True) - def test_create_pipeline_variable(self, check_scope_mock, saagie_api_mock): + def test_create_project_env_vars_error(self, saagie_api_mock): + instance = EnvVars(saagie_api_mock) + with pytest.raises(ValueError): + instance.create(scope="PROJECT", name="env_var_proj_1", value="value1") + + def test_create_pipeline_variable(self, saagie_api_mock): + saagie_api_mock.env_vars.env_vars.check_scope.return_value = True instance = EnvVars(saagie_api_mock) - mock_env_vars = {"saveEnvironmentVariable": {"id": "env_var_id"}} - saagie_api_mock.client.execute.return_value = mock_env_vars - # Set up the expected parameters name = "env_var1" value = "value1" description = "description1" is_password = False scope = "PIPELINE" pipeline_id = "MY_PIPELINE_ID" - project_id = "MY_PROJECT_ID" - result = instance.create( + params = { + "envVar": { + "name": name, + "value": value, + "description": description, + "isPassword": is_password, + "scope": scope, + }, + "entityId": pipeline_id, + } + + expected_query = gql(GQL_CREATE_ENV_VAR) + + instance.create( scope=scope, name=name, value=value, description=description, - project_id=project_id, pipeline_id=pipeline_id, is_password=is_password, ) - # Assertions - check_scope_mock.assert_called_with(scope, project_id, pipeline_id) - arg1 = saagie_api_mock.client.execute.call_args.kwargs["query"] - assert arg1 == gql(GQL_CREATE_ENV_VAR) - assert "saveEnvironmentVariable" in result - assert "id" in result["saveEnvironmentVariable"] - assert isinstance(result["saveEnvironmentVariable"]["id"], str) + saagie_api_mock.client.execute.assert_called_with(query=expected_query, variable_values=params) def test_create_pipeline_env_vars_error(self, saagie_api_mock): instance = EnvVars(saagie_api_mock) with pytest.raises(ValueError): instance.create(scope="PIPELINE", name="env_var1", value="value1") - def test_create_project_env_vars_error(self, saagie_api_mock): + def test_create_app_variable(self, saagie_api_mock): + saagie_api_mock.env_vars.env_vars.check_scope.return_value = True instance = EnvVars(saagie_api_mock) - with pytest.raises(ValueError): - instance.create(scope="PROJECT", name="env_var_proj_1", value="value1") - # Test the `delete` method - @patch("saagieapi.env_vars.env_vars.check_scope", return_value=True) - @patch( - "saagieapi.env_vars.env_vars.EnvVars.list", - return_value=[ - { - "id": "env_var_id", - "name": "env_var1", - "value": "value1", - "description": "description1", - "scope": "GLOBAL", - } - ], - ) - def test_delete_env_var(self, env_list_mock, check_scope_mock, saagie_api_mock): - # Set up the expected parameters - instance = EnvVars(saagie_api_mock) - mock_env_vars = {"deleteEnvironmentVariable": True} - saagie_api_mock.client.execute.return_value = mock_env_vars - scope = "GLOBAL" name = "env_var1" + value = "value1" + description = "description1" + is_password = False + scope = "APP" + app_id = "MY_APP_ID" + + params = { + "envVar": { + "name": name, + "value": value, + "description": description, + "isPassword": is_password, + "scope": scope, + }, + "entityId": app_id, + } - # Test - env_var_result = instance.delete(scope=scope, name=name) + expected_query = gql(GQL_CREATE_ENV_VAR) - # Assertions - check_scope_mock.assert_called_with(scope, None, None) - env_list_mock.assert_called_with(scope, None, None, scope_only=True, pprint_result=False) - arg1 = saagie_api_mock.client.execute.call_args.kwargs["query"] - assert arg1 == gql(GQL_DELETE_ENV_VAR) - assert "deleteEnvironmentVariable" in env_var_result - - @patch("saagieapi.env_vars.env_vars.check_scope", return_value=True) - @patch( - "saagieapi.env_vars.env_vars.EnvVars.list", - return_value=[ - { - "id": "env_var_id", - "name": "env_var1", - "value": "value1", - "description": "description1", - "scope": "GLOBAL", - } - ], - ) - def test_delete_env_var_not_exists(self, env_list_mock, check_scope_mock, saagie_api_mock): - instance = EnvVars(saagie_api_mock) - scope = "GLOBAL" - name = "NON_EXISTING_VARIABLE" + instance.create( + scope=scope, + name=name, + value=value, + description=description, + app_id=app_id, + is_password=is_password, + ) - # Assert that the result is as expected - with pytest.raises(ValueError): - instance.delete(scope=scope, name=name) - check_scope_mock.assert_called_with(scope, None, None) - env_list_mock.assert_called_with(scope, None, None, scope_only=True, pprint_result=False) - saagie_api_mock.client.execute.assert_not_called() - - # Test the `update` method - @patch("saagieapi.env_vars.env_vars.check_scope", return_value=True) - @patch( - "saagieapi.env_vars.env_vars.EnvVars.list", - return_value=[ - { - "id": "env_var_id", - "name": "TEST_VARIABLE", - "value": "value1", - "description": "description1", - "scope": "GLOBAL", - "isPassword": False, - } - ], - ) - def test_update_existing_global_variable(self, env_list_mock, check_scope_mock, saagie_api_mock): + saagie_api_mock.client.execute.assert_called_with(query=expected_query, variable_values=params) + + def test_update_env_var_gql(self): + query = gql(GQL_UPDATE_ENV_VAR) + self.client.validate(query) + + def test_update_existing_global_variable(self, saagie_api_mock): + saagie_api_mock.env_vars.env_vars.check_scope.return_value = True instance = EnvVars(saagie_api_mock) - # Parameters + scope = "GLOBAL" name = "TEST_VARIABLE" value = "new value" - mock_env_vars = {"saveEnvironmentVariable": {"id": "env_var_id"}} - saagie_api_mock.client.execute.return_value = mock_env_vars - - result = instance.update(scope=scope, name=name, value=value) + params = { + "envVar": { + "id": "env_var_id", + "name": name, + "value": value, + "description": "description1", + "isPassword": False, + "scope": scope, + }, + } - # Assert that the result is as expected - check_scope_mock.assert_called_with(scope, None, None) - env_list_mock.assert_called_with(scope, None, None, scope_only=True, pprint_result=False) - assert "saveEnvironmentVariable" in result + expected_query = gql(GQL_UPDATE_ENV_VAR) + + with patch.object(instance, "list") as list_env: + list_env.return_value = [ + { + "id": "env_var_id", + "name": "TEST_VARIABLE", + "value": "value1", + "description": "description1", + "scope": "GLOBAL", + "isPassword": False, + } + ] + instance.update(scope=scope, name=name, value=value) - # Test the `update` method + saagie_api_mock.client.execute.assert_called_with(query=expected_query, variable_values=params) - @patch("saagieapi.env_vars.env_vars.check_scope", return_value=True) - @patch( - "saagieapi.env_vars.env_vars.EnvVars.list", - return_value=[ - { - "id": "env_var_id", - "name": "TEST_VARIABLE", - "value": "value1", - "description": "description1", - "scope": "GLOBAL", - "isPassword": True, - } - ], - ) - def test_update_existing_global_password_variable(self, env_list_mock, check_scope_mock, saagie_api_mock): + def test_update_existing_global_password_variable(self, saagie_api_mock): + saagie_api_mock.env_vars.env_vars.check_scope.return_value = True instance = EnvVars(saagie_api_mock) - # Parameters + scope = "GLOBAL" name = "TEST_VARIABLE" value = "new value" - mock_env_vars = {"saveEnvironmentVariable": {"id": "env_var_id"}} - saagie_api_mock.client.execute.return_value = mock_env_vars + params = { + "envVar": { + "id": "env_var_id", + "name": name, + "value": value, + "description": "description1", + "isPassword": True, + "scope": scope, + }, + } - result = instance.update(scope=scope, name=name, value=value, is_password=True) + expected_query = gql(GQL_UPDATE_ENV_VAR) + + with patch.object(instance, "list") as list_env: + list_env.return_value = [ + { + "id": "env_var_id", + "name": "TEST_VARIABLE", + "value": "value1", + "description": "description1", + "scope": "GLOBAL", + "isPassword": True, + } + ] + instance.update(scope=scope, name=name, value=value) - # Assert that the result is as expected - check_scope_mock.assert_called_with(scope, None, None) - env_list_mock.assert_called_with(scope, None, None, scope_only=True, pprint_result=False) - assert "saveEnvironmentVariable" in result + saagie_api_mock.client.execute.assert_called_with(query=expected_query, variable_values=params) - @patch("saagieapi.env_vars.env_vars.check_scope", return_value=True) - @patch( - "saagieapi.env_vars.env_vars.EnvVars.list", - return_value=[ - { - "id": "env_var_id", - "name": "TEST_VARIABLE", - "value": "value1", - "description": "description1", - "scope": "PROJECT", - "isPassword": False, - } - ], - ) - def test_update_existing_project_variable(self, env_list_mock, check_scope_mock, saagie_api_mock): + def test_update_existing_project_variable(self, saagie_api_mock): + saagie_api_mock.env_vars.env_vars.check_scope.return_value = True instance = EnvVars(saagie_api_mock) - # Parameters + scope = "PROJECT" name = "TEST_VARIABLE" + new_name = "NEW_TEST_VARIABLE" value = "new value" + desc = "new description" project_id = "MY_PROJECT_ID" - mock_env_vars = {"saveEnvironmentVariable": {"id": "env_var_id"}} - saagie_api_mock.client.execute.return_value = mock_env_vars - # Act - result = instance.update(scope=scope, name=name, value=value, project_id=project_id) - - # Assert that the result is as expected - check_scope_mock.assert_called_with(scope, project_id, None) - env_list_mock.assert_called_with(scope, project_id, None, scope_only=True, pprint_result=False) - assert "saveEnvironmentVariable" in result - - @patch("saagieapi.env_vars.env_vars.check_scope", return_value=True) - @patch( - "saagieapi.env_vars.env_vars.EnvVars.list", - return_value=[ - { + params = { + "envVar": { "id": "env_var_id", - "name": "TEST_VARIABLE", - "value": "value1", - "description": "description1", - "scope": "PROJECT", + "name": new_name, + "value": value, + "description": desc, "isPassword": False, - } - ], - ) - def test_update_existing_project_variable_name(self, env_list_mock, check_scope_mock, saagie_api_mock): - instance = EnvVars(saagie_api_mock) - # Parameters - scope = "PROJECT" - name = "TEST_VARIABLE" - new_name = "NEW_TEST_VARIABLE" - value = "new value" - project_id = "MY_PROJECT_ID" - new_description = "new description for tests purpose" + "scope": scope, + }, + "entityId": project_id, + } - mock_env_vars = {"saveEnvironmentVariable": {"id": "env_var_id"}} - saagie_api_mock.client.execute.return_value = mock_env_vars - # Act - result = instance.update( - scope=scope, name=name, value=value, project_id=project_id, new_name=new_name, description=new_description - ) + expected_query = gql(GQL_UPDATE_ENV_VAR) + + with patch.object(instance, "list") as list_env: + list_env.return_value = [ + { + "id": "env_var_id", + "name": "TEST_VARIABLE", + "value": "value1", + "description": "description1", + "scope": "PROJECT", + "isPassword": False, + } + ] + instance.update( + scope=scope, name=name, new_name=new_name, value=value, description=desc, project_id=project_id + ) - # Assert that the result is as expected - check_scope_mock.assert_called_with(scope, project_id, None) - env_list_mock.assert_called_with(scope, project_id, None, scope_only=True, pprint_result=False) - assert "saveEnvironmentVariable" in result + saagie_api_mock.client.execute.assert_called_with(query=expected_query, variable_values=params) - @patch("saagieapi.env_vars.env_vars.check_scope", return_value=True) - @patch( - "saagieapi.env_vars.env_vars.EnvVars.list", - return_value=[ - { - "id": "env_var_id", - "name": "TEST_VARIABLE", - "value": "value1", - "description": "description1", - "scope": "PIPELINE", - "isPassword": False, - } - ], - ) - def test_update_existing_pipeline_variable(self, env_list_mock, check_scope_mock, saagie_api_mock): + def test_update_existing_pipeline_variable(self, saagie_api_mock): + saagie_api_mock.env_vars.env_vars.check_scope.return_value = True instance = EnvVars(saagie_api_mock) - # Parameters + scope = "PIPELINE" name = "TEST_VARIABLE" value = "new value" - project_id = "MY_PROJECT_ID" pipeline_id = "MY_PIPELINE_ID" - # Mocks - mock_env_vars = {"saveEnvironmentVariable": {"id": "env_var_id"}} - saagie_api_mock.client.execute.return_value = mock_env_vars - # Act - result = instance.update(scope=scope, name=name, value=value, project_id=project_id, pipeline_id=pipeline_id) + params = { + "envVar": { + "id": "env_var_id", + "name": name, + "value": value, + "description": "description1", + "isPassword": True, + "scope": scope, + }, + "entityId": pipeline_id, + } + + expected_query = gql(GQL_UPDATE_ENV_VAR) + + with patch.object(instance, "list") as list_env: + list_env.return_value = [ + { + "id": "env_var_id", + "name": "TEST_VARIABLE", + "value": "value1", + "description": "description1", + "scope": "PIPELINE", + "isPassword": False, + } + ] + instance.update(scope=scope, name=name, value=value, is_password=True, pipeline_id=pipeline_id) + + saagie_api_mock.client.execute.assert_called_with(query=expected_query, variable_values=params) - # Assert that the result is as expected - check_scope_mock.assert_called_with(scope, project_id, pipeline_id) - env_list_mock.assert_called_with(scope, project_id, pipeline_id, scope_only=True, pprint_result=False) - assert "saveEnvironmentVariable" in result + def test_update_existing_app_variable(self, saagie_api_mock): + saagie_api_mock.env_vars.env_vars.check_scope.return_value = True + instance = EnvVars(saagie_api_mock) - @patch("saagieapi.env_vars.env_vars.check_scope", return_value=True) - @patch( - "saagieapi.env_vars.env_vars.EnvVars.list", - return_value=[ - { + scope = "APP" + name = "TEST_VARIABLE" + value = "new value" + app_id = "MY_APP_ID" + + params = { + "envVar": { "id": "env_var_id", - "name": "TEST_VARIABLE", - "value": "value1", + "name": name, + "value": value, "description": "description1", - "scope": "PROJECT", - "isPassword": False, - } - ], - ) - def test_update_non_existing_variable(self, env_list_mock, check_scope_mock, saagie_api_mock): - # Arrange + "isPassword": True, + "scope": scope, + }, + "entityId": app_id, + } + + expected_query = gql(GQL_UPDATE_ENV_VAR) + + with patch.object(instance, "list") as list_env: + list_env.return_value = [ + { + "id": "env_var_id", + "name": "TEST_VARIABLE", + "value": "value1", + "description": "description1", + "scope": "APP", + "isPassword": False, + } + ] + instance.update(scope=scope, name=name, value=value, is_password=True, app_id=app_id) + + saagie_api_mock.client.execute.assert_called_with(query=expected_query, variable_values=params) + + def test_update_non_existing_variable(self, saagie_api_mock): + saagie_api_mock.env_vars.env_vars.check_scope.return_value = True + instance = EnvVars(saagie_api_mock) + scope = "PROJECT" name = "NON_EXISTING_VARIABLE" project_id = "MY_PROJECT_ID" - instance = EnvVars(saagie_api_mock) - - # Act and Assert - with pytest.raises(ValueError): + with patch.object(instance, "list") as list_env, pytest.raises(ValueError): + list_env.return_value = [ + { + "id": "env_var_id", + "name": "TEST_VARIABLE", + "value": "value1", + "description": "description1", + "scope": "PROJECT", + "isPassword": False, + } + ] instance.update(scope=scope, name=name, project_id=project_id) - check_scope_mock.assert_called_with(scope, project_id, None) - env_list_mock.assert_called_with(scope, project_id, None, scope_only=True, pprint_result=False) - - # Test the `get` method - @patch("saagieapi.env_vars.env_vars.check_scope", return_value=True) - @patch( - "saagieapi.env_vars.env_vars.EnvVars.list", - return_value=[{"id": "env_var_id", "name": "TEST_VARIABLE", "value": "value1", "scope": "GLOBAL"}], - ) - def test_get_valid_env_var(self, env_list_mock, check_scope_mock, saagie_api_mock): - scope = "GLOBAL" - name = "TEST_VARIABLE" - instance = EnvVars(saagie_api_mock) - - result = instance.get(scope, name) - # Assert that the result is as expected - assert isinstance(result, dict) - assert result["name"] == name - assert result["scope"] == scope - check_scope_mock.assert_called_with(scope, None, None) - env_list_mock.assert_called_with(scope, None, None, False, None) - - @patch("saagieapi.env_vars.env_vars.check_scope", return_value=True) - @patch( - "saagieapi.env_vars.env_vars.EnvVars.list", - return_value=[{"id": "env_var_id", "name": "TEST_VARIABLE", "value": "value1", "scope": "GLOBAL"}], - ) - def test_get_invalid_env_var(self, env_list_mock, check_scope_mock, saagie_api_mock): - # Define the input parameters for the `get` method - scope = "GLOBAL" - name = "INVALID_ENV_VAR" + def test_create_or_update_env_var_create(self, saagie_api_mock): + saagie_api_mock.env_vars.env_vars.check_scope.return_value = True instance = EnvVars(saagie_api_mock) - result = instance.get(scope, name) - - # Assert that the result is None since the environment variable does not exist - assert result is None - check_scope_mock.assert_called_with(scope, None, None) - env_list_mock.assert_called_with(scope, None, None, False, None) - - # Test the `create_or_update` method - @patch("saagieapi.env_vars.env_vars.EnvVars.update", return_value=True) - @patch("saagieapi.env_vars.env_vars.EnvVars.create", return_value=True) - @patch("saagieapi.env_vars.env_vars.check_scope", return_value=True) - @patch( - "saagieapi.env_vars.env_vars.EnvVars.list", - return_value=[{"id": "env_var_id", "name": "TEST_VARIABLE", "value": "value1", "scope": "GLOBAL"}], - ) - def test_create_new_variable_with_create_or_update( - self, env_list_mock, check_scope_mock, create_env_mock, update_env_mock, saagie_api_mock - ): - # Set up the parameters for the create_or_update method scope = "PROJECT" name = "NEW_VARIABLE" value = "new value" description = None is_password = True project_id = "PROJECT_ID" - pipeline_id = None - instance = EnvVars(saagie_api_mock) + create_res = {"saveEnvironmentVariable": {"id": "8aaee333-a9f4-40f5-807a-44f8efa65a2f"}} - # Call the method being tested - instance.create_or_update( - scope=scope, - name=name, - value=value, - description=description, - is_password=is_password, - project_id=project_id, - pipeline_id=pipeline_id, - ) + with patch.object(instance, "list") as list_env, patch.object(instance, "create") as create_env: + list_env.return_value = [ + {"id": "env_var_id", "name": "TEST_VARIABLE", "value": "value1", "scope": "GLOBAL"} + ] + create_env.return_value = create_res + res = instance.create_or_update( + scope=scope, + name=name, + value=value, + description=description, + is_password=is_password, + project_id=project_id, + ) - # Assertions - env_list_mock.assert_called_with(scope, project_id, pipeline_id, scope_only=True, pprint_result=False) - create_env_mock.assert_called_once_with( - scope=scope, name=name, value=value, is_password=is_password, project_id=project_id - ) - check_scope_mock.assert_called_with(scope, project_id, pipeline_id) - update_env_mock.assert_not_called() - - @patch("saagieapi.env_vars.env_vars.EnvVars.update", return_value=True) - @patch("saagieapi.env_vars.env_vars.EnvVars.create", return_value=True) - @patch("saagieapi.env_vars.env_vars.check_scope", return_value=True) - @patch( - "saagieapi.env_vars.env_vars.EnvVars.list", - return_value=[{"id": "env_var_id", "name": "TEST_VARIABLE", "value": "value1", "scope": "GLOBAL"}], - ) - def test_update_existing_variable_with_create_or_update( - self, env_list_mock, check_scope_mock, create_env_mock, update_env_mock, saagie_api_mock - ): - # Test case to update an existing environment variable + assert res == create_res + + def test_create_or_update_env_var_update(self, saagie_api_mock): + saagie_api_mock.env_vars.env_vars.check_scope.return_value = True instance = EnvVars(saagie_api_mock) - # Set up the parameters for the create_or_update method scope = "GLOBAL" - name = "TEST_VARIABLE" # Updating an existing variable + name = "TEST_VARIABLE" value = "updated value" description = None is_password = False project_id = None pipeline_id = None - # Call the method being tested - instance.create_or_update( - scope=scope, - name=name, - value=value, - description=description, - is_password=is_password, - project_id=project_id, - pipeline_id=pipeline_id, - ) + update_res = {"saveEnvironmentVariable": {"id": "8aaee333-a9f4-40f5-807a-44f8efa65a2f"}} - # Assertions - env_list_mock.assert_called_with(scope, project_id, pipeline_id, scope_only=True, pprint_result=False) - update_env_mock.assert_called_once_with( - scope=scope, - name=name, - value=value, - description=description, - new_name=None, - is_password=is_password, - project_id=project_id, - pipeline_id=pipeline_id, - ) - create_env_mock.assert_not_called() - check_scope_mock.assert_called_with(scope, project_id, pipeline_id) + with patch.object(instance, "list") as list_env, patch.object(instance, "update") as update_env: + list_env.return_value = [ + {"id": "env_var_id", "name": "TEST_VARIABLE", "value": "value1", "scope": "GLOBAL"} + ] + update_env.return_value = update_res + res = instance.create_or_update( + scope=scope, + name=name, + value=value, + description=description, + is_password=is_password, + project_id=project_id, + pipeline_id=pipeline_id, + ) + + assert res == update_res + + def test_delete_env_var_gql(self): + query = gql(GQL_DELETE_ENV_VAR) + self.client.validate(query) + + def test_delete_env_var(self, saagie_api_mock): + saagie_api_mock.env_vars.env_vars.check_scope.return_value = True + instance = EnvVars(saagie_api_mock) + + scope = "GLOBAL" + name = "env_var1" + + expected_query = gql(GQL_DELETE_ENV_VAR) + + with patch.object(instance, "list") as list_env: + list_env.return_value = [ + { + "id": "env_var_id", + "name": "env_var1", + "value": "value1", + "description": "description1", + "scope": "GLOBAL", + } + ] + instance.delete(scope=scope, name=name) + + saagie_api_mock.client.execute.assert_called_with(query=expected_query, variable_values={"id": "env_var_id"}) + + def test_delete_env_var_not_exists(self, saagie_api_mock): + saagie_api_mock.env_vars.env_vars.check_scope.return_value = True + instance = EnvVars(saagie_api_mock) + + scope = "GLOBAL" + name = "NON_EXISTING_VARIABLE" + + with patch.object(instance, "list") as list_env, pytest.raises(ValueError): + list_env.return_value = [ + { + "id": "env_var_id", + "name": "env_var1", + "value": "value1", + "description": "description1", + "scope": "GLOBAL", + } + ] + instance.delete(scope=scope, name=name) + + def test_create_pipeline_env_vars_gql(self): + query = gql(GQL_CREATE_PIPELINE_ENV_VAR) + self.client.validate(query) - # Test the `bulk_create_for_pipeline` method def test_bulk_create(self, saagie_api_mock): instance = EnvVars(saagie_api_mock) mock_env_vars = { @@ -635,18 +641,9 @@ def test_bulk_create(self, saagie_api_mock): assert result["replaceEnvironmentVariablesByRawForScope"][0]["id"] == "env_var_id" assert result["replaceEnvironmentVariablesByRawForScope"][0]["name"] == "BULK1" - # Test the `export` method @patch("logging.info") - @patch("saagieapi.utils.folder_functions.create_folder") - @patch("saagieapi.utils.folder_functions.write_to_json_file") - @patch("saagieapi.utils.folder_functions.write_error") - @patch("saagieapi.env_vars.env_vars.EnvVars.list", return_value=[]) def test_export_project_without_var( self, - env_list_mock, - write_error_mock, - write_to_json_file_mock, - create_folder_mock, logging_info_mock, saagie_api_mock, ): @@ -656,32 +653,18 @@ def test_export_project_without_var( project_only = True instance = EnvVars(saagie_api_mock) - # Call the method being tested - result = instance.export(project_id, output_folder, error_folder, project_only) + with patch.object(instance, "list") as list_env: + list_env.return_value = [] + result = instance.export(project_id, output_folder, error_folder, project_only) - # Assertions assert result is True - env_list_mock.assert_called_once_with(scope="PROJECT", project_id=project_id, scope_only=True) - create_folder_mock.assert_not_called() - write_to_json_file_mock.assert_not_called() - write_error_mock.assert_not_called() logging_info_mock.assert_called_once_with( "✅ The project [%s] doesn't have any environment variable", project_id ) @patch("logging.warning") - @patch("logging.error") - @patch("saagieapi.utils.folder_functions.create_folder") - @patch("saagieapi.utils.folder_functions.write_to_json_file") - @patch("saagieapi.utils.folder_functions.write_error") - @patch("saagieapi.env_vars.env_vars.EnvVars.list", side_effect=Exception("Error getting environment variables")) def test_export_list_exception( self, - env_list_mock, - write_error_mock, - write_to_json_file_mock, - create_folder_mock, - logging_error_mock, logging_warning_mock, saagie_api_mock, ): @@ -691,40 +674,18 @@ def test_export_list_exception( project_only = True instance = EnvVars(saagie_api_mock) - # Call the method being tested - result = instance.export(project_id, output_folder, error_folder, project_only) + with patch.object(instance, "list") as list_env: + list_env.side_effect = Exception("Error getting environment variables") + result = instance.export(project_id, output_folder, error_folder, project_only) - # Assertions assert not result - env_list_mock.assert_called_once_with(scope="PROJECT", project_id=project_id, scope_only=True) - create_folder_mock.assert_not_called() - write_to_json_file_mock.assert_not_called() - write_error_mock.assert_not_called() logging_warning_mock.assert_called_once_with( "Cannot get the information of environment variable of the project [%s]", project_id ) - arg1 = logging_error_mock.call_args[0][0] - arg2 = logging_error_mock.call_args[0][1] - assert arg1 == "Something went wrong %s" - assert isinstance(arg2, Exception) - assert str(arg2) == "Error getting environment variables" @patch("logging.info") - @patch("logging.error") - @patch("saagieapi.env_vars.env_vars.create_folder") - @patch("saagieapi.env_vars.env_vars.write_to_json_file") - @patch("saagieapi.env_vars.env_vars.write_error") - @patch( - "saagieapi.env_vars.env_vars.EnvVars.list", - return_value=[{"id": "env_var_id", "name": "TEST_VARIABLE", "value": "value1", "scope": "PROJECT"}], - ) def test_export_project( self, - env_list_mock, - write_error_mock, - write_to_json_file_mock, - create_folder_mock, - logging_error_mock, logging_info_mock, saagie_api_mock, ): @@ -734,31 +695,26 @@ def test_export_project( project_only = True instance = EnvVars(saagie_api_mock) - result = instance.export(project_id, output_folder, error_folder, project_only) + with patch.object(instance, "list") as list_env: + list_env.return_value = [ + {"id": "env_var_id", "name": "TEST_VARIABLE", "value": "value1", "scope": "PROJECT"} + ] + result = instance.export(project_id, output_folder, error_folder, project_only) # Assertions assert result is True - write_error_mock.assert_not_called() - env_list_mock.assert_called_once_with(scope="PROJECT", project_id=project_id, scope_only=True) logging_info_mock.assert_called_with( "✅ Environment variables of the project [%s] have been successfully exported", project_id ) - logging_error_mock.assert_not_called() - create_folder_mock.assert_called() - write_to_json_file_mock.assert_called() - @patch("logging.error") @patch("logging.warning") @patch("saagieapi.env_vars.env_vars.create_folder", side_effect=Exception("Exception raised")) - @patch("saagieapi.env_vars.env_vars.write_to_json_file") @patch("saagieapi.env_vars.env_vars.write_error") def test_export_folder_exception( self, write_error_mock, - write_to_json_file_mock, create_folder_mock, logging_warning_mock, - logging_error_mock, saagie_api_mock, ): project_id = "50033e21-83c2-4431-a723-d54c2693b964" @@ -766,42 +722,22 @@ def test_export_folder_exception( error_folder = "./output/error/" project_only = True instance = EnvVars(saagie_api_mock) - mock_existing_env_var = [ - { - "id": "334c2e0e-e8ea-4639-911e-757bf36bc91b", - "name": "TEST_PASSWORD", - "scope": "PROJECT", - "value": None, - "description": "This is a password", - "isPassword": True, - "isValid": True, - "overriddenValues": [], - "invalidReasons": None, - } - ] - instance.list = MagicMock(return_value=mock_existing_env_var) - - result = instance.export(project_id, output_folder, error_folder, project_only) + with patch.object(instance, "list") as list_env: + list_env.return_value = [ + {"id": "env_var_id", "name": "TEST_VARIABLE", "value": "value1", "scope": "PROJECT"} + ] + result = instance.export(project_id, output_folder, error_folder, project_only) - # Assertions assert not result - instance.list.assert_called_once_with(scope="PROJECT", project_id=project_id, scope_only=True) create_folder_mock.assert_called() - write_to_json_file_mock.assert_not_called() write_error_mock.assert_called() logging_warning_mock.assert_called_once_with( "❌ Environment variables of the project [%s] have not been successfully exported", project_id ) - arg1 = logging_error_mock.call_args[0][0] - arg2 = logging_error_mock.call_args[0][1] - assert arg1 == "Something went wrong %s" - assert isinstance(arg2, Exception) - assert str(arg2) == "Exception raised" @patch("logging.info") def test_import_from_json_success(self, logging_info_mock, saagie_api_mock): - # Prepare test data and environment json_file = "/path/to/the/json/file.json" project_id = "MY_PROJECT_ID" instance = EnvVars(saagie_api_mock) diff --git a/tests/unit/pipelines_unit_test.py b/tests/unit/pipelines_unit_test.py index 8bba781..7d17746 100644 --- a/tests/unit/pipelines_unit_test.py +++ b/tests/unit/pipelines_unit_test.py @@ -183,6 +183,32 @@ def test_get_pipeline_by_name(self, saagie_api_mock): query=expected_query, variable_values=params, pprint_result=None ) + def test_get_pipeline_by_alias_gql(self): + query = gql(GQL_GET_PIPELINE_BY_ALIAS) + self.client.validate(query) + + def test_get_pipeline_by_alias(self, saagie_api_mock): + pipeline = Pipelines(saagie_api_mock) + + project_id = "860b8dc8-e634-4c98-b2e7-f9ec32ab4771" + pipeline_alias = "pipeline_alias" + + params = { + "projectId": project_id, + "pipelineAlias": pipeline_alias, + "instancesLimit": None, + "versionsLimit": None, + "versionsOnlyCurrent": False, + } + + expected_query = gql(GQL_GET_PIPELINE_BY_ALIAS) + + pipeline.get_info_by_alias(project_id=project_id, pipeline_alias=pipeline_alias) + + saagie_api_mock.client.execute.assert_called_with( + query=expected_query, variable_values=params, pprint_result=None + ) + def test_get_pipeline_instance_gql(self): query = gql(GQL_GET_PIPELINE_INSTANCE) self.client.validate(query) @@ -216,6 +242,7 @@ def test_create_graph_pipeline(self, saagie_api_mock): name = "Amazing Pipeline" alias = "amazing_pipeline" desc = "new pipeline" + source_url = "https://my.super.link" job_node1 = JobNode("5d1999f5-fa70-47d9-9f41-55ad48333629") job_node2 = JobNode("5d1999f5-fa70-47d9-9f41-55ad48333629") @@ -235,6 +262,7 @@ def test_create_graph_pipeline(self, saagie_api_mock): name=name, alias=alias, description=desc, + source_url=source_url, ) expected_query = gql(GQL_CREATE_GRAPH_PIPELINE) @@ -250,6 +278,7 @@ def test_create_graph_pipeline(self, saagie_api_mock): "conditionNodes": graph_pipeline.list_conditions_nodes, "alias": alias, "isScheduled": False, + "sourceUrl": source_url, } saagie_api_mock.client.execute.assert_called_with(query=expected_query, variable_values=params) @@ -350,6 +379,7 @@ def test_upgrade_pipeline(self, saagie_api_mock): pipeline = Pipelines(saagie_api_mock) pipeline_id = "860b8dc8-e634-4c98-b2e7-f9ec32ab4771" + source_url = "https://my.super.link" job_node1 = JobNode("5d1999f5-fa70-47d9-9f41-55ad48333629") job_node2 = JobNode("5d1999f5-fa70-47d9-9f41-55ad48333629") @@ -363,10 +393,7 @@ def test_upgrade_pipeline(self, saagie_api_mock): graph_pipeline = GraphPipeline() graph_pipeline.add_root_node(job_node1) - pipeline.upgrade( - pipeline_id=pipeline_id, - graph_pipeline=graph_pipeline, - ) + pipeline.upgrade(pipeline_id=pipeline_id, graph_pipeline=graph_pipeline, source_url=source_url) expected_query = gql(GQL_UPGRADE_PIPELINE) @@ -377,6 +404,7 @@ def test_upgrade_pipeline(self, saagie_api_mock): "jobNodes": graph_pipeline.list_job_nodes, "conditionNodes": graph_pipeline.list_conditions_nodes, "releaseNote": "", + "sourceUrl": source_url, } saagie_api_mock.client.execute.assert_called_with(query=expected_query, variable_values=params) diff --git a/tests/unit/resources/schema.graphqls b/tests/unit/resources/schema.graphqls index f076c51..22292ad 100644 --- a/tests/unit/resources/schema.graphqls +++ b/tests/unit/resources/schema.graphqls @@ -135,6 +135,15 @@ type Query { skip:Int ): [EnvironmentVariable!] + # List environment variables of a specific app. Include all variables usable by this app. + # + # You can only list environment variables if you have at least the viewer role in the project associated to the app or in all projects. + appEnvironmentVariables( + appId: UUID!, + # Allows to get only variables in the scope specified in input. By default if no parameter is specified, all scopes are include. + scope: EnvVarScope + ): [EnvironmentVariable!] + # List technologies, grouped by category, in a given project by providing its UUID. In addition, technologies can be filtered by category. # # You can only list technologies if you have at least the viewer role in the project or in all projects. @@ -289,7 +298,7 @@ type Query { # You can only get a pipeline's details if you have at least the viewer role in the project associated to the pipeline or in all projects. # # This endpoint allows to retrieve a pipeline with versions in linear or graph format. - graphPipelineByAlias(projectId: UUID!, name: String!): Pipeline! + graphPipelineByAlias(projectId: UUID!, alias: String!): Pipeline! # List users by realm. usersByRealm: [UserEmail] @@ -643,14 +652,19 @@ type Mutation { # Add a new pipeline version in linear format to an existing pipeline. # # You can only add a version to a pipeline if you have at least the editor role in the project associated to the pipeline or in all projects. + # The parameters sourceUrl and releaseNote are optional. + # SourceUrl should be an http or https valid url. It represents the source code url for the pipeline definition. # # This deprecated endpoint allows to add a version to a linear pipeline. To add a graph pipeline version, use the endpoint `addGraphPipelineVersion` instead. - addPipelineVersion(pipelineId: UUID!, jobsId: [UUID!]!, releaseNote: String): PipelineVersion! @deprecated(reason: "Only for old linears pipelines without graph, use addGraphPipelineVersion instead") + addPipelineVersion(pipelineId: UUID!, jobsId: [UUID!]!, releaseNote: String, sourceUrl: String): PipelineVersion! @deprecated(reason: "Only for old linears pipelines without graph, use addGraphPipelineVersion instead") # Add a new pipeline version in graph format based on DAG model (Directed Acyclic Graph) to an existing pipeline. # # You can only add a version to a pipeline if you have at least the editor role in the project associated to the pipeline or in all projects. - addGraphPipelineVersion(pipelineId: UUID!, graph: PipelineGraphInput!, releaseNote: String): PipelineVersion! + # The parameters sourceUrl and releaseNote are optional. + # SourceUrl should be an http or https valid url. It represents the source code url for the pipeline definition. + # + addGraphPipelineVersion(pipelineId: UUID!, graph: PipelineGraphInput!, releaseNote: String, sourceUrl: String): PipelineVersion! # Rollback a pipeline to a specific version. # @@ -856,6 +870,8 @@ input JobVersionInput { doesUseGPU: Boolean connectionId: UUID externalJobParams: [ExternalJobParamInput!] + + # Version reference url (http or https) pushed only by API sourceUrl: String } @@ -984,6 +1000,9 @@ input PipelineInput { isScheduled: Boolean! cronScheduling: Cron scheduleTimezone: TimeZone + + # Version reference url (http or https) pushed only by API + sourceUrl: String } # Input to create a pipeline with a Directed Acyclic Graph @@ -1001,6 +1020,9 @@ input GraphPipelineInput { isScheduled: Boolean! cronScheduling: Cron scheduleTimezone: TimeZone + + # Version reference url pushed only by API + sourceUrl: String } # Input for a Directed Acyclic Graph (DAG) of nodes of jobs and conditions of a pipeline in graph format @@ -1201,6 +1223,7 @@ type Volume { duplicationStatus: DuplicationStatus duplicationTargetId: UUID originalVolumeId: UUID + expansionStatus: ExpansionStatus isLocked: Boolean } @@ -1431,6 +1454,7 @@ enum EnvironmentVariableValidationErrorType { NULL_VALUE_UNEXPECTED, NAME_ALREADY_USED, PROJECT_IS_NOT_FOUND, + APP_IS_NOT_FOUND, JOB_IS_NOT_FOUND, UNEXPECTED_LINKED_ENTITY_ON_GLOBAL_SCOPE, UNEXPECTED_LINKED_ENTITY_ID_IS_NULL @@ -1559,6 +1583,8 @@ type JobVersion { doesUseGPU: Boolean @deprecated(reason: "Use Job.doesUseGPU instead") connectionId: UUID externalJobParams: [ExternalJobParam!] + + # Version reference url (http or https) pushed only by API. sourceUrl: String } @@ -1758,6 +1784,9 @@ type PipelineVersion { isCurrent: Boolean! isMajor: Boolean! deletableState: PipelineVersionDeletableState + + # Version reference url (http or https) pushed only by API + sourceUrl: String } type PipelineVersionDeletableState { @@ -2046,6 +2075,7 @@ enum EnvVarScope { PIPELINE # Value is deprecated : it will be removed in future releases JOB + APP } enum StatusReason { @@ -2118,6 +2148,11 @@ enum DuplicationStatus { FAILED } +enum ExpansionStatus { + EXPANSION_IN_PROGRESS + WAITING_FOR_LINK_TO_AN_APP +} + enum TriggerType { MANUAL_JOB, MANUAL_APP,