diff --git a/service-catalog/product-template.yaml b/service-catalog/product-template.yaml index a61c311..1d74856 100644 --- a/service-catalog/product-template.yaml +++ b/service-catalog/product-template.yaml @@ -36,6 +36,7 @@ Metadata: - Label: default: "Optional Metadata" Parameters: + - CreatorUsername - AdditionalTags ParameterLabels: @@ -67,6 +68,8 @@ Metadata: default: "FinOps Project Name" FinOpsProjectNumber: default: "FinOps Project Number" + CreatorUsername: + default: "Your GitHub Username (for admin access)" AdditionalTags: default: "Additional Tags (JSON)" @@ -161,6 +164,13 @@ Parameters: Description: FinOps project number Default: "" + CreatorUsername: + Type: String + Description: >- + Your GitHub Enterprise username. If provided, you will be granted admin + access to the created repository in addition to the owning team. + Default: "" + AdditionalTags: Type: String Description: 'Additional tags as JSON object (e.g., {"key1":"value1"})' @@ -201,6 +211,7 @@ Resources: organization_path: !Ref OrganizationPath finops_project_name: !Ref FinOpsProjectName finops_project_number: !Ref FinOpsProjectNumber + creator_username: !Ref CreatorUsername tags: !Ref AdditionalTags Outputs: diff --git a/template_automation/app.py b/template_automation/app.py index 2df5d37..bbac382 100644 --- a/template_automation/app.py +++ b/template_automation/app.py @@ -43,6 +43,7 @@ class CloudFormationResourceInput(BaseModel): """Input validation model for CloudFormation Custom Resource parameters.""" project_name: str = Field(..., description="Name for the new repository") owning_team: Optional[str] = Field(default="tf-module-admins", description="Team that should own the repository") + creator_username: Optional[str] = Field(default=None, description="GitHub username of the person provisioning this repo; will be granted admin access") # EKS-specific fields (present when this is an EKS cluster deployment) cluster_name: Optional[str] = Field(default=None, description="EKS cluster name") @@ -606,6 +607,20 @@ def lambda_handler(event: dict, context) -> dict: logger.info(f"[{request_id}] Continuing despite team permission error") else: logger.info(f"[{request_id}] Skipping team assignment (no owning_team or not GitHub provider)") + + # Add the individual creator as admin collaborator (GitHub only) + if cfn_input.creator_username and provider_type == "GitHubProvider": + logger.info(f"[{request_id}] Adding creator '{cfn_input.creator_username}' as admin collaborator") + try: + provider.add_collaborator(cfn_input.project_name, cfn_input.creator_username, permission="admin") + logger.info(f"[{request_id}] Creator '{cfn_input.creator_username}' added as admin collaborator") + except Exception as e: + logger.error(f"[{request_id}] Failed to add creator as collaborator: {str(e)}") + logger.error(f"[{request_id}] Full traceback: {traceback.format_exc()}") + # Non-fatal: repo and team perms are already set + logger.info(f"[{request_id}] Continuing despite creator collaborator error") + else: + logger.info(f"[{request_id}] Skipping creator collaborator (no creator_username provided or not GitHub provider)") # Give newly created repositories a moment to initialize if project.get('created_at'): diff --git a/template_automation/github_provider.py b/template_automation/github_provider.py index dec544d..b32d22b 100644 --- a/template_automation/github_provider.py +++ b/template_automation/github_provider.py @@ -159,10 +159,10 @@ def get_repository( settings = RepositorySettings() # Set up repository creation data. - # NOTE: 'private' must be False for 'internal' visibility; GHE treats + # NOTE: 'private' must be False for 'internal'/'public' visibility; GHE treats # private=True as a private-repo request and will 403 if the enterprise # policy blocks private repo creation for org members. - effective_visibility = settings.visibility or 'internal' + effective_visibility = settings.visibility or 'public' create_data = { 'name': name, 'private': effective_visibility == 'private', @@ -678,6 +678,47 @@ def get_repository_url(self, repo_name: str) -> str: web_base = web_base[:-len('/api/v3')] return f"{web_base}/{self.organization}/{repo_name}" + def add_collaborator( + self, + repo_name: str, + username: str, + permission: str = "admin" + ) -> Dict[str, Any]: + """Add an individual user as a collaborator on a repository. + + Args: + repo_name: Repository name + username: GitHub username to add + permission: Permission level (pull, push, maintain, triage, admin) + + Returns: + Response data (empty dict if invite was accepted immediately) + """ + logger.info(f"Adding user '{username}' as {permission} collaborator on repository {repo_name}") + try: + result = self._request( + 'PUT', + f'/repos/{self.organization}/{repo_name}/collaborators/{username}', + json={'permission': permission} + ) + logger.info(f"User '{username}' added as {permission} collaborator on {repo_name}") + return result or {} + except requests.exceptions.RequestException as e: + logger.error(f"Failed to add collaborator '{username}': {str(e)}") + if hasattr(e, 'response') and e.response is not None: + status_code = e.response.status_code + if status_code == 404: + logger.error(f"User '{username}' not found on GitHub Enterprise") + elif status_code == 403: + logger.error("Insufficient permissions to add collaborator") + else: + logger.error(f"Error adding collaborator: status code {status_code}") + try: + logger.error(f"Error details: {json.dumps(e.response.json())}") + except Exception: + logger.error(f"Error response: {e.response.text}") + return {} + def set_team_permission( self, repo_name: str, diff --git a/template_automation/repository_provider.py b/template_automation/repository_provider.py index 1b827c2..4153f39 100644 --- a/template_automation/repository_provider.py +++ b/template_automation/repository_provider.py @@ -10,14 +10,15 @@ def _default_visibility() -> str: - """Read REPO_VISIBILITY env var, defaulting to 'internal'. + """Read REPO_VISIBILITY env var, defaulting to 'public'. - GHE enterprise policy commonly blocks private repo creation for org members. - 'internal' repos are visible to all org members but not to the public, - which is the recommended default for government/enterprise GHE instances. + Repos created by the EKS automation are public within the GitHub Enterprise + organization so all team members can clone and browse them without extra + permission grants. Set REPO_VISIBILITY=internal or REPO_VISIBILITY=private + to override if needed. Accepted values: private | internal | public """ - return os.environ.get("REPO_VISIBILITY", "internal") + return os.environ.get("REPO_VISIBILITY", "public") class RepositorySettings(BaseModel):