Skip to content

Instantly share code, notes, and snippets.

@jeshan
Last active May 17, 2020 20:44
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jeshan/3b8eb9d3feb014006674147e99382c26 to your computer and use it in GitHub Desktop.
Save jeshan/3b8eb9d3feb014006674147e99382c26 to your computer and use it in GitHub Desktop.
cfnbuddy requirements template
AWSTemplateFormatVersion: "2010-09-09"
Description: resources required for clients to use cfnbuddy
Metadata:
AWS::CloudFormation::Interface:
ParameterGroups:
- Label: GitHub/GitLab
Parameters:
- GitIntegrationType
- Namespace
- GitRepositoryName
- PersonalAccessToken
- Label: GitHub
Parameters:
- GitHubDomain
- Label: GitLab
Parameters:
- GitLabDomain
- Label: CfnBuddySettings
Parameters:
- AllowViewBudgets
- ImageBackground
ParameterLabels:
ImageBackground:
default: White or Transparent background for images?
Namespace:
default: GitHub or GitLab namespace under which to create the repository
GitHubDomain:
default: GitHub domain
GitIntegrationType:
default: Your Git Provider
GitLabDomain:
default: GitLab domain
AllowViewBudgets:
default: Allow Viewing Budgets?
GitRepositoryName:
default: Git Repository Name
PersonalAccessToken:
default: Personal Access Token
Parameters:
GitIntegrationType:
Type: String
AllowedValues: [CodeCommit, GitHub, GitLab]
Default: CodeCommit
Description: Which integration to use
Namespace:
Type: String
Default: ''
Description: |
For GitHub: it's either your personal or organization name.
For GitLab: it's either your username or your group name
Otherwise, leave empty.
PersonalAccessToken:
Type: String
Default: ''
Description: 'GitHub or GitLab: the access token that has permissions to clone and create the repository.'
NoEcho: true
GitHubDomain:
Type: String
Description: 'The domain where your GitHub instance is hosted, format: hub.example.com. Leave default if not self-hosting'
Default: github.com
GitLabDomain:
Type: String
Description: 'The domain where your GitLab instance is hosted, format: lab.example.com. Leave default if not self-hosting'
Default: gitlab.com
GitRepositoryName:
Type: String
Default: ''
Description: 'Name of the repository to be created/used by cfnbuddy. Default: cfnbuddy-YOUR_ACCOUNT_ID'
AllowViewBudgets:
Type: String
AllowedValues: ['Yes', 'No']
Default: 'No'
Description: Needed to create the AWS::Budgets::Budget resources (optional because the api is not free and is estimated to cost $7.20 per month)
ImageBackground:
Type: String
AllowedValues: [White, Transparent]
Default: White
Description: Images can be placed on either a white or a transparent background (White is just fine; Changeable at any time)
Conditions:
isGitHub: !Equals [!Ref GitIntegrationType, GitHub]
isGitLab: !Equals [!Ref GitIntegrationType, GitLab]
isNeedSync: !Or [!Condition isGitHub, !Condition isGitLab]
isAllowViewBudgets: !Equals [!Ref AllowViewBudgets, Yes]
hasGitRepositoryName: !Not [!Equals [!Ref GitRepositoryName, '']]
Resources:
Role:
Type: AWS::IAM::Role
Properties:
RoleName: cfnbuddy-client
AssumeRolePolicyDocument:
Statement:
- Action: sts:AssumeRole
Effect: Allow
Principal:
AWS: '880876798629'
ManagedPolicyArns:
- arn:aws:iam::aws:policy/ReadOnlyAccess
- !Ref AcceptPrPolicy
- !Ref GitCredentialsPolicy
- !Ref DenyUnnecessaryPermissionsPolicy
- !If [isNeedSync, !Ref SyncPolicy, !Ref 'AWS::NoValue']
- !If [isAllowViewBudgets, !Ref ViewBudgetPolicy, !Ref 'AWS::NoValue']
AcceptPrPolicy:
Type: AWS::IAM::ManagedPolicy
Properties:
PolicyDocument:
Statement:
- Effect: Allow
Action:
- codecommit:MergePullRequestByFastForward # we need this to merge CodeCommit pull requests into master
Resource: !Sub '${Repo.Arn}'
Version: '2012-10-17'
GitCredentialsPolicy: # we need this to access the new CodeCommit repository
Type: AWS::IAM::ManagedPolicy
Properties:
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- iam:CreateServiceSpecificCredential
- iam:ResetServiceSpecificCredential
- iam:ListServiceSpecificCredentials
Resource: !GetAtt CodeCommitUser.Arn
- Effect: Allow
Action:
- codecommit:CreatePullRequest
- codecommit:GitPull
Resource: !Sub '${Repo.Arn}'
- Effect: Allow
Action:
- codecommit:GitPush
Resource: !Sub '${Repo.Arn}'
Condition:
StringEqualsIfExists:
codecommit:References: [refs/heads/master]
"Null":
codecommit:References: false
SyncPolicy: # needed to launch the GitHub/GitLab sync job
Type: AWS::IAM::ManagedPolicy
Condition: isNeedSync
Properties:
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- codebuild:StartBuild
Resource: !If [isGitHub, !GetAtt GitHubSyncJob.Arn, !If [isGitLab, !GetAtt GitLabSyncJob.Arn, !Ref 'AWS::NoValue']]
DenyUnnecessaryPermissionsPolicy:
Type: AWS::IAM::ManagedPolicy
Properties:
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Deny
Action:
- dynamodb:GetItem
- dynamodb:BatchGetItem
- dynamodb:Query
- dynamodb:Scan
- ec2:GetConsoleOutput
- ec2:GetConsoleScreenshot
- ecr:BatchGetImage
- ecr:GetAuthorizationToken
- ecr:GetDownloadUrlForLayer
- kinesis:Get*
- lambda:GetFunction
- logs:GetLogEvents
- s3:GetObject
- sdb:Select*
- sqs:ReceiveMessage
Resource: '*'
Repo:
Type: AWS::CodeCommit::Repository
DeletionPolicy: Retain
UpdateReplacePolicy: Retain
Properties:
RepositoryName: !If [hasGitRepositoryName, !Ref GitRepositoryName, !Sub 'cfnbuddy-${AWS::AccountId}']
GitHubSyncJob:
Type: AWS::CodeBuild::Project
Condition: isGitHub
Properties:
Artifacts:
Type: NO_ARTIFACTS
Description: Syncs cfnbuddy CodeCommit repo to your GitHub repo
Environment:
ComputeType: BUILD_GENERAL1_SMALL
EnvironmentVariables:
- Name: ACCOUNT_ID
Value: !Ref AWS::AccountId
- Name: BASE_DOMAIN
Value: !Ref GitHubDomain
- Name: REPOSITORY_NAME
Value: !GetAtt Repo.Name
- Name: NAMESPACE
Value: !Ref Namespace
- Name: PERSONAL_ACCESS_TOKEN
Type: SECRETS_MANAGER
Value: !Ref PersonalAccessTokenSecret
Image: aws/codebuild/standard:3.0
Type: LINUX_CONTAINER
QueuedTimeoutInMinutes: 50
ServiceRole: !GetAtt SyncJobRole.Arn
Source:
BuildSpec: |
version: 0.2
phases:
install:
commands:
- pip3 install requests
runtime-versions:
python: 3.8
build:
commands:
- |
cat <<EOF > /tmp/index.py
import os
from subprocess import call
from urllib.parse import quote_plus
import requests
repository_name = os.environ['REPOSITORY_NAME']
namespace = os.environ['NAMESPACE']
account_id = os.environ['ACCOUNT_ID']
token = os.environ['PERSONAL_ACCESS_TOKEN']
region = os.environ['AWS_REGION']
base_domain = os.environ['BASE_DOMAIN']
api_path = f'https://api.{base_domain}' if base_domain == 'github.com' else f'https://{base_domain}/api'
base_branch = 'master'
headers = {'Authorization': f'token {token}', 'Accept': 'application/vnd.github.v3+json'}
def main(tag_name, release_description):
create_repository()
qtoken = quote_plus(token)
run(f'git remote add github https://{namespace}:{qtoken}@{base_domain}/{namespace}/{repository_name}.git')
run('git push -q github --all')
run('git push -q github --tags')
create_release(tag_name, release_description)
def run(command):
code = call(command.split(' '))
if code != 0:
raise Exception('failed to run command')
def create_repository():
body = {
'name': repository_name,
'description': f'Automatically created by cfnbuddy for AWS account {account_id}',
'private': True,
}
is_org = requests.get(f'{api_path}/orgs/{namespace}').status_code == 200
if is_org:
print('Creating repo in org...')
response = requests.post(f'{api_path}/orgs/{namespace}/repos', json=body, headers=headers)
else:
print("Creating repo in user's personal space")
response = requests.post(f'{api_path}/user/repos', json=body, headers=headers)
requests.patch(f'{api_path}/repos/{namespace}/{repository_name}', json={'default_branch': base_branch}, headers=headers)
def create_release(tag_name, body):
print(f'Releasing {tag_name}')
response = requests.post(
f'{api_path}/repos/{namespace}/{repository_name}/releases',
json={'name': tag_name, 'tag_name': tag_name, 'target_commitish': base_branch, 'body': body},
headers=headers,
).json()
print(response)
if __name__ == '__main__':
main(os.environ['TAG_NAME'], os.environ['RELEASE_DESCRIPTION'])
EOF
- "for i in `git branch -a | grep remote | grep -v HEAD | grep -v master`; do git branch -q --track ${i#remotes/origin/} $i; done" # track all branches
- python /tmp/index.py
Location: !GetAtt Repo.CloneUrlHttp
Type: CODECOMMIT
LogsConfig:
CloudWatchLogs:
GroupName: !Ref SyncLogs
Status: ENABLED
SourceVersion: master
GitLabSyncJob:
Type: AWS::CodeBuild::Project
Condition: isGitLab
Properties:
Artifacts:
Type: NO_ARTIFACTS
Description: Syncs cfnbuddy CodeCommit repo to your GitLab repo
Environment:
ComputeType: BUILD_GENERAL1_SMALL
EnvironmentVariables:
- Name: ACCOUNT_ID
Value: !Ref AWS::AccountId
- Name: BASE_DOMAIN
Value: !Ref GitLabDomain
- Name: REPOSITORY_NAME
Value: !GetAtt Repo.Name
- Name: NAMESPACE
Value: !Ref Namespace
- Name: PERSONAL_ACCESS_TOKEN
Type: SECRETS_MANAGER
Value: !Ref PersonalAccessTokenSecret
Image: aws/codebuild/standard:3.0
Type: LINUX_CONTAINER
QueuedTimeoutInMinutes: 50
ServiceRole: !GetAtt SyncJobRole.Arn
Source:
BuildSpec: |
version: 0.2
phases:
install:
commands:
- pip3 install requests
runtime-versions:
python: 3.8
build:
commands:
- |
cat <<EOF > /tmp/index.py
import os
from subprocess import call
from urllib.parse import quote_plus
import requests
repository_name = os.environ['REPOSITORY_NAME']
namespace = os.environ['NAMESPACE']
account_id = os.environ['ACCOUNT_ID']
token = os.environ['PERSONAL_ACCESS_TOKEN']
region = os.environ['AWS_REGION']
base_domain = os.environ['BASE_DOMAIN']
api_path = f'https://{base_domain}/api'
base_branch = 'master'
headers = {'Private-Token': token}
def main(tag_name, release_description):
create_repository()
qtoken = quote_plus(token)
run(f'git remote add gitlab https://{namespace}:{qtoken}@{base_domain}/{namespace}/{repository_name}.git')
run('git push -q gitlab --all')
run('git push -q gitlab --tags')
create_release(tag_name, release_description)
def run(command):
code = call(command.split(' '))
if code != 0:
raise Exception('failed to run command')
def create_repository():
namespace_id = None
user = requests.get(f'{api_path}/v4/user', headers=headers).json()
user_id = user['id']
username = user['username']
print(f'Got user id: {user_id}, {username}')
if username != namespace:
namespace_response = requests.get(f'{api_path}/v4/namespaces/{quote_plus(namespace)}', headers=headers).json()
namespace_id = namespace_response.get('id')
body = {
'name': repository_name,
'description': f'Automatically created by cfnbuddy for AWS account {account_id}',
'visibility': 'private',
'namespace_id': namespace_id,
'tag_list': ['cfnbuddy'],
'request_access_enabled': True,
'issues_enabled': False,
'merge_requests_enabled': False,
'jobs_enabled': False,
'wiki_enabled': False,
'snippets_enabled': False,
'container_registry_enabled': False,
'shared_runners_enabled': False,
}
response = requests.post(f'{api_path}/v4/projects', headers=headers, json=body)
if response.status_code not in [200, 201]:
print(response.json())
print(
f'Could not create repository for user {namespace} (user id: {user_id}) in namespace {namespace_id}, got status code: {response.status_code}'
)
else:
print(f'Created repository for user {namespace}')
print(f'Setting default branch to {base_branch}')
response = requests.put(
f'{api_path}/v4/projects/{namespace_path()}', headers=headers, json={'default_branch': base_branch}
)
print(response.json())
def namespace_path():
return quote_plus(f'{namespace}/{repository_name}')
def create_release(tag_name, description):
print(f'Releasing {tag_name} on {base_domain}')
release_response = requests.post(
f'{api_path}/v4/projects/{namespace_path()}/releases', headers=headers,
json={'name': tag_name, 'tag_name': tag_name, 'ref': base_branch, 'description': description},
).json()
print(release_response)
if __name__ == '__main__':
main(os.environ['TAG_NAME'], os.environ['RELEASE_DESCRIPTION'])
EOF
- "for i in `git branch -a | grep remote | grep -v HEAD | grep -v master`; do git branch -q --track ${i#remotes/origin/} $i; done" # track all branches
- python /tmp/index.py
Location: !GetAtt Repo.CloneUrlHttp
Type: CODECOMMIT
LogsConfig:
CloudWatchLogs:
GroupName: !Ref SyncLogs
Status: ENABLED
SourceVersion: master
SyncJobRole:
Type: AWS::IAM::Role
Condition: isNeedSync
Properties:
AssumeRolePolicyDocument:
Statement:
- Action: sts:AssumeRole
Effect: Allow
Principal:
Service: codebuild.amazonaws.com
Policies:
- PolicyDocument:
Statement:
- Effect: Allow
Action:
- codecommit:GitPull
Resource: !Sub '${Repo.Arn}'
- Effect: Allow
Action:
- secretsmanager:GetSecretValue
Resource: !If [isNeedSync, !Ref PersonalAccessTokenSecret, !Ref 'AWS::NoValue']
- Effect: Allow
Resource:
- !Sub 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:${SyncLogs}'
- !Sub 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:${SyncLogs}/*'
- !Sub 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:${SyncLogs}:log-stream:*'
Action:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
Version: '2012-10-17'
PolicyName: sync-job-policy
PersonalAccessTokenSecret:
Type: AWS::SecretsManager::Secret
Condition: isNeedSync
Properties:
Description: Your GitHub/GitLab access token privately stored in your account. It never leaves your account
Name: /cfnbuddy/personal-access-token
SecretString: !Ref PersonalAccessToken
SyncLogs:
Type: AWS::Logs::LogGroup
Condition: isNeedSync
Properties:
RetentionInDays: 7
CodeCommitUser:
Type: AWS::IAM::User
Properties:
ManagedPolicyArns:
- !Ref GitPullPushPolicy
GitPullPushPolicy:
Type: AWS::IAM::ManagedPolicy
Properties:
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- codecommit:GitPull
- codecommit:GitPush
Resource: !Sub 'arn:aws:codecommit:${AWS::Region}:${AWS::AccountId}:${Repo.Name}'
ViewBudgetPolicy:
Type: AWS::IAM::ManagedPolicy
Condition: isAllowViewBudgets
Properties:
PolicyDocument:
Statement:
- Effect: Allow
Action:
- budgets:ViewBudget
Resource: !Sub 'arn:aws:budgets::${AWS::AccountId}:budget/*'
Version: '2012-10-17'
GitRepositoryNameParam:
Type: AWS::SSM::Parameter
Properties:
Name: /cfnbuddy/git-repository-name
Type: String
Value: !GetAtt Repo.Name
CodeCommitParam:
Type: AWS::SSM::Parameter
Properties:
Name: /cfnbuddy/codecommit-username
Type: String
Value: !Ref CodeCommitUser
SyncJobParam:
Type: AWS::SSM::Parameter
Condition: isNeedSync
Properties:
Name: /cfnbuddy/sync-job
Type: String
Value: !If [isGitHub, !Ref GitHubSyncJob, !If [isGitLab, !Ref GitLabSyncJob, !Ref 'AWS::NoValue']]
ImageBackgroundParam:
Type: AWS::SSM::Parameter
Properties:
Name: /cfnbuddy/image-background
Type: String
Value: !Ref ImageBackground

Click image to expand

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment