Skip to content

Commit

Permalink
Make builder_ca_bundler configurable in reactor config
Browse files Browse the repository at this point in the history
* CLOUDBLD-4345

* The plugin's run method is refactored in order to avoid passing many
  argument to a function, the original function
  add_yum_repos_to_dockerfile.
* Tests are updated according to the refactor.
* New test case is added for the optional builder_ca_bundle.

Signed-off-by: Chenxiong Qi <cqi@redhat.com>
  • Loading branch information
tkdchen committed Mar 9, 2021
1 parent 6ab866f commit 1ce0c8d
Show file tree
Hide file tree
Showing 4 changed files with 170 additions and 93 deletions.
150 changes: 88 additions & 62 deletions atomic_reactor/plugins/pre_inject_yum_repo.py
Expand Up @@ -11,64 +11,64 @@
import os
import shutil
from io import StringIO

from atomic_reactor.constants import YUM_REPOS_DIR, RELATIVE_REPOS_PATH, INSPECT_CONFIG
from atomic_reactor.plugin import PreBuildPlugin
from atomic_reactor.plugins.pre_reactor_config import get_builder_ca_bundle
from atomic_reactor.util import df_parser
from atomic_reactor.utils.yum import YumRepo

BUILDER_CA_BUNDLE = '/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem'
CA_BUNDLE_PEM = os.path.basename(BUILDER_CA_BUNDLE)


def add_yum_repos_to_dockerfile(repo_dir, yumrepos, df, inherited_user, base_from_scratch):
if df.baseimage is None:
raise RuntimeError("No FROM line in Dockerfile")

# Determine the USER the final image should end with
final_user_line = "USER " + inherited_user if inherited_user else None
# Look for the last USER after the last FROM... by looking in reverse
for insndesc in reversed(df.structure):
if insndesc['instruction'] == 'USER':
final_user_line = insndesc['content'] # we will reuse the line verbatim
break
if insndesc['instruction'] == 'FROM':
break # no USER specified in final stage

shutil.copyfile(BUILDER_CA_BUNDLE, os.path.join(repo_dir, CA_BUNDLE_PEM))

# Insert the ADD line at the beginning of each stage
df.add_lines(
f'ADD {CA_BUNDLE_PEM} /tmp/{CA_BUNDLE_PEM}',
"ADD %s* %s" % (RELATIVE_REPOS_PATH, YUM_REPOS_DIR),
all_stages=True, at_start=True, skip_scratch=True
)

# Insert line(s) to remove the repos
cleanup_lines = [
"RUN rm -f " + " ".join(["'%s'" % repo for repo in yumrepos]),
f'RUN rm -f /tmp/{CA_BUNDLE_PEM}',
]
# If needed, change to root in order to RUN rm, then change back.
if final_user_line:
cleanup_lines.insert(0, "USER root")
cleanup_lines.append(final_user_line)

if not base_from_scratch:
df.add_lines(*cleanup_lines)


class InjectYumRepoPlugin(PreBuildPlugin):
key = "inject_yum_repo"
is_allowed_to_fail = False

def run(self):
"""
run the plugin
"""
yum_repos = {k: v for k, v in self.workflow.files.items() if k.startswith(YUM_REPOS_DIR)}
if not yum_repos:
return
# absolute path in containers -> relative path within context
def _final_user_line(self):
user = self._find_final_user()
if user:
return f'USER {user}'

builder = self.workflow.builder
if not builder.dockerfile_images.base_from_scratch:
inspect = builder.base_image_inspect
user = inspect.get(INSPECT_CONFIG).get('User')
if user:
return f'USER {user}'

return ''

def _find_final_user(self):
"""Find the user in USER instruction in the last build stage"""
for insndesc in reversed(self._dockerfile.structure):
if insndesc['instruction'] == 'USER':
return insndesc['content'] # we will reuse the line verbatim
if insndesc['instruction'] == 'FROM':
break # no USER specified in final stage

def _cleanup_lines(self):
lines = [
"RUN rm -f " + " ".join(
(f"'{repo_file}'" for repo_file in self.workflow.files)
)
]
if self._builder_ca_bundle:
lines.append(f'RUN rm -f /tmp/{self._ca_bundle_pem}')

final_user_line = self._final_user_line()
if final_user_line:
lines.insert(0, "USER root")
lines.append(final_user_line)

