Issues (70)

utils/build_ds_container.py (1 issue)

Severity
1
#!/usr/bin/python3
2
3
import argparse
4
import logging as log
5
import os
6
import shutil
7
import subprocess
8
import sys
9
import tempfile
10
import time
11
import uuid
12
13
import yaml
14
15
16
DESCRIPTION = '''
17
A tool for building compliance content for Kubernetes clusters.
18
19
This tool supports building ComplianceAsCode content locally or remotely on an
20
existing kubernetes cluster. This script requires Git, OpenShift CLI,
21
Kubernetes CLI, Podman CLI, and PyYAML.
22
23
'''
24
25
26
parser = argparse.ArgumentParser(
27
    formatter_class=argparse.ArgumentDefaultsHelpFormatter,
28
    description=DESCRIPTION)
29
parser.add_argument(
30
    '-n', '--namespace',
31
    help='Build image in the given namespace.',
32
    default='openshift-compliance')
33
parser.add_argument(
34
    '-p', '--create-profile-bundles',
35
    help='Create ProfileBundle objects for the image.',
36
    action='store_true',
37
    default=False)
38
parser.add_argument(
39
    '-c', '--build-in-cluster',
40
    help=(
41
        'Build content in-cluster. Note that this option ignores the '
42
        '--product and --debug flags.'),
43
    action='store_true',
44
    default=False)
45
parser.add_argument(
46
    '-d', '--debug',
47
    help=(
48
        'Provide debug output during the build process. This option is '
49
        'ignored when building content in the cluster using '
50
        '--build-in-cluster.'),
51
    action='store_true',
52
    default=False)
53
parser.add_argument(
54
    '-P', '--product',
55
    help=(
56
        'The product(s) to build. This option can be provided multiple times. '
57
        'This option is ignored when building content in the cluster using '
58
        '--build-in-cluster.'),
59
    default=['ocp4', 'rhcos4'],
60
    dest='products',
61
    nargs='*')
62
parser.add_argument(
63
    '-r', '--repository',
64
    help=(
65
        'The container image repository to use for images containing built '
66
        'content. Images pushed to this repository must have a tag, which '
67
        'you can specify with the --container-image-tag argument. If you do '
68
        'not supply a tag, one will be generated for you. It is recommended '
69
        'that you properly tag built images for production use. '
70
        'Auto-generated tags are primarily useful for development workflows. '
71
        'This script assumes you have authenticated to the image repository '
72
        'if necessary (e.g, `podman login quay.io`).'))
73
parser.add_argument(
74
    '-t', '--container-image-tag',
75
    help='A unique tag for the container image.')
76
args = parser.parse_args()
77
78
CAPTURE_OUTPUT = not args.debug
79
DEBUG_LEVEL = log.INFO
80
if args.debug:
81
    DEBUG_LEVEL = log.DEBUG
82
83
84
log.basicConfig(
85
    format='%(asctime)s:%(levelname)s: %(message)s', level=DEBUG_LEVEL)
86
87
88
# FIXME(lbragstad): Remove this and replace it with operating system utils if
89
# possible.
90
command = ['git', 'rev-parse', '--show-toplevel']
91
result = subprocess.run(command, capture_output=True, check=True)
92
# Convert the file path from bytes to unicode since we might manipulate it
93
# later. Also, strip off any newlines.
94
REPO_PATH = result.stdout.decode().strip()
95
96
97
def ensure_namespace_exists():
98
    """Function that ensures there is a namespace for the content."""
99
    if args.namespace == 'openshift-compliance':
100
        namespace_file = os.path.join(
101
            REPO_PATH, 'ocp-resources', 'compliance-operator-ns.yaml')
102
        subprocess.run(
103
            ['oc', 'apply', '-f', namespace_file],
104
            capture_output=CAPTURE_OUTPUT)
105
        log.debug(f'Created namespace {args.namespace}')
106
    else:
107
        log.debug(f'Assuming {args.namespace} namespace exists.')
108
109
110
def build_content_locally(products):
111
    """Build compliance content locally for a list of products.
112
113
    :param products: list of strings
114
    """
115
116
    build_binary_path = os.path.join(REPO_PATH, 'build_product')
