koji_test.py: test upload to cloud with AWS
Extend the integration test with a new case, testing that direct upload to the cloud works for Koji composes. Test this using a single cloud provider, specifically AWS. The test case submits a new osbuild-image build using Koji CLI, determines the image information once the build finishes and then checks that such image exists in AWS. The image is then deleted as part of the test case tear-down. The AWS credentials are now configured in the worker's configuration, if the appropriate environment variables are set. Update the SPEC file with a new test dependency and update the required osbuild-composer version.
This commit is contained in:
parent
c76e97ddc9
commit
d1e064aec3
6 changed files with 176 additions and 7 deletions
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
|
|
@ -17,7 +17,7 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
|
|
||||||
- name: Install test dependencies
|
- name: Install test dependencies
|
||||||
run: dnf -y install python3-flexmock python3-httpretty python3-jsonschema python3-koji python3-pylint python3-requests
|
run: dnf -y install python3-boto3 python3-flexmock python3-httpretty python3-jsonschema python3-koji python3-pylint python3-requests
|
||||||
|
|
||||||
- name: Check out code
|
- name: Check out code
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
|
|
||||||
|
|
@ -143,10 +143,11 @@ Requires: jq
|
||||||
Requires: koji
|
Requires: koji
|
||||||
Requires: krb5-workstation
|
Requires: krb5-workstation
|
||||||
Requires: openssl
|
Requires: openssl
|
||||||
Requires: osbuild-composer >= 22
|
Requires: osbuild-composer >= 58
|
||||||
Requires: osbuild-composer-tests
|
Requires: osbuild-composer-tests
|
||||||
Requires: podman
|
Requires: podman
|
||||||
Requires: podman-plugins
|
Requires: podman-plugins
|
||||||
|
Requires: python3-boto3
|
||||||
|
|
||||||
%description tests
|
%description tests
|
||||||
Integration tests for koji-osbuild. To be run on a dedicated system.
|
Integration tests for koji-osbuild. To be run on a dedicated system.
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,25 @@ cp ${TEST_DATA}/osbuild-worker.toml \
|
||||||
|
|
||||||
echo "koji" > /etc/osbuild-worker/oauth-secret
|
echo "koji" > /etc/osbuild-worker/oauth-secret
|
||||||
|
|
||||||
|
# if AWS credentials are defined in the ENV, add them to the worker's configuration
|
||||||
|
# This is needed to test the upload to the cloud
|
||||||
|
V2_AWS_ACCESS_KEY_ID="${V2_AWS_ACCESS_KEY_ID:-}"
|
||||||
|
V2_AWS_SECRET_ACCESS_KEY="${V2_AWS_SECRET_ACCESS_KEY:-}"
|
||||||
|
if [[ -n "$V2_AWS_ACCESS_KEY_ID" && -n "$V2_AWS_SECRET_ACCESS_KEY" ]]; then
|
||||||
|
echo "Adding AWS credentials to the worker's configuration"
|
||||||
|
sudo tee /etc/osbuild-worker/aws-credentials.toml > /dev/null << EOF
|
||||||
|
[default]
|
||||||
|
aws_access_key_id = "$V2_AWS_ACCESS_KEY_ID"
|
||||||
|
aws_secret_access_key = "$V2_AWS_SECRET_ACCESS_KEY"
|
||||||
|
EOF
|
||||||
|
sudo tee -a /etc/osbuild-worker/osbuild-worker.toml > /dev/null << EOF
|
||||||
|
|
||||||
|
[aws]
|
||||||
|
credentials = "/etc/osbuild-worker/aws-credentials.toml"
|
||||||
|
bucket = "${AWS_BUCKET}"
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
|
||||||
echo "Copying system kerberos configuration"
|
echo "Copying system kerberos configuration"
|
||||||
cp ${TEST_DATA}/krb5.local.conf \
|
cp ${TEST_DATA}/krb5.local.conf \
|
||||||
/etc/krb5.conf.d/local
|
/etc/krb5.conf.d/local
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ greenprint "Testing Koji hub API access"
|
||||||
koji --server=http://localhost:8080/kojihub --user=osbuild --password=osbuildpass --authtype=password hello
|
koji --server=http://localhost:8080/kojihub --user=osbuild --password=osbuildpass --authtype=password hello
|
||||||
|
|
||||||
greenprint "Copying credentials, certificates and configuration files"
|
greenprint "Copying credentials, certificates and configuration files"
|
||||||
sudo /usr/libexec/koji-osbuild-tests/copy-creds.sh /usr/share/koji-osbuild-tests
|
sudo -E /usr/libexec/koji-osbuild-tests/copy-creds.sh /usr/share/koji-osbuild-tests
|
||||||
|
|
||||||
greenprint "Starting mock OpenID server"
|
greenprint "Starting mock OpenID server"
|
||||||
sudo /usr/libexec/koji-osbuild-tests/run-openid.sh start
|
sudo /usr/libexec/koji-osbuild-tests/run-openid.sh start
|
||||||
|
|
@ -63,6 +63,9 @@ greenprint "Creating Koji tag infrastructure"
|
||||||
/usr/libexec/koji-osbuild-tests/make-tags.sh
|
/usr/libexec/koji-osbuild-tests/make-tags.sh
|
||||||
|
|
||||||
greenprint "Running integration tests"
|
greenprint "Running integration tests"
|
||||||
|
# export environment variables for the Boto3 client to work out of the box
|
||||||
|
AWS_ACCESS_KEY_ID="${V2_AWS_ACCESS_KEY_ID:-}" \
|
||||||
|
AWS_SECRET_ACCESS_KEY="${V2_AWS_SECRET_ACCESS_KEY:-}" \
|
||||||
python3 -m unittest discover -v /usr/libexec/koji-osbuild-tests/integration/
|
python3 -m unittest discover -v /usr/libexec/koji-osbuild-tests/integration/
|
||||||
|
|
||||||
greenprint "Stopping koji builder"
|
greenprint "Stopping koji builder"
|
||||||
|
|
|
||||||
|
|
@ -4,19 +4,38 @@
|
||||||
|
|
||||||
|
|
||||||
import functools
|
import functools
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
import platform
|
import platform
|
||||||
import unittest
|
import re
|
||||||
|
import shutil
|
||||||
import string
|
import string
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
import boto3
|
||||||
|
from botocore.config import Config as BotoConfig
|
||||||
|
from botocore.exceptions import ClientError as BotoClientError
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
logging.basicConfig(format = '%(asctime)s %(levelname)s: %(message)s', level = logging.INFO)
|
||||||
|
|
||||||
|
|
||||||
def koji_command(*args, _input=None, _globals=None, **kwargs):
|
def koji_command(*args, _input=None, _globals=None, **kwargs):
|
||||||
|
return koji_command_cwd(*args, _input=_input, _globals=_globals, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def koji_command_cwd(*args, cwd=None, _input=None, _globals=None, **kwargs):
|
||||||
args = list(args) + [f'--{k}={v}' for k, v in kwargs.items()]
|
args = list(args) + [f'--{k}={v}' for k, v in kwargs.items()]
|
||||||
if _globals:
|
if _globals:
|
||||||
args = [f'--{k}={v}' for k, v in _globals.items()] + args
|
args = [f'--{k}={v}' for k, v in _globals.items()] + args
|
||||||
cmd = ["koji"] + args
|
cmd = ["koji"] + args
|
||||||
print(cmd)
|
logger.info("Running %s", str(cmd))
|
||||||
return subprocess.run(cmd,
|
return subprocess.run(cmd,
|
||||||
|
cwd=cwd,
|
||||||
encoding="utf-8",
|
encoding="utf-8",
|
||||||
stdout=subprocess.PIPE,
|
stdout=subprocess.PIPE,
|
||||||
stderr=subprocess.STDOUT,
|
stderr=subprocess.STDOUT,
|
||||||
|
|
@ -92,16 +111,28 @@ class SutInfo:
|
||||||
|
|
||||||
|
|
||||||
class TestIntegration(unittest.TestCase):
|
class TestIntegration(unittest.TestCase):
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
global_args = dict(
|
self.koji_global_args = dict(
|
||||||
server="http://localhost:8080/kojihub",
|
server="http://localhost:8080/kojihub",
|
||||||
|
topurl="http://localhost:8080/kojifiles",
|
||||||
user="kojiadmin",
|
user="kojiadmin",
|
||||||
password="kojipass",
|
password="kojipass",
|
||||||
authtype="password")
|
authtype="password")
|
||||||
self.koji = functools.partial(koji_command,
|
self.koji = functools.partial(koji_command,
|
||||||
"osbuild-image",
|
"osbuild-image",
|
||||||
_globals=global_args)
|
_globals=self.koji_global_args)
|
||||||
|
|
||||||
|
self.workdir = tempfile.mkdtemp()
|
||||||
|
# EC2 image ID to clean up in tearDown() if set to a value
|
||||||
|
self.ec2_image_id = None
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
shutil.rmtree(self.workdir)
|
||||||
|
if self.ec2_image_id is not None:
|
||||||
|
self.delete_ec2_image(self.ec2_image_id)
|
||||||
|
self.ec2_image_id = None
|
||||||
|
|
||||||
def check_res(self, res: subprocess.CompletedProcess):
|
def check_res(self, res: subprocess.CompletedProcess):
|
||||||
if res.returncode != 0:
|
if res.returncode != 0:
|
||||||
|
|
@ -117,6 +148,55 @@ class TestIntegration(unittest.TestCase):
|
||||||
"\n error: " + res.stdout)
|
"\n error: " + res.stdout)
|
||||||
self.fail(msg)
|
self.fail(msg)
|
||||||
|
|
||||||
|
def task_id_from_res(self, res: subprocess.CompletedProcess) -> str:
|
||||||
|
"""
|
||||||
|
Extract the Task ID from `koji osbuild-image` command output and return it.
|
||||||
|
"""
|
||||||
|
r = re.compile(r'^Created task:[ \t]+(\d+)$', re.MULTILINE)
|
||||||
|
m = r.search(res.stdout)
|
||||||
|
if not m:
|
||||||
|
self.fail("Could not find task id in output")
|
||||||
|
return m.group(1)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_ec2_client():
|
||||||
|
aws_region = os.getenv("AWS_REGION")
|
||||||
|
return boto3.client('ec2', config=BotoConfig(region_name=aws_region))
|
||||||
|
|
||||||
|
def check_ec2_image_exists(self, image_id: str) -> None:
|
||||||
|
"""
|
||||||
|
Check if an EC2 image with the given ID exists.
|
||||||
|
If not, fail the test case.
|
||||||
|
"""
|
||||||
|
client = self.get_ec2_client()
|
||||||
|
try:
|
||||||
|
resp = client.describe_images(ImageIds=[image_id])
|
||||||
|
except BotoClientError as e:
|
||||||
|
self.fail(str(e))
|
||||||
|
self.assertEqual(len(resp["Images"]), 1)
|
||||||
|
|
||||||
|
def delete_ec2_image(self, image_id: str) -> None:
|
||||||
|
client = self.get_ec2_client()
|
||||||
|
# first get the snapshot ID associated with the image
|
||||||
|
try:
|
||||||
|
resp = client.describe_images(ImageIds=[image_id])
|
||||||
|
except BotoClientError as e:
|
||||||
|
self.fail(str(e))
|
||||||
|
self.assertEqual(len(resp["Images"]), 1)
|
||||||
|
|
||||||
|
snapshot_id = resp["Images"][0]["BlockDeviceMappings"][0]["Ebs"]["SnapshotId"]
|
||||||
|
# deregister the image
|
||||||
|
try:
|
||||||
|
resp = client.deregister_image(ImageId=image_id)
|
||||||
|
except BotoClientError as e:
|
||||||
|
self.logger.warning("Failed to deregister image %s: %s", image_id, str(e))
|
||||||
|
|
||||||
|
# delete the associated snapshot
|
||||||
|
try:
|
||||||
|
resp = client.delete_snapshot(SnapshotId=snapshot_id)
|
||||||
|
except BotoClientError as e:
|
||||||
|
self.logger.warning("Failed to delete snapshot %s: %s", snapshot_id, str(e))
|
||||||
|
|
||||||
def test_compose(self):
|
def test_compose(self):
|
||||||
"""Successful compose"""
|
"""Successful compose"""
|
||||||
# Simple test of a successful compose of RHEL
|
# Simple test of a successful compose of RHEL
|
||||||
|
|
@ -155,3 +235,67 @@ class TestIntegration(unittest.TestCase):
|
||||||
"UNKNOWNTAG",
|
"UNKNOWNTAG",
|
||||||
sut_info.os_arch)
|
sut_info.os_arch)
|
||||||
self.check_fail(res)
|
self.check_fail(res)
|
||||||
|
|
||||||
|
def test_cloud_upload_aws(self):
|
||||||
|
"""Successful compose with cloud upload to AWS"""
|
||||||
|
sut_info = SutInfo()
|
||||||
|
|
||||||
|
repos = []
|
||||||
|
for repo in sut_info.testing_repos():
|
||||||
|
url = repo["url"]
|
||||||
|
package_sets = repo.get("package_sets")
|
||||||
|
repos += ["--repo", url]
|
||||||
|
if package_sets:
|
||||||
|
repos += ["--repo-package-sets", package_sets]
|
||||||
|
|
||||||
|
package = "aws"
|
||||||
|
aws_region = os.getenv("AWS_REGION")
|
||||||
|
|
||||||
|
upload_options = {
|
||||||
|
"region": aws_region,
|
||||||
|
"share_with_accounts": [os.getenv("AWS_API_TEST_SHARE_ACCOUNT")]
|
||||||
|
}
|
||||||
|
|
||||||
|
upload_options_file = os.path.join(self.workdir, "upload_options.json")
|
||||||
|
with open(upload_options_file, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(upload_options, f)
|
||||||
|
|
||||||
|
res = self.koji(package,
|
||||||
|
sut_info.os_version_major,
|
||||||
|
sut_info.composer_distro_name,
|
||||||
|
sut_info.koji_tag,
|
||||||
|
sut_info.os_arch,
|
||||||
|
"--wait",
|
||||||
|
*repos,
|
||||||
|
f"--image-type={package}",
|
||||||
|
f"--upload-options={upload_options_file}")
|
||||||
|
self.check_res(res)
|
||||||
|
|
||||||
|
task_id = self.task_id_from_res(res)
|
||||||
|
# Download files uploaded by osbuild plugins to the Koji build task.
|
||||||
|
# requires koji client of version >= 1.29.1
|
||||||
|
res_download = koji_command_cwd(
|
||||||
|
"download-task", "--all", task_id, cwd=self.workdir, _globals=self.koji_global_args
|
||||||
|
)
|
||||||
|
self.check_res(res_download)
|
||||||
|
|
||||||
|
# Extract information about the uploaded AMI from compose status response.
|
||||||
|
compose_status_file = os.path.join(self.workdir, "compose-status.noarch.json")
|
||||||
|
with open(compose_status_file, "r", encoding="utf-8") as f:
|
||||||
|
compose_status = json.load(f)
|
||||||
|
|
||||||
|
self.assertEqual(compose_status["status"], "success")
|
||||||
|
image_statuses = compose_status["image_statuses"]
|
||||||
|
self.assertEqual(len(image_statuses), 1)
|
||||||
|
|
||||||
|
upload_status = image_statuses[0]["upload_status"]
|
||||||
|
self.assertEqual(upload_status["status"], "success")
|
||||||
|
self.assertEqual(upload_status["type"], "aws")
|
||||||
|
|
||||||
|
upload_options = upload_status["options"]
|
||||||
|
self.assertEqual(upload_options["region"], aws_region)
|
||||||
|
|
||||||
|
image_id = upload_options["ami"]
|
||||||
|
self.assertNotEqual(len(image_id), 0)
|
||||||
|
self.ec2_image_id = image_id
|
||||||
|
self.check_ec2_image_exists(image_id)
|
||||||
|
|
|
||||||
|
|
@ -26,4 +26,6 @@ $KOJI add-pkg --owner kojiadmin "${TAG_CANDIDATE}" rhel-guest
|
||||||
|
|
||||||
$KOJI add-pkg --owner kojiadmin "${TAG_CANDIDATE}" fedora-iot
|
$KOJI add-pkg --owner kojiadmin "${TAG_CANDIDATE}" fedora-iot
|
||||||
|
|
||||||
|
$KOJI add-pkg --owner kojiadmin "${TAG_CANDIDATE}" aws
|
||||||
|
|
||||||
$KOJI regen-repo "${TAG_BUILD}"
|
$KOJI regen-repo "${TAG_BUILD}"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue