File: force-auto.py.in

package info (click to toggle)
charliecloud 0.43-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 3,116 kB
  • sloc: python: 6,021; sh: 4,284; ansic: 3,863; makefile: 598
file content (368 lines) | stat: -rw-r--r-- 11,734 bytes parent folder | download
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
#!%PYTHON_SHEBANG%

# This script generates a BATS file to exercise “ch-image build --force”
# across a variety of distributions. It’s used by Makefile.am.
#
# About each distribution, we remember:
#
#   - base image name
#   - config name it should select
#   - scope
#       standard: all tests in standard scope
#       full: one test in standard scope, the rest in full
#   - any tests invalid for that distro
#
# For each distribution, we test these factors:
#
#   - the value of --force (absent, fakeroot, seccomp) (3)
#   - whether or not preparation for --force is already done (2)
#   - commands that (4)
#       - don’t need --force, and fail
#       - don’t need --force, and succeed
#       - apparently need --force but in fact do not
#       - really do need --force
#
# This would appear to yield 3×2×4 = 24 tests per distribution. However:
#
#   1. We only try pre-prepared images for “really need” commands with --force
#      given, to save time, so it’s at most 9 potential tests.
#
#   2. The pre-preparation step doesn’t make sense for some distros or for
#      --force=seccomp.
#
#   3. We’ve not yet determined an “apparently need” command for some distros.
#
# Bottom line, the number of tests per distro varies. See the code below for
# specific details.


import abc
import enum
import inspect
import sys


@enum.unique
class Scope(enum.Enum):
   STANDARD = "standard"
   FULL = "full"

@enum.unique
class Run(enum.Enum):
   UNNEEDED_FAIL = "unneeded fail"
   UNNEEDED_WIN = "unneeded win"
   FAKE_NEEDED = "fake needed"
   NEEDED = "needed"


class Test(abc.ABC):

   arch_excludes = []
   force_excludes = []
   base = None
   config = None
   scope = Scope.FULL
   skip_reason = None
   prep_run = None
   runs = { Run.UNNEEDED_FAIL: "false",
            Run.UNNEEDED_WIN: "true" }

   def __init__(self, run, force, preprep):
      self.run = run
      self.force = force
      self.preprep = preprep

   def __str__(self):
      preprep = "preprep" if self.preprep else "no preprep"
      return f"{self.base}, {self.run.value}, {self.force}, {preprep}"

   @property
   def build1_post_hook(self):
      return ""

   @property
   def build2_post_hook(self):
      return ""

   @property
   def build_from_hook(self):
      return ""

   @property
   def skip(self):
      if (self.skip_reason is None):
         return ""
      else:
         return "skip '%s'" % self.skip_reason

   def as_grep_files(self, grep_files, image, invert=False):
      cmds = []
      for (re, path) in grep_files:
         path = f"\"$CH_IMAGE_STORAGE\"/img/{image}/{path}"
         cmd = f"ls -lh {path}"
         if (invert):
            cmd = f"! ( {cmd} )"
         cmds.append(cmd)
         if (not invert):
            cmds.append(f"grep -Eq -- '{re}' {path}")
      return "\n".join(cmds)

   def as_outputs(self, outputs, invert=False):
      cmds = []
      for out in outputs:
         out = f"echo \"$output\" | grep -Eq -- \"{out}\""
         if (invert):
            out = f"! ( {out} )"
         cmds.append(out)
      return "\n".join(cmds)

   def as_runs(self, runs):
      return "\n".join("RUN %s" % run for run in runs)

   def test(self):
      # skip?
      if (self.preprep and not (self.force and self.run == Run.NEEDED)):
         print(f"\n# skip: {self}: not needed")
         return
      if (self.preprep and self.prep_run is None):
         print(f"\n# skip: {self}: no preprep command")
         return
      if (self.preprep and self.force == "seccomp"):
         print(f"\n# skip: {self}: no preprep for --force=seccomp")
         return
      if (self.force in self.force_excludes):
         print(f"\n# skip: {self}: --force=%s excluded" % self.force)
         return
      # scope
      if (    (self.scope == Scope.STANDARD or self.run == Run.NEEDED)
          and self.force != "fakeroot"):
         scope = "standard"
      else:
         scope = "full"
      # architecture excludes
      arch_excludes = "\n".join("arch_exclude %s" % i
                                for i in self.arch_excludes)
      # build 1 to make prep-prepped image (e.g. install EPEL) if needed
      if (not self.preprep):
         build1 = "# skipped: no separate prep"
         build2_base = self.base
      else:
         build2_base = "tmpimg"
         build1 = f"""\
run ch-image -v build -t tmpimg -f - . << 'EOF'
FROM {self.base}
{self.build_from_hook}
RUN {self.prep_run}
EOF
echo "$output"
[[ $status -eq 0 ]]
{self.build1_post_hook}"""
      # force
      force = "--force=%s" % (self.force) if self.force else "--force=none"
      # run command we’re testing
      try:
         run = self.runs[self.run]
      except KeyError:
         print(f"\n# skip: {self}: no run command")
         return
      # status
      if (   self.run == Run.UNNEEDED_FAIL
          or ( self.run == Run.NEEDED and not self.force )):
         status = 1
      else:
         status = 0
      # output
      outs = []
      if (self.force == "fakeroot"):
         outs += [f"--force=fakeroot: will use: {self.config}"]
         if (self.run in { Run.NEEDED, Run.FAKE_NEEDED }):
            outs += ["--force=fakeroot: modified 1 RUN instructions"]
      out = self.as_outputs(outs)
      # emit the test
      print(f"""
@test "ch-image --force: {self}" {{
{self.skip}
scope {scope}
{arch_excludes}

# build 1: intermediate image for preparatory commands
{build1}

# build 2: image we're testing
run ch-image -v build {force} -t tmpimg2 -f - . << 'EOF'
FROM {build2_base}
{self.build_from_hook}
RUN {run}
EOF
echo "$output"
[[ $status -eq {status} ]]
{out}
{self.build2_post_hook}
}}
""", end="")


