Skip to content

Feature/clone using private key #9

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
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
3 changes: 3 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,6 @@ tools/
examples/
.vscode/
.idea/
ssh_key*
*.pem
*.pub
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,7 @@
.vscode
*pycache*
.pytest_cache
*.pyc
*.pyc
ssh_key*
*.pem
*.pub
3 changes: 2 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ FROM python:${BASE_TAG}
# ENV variables
ENV GIT_REPOSITORY="" \
APP_NAME="App" \
GIT_BRANCH=""
GIT_BRANCH="" \
PRIVATE_KEY_LOCATION=""
ARG USERNAME=user

# Add a non-root user
Expand Down
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,9 @@ push: ## push built image to dockerhub
docker tag ${IMAGE_NAME} ${PUSH_IMAGE_NAME}
docker push ${PUSH_IMAGE_NAME}

generate-ssh-key: ## create a new public and private key set in current directory
ssh-keygen -b 2048 -t rsa -f ./ssh_key -q -N ""
mv ./ssh_key ./ssh_key.pem

help: ## show this help.
@fgrep -h "##" $(MAKEFILE_LIST) | fgrep -v fgrep | sed -e 's/\\$$//' | sed -e 's/##//'
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ The steps that run when the container starts are:

## Changelog

- 0.2.1
- Allow cloning SSH repositories, and support using private keys
- 0.1.1
- Upload to DockerHub from Github Workflow
- Fix cmd in Dockerfile, change from `bash` to `sh` (Alpine compatibility)
Expand All @@ -105,7 +107,7 @@ The steps that run when the container starts are:
## TODO

- Allow setting GIT repository through CMD
- Load SSH private key for cloning SSH git repositories (from path or secret)
- Specify trusted host for SSH cloning
- Create multi-arch images
- Run as root with an env variable - or another image tag
- Tag & upload images based on official Python image tags, plus versions of this repository
74 changes: 55 additions & 19 deletions scripts/setup_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"""

import os
import shutil
import subprocess
from datetime import datetime
from contextlib import suppress
Expand All @@ -12,76 +13,112 @@
class Settings:
FIRST_RUN_FILENAME = ".setup_app_done"
REQUIREMENTS_FILENAME = "requirements.txt"
PRIVATE_KEY_WRITE_LOCATION = "/tmp/git_private_key.pem"

def __init__(self):
# Required settings
try:
self.app_name = os.environ["APP_NAME"]
self.git_repository = os.environ["GIT_REPOSITORY"]
except KeyError as error:
raise Exception("Environment variable \"{}\" not defined!".format(error))


# Dynamic settings
self.base_dir = os.path.expanduser("~")
self.first_run_file = self.join_home(self.FIRST_RUN_FILENAME)
self.app_dir = self.join_home(self.app_name)
self.requirements_file = self.join_app(self.REQUIREMENTS_FILENAME)

# Optional settings
self.git_branch = os.getenv("GIT_BRANCH")

self.private_key_location = os.getenv("PRIVATE_KEY_LOCATION")

def join_home(self, path):
return os.path.join(self.base_dir, path)

def join_app(self, path):
return os.path.join(self.app_dir, path)

@property
def git_repository_is_ssh(self):
return self.git_repository.startswith("git@")


def log(message):
"""Print log line with the current datetime
"""
"""Print log line with the current datetime"""
print("[{date}] {msg}".format(
date=datetime.now().strftime("%y/%m/%d %H:%M:%S"),
msg=message
))


def is_first_run(settings):
"""Return True if this is the first time the container runs
"""
"""Return True if this is the first time the container runs"""
return not os.path.isfile(settings.first_run_file)


def save_setup_done(settings):
"""Store a file to mark this container already ran
"""
"""Store a file to mark this container already ran"""
os.mknod(settings.first_run_file)
log("Saved 'App installed' status")


def clear_output_dir(settings):
"""Clear output directories
"""
"""Clear output directories"""
with suppress(FileNotFoundError):
os.rmdir(settings.app_dir)
log("Cleared output directories")


def get_private_key_file(settings):
"""Return the location of the private key file, if any.
If specified as file, the file is copied to tmp and its permissions set to 600 so SSH does not complain about it."""
private_key_read_filename = settings.private_key_location
if not private_key_read_filename:
return None

try:
private_key_write_filename = settings.PRIVATE_KEY_WRITE_LOCATION
shutil.copy(private_key_read_filename, private_key_write_filename)
os.chmod(private_key_write_filename, 0o600)

log("Private key read from \"\" and copied to \"\" with correct permissions".format(private_key_read_filename, private_key_write_filename))
return private_key_write_filename

except FileNotFoundError:
log("Private key in \"{}\" not found!".format(private_key_read_filename))


def clone(settings):
"""Clone the app through Git
"""
"""Clone the app through Git"""
log("Cloning app through Git...")
cmd_env = dict()

branch_settings = []
if settings.git_branch:
branch_settings = ["--branch", settings.git_branch]

result = subprocess.call(["git", "clone", *branch_settings, settings.git_repository, settings.app_dir])
git_repository_is_ssh = settings.git_repository_is_ssh
if git_repository_is_ssh:
cmd_env["GIT_SSH_COMMAND"] = "ssh -o StrictHostKeyChecking=no"

private_key_file = get_private_key_file(settings)
if private_key_file:
if not git_repository_is_ssh:
log("The specified repository is not a Git repository, but a private key was set. The key will not be used!")
else:
cmd_env["GIT_SSH_COMMAND"] += "-i {} -o IdentitiesOnly=yes".format(private_key_file)

result = subprocess.call(["git", "clone", *branch_settings, settings.git_repository, settings.app_dir], env=cmd_env)
if result > 0:
# TODO capture git output when fail
raise Exception("Git Clone failed!")

log("App cloned through Git!")


def install_requirements(settings):
"""Install Python package requirements through git, from requirements file
"""
"""Install Python package requirements through git, from requirements file"""
if os.path.isfile(settings.requirements_file):
log("Installing requirements through Pip...")
result = subprocess.call(["pip", "install", "--user", "-r", settings.requirements_file])
Expand All @@ -94,8 +131,7 @@ def install_requirements(settings):


def run():
"""Main run function
"""
"""Main run function"""
try:
settings = Settings()
args = (settings,)
Expand Down