return lines

def __init__(self, tasker, workflow, *args, **kwargs):
super().__init__(tasker, workflow, *args, **kwargs)
self._builder_ca_bundle = None
self._ca_bundle_pem = None
self._dockerfile = None

def _inject_into_repo_files(self):
"""Inject repo files into a relative directory inside the build context"""
host_repos_path = os.path.join(self.workflow.builder.df_dir, RELATIVE_REPOS_PATH)
self.log.info("creating directory for yum repos: %s", host_repos_path)
os.mkdir(host_repos_path)
Expand All @@ -80,26 +80,52 @@ def run(self):
for line in input_buf:
updated_buf.write(line)
# Apply sslcacert to every repo in a repofile
if line.lstrip().startswith('['):
updated_buf.write(f'sslcacert=/tmp/{CA_BUNDLE_PEM}\n')
if line.lstrip().startswith('[') and self._builder_ca_bundle:
updated_buf.write(f'sslcacert=/tmp/{self._ca_bundle_pem}\n')

yum_repo = YumRepo(repourl=repo_filename,
content=updated_buf.getvalue(),
dst_repos_dir=host_repos_path,
add_hash=False)
yum_repo.write_content()

# Find out the USER inherited from the base image
inspect = self.workflow.builder.base_image_inspect
inherited_user = ''
if not self.workflow.builder.dockerfile_images.base_from_scratch:
inherited_user = inspect.get(INSPECT_CONFIG).get('User', '')
df = df_parser(self.workflow.builder.df_path, workflow=self.workflow)
yum_repos = list(self.workflow.files)
base_from_scratch = self.workflow.builder.dockerfile_images.base_from_scratch
repo_dir = self.workflow.builder.df_dir
add_yum_repos_to_dockerfile(
repo_dir, yum_repos, df, inherited_user, base_from_scratch
def _inject_into_dockerfile(self):
self._dockerfile.add_lines(
"ADD %s* %s" % (RELATIVE_REPOS_PATH, YUM_REPOS_DIR),
all_stages=True, at_start=True, skip_scratch=True
)
for repo in yum_repos:

if self._builder_ca_bundle:
shutil.copyfile(
self._builder_ca_bundle,
os.path.join(self.workflow.builder.df_dir, self._ca_bundle_pem)
)
self._dockerfile.add_lines(
f'ADD {self._ca_bundle_pem} /tmp/{self._ca_bundle_pem}',
all_stages=True, at_start=True, skip_scratch=True
)

if not self.workflow.builder.dockerfile_images.base_from_scratch:
self._dockerfile.add_lines(*self._cleanup_lines())

def run(self):
"""
run the plugin
"""
yum_repos = {k: v for k, v in self.workflow.files.items() if k.startswith(YUM_REPOS_DIR)}
if not yum_repos:
return

self._dockerfile = df_parser(self.workflow.builder.df_path, workflow=self.workflow)
if self._dockerfile.baseimage is None:
raise RuntimeError("No FROM line in Dockerfile")

self._builder_ca_bundle = get_builder_ca_bundle(self.workflow, None)
if self._builder_ca_bundle:
self._ca_bundle_pem = os.path.basename(self._builder_ca_bundle)

self._inject_into_repo_files()
self._inject_into_dockerfile()

for repo in self.workflow.files:
self.log.info("injected yum repo: %s", repo)
4 changes: 4 additions & 0 deletions atomic_reactor/plugins/pre_reactor_config.py
Expand Up @@ -446,6 +446,10 @@ def get_image_size_limit(workflow):
}


def get_builder_ca_bundle(workflow, fallback=NO_FALLBACK):
return get_value(workflow, 'builder_ca_bundle', fallback)


class ClusterConfig(object):
"""
Configuration relating to a particular cluster
Expand Down
5 changes: 5 additions & 0 deletions atomic_reactor/schemas/config.json
Expand Up @@ -836,6 +836,11 @@
},
"additionalProperties": false,
"required": ["url"]
},
"builder_ca_bundle": {
"description": "An absolute path to the custom ca-bundle certificate inside the buildroot.",
"type": "string",
"examples": ["/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem"]
}
},
"required": ["version", "koji"]
Expand Down

0 comments on commit 1ce0c8d

Please sign in to comment.