mirror of
https://github.com/cupcakearmy/drone-deploy.git
synced 2024-12-21 15:56:32 +00:00
Drone deploy plugin
This commit is contained in:
parent
5a1e91a5e7
commit
85e5c7f061
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
docker-compose.yml
|
||||
env
|
||||
|
||||
.vscode
|
||||
.idea
|
17
Dockerfile
Normal file
17
Dockerfile
Normal file
@ -0,0 +1,17 @@
|
||||
FROM python:3-alpine as builder
|
||||
RUN apk add --no-cache alpine-sdk libffi-dev openssl-dev python3-dev
|
||||
COPY requirements.txt .
|
||||
RUN pip install -r requirements.txt
|
||||
|
||||
|
||||
FROM python:3-alpine
|
||||
WORKDIR /plugin
|
||||
COPY --from=builder /root/.cache /root/.cache
|
||||
COPY --from=builder requirements.txt .
|
||||
RUN pip install -r requirements.txt && rm -rf /root/.cache
|
||||
|
||||
COPY main.py ./
|
||||
|
||||
WORKDIR /drone/src
|
||||
|
||||
CMD ["python", "/plugin/main.py"]
|
45
README.md
Normal file
45
README.md
Normal file
@ -0,0 +1,45 @@
|
||||
# Drone Deployment Plugin
|
||||
|
||||
## Quickstart 🚀
|
||||
|
||||
```yaml
|
||||
kind: pipeline
|
||||
name: default
|
||||
|
||||
steps:
|
||||
|
||||
- name: build
|
||||
image: node:11-alpine
|
||||
pull: always
|
||||
commands:
|
||||
- npm i
|
||||
- npm run build:prod
|
||||
|
||||
- name: deploy
|
||||
image: cupcakearmy/drone-deploy
|
||||
pull: always
|
||||
environment:
|
||||
PLUGIN_KEY:
|
||||
from_secret: ssh_key
|
||||
settings:
|
||||
host: example.org
|
||||
user: root
|
||||
port: 69
|
||||
target: /my/web/root/project
|
||||
sources:
|
||||
- ./public
|
||||
- ./docker-compose.yml
|
||||
- ./docker-compose.prod.yml
|
||||
commands:
|
||||
- docker-compose -f docker-compose.yml -f docker-compose.prod.yml down
|
||||
- docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d
|
||||
when:
|
||||
event: push
|
||||
branch: master
|
||||
```
|
||||
|
||||
### Details 📒
|
||||
|
||||
The plugins creates a tarball compressing all the files included inside of `sources`.
|
||||
Then the compressed tarball gets uploaded, extracted and deleted, leaving only the files specified by `sources` inside of the `target` folder.
|
||||
Afterwards all the commands inside of `commands` will get executed at the `target` directory.
|
114
main.py
Normal file
114
main.py
Normal file
@ -0,0 +1,114 @@
|
||||
import io
|
||||
import os
|
||||
import random
|
||||
import string
|
||||
import warnings
|
||||
import subprocess
|
||||
from typing import List
|
||||
from os.path import abspath, join, dirname
|
||||
|
||||
import paramiko
|
||||
from paramiko import SSHClient
|
||||
|
||||
|
||||
def get_random_string(length: int = 128) -> str:
|
||||
return ''.join(random.choices(string.ascii_lowercase + string.digits, k=length))
|
||||
|
||||
|
||||
def execute(c: SSHClient, cmd: str, path: str = None) -> str:
|
||||
if path is not None:
|
||||
cmd = 'cd {}; {}'.format(path, cmd)
|
||||
stdin, stdout, stderr = c.exec_command(cmd)
|
||||
output = stdout.read().decode('utf-8').strip()
|
||||
error = stderr.read().decode('utf-8')
|
||||
|
||||
if len(error) is not 0:
|
||||
print('ERROR: {}'.format(error))
|
||||
|
||||
return output
|
||||
|
||||
|
||||
def main():
|
||||
host = os.environ.get('PLUGIN_HOST')
|
||||
port = os.environ.get('PLUGIN_PORT', 22)
|
||||
user = os.environ.get('PLUGIN_USER')
|
||||
|
||||
password = os.environ.get('PLUGIN_PASSWORD')
|
||||
key = os.environ.get('PLUGIN_KEY')
|
||||
|
||||
def clean_array(s: str) -> List[str]:
|
||||
return list(filter(None, s.split(',')))
|
||||
|
||||
commands = clean_array(os.environ.get('PLUGIN_COMMANDS', ''))
|
||||
sources = clean_array(os.environ.get('PLUGIN_SOURCES', ''))
|
||||
deletes = clean_array(os.environ.get('PLUGIN_DELETE', ''))
|
||||
target = os.environ.get('PLUGIN_TARGET')
|
||||
|
||||
for env in [host, port, user]:
|
||||
if env is None:
|
||||
raise Exception('Missing ENV variable')
|
||||
|
||||
if len(list(filter(lambda x: x is not None, [password, key]))) is 0:
|
||||
raise Exception('No authentication method provided')
|
||||
|
||||
if len(sources) is not 0 and target is None:
|
||||
raise Exception('Target not set')
|
||||
|
||||
ssh: SSHClient = paramiko.SSHClient()
|
||||
try:
|
||||
k = paramiko.RSAKey.from_private_key(io.StringIO(key))
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh.connect(hostname=host, username=user, pkey=k, port=port, password=password)
|
||||
|
||||
# If a target is set, make sure the directory is created and writable
|
||||
if target is not None:
|
||||
try:
|
||||
execute(ssh, 'mkdir -p {}'.format(target))
|
||||
tmp_file = get_random_string()
|
||||
execute(ssh, 'touch {}; rm {}'.format(tmp_file, tmp_file), target)
|
||||
except Exception:
|
||||
raise Exception('Could not create directory')
|
||||
|
||||
sftp = ssh.open_sftp()
|
||||
try:
|
||||
# DELETE
|
||||
for delete in deletes:
|
||||
sftp.remove(join(target, delete))
|
||||
|
||||
# COPY
|
||||
if len(sources) is not 0:
|
||||
archive = get_random_string(100) + '.tar.gz' # Keep the max file name length under 128 chars
|
||||
archive_local = abspath(archive)
|
||||
archive_remote = join(target, archive)
|
||||
|
||||
# sources = list(map(lambda x: join(dirname(archive_local), x), sources))
|
||||
# print(sources)
|
||||
|
||||
# Compress
|
||||
cmd = ['tar', '-czf', archive, '-C', dirname(archive_local), *sources]
|
||||
run = subprocess.run(cmd, capture_output=True)
|
||||
if run.returncode is not 0:
|
||||
raise Exception('Error while compressing locally. {}'.format(run.stderr.decode('utf-8').strip()))
|
||||
|
||||
# Upload
|
||||
sftp.put(archive_local, archive_remote)
|
||||
# Extract
|
||||
execute(ssh, 'tar -xzf {}'.format(archive), target)
|
||||
|
||||
# Delete Archives
|
||||
sftp.remove(archive_remote)
|
||||
subprocess.run(['rm', archive_local], capture_output=True)
|
||||
finally:
|
||||
sftp.close()
|
||||
|
||||
for command in commands:
|
||||
output = execute(ssh, command, target)
|
||||
print(output)
|
||||
|
||||
finally:
|
||||
ssh.close()
|
||||
|
||||
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter('ignore')
|
||||
main()
|
9
requirements.txt
Normal file
9
requirements.txt
Normal file
@ -0,0 +1,9 @@
|
||||
asn1crypto==0.24.0
|
||||
bcrypt==3.1.6
|
||||
cffi==1.12.2
|
||||
cryptography==2.6.1
|
||||
paramiko==2.4.2
|
||||
pyasn1==0.4.5
|
||||
pycparser==2.19
|
||||
PyNaCl==1.3.0
|
||||
six==1.12.0
|
Loading…
Reference in New Issue
Block a user