class EPEL_Mixin:

   # Mixin class for RPM distros where we want to pre-install EPEL. I think
   # this should maybe go away and just go into a _Red_Hat base class, i.e.
   # test all RPM distros with EPEL pre-installed, but this matches what
   # existed in 50_fakeroot.bats. Note the install-EPEL command is elsewhere.

   epel_outputs = ["(Updating|Installing).+: epel-release"]
   epel_greps = [("enabled=1", "/etc/yum.repos.d/epel*.repo")]

   @property
   def build1_post_hook(self):
      return "\n".join(["# validate EPEL installed",
                        self.as_outputs(self.epel_outputs),
                        self.as_grep_files(self.epel_greps, "tmpimg")])

   @property
   def build2_post_hook(self):
      return "\n".join([
         "# validate EPEL present if installed by build 1, gone if by --force",
         self.as_grep_files(self.epel_greps, "tmpimg2", not self.preprep)])


class RHEL7(Test):
   config = "rhel7"
   runs = { **Test.runs, **{ Run.FAKE_NEEDED: "yum install -y ed",
                             Run.NEEDED:      "yum install -y openssh" } }
class T_CentOS_7(RHEL7, EPEL_Mixin):
   scope = Scope.STANDARD
   base = "centos:7"
   prep_run = "yum install -y epel-release"

   @property
   def build_from_hook(self):
      return f"""\
RUN sed -i s/mirror.centos.org/vault.centos.org/g /etc/yum.repos.d/*.repo \
    && sed -i s/^#.*baseurl=http/baseurl=http/g /etc/yum.repos.d/*.repo \
    && sed -i s/^mirrorlist=http/#mirrorlist=http/g /etc/yum.repos.d/*.repo"""


class RHEL8(Test):
   config = "rhel8"
   runs = { **Test.runs,
            **{ Run.FAKE_NEEDED: "dnf install -y"
                                 " --setopt=install_weak_deps=false ed",
                Run.NEEDED:      "dnf install -y"
                                 " --setopt=install_weak_deps=false openssh" } }

class T_RHEL_UBI_8(RHEL8):
   base = "registry.access.redhat.com/ubi8/ubi"

class CentOS_8(RHEL8, EPEL_Mixin):
   prep_run = "dnf install -y epel-release"
class T_CentOS_8_Stream(CentOS_8):
   skip_reason = "issue #1904"
   # CentOS Stream pulls from quay.io per the CentOS wiki:
   # https://wiki.centos.org/FAQ/CentOSStream#What_artifacts_are_built.3F
   base = "quay.io/centos/centos:stream8"
