Skip to content

Commit

Permalink
WIP: Port build_ds_container.sh to python
Browse files Browse the repository at this point in the history
The build_ds_container.sh script is useful for building content and
integrating it into your kubernetes clusters. Overtime, it's grown more
and more functionality, like building content in the cluster, locally,
or pushing built container images to repositories.

This commit attempts to reorganize the script and ports it to Python,
based on recommendations from the initial authors.

The primary changes include:

 - Using python kubernetes clients for interacting with clusters
 - Quieter logging by default, while maintaining verbosity using --debug
 - Auto-generated container image tags when publishing content to
   repositories (useful for development workflows)

This patch doesn't include support for OCP deployments as it was
originally developed to build content for EKS benchmarks. That
functionality will come in another revision of this patch and should
remain backwards compatible with `build_ds_container.sh`.

Signed-off-by: Lance Bragstad <lbragsta@redhat.com>
  • Loading branch information
rhmdnd committed Dec 10, 2021
1 parent 9133658 commit a0c378d
Showing 1 changed file with 254 additions and 0 deletions.
254 changes: 254 additions & 0 deletions utils/build_ds_container.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
#!/usr/bin/env python3

import argparse
import logging as log
import os
import subprocess
import sys
import tempfile
import uuid

from kubernetes import client, config
from kubernetes.client import exceptions

config.load_kube_config()


DESCRIPTION = '''
A tool for building compliance content for Kubernetes clusters.
This tool supports building ComplianceAsCode content locally or remotely on an
existing kubernetes cluster.
'''


parser = argparse.ArgumentParser(
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
description=DESCRIPTION)
parser.add_argument(
'-n', '--namespace',
help='Build image in the given namespace.',
default='openshift-compliance')
parser.add_argument(
'-p', '--create-profile-bundles',
help='Create ProfileBundle objects for the image.',
action='store_true',
default=False)
parser.add_argument(
'-c', '--build-in-cluster',
help=(
'Build content in-cluster. Note that this option ignores the '
'--product and --debug flags.'),
action='store_true',
default=False)
parser.add_argument(
'-d', '--debug',
help=(
'Provide debug output during the build process. This option is '
'ignored when building content in the cluster using '
'--build-in-cluster.'),
action='store_true',
default=False)
parser.add_argument(
'-P', '--product',
help=(
'The product(s) to build. This option can be provided multiple times. '
'This option is ignored when building content in the cluster using '
'--build-in-cluster.'),
default=['ocp4', 'rhcos4'],
dest='products',
nargs='*')
parser.add_argument(
'-r', '--repository',
help=(
'The container image repository to use for images containing built '
'content. Images pushed to this repository must have a tag, which '
'you can specify with the --container-image-tag argument. If you do '
'not supply a tag, one will be generated for you. It is recommended '
'that you properly tag built images for production use. '
'Auto-generated tags are primarily useful for development workflows. '
'This script assumes you have authenticated to the image repository '
'if necessary (e.g, `podman login`).'))
parser.add_argument(
'-t', '--container-image-tag',
help='A unique tag for the container image.')
args = parser.parse_args()


log.basicConfig(
format='%(asctime)s:%(levelname)s: %(message)s', level=log.INFO)


# FIXME(lbragstad): Remove this and replace it with operating system utils if
# possible.
result = subprocess.run(
['git', 'rev-parse', '--show-toplevel'], capture_output=True, check=True)
# Convert the file path from bytes to unicode since we might manipulate it
# later. Also, strip off any newlines.
REPO_PATH = result.stdout.decode().strip()


def ensure_namespace_exists(namespace):
"""Function that ensures there is a namespace for the content."""
with client.ApiClient() as api_client:
body = client.V1Namespace(
api_version='v1', kind='Namespace', metadata={'name': namespace})

try:
api = client.CoreV1Api(api_client)
api.create_namespace(body)
log.info('Created namespace %s', namespace)
except exceptions.ApiException as err:
# HTTP 409 - Conflict tells the namespace already exists. Let's
# handle this gracefully and move on, instead of failing. Any other
# error codes should be bubbled up to the end user.
if err.status == 409:
log.info('Reusing existing namespace: %s', namespace)
else:
raise err


