Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 2 additions & 37 deletions docs/constructs/cicd.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,50 +91,15 @@ Rnabled by setting `pEnableLambdaLayerBuilder` to `true` when deploying `templat

### GitLab

- Create a dedicated user on GitLab. Currently the user must be named: `sdlf`.
- Create an access token with the `sdlf` user. The token name must be named `aws`. Permissions must be `api` and `write_repository`.
- Create [CodeConnections](https://docs.aws.amazon.com/codepipeline/latest/userguide/connections-gitlab-managed.html) for the self-managed GitLab instance
The creation of GitLab repositories will be performed through the GitLab API.

Populate:

- `/SDLF/GitLab/Url` :: secure-string :: GitLab URL **with** trailing `/`
- `/SDLF/GitLab/AccessToken` :: secure-string :: User access token
- `/SDLF/GitLab/NamespaceId` :: secure-string :: User/Enterprise namespace ID
- `/SDLF/GitLab/CodeConnection` :: string :: CodeConnections ARN

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this now be removed?

Copy link
Contributor Author

@dlpzx dlpzx Sep 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You still need to connection to the codepipeline. I did not remove it to ensure the pipeline can read the gitlab repo as source (search for "{{resolve:ssm:/SDLF/${pGitPlatform}/CodeConnection}}" in the code)


Create CloudFormation role:

```
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "resources.cloudformation.amazonaws.com"
},
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": {
"aws:SourceAccount": "111111111111"
}
}
]
}
```

Enable `GitLab::Projects::Project` third-party resource type in CloudFormation Registry.

Add configuration (use of ssm-secure is mandatory):

```
{
"GitLabAccess": {
"AccessToken": "{{resolve:ssm-secure:/SDLF/GitLab/AccessToken:1}}",
"Url": "{{resolve:ssm-secure:/SDLF/GitLab/Url:1}}"
}
}
```

## Interface

There is no external interface.
100 changes: 9 additions & 91 deletions sdlf-cicd/lambda/domain-cicd/src/lambda_function.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import boto3
from botocore.client import Config
from botocore.exceptions import ClientError
from repository_manager import create_repositories

logger = logging.getLogger()
logger.setLevel(logging.INFO)
Expand Down Expand Up @@ -117,52 +118,6 @@ def delete_domain_team_role_stack(cloudformation, team):
return (stack_name, "stack_delete_complete")


def create_team_repository_cicd_stack(domain, team_name, template_body_url, cloudformation_role):
response = {}
cloudformation_waiter_type = None
stack_name = f"sdlf-cicd-teams-{domain}-{team_name}-repository"
stack_parameters = [
{
"ParameterKey": "pDomain",
"ParameterValue": domain,
"UsePreviousValue": False,
},
{
"ParameterKey": "pTeamName",
"ParameterValue": team_name,
"UsePreviousValue": False,
},
]
stack_arguments = dict(
StackName=stack_name,
TemplateURL=template_body_url,
Parameters=stack_parameters,
Capabilities=[
"CAPABILITY_AUTO_EXPAND",
],
RoleARN=cloudformation_role,
Tags=[
{"Key": "Framework", "Value": "sdlf"},
],
)

try:
response = cloudformation.create_stack(**stack_arguments)
cloudformation_waiter_type = "stack_create_complete"
except cloudformation.exceptions.AlreadyExistsException:
try:
response = cloudformation.update_stack(**stack_arguments)
cloudformation_waiter_type = "stack_update_complete"
except ClientError as err:
if "No updates are to be performed" in err.response["Error"]["Message"]:
pass
else:
raise err

logger.info("RESPONSE: %s", response)
return (stack_name, cloudformation_waiter_type)


def create_team_pipeline_cicd_stack(
domain,
environment,
Expand Down Expand Up @@ -467,51 +422,14 @@ def lambda_handler(event, context):
###### CREATE STACKS FOR TEAMS ######
for domain, domain_details in domains.items():
# create team repository if it hasn't been created already
cloudformation_waiters = {
"stack_create_complete": [],
"stack_update_complete": [],
}
for team in domain_details["teams"]:
stack_details = create_team_repository_cicd_stack(
domain,
team,
template_cicd_team_repository_url,
cloudformation_role,
)
if stack_details[1]:
cloudformation_waiters[stack_details[1]].append(stack_details[0])
cloudformation_create_waiter = cloudformation.get_waiter("stack_create_complete")
cloudformation_update_waiter = cloudformation.get_waiter("stack_update_complete")
for stack in cloudformation_waiters["stack_create_complete"]:
cloudformation_create_waiter.wait(StackName=stack, WaiterConfig={"Delay": 30, "MaxAttempts": 10})
for stack in cloudformation_waiters["stack_update_complete"]:
cloudformation_update_waiter.wait(StackName=stack, WaiterConfig={"Delay": 30, "MaxAttempts": 10})

if git_platform == "CodeCommit":
for team in domain_details["teams"]:
repository_name = f"{main_repository_prefix}{domain}-{team}"
env_branches = ["dev", "test"]
for env_branch in env_branches:
try:
codecommit.create_branch(
repositoryName=repository_name,
branchName=env_branch,
commitId=codecommit.get_branch(
repositoryName=repository_name,
branchName="main",
)["branch"]["commitId"],
)
logger.info(
"Branch %s created in repository %s",
env_branch,
repository_name,
)
except codecommit.exceptions.BranchNameExistsException:
logger.info(
"Branch %s already created in repository %s",
env_branch,
repository_name,
)
create_repositories(
git_platform,
domain_details,
domain,
template_cicd_team_repository_url,
cloudformation_role,
main_repository_prefix,
)

# and create a CICD stack per team that will be used to deploy team resources in the child account
cloudformation_waiters = {
Expand Down
219 changes: 219 additions & 0 deletions sdlf-cicd/lambda/domain-cicd/src/repository_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
import json
import logging
import os
import ssl
from urllib.request import HTTPError, Request, URLError, urlopen

import boto3
from botocore.exceptions import ClientError

logger = logging.getLogger()

ssm_endpoint_url = "https://ssm." + os.getenv("AWS_REGION") + ".amazonaws.com"
ssm = boto3.client("ssm", endpoint_url=ssm_endpoint_url)
codecommit_endpoint_url = "https://codecommit." + os.getenv("AWS_REGION") + ".amazonaws.com"
codecommit = boto3.client("codecommit", endpoint_url=codecommit_endpoint_url)
cloudformation_endpoint_url = "https://cloudformation." + os.getenv("AWS_REGION") + ".amazonaws.com"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit - is this needed? wouldn't the default endpoint be the same?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using the same way of defining endpoints are before, you can check sdlf-cicd/lambda/domain-cicd/src/lambda_function.py

cloudformation = boto3.client("cloudformation", endpoint_url=cloudformation_endpoint_url)


def _create_team_repository_cicd_stack(domain, team_name, template_body_url, cloudformation_role):
response = {}
cloudformation_waiter_type = None
stack_name = f"sdlf-cicd-teams-{domain}-{team_name}-repository"
stack_parameters = [
{
"ParameterKey": "pDomain",
"ParameterValue": domain,
"UsePreviousValue": False,
},
{
"ParameterKey": "pTeamName",
"ParameterValue": team_name,
"UsePreviousValue": False,
},
]
stack_arguments = dict(
StackName=stack_name,
TemplateURL=template_body_url,
Parameters=stack_parameters,
Capabilities=[
"CAPABILITY_AUTO_EXPAND",
],
RoleARN=cloudformation_role,
Tags=[
{"Key": "Framework", "Value": "sdlf"},
],
)

try:
response = cloudformation.create_stack(**stack_arguments)
cloudformation_waiter_type = "stack_create_complete"
except cloudformation.exceptions.AlreadyExistsException:
try:
response = cloudformation.update_stack(**stack_arguments)
cloudformation_waiter_type = "stack_update_complete"
except ClientError as err:
if "No updates are to be performed" in err.response["Error"]["Message"]:
pass
else:
raise err

logger.info("RESPONSE: %s", response)
return (stack_name, cloudformation_waiter_type)


def _create_codecommit_repositories(
domain_details, domain, template_cicd_team_repository_url, cloudformation_role, main_repository_prefix
):
"""Create CodeCommit repositories and branches for teams"""
cloudformation_waiters = {
"stack_create_complete": [],
"stack_update_complete": [],
}
for team in domain_details["teams"]:
stack_details = _create_team_repository_cicd_stack(
domain,
team,
template_cicd_team_repository_url,
cloudformation_role,
)
if stack_details[1]:
cloudformation_waiters[stack_details[1]].append(stack_details[0])

cloudformation_create_waiter = cloudformation.get_waiter("stack_create_complete")
cloudformation_update_waiter = cloudformation.get_waiter("stack_update_complete")
for stack in cloudformation_waiters["stack_create_complete"]:
cloudformation_create_waiter.wait(StackName=stack, WaiterConfig={"Delay": 30, "MaxAttempts": 10})
for stack in cloudformation_waiters["stack_update_complete"]:
cloudformation_update_waiter.wait(StackName=stack, WaiterConfig={"Delay": 30, "MaxAttempts": 10})

# Create branches for each team repository
for team in domain_details["teams"]:
repository_name = f"{main_repository_prefix}{domain}-{team}"
env_branches = ["dev", "test"]
for env_branch in env_branches:
try:
codecommit.create_branch(
repositoryName=repository_name,
branchName=env_branch,
commitId=codecommit.get_branch(
repositoryName=repository_name,
branchName="main",
)["branch"]["commitId"],
)
logger.info(
"Branch %s created in repository %s",
env_branch,
repository_name,
)
except codecommit.exceptions.BranchNameExistsException:
logger.info(
"Branch %s already created in repository %s",
env_branch,
repository_name,
)


def _create_gitlab_repositories(domain_details, domain, template_cicd_team_repository_url, cloudformation_role):
"""Create GitLab repositories for teams"""
for team in domain_details["teams"]:
# Create GitLab repository via API
# !Sub ${pMainRepositoriesPrefix}${pDomain}-${pTeamName}
repository = f"sdlf-main-{domain}-{team}"
gitlab_url = ssm.get_parameter(Name="/SDLF/GitLab/Url", WithDecryption=True)["Parameter"]["Value"]
gitlab_accesstoken = ssm.get_parameter(Name="/SDLF/GitLab/AccessToken", WithDecryption=True)["Parameter"][
"Value"
]
namespace_id = ssm.get_parameter(Name="/SDLF/GitLab/NamespaceId", WithDecryption=True)["Parameter"]["Value"]

url = f"{gitlab_url}api/v4/projects/"
headers = {"Content-Type": "application/json", "PRIVATE-TOKEN": gitlab_accesstoken}
data = {
"name": repository,
"description": repository,
"path": repository,
"namespace_id": namespace_id,
"initialize_with_readme": "false",
}
json_data = json.dumps(data).encode("utf-8")
req = Request(url, data=json_data, headers=headers, method="POST")
unverified_context = ssl._create_unverified_context()
try:
with urlopen(req, context=unverified_context) as response:
response_body = response.read().decode("utf-8")
logger.info(response_body)
except HTTPError as e:
logger.warning(
f"HTTP error occurred: {e.code} {e.reason}. Most likely the repository {repository} already exists"
)
except URLError as e:
logger.error(f"URL error occurred: {e.reason}")

# Create CloudFormation stacks for GitLab repositories
cloudformation_waiters = {
"stack_create_complete": [],
"stack_update_complete": [],
}
for team in domain_details["teams"]:
stack_details = _create_team_repository_cicd_stack(
domain,
team,
template_cicd_team_repository_url,
cloudformation_role,
)
if stack_details[1]:
cloudformation_waiters[stack_details[1]].append(stack_details[0])

cloudformation_create_waiter = cloudformation.get_waiter("stack_create_complete")
cloudformation_update_waiter = cloudformation.get_waiter("stack_update_complete")
for stack in cloudformation_waiters["stack_create_complete"]:
cloudformation_create_waiter.wait(StackName=stack, WaiterConfig={"Delay": 30, "MaxAttempts": 10})
for stack in cloudformation_waiters["stack_update_complete"]:
cloudformation_update_waiter.wait(StackName=stack, WaiterConfig={"Delay": 30, "MaxAttempts": 10})


def _create_github_repositories(domain_details, domain, template_cicd_team_repository_url, cloudformation_role):
"""Create GitHub repositories for teams"""
# GitHub repositories are created via CloudFormation template
cloudformation_waiters = {
"stack_create_complete": [],
"stack_update_complete": [],
}
for team in domain_details["teams"]:
stack_details = _create_team_repository_cicd_stack(
domain,
team,
template_cicd_team_repository_url,
cloudformation_role,
)
if stack_details[1]:
cloudformation_waiters[stack_details[1]].append(stack_details[0])

cloudformation_create_waiter = cloudformation.get_waiter("stack_create_complete")
cloudformation_update_waiter = cloudformation.get_waiter("stack_update_complete")
for stack in cloudformation_waiters["stack_create_complete"]:
cloudformation_create_waiter.wait(StackName=stack, WaiterConfig={"Delay": 30, "MaxAttempts": 10})
for stack in cloudformation_waiters["stack_update_complete"]:
cloudformation_update_waiter.wait(StackName=stack, WaiterConfig={"Delay": 30, "MaxAttempts": 10})


def create_repositories(
git_platform,
domain_details,
domain,
template_cicd_team_repository_url,
cloudformation_role,
main_repository_prefix=None,
):
"""Create team repositories based on git platform"""
if git_platform == "CodeCommit":
_create_codecommit_repositories(
domain_details, domain, template_cicd_team_repository_url, cloudformation_role, main_repository_prefix
)
elif git_platform == "GitHub":
_create_github_repositories(domain_details, domain, template_cicd_team_repository_url, cloudformation_role)
elif git_platform == "GitLab":
_create_gitlab_repositories(domain_details, domain, template_cicd_team_repository_url, cloudformation_role)
else:
raise logging.exception("Git provider {} is not supported".format(git_platform))
Loading
Loading