117
    command = [build_binary_path] + products
118
    if args.debug:
119
        command.append('--debug')
120
    subprocess.run(command, check=True, capture_output=CAPTURE_OUTPUT)
121
    log.info(f'Successfully built content for {", ".join(products)}')
122
123
124
# NOTE(lbragstad): This could use something like podman-py.
125
def build_container_image():
126
    """Use Podman to build a container image for the content."""
127
    dockerfile_path = os.path.join(
128
        REPO_PATH, '/Dockerfiles/compliance_operator_content.Dockerfile')
129
    command = [
130
        'podman', 'build', '-f', dockerfile_path, '-t',
131
        'localcontentbuild:latest', '.']
132
    subprocess.run(command, check=True, capture_output=CAPTURE_OUTPUT)
133
    log.info('Successfully built container image')
134
135
136
# NOTE(lbragstad): This could use something like podman-py.
137
def push_container_to_repository(repository, tag):
138
    """Push a container image to an image repository.
139
140
    :param repository: string representing the location of the repository
141
    :param tag: string representing the container image tag
142
    """
143
    repository_string = repository + ':' + tag
144
    command = [
145
        'podman', 'push', 'localhost/localcontentbuild:latest',
146
        repository_string]
147
    result = subprocess.run(command, capture_output=True)
148
    if result.returncode == 0:
149
        log.info(f'Pushed image to {repository}:{tag}')
150
    elif result.returncode == 125:
151
        log.error(
152
            'Failed to push container image due to authentication issues. '
153
            'Make sure you have authenticated to the registry before '
154
            'running this script.')
155
        sys.exit(2)
156
    else:
157
        log.error(f'Failed to push container image to {repository_string}')
158
        sys.exit(2)
159
160
161
def setup_remote_build_resources():
162
    """Create an ImageStream and BuildConfig to build content in cluster.
163
164
    This method is only intended to run on OpenShift clusters since it relies
165
    on ImageStreams and BuildConfigs, which are not available in vanilla
166
    Kubernetes deployments.
167
    """
168
    remote_resources_file = os.path.join(
169
        REPO_PATH, 'ocp-resources', 'ds-build-remote.yaml')
170
    command = ['oc', 'apply', '-n', args.namespace, '-f', remote_resources_file]
171
    subprocess.run(command, check=True, capture_output=CAPTURE_OUTPUT)
172
173
174
def setup_local_build_resources():
175
    """Create an ImageStream and BuildConfig to use locally built content.
176
177
    This method is only intended to run on OpenShift clusters since it relies
178
    on ImageStreams and BuildConfigs, which are not available in vanilla
179
    Kubernetes deployments.
180
    """
181
    local_resources_file = os.path.join(
182
        REPO_PATH, 'ocp-resources', 'ds-from-local-build.yaml')
183
    command = ['oc', 'apply', '-n', args.namespace, '-f', local_resources_file]
184
    subprocess.run(command, check=True, capture_output=CAPTURE_OUTPUT)
185
186
187
def copy_build_files_to_output_directory(output_directory):
188
    """Copy build resources to a dedicated directory.
189
190
    :param output_directory: string representing the directory path
191
    """
192
    build_directory = os.path.join(REPO_PATH, 'build')
193
    for f in os.listdir(build_directory):
194
        filepath = os.path.join(build_directory, f)
195
        if os.path.isfile(filepath) and filepath.endswith('-ds.xml'):
196
            shutil.copy(filepath, output_directory)
197
198
199
def start_build(output_directory):
200
    """Start an OpenShift build for OpenSCAP content.
201
202
    :param output_directory: string representing the directory path
203
    """
204
    command = [
205
        'oc', 'start-build', '-n', args.namespace, 'openscap-ocp4-ds',
206
        '--from-dir=%s' % output_directory]
207
    subprocess.run(command, check=True, capture_output=CAPTURE_OUTPUT)
208
209
210
def create_profile_bundles(products, content_image=None):
211
    """Create ProfileBundle custom resources for built content.
212
213
    :param products: a list of strings
214
    :param content_image: a string representing the repository and tag for
215
                          content (optional)
216
    """
217
    for product in products:
218
        content_file = 'ssg-' + product + '-ds.xml'
