From 79af23aeb70fcbd37d56c73e5b026fff71688da0 Mon Sep 17 00:00:00 2001 From: b0bu <7663736+b0bu@users.noreply.github.com> Date: Thu, 14 Jul 2022 16:57:04 +0100 Subject: [PATCH] Add support for codebuild (#5282) --- IMPLEMENTATION_COVERAGE.md | 109 +++- docs/docs/services/codebuild.rst | 75 +++ moto/__init__.py | 1 + moto/backend_index.py | 1 + moto/codebuild/__init__.py | 4 + moto/codebuild/exceptions.py | 24 + moto/codebuild/models.py | 263 ++++++++++ moto/codebuild/responses.py | 195 +++++++ moto/codebuild/urls.py | 5 + tests/test_codebuild/test_codebuild.py | 699 +++++++++++++++++++++++++ 10 files changed, 1365 insertions(+), 11 deletions(-) create mode 100644 docs/docs/services/codebuild.rst create mode 100644 moto/codebuild/__init__.py create mode 100644 moto/codebuild/exceptions.py create mode 100644 moto/codebuild/models.py create mode 100644 moto/codebuild/responses.py create mode 100644 moto/codebuild/urls.py create mode 100644 tests/test_codebuild/test_codebuild.py diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md index c921e2342..20f505d6c 100644 --- a/IMPLEMENTATION_COVERAGE.md +++ b/IMPLEMENTATION_COVERAGE.md @@ -338,7 +338,7 @@ ## autoscaling
-49% implemented +50% implemented - [X] attach_instances - [X] attach_load_balancer_target_groups @@ -382,7 +382,7 @@ - [X] detach_load_balancer_target_groups - [X] detach_load_balancers - [ ] disable_metrics_collection -- [ ] enable_metrics_collection +- [X] enable_metrics_collection - [ ] enter_standby - [X] execute_policy - [ ] exit_standby @@ -462,6 +462,47 @@ - [ ] update_subscriber
+## ce +
+11% implemented + +- [ ] create_anomaly_monitor +- [ ] create_anomaly_subscription +- [X] create_cost_category_definition +- [ ] delete_anomaly_monitor +- [ ] delete_anomaly_subscription +- [X] delete_cost_category_definition +- [X] describe_cost_category_definition +- [ ] get_anomalies +- [ ] get_anomaly_monitors +- [ ] get_anomaly_subscriptions +- [ ] get_cost_and_usage +- [ ] get_cost_and_usage_with_resources +- [ ] get_cost_categories +- [ ] get_cost_forecast +- [ ] get_dimension_values +- [ ] get_reservation_coverage +- [ ] get_reservation_purchase_recommendation +- [ ] get_reservation_utilization +- [ ] get_rightsizing_recommendation +- [ ] get_savings_plans_coverage +- [ ] get_savings_plans_purchase_recommendation +- [ ] get_savings_plans_utilization +- [ ] get_savings_plans_utilization_details +- [ ] get_tags +- [ ] get_usage_forecast +- [ ] list_cost_allocation_tags +- [ ] list_cost_category_definitions +- [ ] list_tags_for_resource +- [ ] provide_anomaly_feedback +- [ ] tag_resource +- [ ] untag_resource +- [ ] update_anomaly_monitor +- [ ] update_anomaly_subscription +- [ ] update_cost_allocation_tags_status +- [X] update_cost_category_definition +
+ ## cloudformation
30% implemented @@ -709,6 +750,57 @@ - [X] untag_resource
+## codebuild +
+17% implemented + +- [ ] batch_delete_builds +- [ ] batch_get_build_batches +- [X] batch_get_builds +- [ ] batch_get_projects +- [ ] batch_get_report_groups +- [ ] batch_get_reports +- [X] create_project +- [ ] create_report_group +- [ ] create_webhook +- [ ] delete_build_batch +- [X] delete_project +- [ ] delete_report +- [ ] delete_report_group +- [ ] delete_resource_policy +- [ ] delete_source_credentials +- [ ] delete_webhook +- [ ] describe_code_coverages +- [ ] describe_test_cases +- [ ] get_report_group_trend +- [ ] get_resource_policy +- [ ] import_source_credentials +- [ ] invalidate_project_cache +- [ ] list_build_batches +- [ ] list_build_batches_for_project +- [X] list_builds +- [X] list_builds_for_project +- [ ] list_curated_environment_images +- [X] list_projects +- [ ] list_report_groups +- [ ] list_reports +- [ ] list_reports_for_report_group +- [ ] list_shared_projects +- [ ] list_shared_report_groups +- [ ] list_source_credentials +- [ ] put_resource_policy +- [ ] retry_build +- [ ] retry_build_batch +- [X] start_build +- [ ] start_build_batch +- [X] stop_build +- [ ] stop_build_batch +- [ ] update_project +- [ ] update_project_visibility +- [ ] update_report_group +- [ ] update_webhook +
+ ## codecommit
3% implemented @@ -1289,7 +1381,7 @@ ## ds
-18% implemented +19% implemented - [ ] accept_shared_directory - [ ] add_ip_routes @@ -1320,7 +1412,6 @@ - [ ] describe_event_topics - [ ] describe_ldaps_settings - [ ] describe_regions -- [ ] describe_settings - [ ] describe_shared_directories - [ ] describe_snapshots - [ ] describe_trusts @@ -1353,7 +1444,6 @@ - [ ] update_conditional_forwarder - [ ] update_number_of_domain_controllers - [ ] update_radius -- [ ] update_settings - [ ] update_trust - [ ] verify_trust
@@ -2457,7 +2547,7 @@ ## emr-serverless
-71% implemented +50% implemented - [ ] cancel_job_run - [X] create_application @@ -2468,7 +2558,7 @@ - [ ] list_job_runs - [ ] list_tags_for_resource - [X] start_application -- [X] start_job_run +- [ ] start_job_run - [X] stop_application - [ ] tag_resource - [ ] untag_resource @@ -2828,7 +2918,6 @@ - [ ] import_catalog_to_glue - [ ] list_blueprints - [X] list_crawlers -- [ ] list_crawls - [ ] list_custom_entity_types - [ ] list_dev_endpoints - [X] list_jobs @@ -5998,7 +6087,6 @@ - backup-gateway - billingconductor - braket -- ce - chime - chime-sdk-identity - chime-sdk-media-pipelines @@ -6012,7 +6100,6 @@ - cloudsearch - cloudsearchdomain - codeartifact -- codebuild - codedeploy - codeguru-reviewer - codeguruprofiler @@ -6040,7 +6127,6 @@ - drs - ecr-public - elastic-inference -- emr-serverless - evidently - finspace - finspace-data @@ -6138,6 +6224,7 @@ - qldb-session - rbin - rds-data +- redshiftserverless - resiliencehub - robomaker - route53-recovery-cluster diff --git a/docs/docs/services/codebuild.rst b/docs/docs/services/codebuild.rst new file mode 100644 index 000000000..79d8f104e --- /dev/null +++ b/docs/docs/services/codebuild.rst @@ -0,0 +1,75 @@ +.. _implementedservice_codebuild: + +.. |start-h3| raw:: html + +

+ +.. |end-h3| raw:: html + +

+ +========= +codebuild +========= + +.. autoclass:: moto.codebuild.models.CodeBuildBackend + +|start-h3| Example usage |end-h3| + +.. sourcecode:: python + + @mock_codebuild + def test_codebuild_behaviour: + boto3.client("codebuild") + ... + + + +|start-h3| Implemented features for this service |end-h3| + +- [ ] batch_delete_builds +- [ ] batch_get_build_batches +- [X] batch_get_builds +- [ ] batch_get_projects +- [ ] batch_get_report_groups +- [ ] batch_get_reports +- [X] create_project +- [ ] create_report_group +- [ ] create_webhook +- [ ] delete_build_batch +- [X] delete_project +- [ ] delete_report +- [ ] delete_report_group +- [ ] delete_resource_policy +- [ ] delete_source_credentials +- [ ] delete_webhook +- [ ] describe_code_coverages +- [ ] describe_test_cases +- [ ] get_report_group_trend +- [ ] get_resource_policy +- [ ] import_source_credentials +- [ ] invalidate_project_cache +- [ ] list_build_batches +- [ ] list_build_batches_for_project +- [X] list_builds +- [X] list_builds_for_project +- [ ] list_curated_environment_images +- [X] list_projects +- [ ] list_report_groups +- [ ] list_reports +- [ ] list_reports_for_report_group +- [ ] list_shared_projects +- [ ] list_shared_report_groups +- [ ] list_source_credentials +- [ ] put_resource_policy +- [ ] retry_build +- [ ] retry_build_batch +- [X] start_build +- [ ] start_build_batch +- [X] stop_build +- [ ] stop_build_batch +- [ ] update_project +- [ ] update_project_visibility +- [ ] update_report_group +- [ ] update_webhook + diff --git a/moto/__init__.py b/moto/__init__.py index de09ef8c6..f1ac3e2ff 100644 --- a/moto/__init__.py +++ b/moto/__init__.py @@ -63,6 +63,7 @@ mock_cloudfront = lazy_load(".cloudfront", "mock_cloudfront") mock_cloudtrail = lazy_load(".cloudtrail", "mock_cloudtrail") mock_cloudwatch = lazy_load(".cloudwatch", "mock_cloudwatch") mock_codecommit = lazy_load(".codecommit", "mock_codecommit") +mock_codebuild = lazy_load(".codebuild", "mock_codebuild") mock_codepipeline = lazy_load(".codepipeline", "mock_codepipeline") mock_cognitoidentity = lazy_load( ".cognitoidentity", "mock_cognitoidentity", boto3_name="cognito-identity" diff --git a/moto/backend_index.py b/moto/backend_index.py index 464e15b9c..4ff8408f4 100644 --- a/moto/backend_index.py +++ b/moto/backend_index.py @@ -18,6 +18,7 @@ backend_url_patterns = [ ("cloudfront", re.compile("https?://cloudfront\\.amazonaws\\.com")), ("cloudtrail", re.compile("https?://cloudtrail\\.(.+)\\.amazonaws\\.com")), ("cloudwatch", re.compile("https?://monitoring\\.(.+)\\.amazonaws.com")), + ("codebuild", re.compile("https?://codebuild\\.(.+)\\.amazonaws\\.com")), ("codecommit", re.compile("https?://codecommit\\.(.+)\\.amazonaws\\.com")), ("codepipeline", re.compile("https?://codepipeline\\.(.+)\\.amazonaws\\.com")), ( diff --git a/moto/codebuild/__init__.py b/moto/codebuild/__init__.py new file mode 100644 index 000000000..3b7d181d1 --- /dev/null +++ b/moto/codebuild/__init__.py @@ -0,0 +1,4 @@ +from .models import codebuild_backends +from ..core.models import base_decorator + +mock_codebuild = base_decorator(codebuild_backends) diff --git a/moto/codebuild/exceptions.py b/moto/codebuild/exceptions.py new file mode 100644 index 000000000..84bfed944 --- /dev/null +++ b/moto/codebuild/exceptions.py @@ -0,0 +1,24 @@ +from moto.core.exceptions import JsonRESTError + +""" will need exceptions for each api endpoint hit """ + + +class InvalidInputException(JsonRESTError): + code = 400 + + def __init__(self, message): + super().__init__("InvalidInputException", message) + + +class ResourceNotFoundException(JsonRESTError): + code = 400 + + def __init__(self, message): + super().__init__("ResourceNotFoundException", message) + + +class ResourceAlreadyExistsException(JsonRESTError): + code = 400 + + def __init__(self, message): + super().__init__("ResourceAlreadyExistsException", message) diff --git a/moto/codebuild/models.py b/moto/codebuild/models.py new file mode 100644 index 000000000..ac96dd47e --- /dev/null +++ b/moto/codebuild/models.py @@ -0,0 +1,263 @@ +from moto.core import BaseBackend, BaseModel +from moto.core.utils import iso_8601_datetime_with_milliseconds, BackendDict +from moto.core import get_account_id +from collections import defaultdict +from random import randint +from dateutil import parser +import datetime +import uuid + + +class CodeBuildProjectMetadata(BaseModel): + def __init__(self, project_name, source_version, artifacts, build_id, service_role): + current_date = iso_8601_datetime_with_milliseconds(datetime.datetime.utcnow()) + self.build_metadata = dict() + + self.build_metadata["id"] = build_id + self.build_metadata["arn"] = "arn:aws:codebuild:eu-west-2:{0}:build/{1}".format( + get_account_id(), build_id + ) + + self.build_metadata["buildNumber"] = randint(1, 100) + self.build_metadata["startTime"] = current_date + self.build_metadata["currentPhase"] = "QUEUED" + self.build_metadata["buildStatus"] = "IN_PROGRESS" + self.build_metadata["sourceVersion"] = ( + source_version if source_version else "refs/heads/main" + ) + self.build_metadata["projectName"] = project_name + + self.build_metadata["phases"] = [ + { + "phaseType": "SUBMITTED", + "phaseStatus": "SUCCEEDED", + "startTime": current_date, + "endTime": current_date, + "durationInSeconds": 0, + }, + {"phaseType": "QUEUED", "startTime": current_date}, + ] + + self.build_metadata["source"] = { + "type": "CODECOMMIT", # should be different based on what you pass in + "location": "https://git-codecommit.eu-west-2.amazonaws.com/v1/repos/testing", + "gitCloneDepth": 1, + "gitSubmodulesConfig": {"fetchSubmodules": False}, + "buildspec": "buildspec/stuff.yaml", # should present in the codebuild project somewhere + "insecureSsl": False, + } + + self.build_metadata["secondarySources"] = [] + self.build_metadata["secondarySourceVersions"] = [] + self.build_metadata["artifacts"] = artifacts + self.build_metadata["secondaryArtifacts"] = [] + self.build_metadata["cache"] = {"type": "NO_CACHE"} + + self.build_metadata["environment"] = { + "type": "LINUX_CONTAINER", + "image": "aws/codebuild/amazonlinux2-x86_64-standard:3.0", + "computeType": "BUILD_GENERAL1_SMALL", + "environmentVariables": [], + "privilegedMode": False, + "imagePullCredentialsType": "CODEBUILD", + } + + self.build_metadata["serviceRole"] = service_role + + self.build_metadata["logs"] = { + "deepLink": "https://console.aws.amazon.com/cloudwatch/home?region=eu-west-2#logEvent:group=null;stream=null", + "cloudWatchLogsArn": "arn:aws:logs:eu-west-2:{0}:log-group:null:log-stream:null".format( + get_account_id() + ), + "cloudWatchLogs": {"status": "ENABLED"}, + "s3Logs": {"status": "DISABLED", "encryptionDisabled": False}, + } + + self.build_metadata["timeoutInMinutes"] = 45 + self.build_metadata["queuedTimeoutInMinutes"] = 480 + self.build_metadata["buildComplete"] = False + self.build_metadata["initiator"] = "rootme" + self.build_metadata[ + "encryptionKey" + ] = "arn:aws:kms:eu-west-2:{0}:alias/aws/s3".format(get_account_id()) + + +class CodeBuild(BaseModel): + def __init__( + self, + region, + project_name, + project_source, + artifacts, + environment, + serviceRole="some_role", + ): + current_date = iso_8601_datetime_with_milliseconds(datetime.datetime.utcnow()) + self.project_metadata = dict() + + self.project_metadata["name"] = project_name + self.project_metadata["arn"] = "arn:aws:codebuild:{0}:{1}:project/{2}".format( + region, get_account_id(), self.project_metadata["name"] + ) + self.project_metadata[ + "encryptionKey" + ] = "arn:aws:kms:{0}:{1}:alias/aws/s3".format(region, get_account_id()) + self.project_metadata[ + "serviceRole" + ] = "arn:aws:iam::{0}:role/service-role/{1}".format( + get_account_id(), serviceRole + ) + self.project_metadata["lastModifiedDate"] = current_date + self.project_metadata["created"] = current_date + self.project_metadata["badge"] = dict() + self.project_metadata["badge"][ + "badgeEnabled" + ] = False # this false needs to be a json false not a python false + self.project_metadata["environment"] = environment + self.project_metadata["artifacts"] = artifacts + self.project_metadata["source"] = project_source + self.project_metadata["cache"] = dict() + self.project_metadata["cache"]["type"] = "NO_CACHE" + self.project_metadata["timeoutInMinutes"] = "" + self.project_metadata["queuedTimeoutInMinutes"] = "" + + +class CodeBuildBackend(BaseBackend): + def __init__(self, region_name, account_id): + super().__init__(region_name, account_id) + self.codebuild_projects = dict() + self.build_history = dict() + self.build_metadata = dict() + self.build_metadata_history = defaultdict(list) + + def create_project( + self, project_name, project_source, artifacts, environment, service_role + ): + # required in other functions that don't + self.project_name = project_name + self.service_role = service_role + + self.codebuild_projects[project_name] = CodeBuild( + self.region_name, + project_name, + project_source, + artifacts, + environment, + service_role, + ) + + # empty build history + self.build_history[project_name] = list() + + return self.codebuild_projects[project_name].project_metadata + + def list_projects(self): + + projects = [] + + for project in self.codebuild_projects.keys(): + projects.append(project) + + return projects + + def start_build(self, project_name, source_version=None, artifact_override=None): + + build_id = "{0}:{1}".format(project_name, uuid.uuid4()) + + # construct a new build + self.build_metadata[project_name] = CodeBuildProjectMetadata( + project_name, source_version, artifact_override, build_id, self.service_role + ) + + self.build_history[project_name].append(build_id) + + # update build histroy with metadata for build id + self.build_metadata_history[project_name].append( + self.build_metadata[project_name].build_metadata + ) + + return self.build_metadata[project_name].build_metadata + + def _set_phases(self, phases): + current_date = iso_8601_datetime_with_milliseconds(datetime.datetime.utcnow()) + # No phaseStatus for QUEUED on first start + for existing_phase in phases: + if existing_phase["phaseType"] == "QUEUED": + existing_phase["phaseStatus"] = "SUCCEEDED" + + statuses = [ + "PROVISIONING", + "DOWNLOAD_SOURCE", + "INSTALL", + "PRE_BUILD", + "BUILD", + "POST_BUILD", + "UPLOAD_ARTIFACTS", + "FINALIZING", + "COMPLETED", + ] + + for status in statuses: + phase = dict() + phase["phaseType"] = status + phase["phaseStatus"] = "SUCCEEDED" + phase["startTime"] = current_date + phase["endTime"] = current_date + phase["durationInSeconds"] = randint(10, 100) + phases.append(phase) + + return phases + + def batch_get_builds(self, ids): + batch_build_metadata = [] + + for metadata in self.build_metadata_history.values(): + for build in metadata: + if build["id"] in ids: + build["phases"] = self._set_phases(build["phases"]) + build["endTime"] = iso_8601_datetime_with_milliseconds( + parser.parse(build["startTime"]) + + datetime.timedelta(minutes=randint(1, 5)) + ) + build["currentPhase"] = "COMPLETED" + build["buildStatus"] = "SUCCEEDED" + + batch_build_metadata.append(build) + + return batch_build_metadata + + def list_builds_for_project(self, project_name): + try: + return self.build_history[project_name] + except KeyError: + return list() + + def list_builds(self): + ids = [] + + for build_ids in self.build_history.values(): + ids += build_ids + return ids + + def delete_project(self, project_name): + self.build_metadata.pop(project_name, None) + self.codebuild_projects.pop(project_name, None) + + def stop_build(self, build_id): + + for metadata in self.build_metadata_history.values(): + for build in metadata: + if build["id"] == build_id: + # set completion properties with variable completion time + build["phases"] = self._set_phases(build["phases"]) + build["endTime"] = iso_8601_datetime_with_milliseconds( + parser.parse(build["startTime"]) + + datetime.timedelta(minutes=randint(1, 5)) + ) + build["currentPhase"] = "COMPLETED" + build["buildStatus"] = "STOPPED" + + return build + + +codebuild_backends = BackendDict(CodeBuildBackend, "codebuild") diff --git a/moto/codebuild/responses.py b/moto/codebuild/responses.py new file mode 100644 index 000000000..c3ce1b6ff --- /dev/null +++ b/moto/codebuild/responses.py @@ -0,0 +1,195 @@ +from moto.core.responses import BaseResponse +from .models import codebuild_backends +from .exceptions import ( + InvalidInputException, + ResourceAlreadyExistsException, + ResourceNotFoundException, +) +from moto.core import get_account_id +import json +import re + + +def _validate_required_params_source(source): + if source["type"] not in [ + "BITBUCKET", + "CODECOMMIT", + "CODEPIPELINE", + "GITHUB", + "GITHUB_ENTERPRISE", + "NO_SOURCE", + "S3", + ]: + raise InvalidInputException("Invalid type provided: Project source type") + + if "location" not in source: + raise InvalidInputException("Project source location is required") + + if source["location"] == "": + raise InvalidInputException("Project source location is required") + + +def _validate_required_params_service_role(service_role): + if ( + "arn:aws:iam::{0}:role/service-role/".format(get_account_id()) + not in service_role + ): + raise InvalidInputException( + "Invalid service role: Service role account ID does not match caller's account" + ) + + +def _validate_required_params_artifacts(artifacts): + + if artifacts["type"] not in ["CODEPIPELINE", "S3", "NO_ARTIFACTS"]: + raise InvalidInputException("Invalid type provided: Artifact type") + + if artifacts["type"] == "NO_ARTIFACTS": + if "location" in artifacts: + raise InvalidInputException( + "Invalid artifacts: artifact type NO_ARTIFACTS should have null location" + ) + elif "location" not in artifacts or artifacts["location"] == "": + raise InvalidInputException("Project source location is required") + + +def _validate_required_params_environment(environment): + + if environment["type"] not in [ + "WINDOWS_CONTAINER", + "LINUX_CONTAINER", + "LINUX_GPU_CONTAINER", + "ARM_CONTAINER", + ]: + raise InvalidInputException( + "Invalid type provided: {0}".format(environment["type"]) + ) + + if environment["computeType"] not in [ + "BUILD_GENERAL1_SMALL", + "BUILD_GENERAL1_MEDIUM", + "BUILD_GENERAL1_LARGE", + "BUILD_GENERAL1_2XLARGE", + ]: + raise InvalidInputException( + "Invalid compute type provided: {0}".format(environment["computeType"]) + ) + + +def _validate_required_params_project_name(name): + if len(name) >= 150: + raise InvalidInputException( + "Only alphanumeric characters, dash, and underscore are supported" + ) + + if not re.match(r"^[A-Za-z]{1}.*[^!£$%^&*()+=|?`¬{}@~#:;<>\\/\[\]]$", name): + raise InvalidInputException( + "Only alphanumeric characters, dash, and underscore are supported" + ) + + +def _validate_required_params_id(build_id, build_ids): + if ":" not in build_id: + raise InvalidInputException("Invalid build ID provided") + + if build_id not in build_ids: + raise ResourceNotFoundException("Build {0} does not exist".format(build_id)) + + +class CodeBuildResponse(BaseResponse): + @property + def codebuild_backend(self): + return codebuild_backends[self.region] + + def list_builds_for_project(self): + _validate_required_params_project_name(self._get_param("projectName")) + + if ( + self._get_param("projectName") + not in self.codebuild_backend.codebuild_projects.keys() + ): + raise ResourceNotFoundException( + "The provided project arn:aws:codebuild:{0}:{1}:project/{2} does not exist".format( + self.region, get_account_id(), self._get_param("projectName") + ) + ) + + ids = self.codebuild_backend.list_builds_for_project( + self._get_param("projectName") + ) + + return json.dumps({"ids": ids}) + + def create_project(self): + _validate_required_params_source(self._get_param("source")) + _validate_required_params_service_role(self._get_param("serviceRole")) + _validate_required_params_artifacts(self._get_param("artifacts")) + _validate_required_params_environment(self._get_param("environment")) + _validate_required_params_project_name(self._get_param("name")) + + if self._get_param("name") in self.codebuild_backend.codebuild_projects.keys(): + raise ResourceAlreadyExistsException( + "Project already exists: arn:aws:codebuild:{0}:{1}:project/{2}".format( + self.region, get_account_id(), self._get_param("name") + ) + ) + + project_metadata = self.codebuild_backend.create_project( + self._get_param("name"), + self._get_param("source"), + self._get_param("artifacts"), + self._get_param("environment"), + self._get_param("serviceRole"), + ) + + return json.dumps({"project": project_metadata}) + + def list_projects(self): + project_metadata = self.codebuild_backend.list_projects() + return json.dumps({"projects": project_metadata}) + + def start_build(self): + _validate_required_params_project_name(self._get_param("projectName")) + + if ( + self._get_param("projectName") + not in self.codebuild_backend.codebuild_projects.keys() + ): + raise ResourceNotFoundException( + "Project cannot be found: arn:aws:codebuild:{0}:{1}:project/{2}".format( + self.region, get_account_id(), self._get_param("projectName") + ) + ) + + metadata = self.codebuild_backend.start_build( + self._get_param("projectName"), + self._get_param("sourceVersion"), + self._get_param("artifactsOverride"), + ) + return json.dumps({"build": metadata}) + + def batch_get_builds(self): + for build_id in self._get_param("ids"): + if ":" not in build_id: + raise InvalidInputException("Invalid build ID provided") + + metadata = self.codebuild_backend.batch_get_builds(self._get_param("ids")) + return json.dumps({"builds": metadata}) + + def list_builds(self): + ids = self.codebuild_backend.list_builds() + return json.dumps({"ids": ids}) + + def delete_project(self): + _validate_required_params_project_name(self._get_param("name")) + + self.codebuild_backend.delete_project(self._get_param("name")) + return + + def stop_build(self): + _validate_required_params_id( + self._get_param("id"), self.codebuild_backend.list_builds() + ) + + metadata = self.codebuild_backend.stop_build(self._get_param("id")) + return json.dumps({"build": metadata}) diff --git a/moto/codebuild/urls.py b/moto/codebuild/urls.py new file mode 100644 index 000000000..60990963f --- /dev/null +++ b/moto/codebuild/urls.py @@ -0,0 +1,5 @@ +from .responses import CodeBuildResponse + +url_bases = [r"https?://codebuild\.(.+)\.amazonaws\.com"] + +url_paths = {"{0}/$": CodeBuildResponse.dispatch} diff --git a/tests/test_codebuild/test_codebuild.py b/tests/test_codebuild/test_codebuild.py new file mode 100644 index 000000000..c6ec1320f --- /dev/null +++ b/tests/test_codebuild/test_codebuild.py @@ -0,0 +1,699 @@ +import boto3 +import sure # noqa # pylint: disable=unused-import +from moto import mock_codebuild +from moto.core import ACCOUNT_ID +from botocore.exceptions import ClientError, ParamValidationError +from uuid import uuid1 +import pytest + + +@mock_codebuild +def test_codebuild_create_project_s3_artifacts(): + client = boto3.client("codebuild", region_name="eu-central-1") + + name = "some_project" + source = dict() + source["type"] = "S3" + # repository location for S3 + source["location"] = "bucketname/path/file.zip" + # output artifacts + artifacts = dict() + artifacts["type"] = "S3" + artifacts["location"] = "bucketname" + + environment = dict() + environment["type"] = "LINUX_CONTAINER" + environment["image"] = "contents_not_validated" + environment["computeType"] = "BUILD_GENERAL1_SMALL" + + service_role = ( + "arn:aws:iam::{0}:role/service-role/my-codebuild-service-role".format( + ACCOUNT_ID + ) + ) + + response = client.create_project( + name=name, + source=source, + artifacts=artifacts, + environment=environment, + serviceRole=service_role, + ) + + response.should_not.be.none + response["project"].should_not.be.none + response["project"]["serviceRole"].should_not.be.none + response["project"]["name"].should_not.be.none + + response["project"]["environment"].should.equal( + { + "computeType": "BUILD_GENERAL1_SMALL", + "image": "contents_not_validated", + "type": "LINUX_CONTAINER", + } + ) + + response["project"]["source"].should.equal( + {"location": "bucketname/path/file.zip", "type": "S3"} + ) + + response["project"]["artifacts"].should.equal( + {"location": "bucketname", "type": "S3"} + ) + + +@mock_codebuild +def test_codebuild_create_project_no_artifacts(): + client = boto3.client("codebuild", region_name="eu-central-1") + + name = "some_project" + source = dict() + source["type"] = "S3" + # repository location for S3 + source["location"] = "bucketname/path/file.zip" + # output artifacts + artifacts = {"type": "NO_ARTIFACTS"} + + environment = dict() + environment["type"] = "LINUX_CONTAINER" + environment["image"] = "contents_not_validated" + environment["computeType"] = "BUILD_GENERAL1_SMALL" + service_role = ( + "arn:aws:iam::{0}:role/service-role/my-codebuild-service-role".format( + ACCOUNT_ID + ) + ) + + response = client.create_project( + name=name, + source=source, + artifacts=artifacts, + environment=environment, + serviceRole=service_role, + ) + + response.should_not.be.none + response["project"].should_not.be.none + response["project"]["serviceRole"].should_not.be.none + response["project"]["name"].should_not.be.none + + response["project"]["environment"].should.equal( + { + "computeType": "BUILD_GENERAL1_SMALL", + "image": "contents_not_validated", + "type": "LINUX_CONTAINER", + } + ) + + response["project"]["source"].should.equal( + {"location": "bucketname/path/file.zip", "type": "S3"} + ) + + response["project"]["artifacts"].should.equal({"type": "NO_ARTIFACTS"}) + + +@mock_codebuild +def test_codebuild_create_project_with_invalid_name(): + client = boto3.client("codebuild", region_name="eu-central-1") + + name = "!some_project" + source = dict() + source["type"] = "S3" + # repository location for S3 + source["location"] = "bucketname/path/file.zip" + # output artifacts + artifacts = {"type": "NO_ARTIFACTS"} + + environment = dict() + environment["type"] = "LINUX_CONTAINER" + environment["image"] = "contents_not_validated" + environment["computeType"] = "BUILD_GENERAL1_SMALL" + service_role = ( + "arn:aws:iam::{0}:role/service-role/my-codebuild-service-role".format( + ACCOUNT_ID + ) + ) + + with pytest.raises(client.exceptions.from_code("InvalidInputException")) as err: + client.create_project( + name=name, + source=source, + artifacts=artifacts, + environment=environment, + serviceRole=service_role, + ) + err.value.response["Error"]["Code"].should.equal("InvalidInputException") + + +@mock_codebuild +def test_codebuild_create_project_with_invalid_name_length(): + client = boto3.client("codebuild", region_name="eu-central-1") + + name = "some_project_" * 12 + source = dict() + source["type"] = "S3" + # repository location for S3 + source["location"] = "bucketname/path/file.zip" + # output artifacts + artifacts = {"type": "NO_ARTIFACTS"} + + environment = dict() + environment["type"] = "LINUX_CONTAINER" + environment["image"] = "contents_not_validated" + environment["computeType"] = "BUILD_GENERAL1_SMALL" + service_role = ( + "arn:aws:iam::{0}:role/service-role/my-codebuild-service-role".format( + ACCOUNT_ID + ) + ) + + with pytest.raises(client.exceptions.from_code("InvalidInputException")) as err: + client.create_project( + name=name, + source=source, + artifacts=artifacts, + environment=environment, + serviceRole=service_role, + ) + err.value.response["Error"]["Code"].should.equal("InvalidInputException") + + +@mock_codebuild +def test_codebuild_create_project_when_exists(): + client = boto3.client("codebuild", region_name="eu-central-1") + + name = "some_project" + source = dict() + source["type"] = "S3" + # repository location for S3 + source["location"] = "bucketname/path/file.zip" + artifacts = {"type": "NO_ARTIFACTS"} + + environment = dict() + environment["type"] = "LINUX_CONTAINER" + environment["image"] = "contents_not_validated" + environment["computeType"] = "BUILD_GENERAL1_SMALL" + service_role = ( + "arn:aws:iam::{0}:role/service-role/my-codebuild-service-role".format( + ACCOUNT_ID + ) + ) + + client.create_project( + name=name, + source=source, + artifacts=artifacts, + environment=environment, + serviceRole=service_role, + ) + + with pytest.raises(ClientError) as err: + client.create_project( + name=name, + source=source, + artifacts=artifacts, + environment=environment, + serviceRole=service_role, + ) + err.value.response["Error"]["Code"].should.equal("ResourceAlreadyExistsException") + + +@mock_codebuild +def test_codebuild_list_projects(): + client = boto3.client("codebuild", region_name="eu-central-1") + + source = dict() + source["type"] = "S3" + # repository location for S3 + source["location"] = "bucketname/path/file.zip" + # output artifacts + artifacts = dict() + artifacts["type"] = "S3" + artifacts["location"] = "bucketname" + + environment = dict() + environment["type"] = "LINUX_CONTAINER" + environment["image"] = "contents_not_validated" + environment["computeType"] = "BUILD_GENERAL1_SMALL" + service_role = ( + "arn:aws:iam::{0}:role/service-role/my-codebuild-service-role".format( + ACCOUNT_ID + ) + ) + + client.create_project( + name="project1", + source=source, + artifacts=artifacts, + environment=environment, + serviceRole=service_role, + ) + client.create_project( + name="project2", + source=source, + artifacts=artifacts, + environment=environment, + serviceRole=service_role, + ) + + projects = client.list_projects() + + projects["projects"].should_not.be.none + projects["projects"].should.equal(["project1", "project2"]) + + +@mock_codebuild +def test_codebuild_list_builds_for_project_no_history(): + client = boto3.client("codebuild", region_name="eu-central-1") + + name = "some_project" + source = dict() + source["type"] = "S3" + # repository location for S3 + source["location"] = "bucketname/path/file.zip" + artifacts = {"type": "NO_ARTIFACTS"} + + environment = dict() + environment["type"] = "LINUX_CONTAINER" + environment["image"] = "contents_not_validated" + environment["computeType"] = "BUILD_GENERAL1_SMALL" + service_role = ( + "arn:aws:iam::{0}:role/service-role/my-codebuild-service-role".format( + ACCOUNT_ID + ) + ) + + client.create_project( + name=name, + source=source, + artifacts=artifacts, + environment=environment, + serviceRole=service_role, + ) + history = client.list_builds_for_project(projectName=name) + + # no build history if it's never started + history["ids"].should.be.empty + + +@mock_codebuild +def test_codebuild_list_builds_for_project_with_history(): + client = boto3.client("codebuild", region_name="eu-central-1") + + name = "some_project" + source = dict() + source["type"] = "S3" + # repository location for S3 + source["location"] = "bucketname/path/file.zip" + artifacts = {"type": "NO_ARTIFACTS"} + + environment = dict() + environment["type"] = "LINUX_CONTAINER" + environment["image"] = "contents_not_validated" + environment["computeType"] = "BUILD_GENERAL1_SMALL" + service_role = ( + "arn:aws:iam::{0}:role/service-role/my-codebuild-service-role".format( + ACCOUNT_ID + ) + ) + + client.create_project( + name=name, + source=source, + artifacts=artifacts, + environment=environment, + serviceRole=service_role, + ) + client.start_build(projectName=name) + response = client.list_builds_for_project(projectName=name) + + response["ids"].should_not.be.empty + + +# project never started +@mock_codebuild +def test_codebuild_get_batch_builds_for_project_no_history(): + client = boto3.client("codebuild", region_name="eu-central-1") + + name = "some_project" + source = dict() + source["type"] = "S3" + # repository location for S3 + source["location"] = "bucketname/path/file.zip" + artifacts = {"type": "NO_ARTIFACTS"} + + environment = dict() + environment["type"] = "LINUX_CONTAINER" + environment["image"] = "contents_not_validated" + environment["computeType"] = "BUILD_GENERAL1_SMALL" + service_role = ( + "arn:aws:iam::{0}:role/service-role/my-codebuild-service-role".format( + ACCOUNT_ID + ) + ) + + client.create_project( + name=name, + source=source, + artifacts=artifacts, + environment=environment, + serviceRole=service_role, + ) + + response = client.list_builds_for_project(projectName=name) + response.should_not.be.none + response["ids"].should.be.empty + + with pytest.raises(ParamValidationError) as err: + client.batch_get_builds(ids=response["ids"]) + err.typename.should.equal("ParamValidationError") + + +@mock_codebuild +def test_codebuild_start_build_no_project(): + + client = boto3.client("codebuild", region_name="eu-central-1") + + name = "some_project" + + with pytest.raises(client.exceptions.from_code("ResourceNotFoundException")) as err: + client.start_build(projectName=name) + err.value.response["Error"]["Code"].should.equal("ResourceNotFoundException") + + +@mock_codebuild +def test_codebuild_start_build_no_overrides(): + + client = boto3.client("codebuild", region_name="eu-central-1") + + name = "some_project" + source = dict() + source["type"] = "S3" + # repository location for S3 + source["location"] = "bucketname/path/file.zip" + artifacts = {"type": "NO_ARTIFACTS"} + + environment = dict() + environment["type"] = "LINUX_CONTAINER" + environment["image"] = "contents_not_validated" + environment["computeType"] = "BUILD_GENERAL1_SMALL" + service_role = ( + "arn:aws:iam::{0}:role/service-role/my-codebuild-service-role".format( + ACCOUNT_ID + ) + ) + + client.create_project( + name=name, + source=source, + artifacts=artifacts, + environment=environment, + serviceRole=service_role, + ) + response = client.start_build(projectName=name) + + response.should_not.be.none + response["build"].should_not.be.none + response["build"]["sourceVersion"].should.equal("refs/heads/main") + + +@mock_codebuild +def test_codebuild_start_build_multiple_times(): + client = boto3.client("codebuild", region_name="eu-central-1") + + name = "some_project" + source = dict() + source["type"] = "S3" + # repository location for S3 + source["location"] = "bucketname/path/file.zip" + artifacts = {"type": "NO_ARTIFACTS"} + + environment = dict() + environment["type"] = "LINUX_CONTAINER" + environment["image"] = "contents_not_validated" + environment["computeType"] = "BUILD_GENERAL1_SMALL" + service_role = ( + "arn:aws:iam::{0}:role/service-role/my-codebuild-service-role".format( + ACCOUNT_ID + ) + ) + + client.create_project( + name=name, + source=source, + artifacts=artifacts, + environment=environment, + serviceRole=service_role, + ) + + client.start_build(projectName=name) + client.start_build(projectName=name) + client.start_build(projectName=name) + + len(client.list_builds()["ids"]).should.equal(3) + + +@mock_codebuild +def test_codebuild_start_build_with_overrides(): + + client = boto3.client("codebuild", region_name="eu-central-1") + + name = "some_project" + source = dict() + source["type"] = "S3" + # repository location for S3 + source["location"] = "bucketname/path/file.zip" + artifacts = {"type": "NO_ARTIFACTS"} + + environment = dict() + environment["type"] = "LINUX_CONTAINER" + environment["image"] = "contents_not_validated" + environment["computeType"] = "BUILD_GENERAL1_SMALL" + service_role = ( + "arn:aws:iam::{0}:role/service-role/my-codebuild-service-role".format( + ACCOUNT_ID + ) + ) + + branch_override = "fix/testing" + artifacts_override = {"type": "NO_ARTIFACTS"} + + client.create_project( + name=name, + source=source, + artifacts=artifacts, + environment=environment, + serviceRole=service_role, + ) + response = client.start_build( + projectName=name, + sourceVersion=branch_override, + artifactsOverride=artifacts_override, + ) + + response.should_not.be.none + response["build"].should_not.be.none + response["build"]["sourceVersion"].should.equal("fix/testing") + + +@mock_codebuild +def test_codebuild_batch_get_builds_1_project(): + client = boto3.client("codebuild", region_name="eu-central-1") + + name = "some_project" + source = dict() + source["type"] = "S3" + # repository location for S3 + source["location"] = "bucketname/path/file.zip" + artifacts = {"type": "NO_ARTIFACTS"} + + environment = dict() + environment["type"] = "LINUX_CONTAINER" + environment["image"] = "contents_not_validated" + environment["computeType"] = "BUILD_GENERAL1_SMALL" + service_role = ( + "arn:aws:iam::{0}:role/service-role/my-codebuild-service-role".format( + ACCOUNT_ID + ) + ) + + client.create_project( + name=name, + source=source, + artifacts=artifacts, + environment=environment, + serviceRole=service_role, + ) + client.start_build(projectName=name) + + history = client.list_builds_for_project(projectName=name) + response = client.batch_get_builds(ids=history["ids"]) + + response.should_not.be.none + response["builds"].should_not.be.none + response["builds"][0]["currentPhase"].should.equal("COMPLETED") + response["builds"][0]["buildNumber"].should.be.a(int) + response["builds"][0]["phases"].should_not.be.none + len(response["builds"][0]["phases"]).should.equal(11) + + +@mock_codebuild +def test_codebuild_batch_get_builds_2_projects(): + client = boto3.client("codebuild", region_name="eu-central-1") + + source = dict() + source["type"] = "S3" + # repository location for S3 + source["location"] = "bucketname/path/file.zip" + artifacts = {"type": "NO_ARTIFACTS"} + + environment = dict() + environment["type"] = "LINUX_CONTAINER" + environment["image"] = "contents_not_validated" + environment["computeType"] = "BUILD_GENERAL1_SMALL" + service_role = ( + "arn:aws:iam::{0}:role/service-role/my-codebuild-service-role".format( + ACCOUNT_ID + ) + ) + + client.create_project( + name="project-1", + source=source, + artifacts=artifacts, + environment=environment, + serviceRole=service_role, + ) + client.start_build(projectName="project-1") + + client.create_project( + name="project-2", + source=source, + artifacts=artifacts, + environment=environment, + serviceRole=service_role, + ) + client.start_build(projectName="project-2") + + response = client.list_builds() + response["ids"].should_not.be.empty + + "project-1".should.be.within(response["ids"][0]) + "project-2".should.be.within(response["ids"][1]) + + metadata = client.batch_get_builds(ids=response["ids"])["builds"] + metadata.should_not.be.none + "project-1".should.be.within(metadata[0]["id"]) + "project-2".should.be.within(metadata[1]["id"]) + + +@mock_codebuild +def test_codebuild_batch_get_builds_invalid_build_id(): + client = boto3.client("codebuild", region_name="eu-central-1") + + with pytest.raises(client.exceptions.InvalidInputException) as err: + client.batch_get_builds(ids=["some_project{}".format(uuid1())]) + err.value.response["Error"]["Code"].should.equal("InvalidInputException") + + +@mock_codebuild +def test_codebuild_batch_get_builds_empty_build_id(): + client = boto3.client("codebuild", region_name="eu-central-1") + + with pytest.raises(ParamValidationError) as err: + client.batch_get_builds(ids=[]) + err.typename.should.equal("ParamValidationError") + + +@mock_codebuild +def test_codebuild_delete_project(): + client = boto3.client("codebuild", region_name="eu-central-1") + + name = "some_project" + source = dict() + source["type"] = "S3" + # repository location for S3 + source["location"] = "bucketname/path/file.zip" + artifacts = {"type": "NO_ARTIFACTS"} + + environment = dict() + environment["type"] = "LINUX_CONTAINER" + environment["image"] = "contents_not_validated" + environment["computeType"] = "BUILD_GENERAL1_SMALL" + service_role = ( + "arn:aws:iam::{0}:role/service-role/my-codebuild-service-role".format( + ACCOUNT_ID + ) + ) + + client.create_project( + name=name, + source=source, + artifacts=artifacts, + environment=environment, + serviceRole=service_role, + ) + client.start_build(projectName=name) + + response = client.list_builds_for_project(projectName=name) + response["ids"].should_not.be.empty + + client.delete_project(name=name) + + with pytest.raises(ClientError) as err: + client.list_builds_for_project(projectName=name) + err.value.response["Error"]["Code"].should.equal("ResourceNotFoundException") + + +@mock_codebuild +def test_codebuild_stop_build(): + client = boto3.client("codebuild", region_name="eu-central-1") + + name = "some_project" + source = dict() + source["type"] = "S3" + # repository location for S3 + source["location"] = "bucketname/path/file.zip" + artifacts = {"type": "NO_ARTIFACTS"} + + environment = dict() + environment["type"] = "LINUX_CONTAINER" + environment["image"] = "contents_not_validated" + environment["computeType"] = "BUILD_GENERAL1_SMALL" + service_role = ( + "arn:aws:iam::{0}:role/service-role/my-codebuild-service-role".format( + ACCOUNT_ID + ) + ) + + client.create_project( + name=name, + source=source, + artifacts=artifacts, + environment=environment, + serviceRole=service_role, + ) + client.start_build(projectName=name) + + builds = client.list_builds() + + response = client.stop_build(id=builds["ids"][0]) + response["build"]["buildStatus"].should.equal("STOPPED") + + +@mock_codebuild +def test_codebuild_stop_build_no_build(): + client = boto3.client("codebuild", region_name="eu-central-1") + + with pytest.raises(client.exceptions.ResourceNotFoundException) as err: + client.stop_build(id="some_project:{0}".format(uuid1())) + err.value.response["Error"]["Code"].should.equal("ResourceNotFoundException") + + +@mock_codebuild +def test_codebuild_stop_build_bad_uid(): + client = boto3.client("codebuild", region_name="eu-central-1") + + with pytest.raises(client.exceptions.InvalidInputException) as err: + client.stop_build(id="some_project{0}".format(uuid1())) + err.value.response["Error"]["Code"].should.equal("InvalidInputException")