#class T_CentOS_9_Stream(CentOS_8):   # FIXME: fails importing GPG key
#   base = "quay.io/centos/centos:stream9"

class T_Alma_8(CentOS_8):
   scope = Scope.STANDARD
   base = "almalinux:8"  # :latest same as :8 as of 2022-03-01
class T_Rocky_8(CentOS_8):
   base = "rockylinux:8"  # :latest same as :8 as of 2022-03-01

# With many of the following images, we test two versions of each image, the
# latest version and the minimum supported version. “Minimum supported” here
# meaning the minimum of (a) what’s available on Docker hub and we think won’t
# vanish too quickly and (b) what we support with fakeroot.

class Fedora(RHEL8):
   config = "fedora"
class T_Fedora_26(Fedora):
   skip_reason = "issue #1904"
   # We would prefer to test the lowest supported --force version, 24,
   # but the ancient version of dnf it has doesn't fail the transaction when
   # a package fails so we test with 26 instead.
   base = "fedora:26"
class T_Fedora_34(Fedora):
   base = "fedora:34"
# No worky as of Fedora 35; see issue #1163.
class Fedora_Latest(Fedora):
   base = "fedora:latest"


class Debian(Test):
   config = "debderiv"
   runs = { **Test.runs,
            **{ Run.NEEDED: "    apt-get update"
                            " && apt-get install -y openssh-client" } }
class T_Debian_10(Debian):
   base = "debian:10"
   arch_excludes = ["ppc64le"]  # base image unavailable
class T_Debian_Latest(Debian):
   scope = Scope.STANDARD
   base = "debian:latest"
class T_Ubuntu_16(Debian):
   base = "ubuntu:16.04"
class T_Ubuntu_Latest(Debian):
   base = "ubuntu:latest"

class SUSE(Test):
   config = "suse"
   runs = { **Test.runs,
            # No openssh packages seem to need --force.
            **{ Run.FAKE_NEEDED: "zypper install -y ed",
                Run.NEEDED:      "zypper install -y dbus-1" } }
# As of 2022-06-03, fails with “Signature verification failed for file
# 'repomd.xml' from repository 'OSS Update'.”. Neither opensuse/archive:42.3
# nor opensuse/leap:42.3 work, though the latter has more architectures.
#class T_OpenSUSE_42_3(SUSE):
#   base = "opensuse/archive:42.3"
#   arch_excludes = ["aarch64", "ppc64le"]  # base only amd64
class T_OpenSUSE_Leap_15_0(SUSE):
   base = "opensuse/leap:15.0"
class T_OpenSUSE_Leap_Latest(SUSE):
   base = "opensuse/leap:latest"

# Arch has tons of old tags, versioned by date, on Docker Hub. However, only
# :latest (or equivalently :base) is documented.
class T_Arch_Latest(Test):
   config = "arch"
   base = "archlinux:latest"
   arch_excludes = ["aarch64", "ppc64le"]  # base only amd64
   force_excludes = ["seccomp"]  # issue #1567
   # pacman does not exit non-zero when installing openssh fails; work
   # around this bug by grepping the pacman log.
   runs = { **Test.runs, **{ Run.FAKE_NEEDED: "pacman -Syq --noconfirm ed; ! fgrep failed: /var/log/pacman.log",
                             Run.NEEDED:      "pacman -Syq --noconfirm openssh; ! fgrep failed: /var/log/pacman.log" } }

class Alpine(Test):
   config = "alpine"
   # openssh does not need --force on Alpine
   runs = { **Test.runs, **{ Run.FAKE_NEEDED: "apk add ed",
                             Run.NEEDED:      "apk add dbus" } }
class T_Alpine_39(Alpine):
   base = "alpine:3.9"
class T_Alpine_Latest(Alpine):
   base = "alpine:latest"


# main loop

print("""\
# NOTE: This file is auto-generated. Do not modify.

load common

setup () {
    [[ $CH_TEST_BUILDER = ch-image ]] || skip 'ch-image only'
    [[ $CH_IMAGE_CACHE = enabled ]] || skip 'bucache enabled only'
}
""")


# All classes starting with T_ get turned into a test.
for (name, test) in (i for i in inspect.getmembers(sys.modules[__name__])
                     if i[0].startswith("T_")):
   for run in Run:
      for force in (None, "fakeroot", "seccomp"):
         for preprep in (False, True):
            test(run, force, preprep).test()