-
Notifications
You must be signed in to change notification settings - Fork 706
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
WIP: Port build_ds_container.sh to python
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
Showing
1 changed file
with
256 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,256 @@ | ||
#!/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. | ||
''' | ||
|
||
HTTP_CONFLICT_STATUS_CODE = 409 | ||
|
||
|
||
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 us 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 == HTTP_CONFLICT_STATUS_CODE: | ||
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 == HTTP_CONFLICT_STATUS_CODE: | ||
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) |