-
Notifications
You must be signed in to change notification settings - Fork 27
Expand file tree
/
Copy pathcreate-oifs-docker.py
More file actions
387 lines (328 loc) · 15.7 KB
/
Copy pathcreate-oifs-docker.py
File metadata and controls
387 lines (328 loc) · 15.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
#! /usr/bin/env python3
#
# (C) Copyright 2011- ECMWF.
#
# This software is licensed under the terms of the Apache Licence Version 2.0
# which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
#
# In applying this licence, ECMWF does not waive the privileges and immunities
# granted to it by virtue of its status as an intergovernmental organisation
# nor does it submit to any jurisdiction.
#
import argparse
import logging
import os
import shutil
import subprocess
import sys
import time
# Generic helpers and Docker helpers all live in scripts/shared/.
_SHARED_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", "shared")
if _SHARED_DIR not in sys.path:
sys.path.insert(0, _SHARED_DIR)
import setup_logging
import read_yml_config
import find_py_packages
# Docker-specific helpers from scripts/shared/docker_lib.py.
from docker_lib import ( # type: ignore[import-not-found]
build_docker_image,
check_docker_image_exists,
is_official_docker_image,
modify_dockerfile,
pull_docker_image,
)
# Generic (non-Docker) helpers from scripts/shared/shared_helpers.py.
from shared_helpers import ( # type: ignore[import-not-found]
format_duration,
move_to_backup,
resolve_openifs_source,
shallow_clone,
slug,
timer,
)
def parse_arguments() :
parser = argparse.ArgumentParser(
description=f"""
create_openifs_docker and the associated modules creates a
container for the stand-alone package for OpenIFS.
This script automates:
1. Cloning OpenIFS from the specified branch
2. Copying SCM experiment data
3. Building a Docker image with GCC and required libraries
4. Running OpenIFS tests to verify the installation
For detailed documentation, see README.md
Prerequisites:
- Docker installed and running
- Python 3 with git, yaml modules (see README.md for setup)
- SSH access to OpenIFS repository
Usage:
python3 create-oifs-docker.py -c config/create_openifs_docker.yml
For more information: README.md#detailed-configuration
""",
formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("--config", "-c", type=str,
help="YAML configuration file (see config/create_openifs_docker.yml)")
args = parser.parse_args()
######### Check for command line arguments ###########################################
#
# Check that user has provided a branch name, if not exit
#
if args.config is None :
parser.print_help()
print(f"""
[ERROR]: User must provide an a yml config file using --config, e.g.
<path_to_script>/create_openifs_driver.py -c config/create_openifs_config.yml
""")
sys.exit()
########################################################################################
return args
def run_openifs_test(openifs_version, image_name,
run_tests=True,
run_scm_test=True,
remove_container=True):
"""
Run openifs-test build inside the Docker container and report results.
Tests are also run, depending on the arguments and the yml config
Args:
openifs_version: OpenIFS version string
image_name: Docker image name to test
run_tests : Run the OpenIFS tests
run_scm_test : Run the standard SCM cases
remove_container: If True, remove container after test completes (default: True)
"""
logger = logging.getLogger(__name__)
container_name = f"oifs-{openifs_version}"
# Remove any existing container with the same name
check_result = subprocess.run(
["docker", "inspect", "--format", "{{.Name}}", container_name],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
)
if check_result.returncode == 0:
logger.warning(f"Container '{container_name}' already exists and will be removed")
subprocess.run(["docker", "rm", "-f", container_name], check=True,
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
logger.info(f"Existing container '{container_name}' removed successfully")
# Start container with /bin/bash as main process
logger.info(f"Starting container '{container_name}' from image {image_name}...")
run_cmd = [
"docker", "run", "-dit",
"--name", container_name,
image_name,
"/bin/bash"
]
subprocess.run(run_cmd, check=True)
logger.info(f"Container '{container_name}' started. Re-enter later with:")
logger.info(f" docker start {container_name} && docker exec -it {container_name} /bin/bash")
# Build test command (unchanged)
test_cmd = (
f"source ~/{openifs_version}/oifs-config.edit_me.sh && "
f"$OIFS_TEST/openifs-test.sh -cb -j 8"
)
if run_tests:
test_cmd += " && $OIFS_TEST/openifs-test.sh -t"
if run_scm_test:
test_cmd += " && cd $OIFS_HOME && $SCM_TEST/callscm"
# Execute test command inside the running container via exec
exec_cmd = [
"docker", "exec", "-it",
container_name,
"bash", "-lc",
test_cmd
]
logger.info(f"Running tests via exec: {' '.join(exec_cmd)}\n")
try:
subprocess.run(exec_cmd, check=True)
logger.info("OpenIFS built successfully")
if run_tests:
logger.info("OpenIFS tests passed successfully")
if run_scm_test:
logger.info("SCM test also passed successfully")
if remove_container:
subprocess.run(["docker", "rm", "-f", container_name], check=True)
logger.info(f"Container '{container_name}' removed")
else:
logger.info(f"Container '{container_name}' left running. Use 'docker ps' to see it.")
logger.info(f"Container can be restarted using 'docker exec -it {container_name} /bin/bash'")
return True
except subprocess.CalledProcessError as e:
logger.error(f"OpenIFS tests failed: {e}")
logger.error(f"stdout: {e.stdout}")
logger.error(f"stderr: {e.stderr}")
if not remove_container:
logger.info("Container was not removed. Use 'docker ps -a' to inspect it.")
return False
def main():
script_start_time = time.time()
timings = {}
# Read yaml config path from the command line
cli_args = parse_arguments()
# As the command line arguments have been accepted, now
# check that the "non-standard" python modules are available
pymod_list=["git","yaml"]
#
find_py_packages.main(pymod_list)
config = read_yml_config.main(cli_args.config)
# Resolve openifs_source once and reuse the result everywhere.
# The raw value comes from openifs_source (preferred) or the legacy
# openifs_branch key. resolve_openifs_source maps "" -> auto-detect.
_raw = config.get('openifs_source') or config.get('openifs_branch', '')
_source_kind, _source_value = resolve_openifs_source(_raw, __file__)
if _source_kind == 'remote':
_tag = slug(_source_value)
else:
_tag = f"local-{slug(os.path.basename(os.path.realpath(_source_value)))}"
config.setdefault('openifs_branch', _tag)
log_dir = os.path.join(config['openifs_build_docker_dir'], "docker_bld_logfiles")
# Create directory if it doesn't exist
os.makedirs(log_dir, exist_ok=True)
log_file_path = os.path.join(log_dir, f"log_{config['openifs_version']}_{config['base_docker_image']}.log")
# Setup to write logfile in the current working directory. Using default log info
setup_logging.main(log_file_path)
logger = logging.getLogger(__name__)
# Docker Base Image Validation
with timer("Docker Base Image Validation", timings, 'image_validation'):
base_image = f"gcc:{config['base_docker_image']}"
# Security check: only allow official/vetted images
logger.info(f"Validating base Docker image {base_image}...")
if not is_official_docker_image(base_image):
logger.error(f"Security check failed: '{base_image}' is not an approved official image")
logger.error("Only official Docker images are allowed for security reasons")
logger.error("If you need to use a different image, add it to ALLOWED_OFFICIAL_IMAGES in the code")
sys.exit(1)
logger.info(f"Security check passed: {base_image} is an official image")
# Check if image exists locally
logger.info(f"Checking if base Docker image {base_image} exists locally...")
if not check_docker_image_exists(base_image):
logger.warning(f"Base Docker image {base_image} not found locally")
logger.info("Attempting to pull from Docker Hub...")
if not pull_docker_image(base_image):
logger.error(f"Failed to pull base Docker image {base_image}")
logger.error("Please check your internet connection and Docker Hub status")
logger.error(f"You can try manually: docker pull {base_image}")
sys.exit(1)
else:
logger.info(f"Base Docker image {base_image} is available locally")
# Dockerfile Preparation
with timer("Dockerfile Preparation", timings, 'dockerfile_prep'):
docker_file_name = f"Dockerfile_{config['openifs_version']}_{config['base_docker_image']}"
dockerfile_path = os.path.join(config['openifs_build_docker_dir'], docker_file_name)
# Check if Dockerfile exists and create backup
if os.path.exists(dockerfile_path):
logger.warning(f"Dockerfile {dockerfile_path} already exists, creating backup")
shutil.copyfile(dockerfile_path, f"{dockerfile_path}.bak")
else:
logger.info(f"Creating Dockerfile {dockerfile_path}")
# Check if template exists
docker_template = config['docker_template']
if not os.path.exists(docker_template):
logger.error(f"Docker template file not found: {docker_template}")
logger.error("Please check 'docker_template' path in your config file")
sys.exit(1)
shutil.copyfile(docker_template, dockerfile_path)
modify_dockerfile(dockerfile_path, config)
# OpenIFS Repository Setup
with timer("OpenIFS Repository Setup", timings, 'repo_setup'):
openifs_dir = os.path.join(config['openifs_build_docker_dir'], config['openifs_version'])
# Reuse the resolution computed at the top of main() so we don't
# accidentally re-interpret the derived openifs_branch tag as a
# remote branch name (the bug that caused clone of
# "local-openifs-casim" when openifs_source was empty).
source_kind, source_value = _source_kind, _source_value
if source_kind == 'remote':
logger.info(f"Cloning branch '{source_value}' to {openifs_dir}")
shallow_clone(
config['openifs_repo_url'],
openifs_dir,
branch=source_value,
force=config.get('force_reclone', False),
)
source_tag = slug(source_value)
else:
# 'auto' or 'local' — stage the resolved local path into the build dir.
local_src = source_value
if not os.path.isdir(local_src):
logger.error(f"OpenIFS source not found at {local_src}")
logger.error("Check 'openifs_source' in your config or re-run from inside the checkout")
sys.exit(1)
if os.path.abspath(local_src) == os.path.abspath(openifs_dir):
logger.info(f"Source and build dir are the same ({openifs_dir}); skipping copy")
elif os.path.exists(openifs_dir) and not config.get('force_reclone', False):
logger.info(f"Using existing staged source at {openifs_dir} (force_reclone=False)")
else:
if os.path.exists(openifs_dir):
# force_reclone=True path: rename the existing staged
# tree to a timestamped backup so any uncommitted or
# unpushed work survives instead of being deleted.
move_to_backup(openifs_dir)
logger.info(f"Copying local source {local_src} -> {openifs_dir}")
shutil.copytree(
local_src, openifs_dir,
symlinks=True,
ignore=shutil.ignore_patterns(
'.git', 'build', '.cache', '.bootstrap',
'__pycache__', '*.pyc', '*.pyo', 'openifs-env',
),
)
source_tag = f"local-{slug(os.path.basename(os.path.realpath(local_src)))}"
# Propagate source_tag so modify_dockerfile and the image name both
# reflect where this build came from (mirrors ci-oifs-docker.py).
config['openifs_branch'] = source_tag
# Docker Image Build
oifs_image_name = f"openifs-{config['openifs_version']}-gcc{config['base_docker_image']}:{config['openifs_branch']}"
force_rebuild = config.get('force_rebuild', False)
with timer("Docker Image Build", timings, 'image_build'):
logger.info(f"Building Docker image {oifs_image_name}...")
if force_rebuild:
logger.info("force_rebuild=True: building without cache")
else:
logger.info("force_rebuild=False: building with cache")
logger.info(f"Building Docker image {oifs_image_name}...")
build_docker_image(dockerfile_path, oifs_image_name, config['openifs_build_docker_dir'], no_cache=force_rebuild)
logger.info(f"Docker image {oifs_image_name} built successfully!")
# OpenIFS Build and Test
run_build = config.get('run_build', True)
run_tests = config.get('run_tests', True)
run_scm_test = config.get('run_scm_test', True)
test_success = False
if run_build:
with timer("OpenIFS Build and Test", timings, 'build_and_test'):
test_success = run_openifs_test(
config['openifs_version'],
oifs_image_name,
run_tests,
run_scm_test,
config.get('remove_test_container', True),
)
if test_success:
logger.info("All tests passed successfully")
else:
logger.error("Tests failed - check build configuration")
else:
logger.info("Skipping build and tests (run_build: False in config)")
timings['build_and_test'] = 0
# Final Summary
total_time = time.time() - script_start_time
logger.info("=" * 70)
logger.info("FINAL SUMMARY")
logger.info("=" * 70)
logger.info("Configuration:")
logger.info(f" Image: {oifs_image_name}")
logger.info(f" Cache: {'disabled (--no-cache)' if force_rebuild else 'enabled'}")
logger.info(f" OpenIFS Build: {'Passed' if run_build and test_success else 'Failed' if run_build else 'Skipped'}")
logger.info(f" OpenIFS Tests: {'Passed' if run_tests and test_success else 'Failed' if run_tests else 'Skipped'}")
logger.info(f" SCM Tests: {'Passed' if run_scm_test and test_success else 'Failed' if run_scm_test else 'Skipped'}")
logger.info("=" * 70)
logger.info("Timing Summary:")
logger.info(f" Image Validation: {format_duration(timings['image_validation'])}")
logger.info(f" Dockerfile Prep: {format_duration(timings['dockerfile_prep'])}")
logger.info(f" Repository Setup: {format_duration(timings['repo_setup'])}")
logger.info(f" Image Build: {format_duration(timings['image_build'])}")
if run_build:
logger.info(f" Build & Test: {format_duration(timings['build_and_test'])}")
else:
logger.info(f" Build & Test: Skipped")
logger.info(" " + "-" * 66)
logger.info(f" Total: {format_duration(total_time)}")
logger.info("=" * 70)
if __name__ == "__main__":
main()