def supports_custom_resource(resource_name):
with client.ApiClient() as api_client:
api = client.ApiextensionsV1Api(api_client)
try:
api.read_custom_resource_definition(resource_name)
return True
except exceptions.ApiException:
log.warning(
'Unable to determine if %s custom resources are supported in '
'this deployment', resource_name)
return False


def create_image_stream():
with client.ApiClient() as api_client:
body = client.V1CustomResourceDefinition(
api_version='image.openshift.io/v1',
kind='ImageStream',
metadata={'name': 'openscap-ocp4-ds'},
spec={'lookupPolicy': {'local': True}})

api = client.ApiextensionsV1Api(api_client)
api.create_custom_resource_definition(body)


def create_build_config(build_config):
# FIXME(lbragstad) This needs to be implemented against an OCP deployment.
pass


def build_content_locally(products, debug=False):
build_binary_path = os.path.join(REPO_PATH, 'build_product')
command = [build_binary_path] + products
capture_output = True
if debug:
command.append('--debug')
capture_output = False
subprocess.run(command, check=True, capture_output=capture_output)
log.info(f"Successfully built content for {', '.join(args.products)}")


def build_content_remotely(products):
pass


# FIXME(lbragstad): This should use something like podman-py.
def build_container(debug=False):
dockerfile = (
REPO_PATH + '/Dockerfiles/compliance_operator_content.Dockerfile')
command = [
'podman', 'build', '-f', dockerfile, '-t', 'localcontentbuild:latest',
'.']
capture_output = True
if debug:
capture_output = False
subprocess.run(command, check=True, capture_output=capture_output)
log.info('Successfully built container image')


# FIXME(lbragstad): This should use something like podman-py.
def push_container(repository, tag, debug=False):
repository_string = repository + ':' + tag
command = [
'podman', 'push', 'localhost/localcontentbuild:latest',
repository_string]
capture_output = True
if debug:
capture_output = False
result = subprocess.run(command, capture_output=capture_output)
if result.returncode == 0:
log.info(f'Pushed image to {repository}:{tag}')
elif result.returncode == 125:
log.error(
'Failed to push container image due to authentication issues. '
'Make sure you have authenticated to the registry before '
'running this script.')
sys.exit(2)
else:
log.error('Failed to push container image')
sys.exit(2)


def create_profile_bundles(products, namespace):
for product in products:
metadata = {'name': 'upstream-' + product}
spec = {
'contentImage': 'openscap-ocp4-ds:latest',
'contentFile': 'ssg-' + product + '-ds.xml'}

with client.ApiClient() as api_client:
body = {
'apiVersion': 'compliance.openshift.io/v1alpha1',
'kind': 'ProfileBundle',
'metadata': metadata,
'spec': spec}
api = client.CustomObjectsApi(api_client)
try:
api.create_namespaced_custom_object(
'compliance.openshift.io', 'v1alpha1', namespace,
'profilebundles', body)
log.info(f'Created profile bundle(s) for {product}')
except exceptions.ApiException as err:
if err.status == 409:
log.info(f'Reusing existing profile bundle for {product}')
continue
raise err


log.info(f'Building content for {", ".join(args.products)}')

working_directory = os.getcwd()
if args.build_in_cluster:
output_directory = REPO_PATH
else:
output_directory = tempfile.mkdtemp()

ensure_namespace_exists(args.namespace)

if not supports_custom_resource('ImageStream') and args.build_in_cluster:
log.error(
'ImageStream custom resources are not supported in this deployment, '
'unable to build content remotely')
sys.exit(1)


if args.build_in_cluster:
create_image_stream()
build_config = None
create_build_config(build_config)
build_content_remotely(args.products)
else:
build_content_locally(args.products, debug=args.debug)

if args.repository:
build_container(debug=args.debug)
if args.container_image_tag:
tag = args.container_image_tag
else:
tag = uuid.uuid4().hex[:16]
push_container(args.repository, tag, debug=args.debug)

if args.create_profile_bundles:
create_profile_bundles(args.products, args.namespace)

0 comments on commit a0c378d

Please sign in to comment.