219
        product_name = 'upstream-' + product
220
        profile_bundle_update = {
221
            'apiVersion': 'compliance.openshift.io/v1alpha1',
222
            'kind': 'ProfileBundle',
223
            'metadata': {'name': product_name},
224
            'spec': {
225
                'contentImage': content_image or 'openscap-ocp4-ds:latest',
226
                'contentFile': content_file}}
227
        with tempfile.NamedTemporaryFile() as f:
228
            yaml.dump(profile_bundle_update, f, encoding='utf-8')
229
            command = ['kubectl', 'apply', '-n', args.namespace, '-f', f.name]
230
            subprocess.run(command, check=True, capture_output=CAPTURE_OUTPUT)
231
    log.info(f'Created profile bundles for {", ".join(products)}')
232
233
234
def get_latest_build():
235
    """Return the latest version of a build.
236
237
    :returns: a string representing the latest version of the build, typically
238
              an integer
239
    """
240
    command = [
241
        'oc', 'get', 'buildconfigs', '-n', args.namespace, '--no-headers',
242
        'openscap-ocp4-ds', '-o', 'custom-columns=status:.status.lastVersion']
243
    build = subprocess.run(command, check=True, capture_output=True).stdout
244
    return build.decode().strip()
245
246
247
def get_build_status(build):
248
    """Return the status of a build.
249
250
    :param build: a string representing the build version
251
    :returns: a string representing the build status
252
    """
253
    command = [
254
        'oc', 'get', '-n', args.namespace, 'builds',
255
        'openscap-ocp4-ds-' + build, '--no-headers', '-o',
256
        'custom-columns=status:.status.phase']
257
    status = subprocess.run(command, check=True, capture_output=True).stdout
258
    return status.decode().strip()
259
260
261
def get_image_repository():
262
    """Return the image repository for an ImageStream containing content.
263
264
    :returns: a string representing the image repository and tag
265
    """
266
    command = [
267
        'oc', 'get', '-n', args.namespace, 'imagestreams', 'openscap-ocp4-ds',
268
        '--no-headers', '-o', 'custom-columns=repo:.status.dockerImageRepository']
269
    image_repo = subprocess.run(command, check=True, capture_output=True).stdout
270
    return image_repo.decode().strip()
271
272
273
log.info(f'Building content for {", ".join(args.products)}')
274
ensure_namespace_exists()
275
276
277
if args.repository:
278
    build_content_locally(args.products)
279
    build_container_image()
280
281
    # generate a container image tag if the user didn't supply one
282
    if args.container_image_tag:
283
        tag = args.container_image_tag
284
    else:
285
        tag = uuid.uuid4().hex[:16]
286
287
    push_container_to_repository(args.repository, tag)
288
    content_image = args.repository + ':' + tag
289
    if args.create_profile_bundles:
290
        create_profile_bundles(args.products, content_image=content_image)
291
    sys.exit(0)
292
elif args.build_in_cluster:
293
    setup_remote_build_resources()
294
    output_directory = REPO_PATH
295
else:
296
    build_content_locally(args.products)
297
    setup_local_build_resources()
298
    output_directory = tempfile.mkdtemp()
299
    copy_build_files_to_output_directory(output_directory)
300
301
302
start_build(output_directory)
0 ignored issues
show
The variable output_directory does not seem to be defined for all execution paths.
Loading history...
303
304
# clean up output directory for local builds
305
if not args.build_in_cluster:
306
    shutil.rmtree(output_directory)
307
308
# wait some time before asking for build status
309
time.sleep(5)
310
311
BUILD = get_latest_build()
312
313
while True:
314
    status = get_build_status(BUILD)
315
    if status == 'Complete':
316
        image = get_image_repository()
317
        log.info(f'Your image is available at {image}')
318
        break
319
    elif status == 'Error':
320
        log.error(f'Build openscap-ocp-ds-{BUILD} failed. Check the log for more information')
321
        sys.exit(1)
322
    log.info('Build status: %s', status)
323
    polling_interval_second = 3
324
    # allow at least a little time before we fetch the status
325
    time.sleep(polling_interval_second)
326
327
if args.create_profile_bundles:
328
    create_profile_bundles(args.products)
329