Esempio n. 1
0
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))
Esempio n. 2
0
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)
Esempio n. 3
0
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))