mirror of
https://github.com/linuxserver/docker-ci.git
synced 2026-01-20 20:01:40 +08:00
374 lines
12 KiB
Python
Executable File
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)
|