docker-ci/ci/ci.py
2021-06-28 10:29:16 -07:00

374 lines
12 KiB
Python
Executable File

#!/usr/bin/env python
import os
import boto3
import time
import sys
import docker
import requests
import anybadge
from requests.adapters import HTTPAdapter
from requests.packages.urllib3.util.retry import Retry
from multiprocessing.pool import Pool
from selenium import webdriver
from selenium.common.exceptions import ErrorInResponseException,TimeoutException
from jinja2 import Template
client = docker.from_env()
session = boto3.session.Session()
reload(sys)
sys.setdefaultencoding('utf8')
# Global Vars
global report_status
global report_tests
global report_containers
report_tests = []
report_containers = []
report_status = 'PASS'
#############
# Functions #
#############
# If the tests cannot even be run just fail the job
def core_fail(message):
print(message)
sys.exit(1)
# Convert env input to dictionary
def convert_env(vars):
global dockerenv
dockerenv = {}
try:
if '|' in vars:
for varpair in vars.split('|'):
var = varpair.split('=')
dockerenv[var[0]] = var[1]
else:
var = vars.split('=')
dockerenv[var[0]] = var[1]
except Exception as error:
core_fail(str(error))
# Update global variables from threaded testing process
def update_globals(data):
global report_status
for (tests,containers,status) in data:
for test in tests:
report_tests.append(test)
for container in containers:
report_containers.append(container)
if status == 'FAIL':
report_status = 'FAIL'
# Set the optional parameters
global webauth
global webpath
global dockerenv
global region
global bucket
global screenshot
global port
global ssl
global testdelay
try:
webauth = os.environ["WEB_AUTH"]
except KeyError:
webauth = 'user:password'
try:
webpath = os.environ["WEB_PATH"]
except KeyError:
webpath = ''
try:
convert_env(os.environ["DOCKER_ENV"])
except KeyError:
dockerenv = {}
try:
region = os.environ["S3_REGION"]
except KeyError:
region = 'us-east-1'
try:
bucket = os.environ["S3_BUCKET"]
except KeyError:
bucket = 'ci-tests.linuxserver.io'
try:
screenshot = os.environ["WEB_SCREENSHOT"]
except KeyError:
screenshot = 'false'
try:
port = os.environ["PORT"]
except KeyError:
port = '80'
try:
ssl = os.environ["SSL"]
except KeyError:
ssl = 'false'
try:
testdelay = os.environ["DELAY_START"]
except KeyError:
testdelay = '5'
# Make sure all needed env variables are set
def check_env():
try:
global image
global tags
global meta_tag
global base
global S3_key
global S3_secret
image = os.environ["IMAGE"]
base = os.environ["BASE"]
S3_key = os.environ["ACCESS_KEY"]
S3_secret = os.environ["SECRET_KEY"]
meta_tag = os.environ["META_TAG"]
tags_env = os.environ["TAGS"]
tags = []
if '|' in tags_env:
for tag in tags_env.split('|'):
tags.append(tag)
else:
tags.append(tags_env)
except KeyError as error:
core_fail(str(error) + ' is not set in ENV')
# Create output path
def create_dir():
global outdir
outdir = os.path.dirname(os.path.realpath(__file__)) + '/output/' + image + '/' + meta_tag + '/'
try:
os.stat(outdir)
except:
os.makedirs(outdir)
# Main container test logic
def container_test(tag):
# Vars for the threaded process
report_tests = []
report_containers = []
report_status = 'PASS'
# End the test with as much info as we have
def endtest(container,report_tests,report_containers,report_status,tag,build_version,packages):
logblob = container.logs().decode("utf-8")
container.remove(force='true')
# Add the info to the report
report_containers.append({
"tag":tag,
"logs":logblob,
"sysinfo":packages,
"build_version":build_version
})
return (report_tests,report_containers,report_status)
# Start the container
print('Starting ' + tag)
container = client.containers.run(image + ':' + tag,
detach=True,
environment=dockerenv)
# Watch the logs for no more than 5 minutes
t_end = time.time() + 60 * 5
logsfound = False
while time.time() < t_end:
try:
logblob = container.logs().decode("utf-8")
if '[services.d] done.' in logblob:
logsfound = True
break
time.sleep(1)
except Exception as error:
print('Startup failed for ' + tag)
report_tests.append(['Startup ' + tag,'FAIL INIT NOT FINISHED'])
report_status = 'FAIL'
(report_tests,report_containers,report_status) = endtest(container,report_tests,report_containers,report_status,tag,'ERROR','ERROR')
return (report_tests,report_containers,report_status)
# Grab build version
try:
build_version = container.attrs["Config"]["Labels"]["build_version"]
report_tests.append(['Get Build Version ' + tag,'PASS'])
except Exception as error:
build_version = 'ERROR'
report_tests.append(['Get Build Version ' + tag,'FAIL'])
report_status = 'FAIL'
(report_tests,report_containers,report_status) = endtest(container,report_tests,report_containers,report_status,tag,build_version,'ERROR')
return (report_tests,report_containers,report_status)
# Check if the startup marker was found in the logs during the 2 minute spinup
if logsfound == True:
print('Startup completed for ' + tag)
report_tests.append(['Startup ' + tag,'PASS'])
elif logsfound == False:
print('Startup failed for ' + tag)
report_tests.append(['Startup ' + tag,'FAIL INIT NOT FINISHED'])
report_status = 'FAIL'
(report_tests,report_containers,report_status) = endtest(container,report_tests,report_containers,report_status,tag,build_version,'ERROR')
return (report_tests,report_containers,report_status)
# Dump package information
print('Dumping package info for ' + tag)
if base == 'alpine':
command = 'apk info -v'
elif base == 'debian' or base == 'ubuntu':
command = 'apt list'
elif base == 'fedora':
command = 'rpm -qa'
try:
info = container.exec_run(command)
packages = info[1].decode("utf-8")
report_tests.append(['Dump Versions ' + tag,'PASS'])
print('Got Package info for ' + tag)
except Exception as error:
packages = 'ERROR'
print(str(error))
report_tests.append(['Dump Versions ' + tag,'FAIL'])
report_status = 'FAIL'
(report_tests,report_containers,report_status) = endtest(container,report_tests,report_containers,report_status,tag,build_version,packages)
return (report_tests,report_containers,report_status)
# Sleep for the user specified amount of time
time.sleep(int(testdelay))
# Screenshot web interface and check connectivity
if screenshot == 'true':
# Take a screenshot
if ssl == 'true':
proto = 'https://'
else:
proto = 'http://'
container.reload()
ip = container.attrs["NetworkSettings"]["Networks"]["bridge"]["IPAddress"]
endpoint = proto + webauth + '@' + ip + ':' + port + webpath
print('Taking screenshot of ' + tag + ' at ' + endpoint)
testercontainer = client.containers.run('lsiodev/tester:latest',
shm_size='1G',
detach=True,
environment={'URL': endpoint})
time.sleep(30)
testercontainer.reload()
testerip = testercontainer.attrs["NetworkSettings"]["Networks"]["bridge"]["IPAddress"]
testerendpoint = "http://" + testerip + ":3000"
try:
# Selenium webdriver options
chrome_options = webdriver.ChromeOptions()
chrome_options.add_argument('--no-sandbox')
chrome_options.add_argument('--headless')
chrome_options.add_argument('--disable-gpu')
chrome_options.add_argument('--window-size=1920x1080')
driver = webdriver.Chrome(options=chrome_options)
driver.set_page_load_timeout(60)
session = requests.Session()
retries = Retry(total=4, backoff_factor=2, status_forcelist=[ 502, 503, 504 ])
session.mount(proto, HTTPAdapter(max_retries=retries))
session.get(testerendpoint)
driver.get(testerendpoint)
time.sleep(15)
driver.get_screenshot_as_file(outdir + tag + '.png')
report_tests.append(['Screenshot ' + tag,'PASS'])
# Quit selenium webdriver
driver.quit()
except (requests.Timeout, requests.ConnectionError, KeyError) as e:
report_tests.append(['Screenshot ' + tag,'FAIL CONNECTION ERROR'])
except ErrorInResponseException as error:
report_tests.append(['Screenshot ' + tag,'FAIL SERVER ERROR'])
except TimeoutException as error:
report_tests.append(['Screenshot ' + tag,'FAIL TIMEOUT'])
except WebDriverException as error:
report_tests.append(['Screenshot ' + tag,'FAIL UNKNOWN'])
testercontainer.remove(force='true')
# If all info is present end test
(report_tests,report_containers,report_status) = endtest(container,report_tests,report_containers,report_status,tag,build_version,packages)
return (report_tests,report_containers,report_status)
# Render the markdown file for upload
def report_render():
print('Rendering Report')
with open(os.path.dirname(os.path.realpath(__file__)) + '/results.template') as file_:
template = Template(file_.read())
markdown = template.render(
report_tests=report_tests,
report_containers=report_containers,
report_status=report_status,
meta_tag=meta_tag,
image=image,
bucket=bucket,
region=region,
screenshot=screenshot)
with open(outdir + 'report.md', 'w') as f:
f.write(markdown)
# Render the badge file for upload
def badge_render():
try:
badge = anybadge.Badge('CI', report_status, thresholds={'PASS': 'green', 'FAIL': 'red'})
badge.write_badge(outdir + 'badge.svg')
with open(outdir + 'ci-status.yml', 'w') as f:
f.write('CI: "' + report_status + '"')
except Exception as error:
print(error)
# Upload report to S3
def report_upload():
print('Uploading Report')
destination_dir = image + '/' + meta_tag + '/'
latest_dir = image + '/latest/'
s3 = session.client(
's3',
region_name=region,
aws_access_key_id=S3_key,
aws_secret_access_key=S3_secret)
# Index file upload
index_file = os.path.dirname(os.path.realpath(__file__)) + '/index.html'
try:
s3.upload_file(
index_file,
bucket,
destination_dir + 'index.html',
ExtraArgs={'ContentType': "text/html", 'ACL': "public-read"})
s3.upload_file(
index_file,
bucket,
latest_dir + 'index.html',
ExtraArgs={'ContentType': "text/html", 'ACL': "public-read"})
except Exception as error:
core_fail('Upload Error ' + str(error))
# Loop for all others
for filename in os.listdir(outdir):
time.sleep(0.5)
# Set content types for files
if filename.lower().endswith('.svg'):
CT = 'image/svg+xml'
elif filename.lower().endswith('.png'):
CT = 'image/png'
elif filename.lower().endswith('.md'):
CT = 'text/markdown'
elif filename.lower().endswith('.yml'):
CT = 'text/yaml'
try:
s3.upload_file(
outdir + filename,
bucket,
destination_dir + filename,
ExtraArgs={'ContentType': CT,'ACL': "public-read",'CacheControl': 'no-cache'})
s3.upload_file(
outdir + filename,
bucket,
latest_dir + filename,
ExtraArgs={'ContentType': CT,'ACL': "public-read",'CacheControl': 'no-cache'})
except Exception as error:
core_fail('Upload Error ' + str(error))
##################
# Test Run Logic #
##################
check_env()
create_dir()
# Run through all the tags
pool=Pool(processes=3)
r = pool.map_async(container_test, tags, callback=update_globals)
r.wait()
report_render()
badge_render()
report_upload()
# Exit based on test results
if report_status == 'PASS':
print('Tests Passed exiting 0')
sys.exit(0)
elif report_status == 'FAIL':
print('Tests Failed exiting 1')
sys.exit(1)