Skip to content

Commit

Permalink
Github Actions; deploy via ArgoCD (#359)
Browse files Browse the repository at this point in the history
Utilize ArgoCD CLI via Python wrapper script to "deploy" applications
via updating Docker image tag
Tested
https://github.com/codeforsanjose/project-happening-atm/actions/runs/7861314861/job/21449532044
  • Loading branch information
darpham authored Feb 15, 2024
1 parent a7e901c commit 8192cea
Show file tree
Hide file tree
Showing 5 changed files with 198 additions and 13 deletions.
132 changes: 132 additions & 0 deletions .github/scripts/argocd_deploy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import logging
import typing as t
from argparse import ArgumentParser, Namespace
from json import loads
from logging import Formatter, StreamHandler
from math import floor
from os import environ, getenv
from subprocess import run as subproccess_run
from sys import exit, stdout


# Wrapper for ArgoCD CLI
# https://argo-cd.readthedocs.io/en/stable/user-guide/commands/argocd/
class ArgoCDeployer:
def __init__(self, application: str, docker_tag: str) -> None:
self.application = application or getenv('ARGO_CD_APPLICATION', environ['DEPLOYMENT'])
self.docker_tag = docker_tag or environ['DOCKER_TAG']
self.image_tag_parameter = getenv('IMAGE_TAG_PARAMETER', 'global.image.tag=')
self.argo_cd_credentials = [
f'--username={environ["ARGO_CD_USERNAME"]}',
f'--password={environ["ARGO_CD_PASSWORD"]}',
]

def _execute_cli(self, command: t.List[str], return_stdout: bool = False) -> t.Optional[str]:
command = ['argocd'] + command
LOG.info(' '.join(command).replace(environ['ARGO_CD_PASSWORD'], 'redacted'))
output = subproccess_run(command, capture_output=True, encoding='utf-8')
if output.returncode != 0:
LOG.fatal(f'ArgoCD CLI failed {command=} returncode={output.returncode} standard_error={output.stderr}')
if return_stdout:
return output.stdout

def login(self):
self._execute_cli(
command=[
'login',
'--grpc-web',
environ['ARGO_CD_SERVER'],
*self.argo_cd_credentials,
]
)

def update_image_tag(self):
args = []
parameters = [
f'{self.image_tag_parameter}{self.docker_tag}',
]
for parameter in parameters:
args.extend(['--parameter', parameter])
self._execute_cli(command=['app', 'set', self.application, *args])

def sync_app(self, preview: bool = False, timeout: int = 1200):
args = [
'--assumeYes',
'--prune',
'--retry-backoff-max-duration',
f'{floor(timeout/60)}m',
'--timeout',
str(timeout + 30),
]
if preview:
args.append('--dry-run')
self._execute_cli(command=['app', 'sync', self.application, *args])

def preview_deploy(self):
self.sync_app(preview=True)

def wait_health(self):
args = [
'--health',
'--sync',
'--timeout',
'600',
]
self._execute_cli(command=['app', 'wait', self.application, *args])

def verify_health(self):
args = [
'--output',
'json',
]
info = self._execute_cli(command=['app', 'get', self.application, *args], return_stdout=True)
info = loads(info)
status = info['status']
if status['health']['status'] == 'Degraded':
LOG.critical(
f'Application is unhealthy, aborting deploy\nCheck Application https://ARGO_CD_SERVER/applications/argocd/{self.application}?view=network&resource='
)
exit(1)
sync_status = status['sync']['status']
if sync_status not in ['Synced', 'OutofSync']:
LOG.info('Application non-deployable SyncStatus={sync_status} - Will try to wait before aborting')
self.wait_health()


def Logger(logger_name):
logger = logging.getLogger(logger_name)

logger.setLevel(logging.INFO)

standard_output = StreamHandler(stdout)
standard_output.setFormatter(
Formatter(
'{asctime} {name} {levelname:8} {message}',
'%Y-%m-%d %H:%M:%S',
'{',
)
)
logger.addHandler(standard_output)
return logger


if __name__ == '__main__':
LOG = Logger('argocd.deploy')

parser = ArgumentParser(
prog='ArgoCDeployer',
description='Deploy ArgoCD Applications utilizing argocd CLI',
)
# TODO: Convert arguments to non-positional args and enforce required=True
# Requires CICD changes where command is utilized
parser.add_argument('--application', help='ArgoCD Application Name i.e., foobar-dev')
parser.add_argument('--docker_tag', help='Docker Tag to be deployed, usually the full Git SHA')
args: Namespace = parser.parse_args()

argocd = ArgoCDeployer(application=args.application, docker_tag=args.docker_tag)
argocd.login()
argocd.verify_health()
argocd.update_image_tag()
argocd.sync_app()

LOG.info(f'Deploy succeeded {args.application} docker_tag={args.docker_tag}')
3 changes: 3 additions & 0 deletions .github/scripts/requirements.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
boto3
boto3-stubs[ecr]
botocore
39 changes: 39 additions & 0 deletions .github/scripts/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#
# This file is autogenerated by pip-compile with Python 3.10
# by the following command:
#
# pip-compile requirements.in
#
boto3==1.34.7
# via -r requirements.in
boto3-stubs[ecr]==1.34.7
# via -r requirements.in
botocore==1.34.7
# via
# -r requirements.in
# boto3
# s3transfer
botocore-stubs==1.34.7
# via boto3-stubs
jmespath==1.0.1
# via
# boto3
# botocore
mypy-boto3-ecr==1.34.0
# via boto3-stubs
python-dateutil==2.8.2
# via botocore
s3transfer==0.10.0
# via boto3
six==1.16.0
# via python-dateutil
types-awscrt==0.20.0
# via botocore-stubs
types-s3transfer==0.10.0
# via boto3-stubs
typing-extensions==4.9.0
# via
# boto3-stubs
# mypy-boto3-ecr
urllib3==2.0.7
# via botocore
2 changes: 1 addition & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: ci
run-name: ci ${{ github.event_name}} ${{ github.ref_name }}
run-name: ci ${{ github.event_name }} ${{ github.ref_name }}
on:
pull_request:
push:
Expand Down
35 changes: 23 additions & 12 deletions .github/workflows/deploy.yaml
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
---
name: deploy
run-name: ${{ github.event.inputs.env }} deploy ${{ github.ref_name }} ${{ github.event_name }} by ${{ github.actor }}
run-name: ${{ github.event.inputs.env }} deploy ${{ github.ref_name }}
on:
workflow_dispatch:
inputs:
env:
type: environment
description: 'ArgoCD Deployed environment'
ref:
description: 'Branch, Tag, or Full SHA'
required: true
default: 'develop'
default: 'main'

concurrency:
group: ${{ github.event.inputs.env }}
Expand All @@ -26,15 +27,25 @@ jobs:
env:
ENVIRONMENT: ${{ github.event.inputs.env }}
steps:
- uses: actions/checkout@v3
- uses: aws-actions/configure-aws-credentials@v3
- uses: actions/checkout@v4
with:
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/github-actions
role-session-name: gha
aws-region: us-west-2
- id: login-ecr
uses: aws-actions/amazon-ecr-login@v1
ref: ${{ github.event.inputs.ref }}
- uses: clowdhaus/argo-cd-action/@main
with:
mask: 'true'
- run: make deploy
- run: make deploy-status
version: 2.10.0
command: version
options: --client
- uses: actions/setup-python@v5
with:
python-version: '3.12'
cache: 'pip'
- run: pip install -r .github/scripts/requirements.txt
- name: deploy
run: |-
python .github/scripts/argocd_deploy.py \
--application=happeningatm-${{ env.ENVIRONMENT }} \
--docker_tag=$(git rev-parse HEAD)
env:
ARGO_CD_SERVER: ${{ secrets.ARGO_CD_SERVER }}
ARGO_CD_USERNAME: ${{ secrets.ARGO_CD_USERNAME }}
ARGO_CD_PASSWORD: ${{ secrets.ARGO_CD_PASSWORD }}

0 comments on commit 8192cea

Please sign in to comment.