class ConcourseApi(object): ''' Implements a subset of concourse REST API functionality. After creation, `login` ought to be invoked at least once to allow for the execution of requests that required autorization. @param base_url: concourse endpoint (e.g. https://ci.concourse.ci) @param team_name: the team name used for authentication @param verify_ssl: whether or not certificate validation is to be done ''' @ensure_annotations def __init__(self, base_url: str, team_name: str, verify_ssl=False): self.base_url = base_url self.team = team_name self.routes = ConcourseApiRoutes(base_url=base_url, team=team_name) self.verify_ssl = verify_ssl @ensure_annotations def _get(self, url: str): return self.request_builder.get(url, return_type='json') @ensure_annotations def _put(self, url: str, body: str, headers={}, use_auth_token=True): return self.request_builder.put(url, body=body, headers=headers) @ensure_annotations def _post(self, url: str, body: str = "", headers={}): return self.request_builder.post(url, body=body, headers=headers) @ensure_annotations def _delete(self, url: str): return self.request_builder.delete(url) @ensure_annotations def login(self, team: str, username: str, passwd: str): login_url = self.routes.login() request_builder = AuthenticatedRequestBuilder( basic_auth_username=username, basic_auth_passwd=passwd, verify_ssl=self.verify_ssl) response = request_builder.get(login_url, return_type='json') self.auth_token = response['value'] self.team = team self.request_builder = AuthenticatedRequestBuilder( auth_token=self.auth_token, verify_ssl=self.verify_ssl) return self.auth_token @ensure_annotations def set_pipeline(self, name: str, pipeline_definition): previous_version = self.pipeline_config_version(name) headers = {'x-concourse-config-version': previous_version} url = self.routes.pipeline_cfg(name) self._put(url, str(pipeline_definition), headers=headers) @ensure_annotations def delete_pipeline(self, name: str): url = self.routes.pipeline(pipeline_name=name) self._delete(url) def pipelines(self): pipelines_url = self.routes.pipelines() response = self._get(pipelines_url) return map(select_attr('name'), response) def order_pipelines(self, pipeline_names): url = self.routes.order_pipelines() self._put(url, json.dumps(pipeline_names)) @ensure_annotations def pipeline_cfg(self, pipeline_name: str): pipeline_cfg_url = self.routes.pipeline_cfg(pipeline_name) response = self._get(pipeline_cfg_url) ensure_not_empty(response) return PipelineConfig(response, concourse_api=self, name=pipeline_name) @ensure_annotations def pipeline_config_version(self, pipeline_name: str): pipeline_cfg_url = self.routes.pipeline_cfg(pipeline_name) response = self.request_builder.get(pipeline_cfg_url, return_type=None, check_http_code=False) if response.status_code == 404: return None # pipeline did not exist yet # ensure we did receive an error other than 404 self.request_builder._check_http_code(response, pipeline_cfg_url) return response.headers['X-Concourse-Config-Version'] @ensure_annotations def unpause_pipeline(self, pipeline_name: str): unpause_url = self.routes.unpause_pipeline(pipeline_name) self.request_builder.put(unpause_url, body="") @ensure_annotations def expose_pipeline(self, pipeline_name: str): expose_url = self.routes.expose_pipeline(pipeline_name) self.request_builder.put( expose_url, body="", ) @ensure_annotations def job_builds(self, pipeline_name: str, job_name: str): ''' Returns a list of Build objects for the specified job. The list is sorted by the build number, newest build last ''' builds_url = self.routes.job_builds(pipeline_name, job_name) response = self._get(builds_url) builds = [Build(build_dict, self) for build_dict in response] builds = sorted(builds, key=lambda b: b.id()) return builds @ensure_annotations def trigger_build(self, pipeline_name: str, job_name: str): trigger_url = self.routes.job_builds(pipeline_name, job_name) response = self._post(trigger_url) @ensure_annotations def build_plan(self, build_id): build_plan_url = self.routes.build_plan(build_id) response = self._get(build_plan_url) return BuildPlan(response, self) @ensure_annotations def build_events(self, build_id): build_plan_url = self.routes.build_events(build_id) # TODO: this request never seems to send an "EOF" # (probably to support streaming) # --> properly handle this special case response = self.request_builder.get( build_plan_url, return_type=None, stream=True # passed to sseclient ) return BuildEvents(response, self) def set_team(self, team_credentials: ConcourseTeamCredentials): body = {} if team_credentials.has_basic_auth_credentials(): basic_auth_cfg = { 'username': team_credentials.username(), 'password': team_credentials.passwd(), } body['auth'] = {'basicauth': basic_auth_cfg} if team_credentials.has_github_oauth_credentials(): github_org, github_team = team_credentials.github_auth_team( split=True) github_cfg = { 'client_id': team_credentials.github_auth_client_id(), 'client_secret': team_credentials.github_auth_client_secret(), 'teams': [{ 'organization_name': github_org, 'team_name': github_team, }], } if team_credentials.has_custom_github_auth_urls(): github_cfg.update({ 'auth_url': team_credentials.github_auth_auth_url(), 'token_url': team_credentials.github_auth_token_url(), 'api_url': team_credentials.github_auth_api_url(), }) if 'auth' in body: body['auth'].update({'github': github_cfg}) else: body['auth'] = {'github': github_cfg} team_url = self.routes.team_url(team_credentials.teamname()) self._put(team_url, json.dumps(body))
class ConcourseApiBase(object): ''' Implements a subset of concourse REST API functionality. After creation, `login` ought to be invoked at least once to allow for the execution of requests that required autorization. @param base_url: concourse endpoint (e.g. https://ci.concourse.ci) @param team_name: the team name used for authentication @param verify_ssl: whether or not certificate validation is to be done ''' @ensure_annotations def __init__( self, routes: ConcourseApiRoutesBase, request_builder: AuthenticatedRequestBuilder, verify_ssl=False, ): self.routes = routes self.request_builder = request_builder self.verify_ssl = verify_ssl @ensure_annotations def _get(self, url: str): return self.request_builder.get(url, return_type='json') @ensure_annotations def _put(self, url: str, body: str, headers={}, use_auth_token=True): return self.request_builder.put(url, body=body, headers=headers) @ensure_annotations def _post(self, url: str, body: str = "", headers={}): return self.request_builder.post(url, body=body, headers=headers) @ensure_annotations def _delete(self, url: str): return self.request_builder.delete(url) def login(self, username: str, passwd: str): login_url = self.routes.login() form_data = "grant_type=password&password="******"&scope=openid+profile+email+federated%3Aid+groups&username="******"content-type": "application/x-www-form-urlencoded"}) auth_token = response.json()['access_token'] self.request_builder = AuthenticatedRequestBuilder( auth_token=auth_token, verify_ssl=self.verify_ssl) return auth_token @ensure_annotations def set_pipeline(self, name: str, pipeline_definition): previous_version = self.pipeline_config_version(name) headers = {'x-concourse-config-version': previous_version} url = self.routes.pipeline_cfg(name) self._put(url, str(pipeline_definition), headers=headers) return SetPipelineResult.CREATED if previous_version is None else SetPipelineResult.UPDATED @ensure_annotations def delete_pipeline(self, name: str): url = self.routes.pipeline(pipeline_name=name) self._delete(url) def pipelines(self): pipelines_url = self.routes.pipelines() response = self._get(pipelines_url) return map(select_attr('name'), response) def order_pipelines(self, pipeline_names): url = self.routes.order_pipelines() self._put(url, json.dumps(pipeline_names)) @ensure_annotations def pipeline_cfg(self, pipeline_name: str): pipeline_cfg_url = self.routes.pipeline_cfg(pipeline_name) response = self._get(pipeline_cfg_url) not_empty(response) return PipelineConfig(response, concourse_api=self, name=pipeline_name) def pipeline_resources( self, pipeline_names: typing.Union[typing.Sequence[str], str], resource_type: typing.Optional[ResourceType] = None, ) -> PipelineConfigResource: if isinstance(pipeline_names, str): pipeline_names = [pipeline_names] resources = map( lambda name: self.pipeline_cfg(pipeline_name=name).resources, pipeline_names) for resource_list in resources: for resource in resource_list: # resource is of type concourse.client.model.PipelineConfigResource if not resource_type or ResourceType( resource.type) is resource_type: yield resource @ensure_annotations def pipeline_config_version(self, pipeline_name: str): pipeline_cfg_url = self.routes.pipeline_cfg(pipeline_name) response = self.request_builder.get(pipeline_cfg_url, return_type=None, check_http_code=False) if response.status_code == 404: return None # pipeline did not exist yet # ensure we did receive an error other than 404 self.request_builder._check_http_code(response, pipeline_cfg_url) return response.headers['X-Concourse-Config-Version'] @ensure_annotations def unpause_pipeline(self, pipeline_name: str): unpause_url = self.routes.unpause_pipeline(pipeline_name) self.request_builder.put(unpause_url, body="") @ensure_annotations def pause_pipeline(self, pipeline_name: str): pause_url = self.routes.pause_pipeline(pipeline_name) self.request_builder.put(pause_url, body="") @ensure_annotations def expose_pipeline(self, pipeline_name: str): expose_url = self.routes.expose_pipeline(pipeline_name) self.request_builder.put( expose_url, body="", ) @ensure_annotations def job_builds(self, pipeline_name: str, job_name: str): ''' Returns a list of Build objects for the specified job. The list is sorted by the build number, newest build last ''' builds_url = self.routes.job_builds(pipeline_name, job_name) response = self._get(builds_url) builds = [Build(build_dict, self) for build_dict in response] builds = sorted(builds, key=lambda b: b.id()) return builds @ensure_annotations def job_build(self, pipeline_name: str, job_name: str, build_name: str): build_url = self.routes.job_build(pipeline_name, job_name, build_name) response = self._get(build_url) return Build(response, self) @ensure_annotations def trigger_build(self, pipeline_name: str, job_name: str): trigger_url = self.routes.job_builds(pipeline_name, job_name) response = self._post(trigger_url) return Build(response.json(), self) @ensure_annotations def build_plan(self, build_id): build_plan_url = self.routes.build_plan(build_id) response = self._get(build_plan_url) return BuildPlan(response, self) @ensure_annotations def build_events(self, build_id): build_plan_url = self.routes.build_events(build_id) # TODO: this request never seems to send an "EOF" # (probably to support streaming) # --> properly handle this special case response = self.request_builder.get( build_plan_url, return_type=None, stream=True # passed to sseclient ) return BuildEvents(response, self) @ensure_annotations def trigger_resource_check(self, pipeline_name: str, resource_name: str, retries: int = 5): url = self.routes.resource_check(pipeline_name=pipeline_name, resource_name=resource_name) # Resource checks are triggered by a POST with an empty JSON-document as body against # the resource's check-url response = self.request_builder.post(url, check_http_code=False, body='{}', headers={}) if response.ok: return if response.status_code == 500 and response.reason.startswith( 'parent type has no version'): if retries > 0: self.trigger_resource_check(pipeline_name, resource_name, retries - 1) else: response.reason = ( f"Unable to check resource '{resource_name}' in pipeline " f"'{pipeline_name}'. If this pipeline was recently deployed, please " "try again in a minute.") response.raise_for_status() else: response.raise_for_status() @ensure_annotations def resource(self, pipeline_name: str, resource_name: str): url = self.routes.resource(pipeline_name, resource_name) response = self._get(url) return PipelineResource(response, self) @ensure_annotations def resource_versions(self, pipeline_name: str, resource_name: str): url = self.routes.resource_versions(pipeline_name=pipeline_name, resource_name=resource_name) response = self._get(url) if not response: return [] # new resources can have no versions return [ ResourceVersion(raw=raw, concourse_api=None) for raw in response ] @ensure_annotations def list_workers(self): url = self.routes.list_workers() workers_list = self._get(url) return [ Worker(raw=worker, concourse_api=None) for worker in workers_list ] @ensure_annotations def prune_worker(self, worker_name: str): url = self.routes.prune_worker(worker_name) self._put(url, "") @ensure_annotations def abort_build(self, build_id): url = self.routes.abort_build(build_id) self._put(url, "") @ensure_annotations def pin_resource_version(self, pipeline_name: str, resource_name: str, resource_version_id: int): url = self.routes.pin_resource_version(pipeline_name, resource_name, resource_version_id) self._put(url, "") @ensure_annotations def pin_and_comment_resource_version(self, pipeline_name: str, resource_name: str, resource_version_id: int, comment: str): self.pin_resource_version(pipeline_name, resource_name, resource_version_id) self.pin_comment(pipeline_name, resource_name, comment) @ensure_annotations def unpin_resource(self, pipeline_name: str, resource_name: str): url = self.routes.unpin_resource(pipeline_name, resource_name) self._put(url, "") @ensure_annotations def pin_comment(self, pipeline_name: str, resource_name: str, comment: str): url = self.routes.pin_comment(pipeline_name, resource_name) pin_comment = {'pin_comment': comment} self._put(url, pin_comment)
class ConcourseApiBase: ''' Implements a subset of concourse REST API functionality. After creation, `login` ought to be invoked at least once to allow for the execution of requests that required autorization. @param base_url: concourse endpoint (e.g. https://ci.concourse.ci) @param team_name: the team name used for authentication @param verify_ssl: whether or not certificate validation is to be done ''' AUTH_TOKEN_ATTRIBUTE_NAME = 'access_token' @ensure_annotations def __init__( self, routes: ConcourseApiRoutesBase, request_builder: AuthenticatedRequestBuilder = None, verify_ssl=False, ): if request_builder: logger.warning('passing-in requests-builder is deprecated') self.routes = routes self.request_builder = None self.verify_ssl = verify_ssl @ensure_annotations def _get(self, url: str): if not self.request_builder: raise ValueError('login first') return self.request_builder.get(url, return_type='json') @ensure_annotations def _put(self, url: str, body: str, headers={}, use_auth_token=True): if not self.request_builder: raise ValueError('login first') return self.request_builder.put(url, body=body, headers=headers) @ensure_annotations def _post(self, url: str, body: str = "", headers={}): if not self.request_builder: raise ValueError('login first') return self.request_builder.post(url, body=body, headers=headers) @ensure_annotations def _delete(self, url: str): if not self.request_builder: raise ValueError('login first') return self.request_builder.delete(url) def login(self, username: str, passwd: str): login_url = self.routes.login() form_data = { 'grant_type': 'password', 'password': passwd, 'scope': 'openid profile email federated:id groups', 'username': username, } response = requests.post( url=login_url, data=form_data, auth=(AUTH_TOKEN_REQUEST_USER, AUTH_TOKEN_REQUEST_PWD), headers={'content-type': 'application/x-www-form-urlencoded'}) response.raise_for_status() auth_token = response.json()[self.AUTH_TOKEN_ATTRIBUTE_NAME] self.request_builder = AuthenticatedRequestBuilder( auth_token=auth_token, verify_ssl=self.verify_ssl) return auth_token @ensure_annotations def set_pipeline(self, name: str, pipeline_definition): previous_version = self.pipeline_config_version(name) headers = {'x-concourse-config-version': previous_version} url = self.routes.pipeline_cfg(name) self._put(url, str(pipeline_definition), headers=headers) return SetPipelineResult.CREATED if previous_version is None else SetPipelineResult.UPDATED @ensure_annotations def delete_pipeline(self, name: str): url = self.routes.pipeline(pipeline_name=name) self._delete(url) def pipelines(self): pipelines_url = self.routes.pipelines() response = self._get(pipelines_url) return map(select_attr('name'), response) def order_pipelines(self, pipeline_names): url = self.routes.order_pipelines() self._put(url, json.dumps(pipeline_names)) @ensure_annotations def pipeline_cfg(self, pipeline_name: str): pipeline_cfg_url = self.routes.pipeline_cfg(pipeline_name) response = self._get(pipeline_cfg_url) return PipelineConfig(response, concourse_api=self, name=pipeline_name) def pipeline_resources( self, pipeline_names: typing.Union[typing.Sequence[str], str], resource_type: typing.Optional[ResourceType] = None, ) -> typing.Generator[PipelineConfigResource, None, None]: if isinstance(pipeline_names, str): pipeline_names = [pipeline_names] resources = (self.pipeline_cfg(pipeline_name=name).resources for name in pipeline_names) for resource_list in resources: for resource in resource_list: resource: PipelineConfigResource if not resource_type or ResourceType( resource.type) is resource_type: yield resource @ensure_annotations def pipeline_config_version(self, pipeline_name: str): pipeline_cfg_url = self.routes.pipeline_cfg(pipeline_name) response = self.request_builder.get(pipeline_cfg_url, return_type=None, check_http_code=False) if response.status_code == 404: return None # pipeline did not exist yet # ensure we did receive an error other than 404 self.request_builder._check_http_code(response, pipeline_cfg_url) return response.headers['X-Concourse-Config-Version'] @ensure_annotations def unpause_pipeline(self, pipeline_name: str): unpause_url = self.routes.unpause_pipeline(pipeline_name) self.request_builder.put(unpause_url, body="") @ensure_annotations def pause_pipeline(self, pipeline_name: str): pause_url = self.routes.pause_pipeline(pipeline_name) self.request_builder.put(pause_url, body="") @ensure_annotations def expose_pipeline(self, pipeline_name: str): expose_url = self.routes.expose_pipeline(pipeline_name) self.request_builder.put( expose_url, body="", ) @ensure_annotations def job_builds(self, pipeline_name: str, job_name: str): ''' Returns a list of Build objects for the specified job. The list is sorted by the build number, newest build last ''' builds_url = self.routes.job_builds(pipeline_name, job_name) response = self._get(builds_url) builds = [Build(build_dict, self) for build_dict in response] builds = sorted(builds, key=lambda b: b.id()) return builds @ensure_annotations def job_build(self, pipeline_name: str, job_name: str, build_name: str): build_url = self.routes.job_build(pipeline_name, job_name, build_name) response = self._get(build_url) return Build(response, self) @ensure_annotations def pause_job(self, pipeline_name: str, job_name: str): pause_url = self.routes.pause_job(pipeline_name, job_name) self.request_builder.put(pause_url, body="") @ensure_annotations def unpause_job(self, pipeline_name: str, job_name: str): unpause_url = self.routes.unpause_job(pipeline_name, job_name) self.request_builder.put(unpause_url, body="") @ensure_annotations def trigger_build(self, pipeline_name: str, job_name: str): trigger_url = self.routes.job_builds(pipeline_name, job_name) response = self._post(trigger_url) return Build(response.json(), self) @ensure_annotations def build_plan(self, build_id): build_plan_url = self.routes.build_plan(build_id) response = self._get(build_plan_url) return BuildPlan(response, self) @ensure_annotations def build_events(self, build_id): build_plan_url = self.routes.build_events(build_id) # TODO: this request never seems to send an "EOF" # (probably to support streaming) # --> properly handle this special case response = self.request_builder.get( build_plan_url, return_type=None, stream=True # passed to sseclient ) return BuildEvents(response, self) @ensure_annotations def trigger_resource_check(self, pipeline_name: str, resource_name: str, retries: int = 5): url = self.routes.resource_check(pipeline_name=pipeline_name, resource_name=resource_name) # Resource checks are triggered by a POST with an empty JSON-document as body against # the resource's check-url response = self.request_builder.post(url, check_http_code=False, body='{}', headers={}) if response.ok: return if response.status_code == 500 and response.reason.startswith( 'parent type has no version'): if retries > 0: self.trigger_resource_check(pipeline_name, resource_name, retries - 1) else: response.reason = ( f"Unable to check resource '{resource_name}' in pipeline " f"'{pipeline_name}'. If this pipeline was recently deployed, please " "try again in a minute.") response.raise_for_status() else: response.raise_for_status() @ensure_annotations def resource(self, pipeline_name: str, resource_name: str): url = self.routes.resource(pipeline_name, resource_name) response = self._get(url) return PipelineResource(response, self) @ensure_annotations def resource_versions(self, pipeline_name: str, resource_name: str): url = self.routes.resource_versions(pipeline_name=pipeline_name, resource_name=resource_name) response = self._get(url) if not response: return [] # new resources can have no versions return [ ResourceVersion(raw=raw, concourse_api=None) for raw in response ] @ensure_annotations def list_workers(self): url = self.routes.list_workers() workers_list = self._get(url) return [ Worker(raw=worker, concourse_api=None) for worker in workers_list ] @ensure_annotations def prune_worker(self, worker_name: str): url = self.routes.prune_worker(worker_name) self._put(url, "") @ensure_annotations def abort_build(self, build_id): url = self.routes.abort_build(build_id) self._put(url, "") @ensure_annotations def pin_resource_version(self, pipeline_name: str, resource_name: str, resource_version_id: int): url = self.routes.pin_resource_version(pipeline_name, resource_name, resource_version_id) self._put(url, "") @ensure_annotations def pin_and_comment_resource_version(self, pipeline_name: str, resource_name: str, resource_version_id: int, comment: str): self.pin_resource_version(pipeline_name, resource_name, resource_version_id) self.pin_comment(pipeline_name, resource_name, comment) @ensure_annotations def unpin_resource(self, pipeline_name: str, resource_name: str) -> bool: resource = self.resource(pipeline_name, resource_name) if resource.is_pinned(): url = self.routes.unpin_resource(pipeline_name, resource_name) self._put(url, "") return True return False @ensure_annotations def pin_comment(self, pipeline_name: str, resource_name: str, comment: str): url = self.routes.pin_comment(pipeline_name, resource_name) pin_comment = json.dumps({'pin_comment': comment}) self._put(url, pin_comment) def get_job(self, pipeline_name: str, job_name: str): url = self.routes.job(pipeline_name, job_name) return ConcourseJob(self._get(url)) def set_team( self, team_name: str, username: str, role: str = 'member', github_auth_team: str = None, ): body = { "auth": { role: { "users": [ "local:" + username, ] } } } if github_auth_team: body["auth"][role].update( {"groups": [ "github:" + github_auth_team, ]}) team_url = self.routes.team_url(team_name) self._put(team_url, json.dumps(body))