Merge branch 'feature/ci-checking-openshift-config' into 'master'
Add CI to validate the test-cara openshift configuration See merge request cara/cara!223
This commit is contained in:
commit
234ca99d28
7 changed files with 325 additions and 6 deletions
|
|
@ -20,6 +20,45 @@ test_dev:
|
|||
extends: .acc_py_dev_test
|
||||
|
||||
|
||||
# A development installation of CARA tested with pytest.
|
||||
.test_openshift_config:
|
||||
rules:
|
||||
- if: '$OC_TOKEN && $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME == $BRANCH'
|
||||
allow_failure: false # The branch must represent what is deployed.
|
||||
- if: '$OC_TOKEN && $CI_MERGE_REQUEST_EVENT_TYPE != "detached"'
|
||||
allow_failure: true # Anything other than the branch may fail without blocking the pipeline.
|
||||
image: registry.cern.ch/docker.io/mambaorg/micromamba
|
||||
before_script:
|
||||
- micromamba create --yes -p $HOME/env python=3.9 ruamel.yaml wget -c conda-forge
|
||||
- export PATH=$HOME/env/bin/:$PATH
|
||||
- wget https://github.com/openshift/origin/releases/download/v3.11.0/openshift-origin-client-tools-v3.11.0-0cbc58b-linux-64bit.tar.gz
|
||||
- tar xzf ./openshift-origin-client-tools-v3.11.0-0cbc58b-linux-64bit.tar.gz
|
||||
- mv openshift-origin-client-tools-v3.11.0-0cbc58b-linux-64bit/oc $HOME/env/bin/
|
||||
|
||||
script:
|
||||
- cd ./app-config/openshift
|
||||
- oc login ${OC_SERVER} --token="${OC_TOKEN}"
|
||||
- python ./config-fetch.py ${CARA_INSTANCE} --output-directory ./${CARA_INSTANCE}/actual
|
||||
- python ./config-generate.py ${CARA_INSTANCE} --output-directory ./${CARA_INSTANCE}/expected
|
||||
- python ./config-normalise.py ./${CARA_INSTANCE}/actual ./${CARA_INSTANCE}/actual-normed
|
||||
- python ./config-normalise.py ./${CARA_INSTANCE}/expected ./${CARA_INSTANCE}/expected-normed
|
||||
- diff -u ./test-cara/actual-normed/ ./${CARA_INSTANCE}/expected-normed/
|
||||
|
||||
artifacts:
|
||||
paths:
|
||||
- ./app-config/openshift/${CARA_INSTANCE}/actual
|
||||
- ./app-config/openshift/${CARA_INSTANCE}/expected
|
||||
|
||||
|
||||
check_openshift_config_test-cara:
|
||||
extends: .test_openshift_config
|
||||
variables:
|
||||
CARA_INSTANCE: 'test-cara'
|
||||
BRANCH: 'live/test-cara'
|
||||
OC_SERVER: openshift-dev.cern.ch
|
||||
OC_TOKEN: "${OPENSHIFT_CONFIG_CHECKER_TOKEN_TEST_CARA}"
|
||||
|
||||
|
||||
# A development installation of CARA tested with pytest.
|
||||
test_dev-39:
|
||||
variables:
|
||||
|
|
@ -86,3 +125,4 @@ oci_calculator:
|
|||
script:
|
||||
- echo "{\"auths\":{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_REGISTRY_PASSWORD\"}}}" > /kaniko/.docker/config.json
|
||||
- /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile --destination $CI_REGISTRY_IMAGE/calculator:latest
|
||||
|
||||
|
|
|
|||
20
README.md
20
README.md
|
|
@ -194,10 +194,12 @@ If you need to create the application in a new project, run:
|
|||
```console
|
||||
$ cd app-config/openshift
|
||||
|
||||
$ oc process -f application.yaml --param PROJECT_NAME='test-cara' --param GIT_BRANCH='live/test-cara' | oc create -f -
|
||||
$ oc process -f routes.yaml --param HOST='test-cara.web.cern.ch' | oc create -f -
|
||||
$ oc process -f configmap.yaml | oc create -f -
|
||||
$ oc process -f services.yaml | oc create -f -
|
||||
$ oc process -f route.yaml --param HOST='test-cara.web.cern.ch' | oc create -f -
|
||||
$ oc process -f imagestreams.yaml | oc create -f -
|
||||
$ oc process -f buildconfig.yaml --param GIT_BRANCH='live/test-cara' | oc create -f -
|
||||
$ oc process -f deploymentconfig.yaml --param PROJECT_NAME='test-cara' | oc create -f -
|
||||
```
|
||||
|
||||
Then, create the webhook secret to be able to trigger automatic builds from GitLab.
|
||||
|
|
@ -211,6 +213,17 @@ $ oc create secret generic \
|
|||
gitlab-cara-webhook-secret
|
||||
```
|
||||
|
||||
For CI usage, we also suggest creating a service account:
|
||||
|
||||
```console
|
||||
oc create sa gitlab-config-checker
|
||||
```
|
||||
|
||||
Under ``Resources`` -> ``Membership`` enable the ``View`` role for this new service account.
|
||||
|
||||
To get this new user's authentication token go to ``Resources`` -> ``Secrets`` and locate the token in the newly
|
||||
created secret associated with the user (in this case ``gitlab-config-checker-token-XXXX``).
|
||||
|
||||
### CERN SSO integration
|
||||
|
||||
The SSO integration uses OpenID credentials configured in [CERN Applications portal](https://application-portal.web.cern.ch/).
|
||||
|
|
@ -261,7 +274,7 @@ $ cd app-config/openshift
|
|||
|
||||
$ oc process -f configmap.yaml | oc replace -f -
|
||||
$ oc process -f services.yaml | oc replace -f -
|
||||
$ oc process -f route.yaml --param HOST='test-cara.web.cern.ch' | oc replace -f -
|
||||
$ oc process -f routes.yaml --param HOST='test-cara.web.cern.ch' | oc replace -f -
|
||||
$ oc process -f imagestreams.yaml | oc replace -f -
|
||||
$ oc process -f buildconfig.yaml --param GIT_BRANCH='live/test-cara' | oc replace -f -
|
||||
$ oc process -f deploymentconfig.yaml --param PROJECT_NAME='test-cara' | oc replace -f -
|
||||
|
|
@ -269,4 +282,3 @@ $ oc process -f deploymentconfig.yaml --param PROJECT_NAME='test-cara' | oc repl
|
|||
|
||||
Be aware that if you change/replace the **route** of the PROD instance,
|
||||
it will lose the annotation to be exposed outside CERN (not committed in this repo).
|
||||
|
||||
|
|
|
|||
72
app-config/openshift/config-fetch.py
Normal file
72
app-config/openshift/config-fetch.py
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
import argparse
|
||||
import pathlib
|
||||
import subprocess
|
||||
import sys
|
||||
import typing
|
||||
|
||||
|
||||
def configure_parser(parser: argparse.ArgumentParser) -> None:
|
||||
parser.description = "Fetch the openshift config for CARA"
|
||||
parser.set_defaults(handler=handler)
|
||||
parser.add_argument(
|
||||
"instance", choices=['cara', 'test-cara'],
|
||||
help="Pick the instance for which you want to fetch the config",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-o", "--output-directory", default='config',
|
||||
help="Location to put the config files",
|
||||
)
|
||||
|
||||
|
||||
def get_oc_server() -> typing.Optional[str]:
|
||||
# Return the openshift server that is currently logged in, or None if not logged in
|
||||
# (or other issues getting the information from the oc client).
|
||||
try:
|
||||
subprocess.check_output(['oc', 'whoami'], stderr=subprocess.PIPE)
|
||||
except subprocess.CalledProcessError:
|
||||
# User not logged on, or oc command missing.
|
||||
return None
|
||||
|
||||
return subprocess.run([
|
||||
'oc', 'whoami', '--show-server'
|
||||
], check=True, stdout=subprocess.PIPE).stdout.decode().strip()
|
||||
|
||||
|
||||
def fetch_config(output_directory: pathlib.Path):
|
||||
output_directory.mkdir(exist_ok=True, parents=True)
|
||||
|
||||
for component in ['routes', 'configmap', 'services', 'imagestreams', 'buildconfig', 'deploymentconfig']:
|
||||
with (output_directory / f'{component}.yaml').open('wt') as fh:
|
||||
cmd = ['oc', 'get', '--export', '-o', 'yaml', component]
|
||||
print(f'Running: {" ".join(cmd)}')
|
||||
subprocess.run(cmd, stdout=fh, check=True)
|
||||
print(f'Config in: {output_directory.absolute()}')
|
||||
|
||||
|
||||
def handler(args: argparse.ArgumentParser) -> None:
|
||||
if args.instance == 'cara':
|
||||
login_server = 'https://openshift.cern.ch:443'
|
||||
project_name = 'cara'
|
||||
elif args.instance == 'test-cara':
|
||||
login_server = 'https://openshift-dev.cern.ch:443'
|
||||
project_name = 'test-cara'
|
||||
|
||||
actual_login_server = get_oc_server()
|
||||
if actual_login_server != login_server:
|
||||
print(f'\nPlease login to the correct openshift server with: \n\n oc login {login_server}\n', file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
subprocess.run(['oc', 'project', project_name], stdout=subprocess.DEVNULL, check=True)
|
||||
|
||||
fetch_config(pathlib.Path(args.output_directory))
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
configure_parser(parser)
|
||||
args = parser.parse_args()
|
||||
args.handler(args)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
63
app-config/openshift/config-generate.py
Normal file
63
app-config/openshift/config-generate.py
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import argparse
|
||||
import pathlib
|
||||
import subprocess
|
||||
import typing
|
||||
|
||||
|
||||
def configure_parser(parser: argparse.ArgumentParser) -> None:
|
||||
parser.description = "Generate the config files which can be later submitted to openshift"
|
||||
parser.set_defaults(handler=handler)
|
||||
parser.add_argument(
|
||||
"instance", choices=['cara', 'test-cara'],
|
||||
help="Pick the instance for which you want to generate the config",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-o", "--output-directory", default='config',
|
||||
help="Location to put the config files",
|
||||
)
|
||||
|
||||
|
||||
def generate_config(output_directory: pathlib.Path, project_name: str, hostname: str, branch: str):
|
||||
output_directory.mkdir(exist_ok=True, parents=True)
|
||||
|
||||
def oc_process(component_name: str, context: typing.Optional[dict] = None):
|
||||
cmd = ['oc', 'process', '--local', '-f', f'{component_name}.yaml', '-o', 'yaml']
|
||||
for ctx_name, ctx_value in (context or {}).items():
|
||||
cmd.extend(['--param', f'{ctx_name}={ctx_value}'])
|
||||
with (output_directory / f'{component_name}.yaml').open('wt') as fh:
|
||||
print(f'Running: {" ".join(cmd)}')
|
||||
subprocess.run(cmd, stdout=fh, check=True)
|
||||
|
||||
oc_process('routes', context={'HOST': hostname})
|
||||
oc_process('configmap')
|
||||
oc_process('services')
|
||||
oc_process('imagestreams')
|
||||
oc_process('buildconfig', context={'GIT_BRANCH': branch})
|
||||
oc_process('deploymentconfig', context={'PROJECT_NAME': project_name})
|
||||
|
||||
print(f'Config in: {output_directory.absolute()}')
|
||||
|
||||
|
||||
def handler(args: argparse.ArgumentParser) -> None:
|
||||
if args.instance == 'cara':
|
||||
project_name = 'cara'
|
||||
branch = 'master'
|
||||
hostname = 'cara.web.cern.ch'
|
||||
elif args.instance == 'test-cara':
|
||||
branch = 'live/test-cara'
|
||||
project_name = 'test-cara'
|
||||
hostname = 'test-cara.web.cern.ch'
|
||||
|
||||
generate_config(pathlib.Path(args.output_directory), project_name, hostname, branch)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
configure_parser(parser)
|
||||
args = parser.parse_args()
|
||||
args.handler(args)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
117
app-config/openshift/config-normalise.py
Normal file
117
app-config/openshift/config-normalise.py
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
import argparse
|
||||
import pathlib
|
||||
|
||||
import ruamel.yaml
|
||||
|
||||
|
||||
def configure_parser(parser: argparse.ArgumentParser) -> None:
|
||||
parser.description = "Normalise openshift config files (by sorting and removing ephemeral values)"
|
||||
parser.set_defaults(handler=handler)
|
||||
parser.add_argument(
|
||||
"config-directory",
|
||||
help="The directory from which to find yaml files",
|
||||
)
|
||||
parser.add_argument(
|
||||
"output-directory",
|
||||
help="The directory to put normalized files (can be the same as config-directory)",
|
||||
)
|
||||
|
||||
|
||||
def clean_ephemeral_config(config: dict):
|
||||
config = config.copy()
|
||||
config.get('metadata', []).clear()
|
||||
|
||||
METADATA_TO_PRESERVE = ['labels', 'name']
|
||||
|
||||
for item in config['items']:
|
||||
item.pop('status', None)
|
||||
|
||||
for key in list(item['metadata'].keys()):
|
||||
if key not in METADATA_TO_PRESERVE:
|
||||
del item['metadata'][key]
|
||||
|
||||
item.get('spec', {}).pop('clusterIP', None)
|
||||
|
||||
if item['kind'] == 'BuildConfig':
|
||||
for trigger in item.get('spec', {}).get('triggers', []):
|
||||
trigger.get('imageChange', {}).pop('lastTriggeredImageID', None)
|
||||
|
||||
if item['kind'] == 'DeploymentConfig':
|
||||
item['spec'].get('template', {}).get('metadata', {}).pop('creationTimestamp', None)
|
||||
|
||||
for container in item['spec'].get('template', {}).get('spec', {}).get('containers', []):
|
||||
# Drop the specific image name (and hash).
|
||||
container.pop('image', None)
|
||||
item['spec'].get('template', {}).get('metadata', {}).pop('creationTimestamp', None)
|
||||
for trigger in item['spec'].get('triggers', []):
|
||||
trigger.get('imageChangeParams', {}).pop('lastTriggeredImage', None)
|
||||
|
||||
# Drop the template part of the config for now.
|
||||
# TODO: Remove this constraint to ensure our deployments reflect the fact that they are templated.
|
||||
r = item['metadata'].get('labels', {}).pop('template', None)
|
||||
|
||||
if r is not None and not item['metadata']['labels']:
|
||||
# Remove the empty labels dict if there is nothing left after popping the template item.
|
||||
item['metadata'].pop('labels')
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def deep_sort(item):
|
||||
if isinstance(item, dict):
|
||||
# Sort by the key.
|
||||
return {k: deep_sort(v) for k, v in sorted(item.items(), key=lambda i: i[0])}
|
||||
elif isinstance(item, list):
|
||||
# Use the metadata/name and fallback to the str representation to give a sort order.
|
||||
def sort_key(value):
|
||||
if isinstance(value, dict):
|
||||
return value.get('metadata', {}).get('name', '') or str(value)
|
||||
else:
|
||||
return str(value)
|
||||
|
||||
return sorted(
|
||||
[deep_sort(v) for v in item],
|
||||
key=sort_key,
|
||||
)
|
||||
else:
|
||||
return item
|
||||
|
||||
|
||||
def normalise_config(input_directory: pathlib.Path, output_directory: pathlib.Path):
|
||||
output_directory.mkdir(exist_ok=True, parents=True)
|
||||
|
||||
files = sorted(input_directory.glob('*.yaml'))
|
||||
|
||||
yaml = ruamel.yaml.YAML(typ='safe')
|
||||
|
||||
for file in files:
|
||||
with file.open('rt') as fh:
|
||||
content = yaml.load(fh)
|
||||
|
||||
config = clean_ephemeral_config(content)
|
||||
config = deep_sort(config)
|
||||
|
||||
destination = output_directory / file.name
|
||||
with destination.open('wt') as fh:
|
||||
yaml.dump(config, fh)
|
||||
print(f'Normalised {file.name} in {destination}')
|
||||
print(f'Config in: {output_directory.absolute()}')
|
||||
|
||||
|
||||
def handler(args: argparse.ArgumentParser) -> None:
|
||||
|
||||
normalise_config(
|
||||
pathlib.Path(getattr(args, 'config-directory')),
|
||||
pathlib.Path(getattr(args, 'output-directory')),
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
configure_parser(parser)
|
||||
args = parser.parse_args()
|
||||
args.handler(args)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
|
@ -207,7 +207,23 @@
|
|||
- containerPort: 8080
|
||||
protocol: TCP
|
||||
imagePullPolicy: Always
|
||||
resources: {}
|
||||
readinessProbe:
|
||||
failureThreshold: 3
|
||||
httpGet:
|
||||
path: /calculator-cern
|
||||
port: 8080
|
||||
scheme: HTTP
|
||||
initialDelaySeconds: 3
|
||||
periodSeconds: 10
|
||||
successThreshold: 1
|
||||
timeoutSeconds: 1
|
||||
resources:
|
||||
limits:
|
||||
cpu: '3'
|
||||
memory: 3Gi
|
||||
requests:
|
||||
cpu: '1'
|
||||
memory: 1Gi
|
||||
terminationMessagePath: /dev/termination-log
|
||||
terminationMessagePolicy: File
|
||||
dnsPolicy: ClusterFirst
|
||||
|
|
@ -229,7 +245,6 @@
|
|||
selector:
|
||||
app: cara-webservice
|
||||
triggers:
|
||||
- type: ConfigChange
|
||||
- type: ImageChange
|
||||
imageChangeParams:
|
||||
automatic: true
|
||||
|
|
|
|||
Loading…
Reference in a new issue