cem_acpt 0.11.0 → 0.11.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +8 -0
- data/.worktreeinclude +1 -0
- data/CLAUDE.md +64 -25
- data/Gemfile.lock +1 -1
- data/README.md +20 -7
- data/docs/ARCHITECTURE.md +1042 -0
- data/docs/rfcs/0000-template.md +54 -0
- data/docs/rfcs/0001-fix-bolt-missing-skip-path.md +105 -0
- data/docs/rfcs/0002-fix-default-character-substitutions.md +119 -0
- data/docs/rfcs/0003-windows-image-builder-template.md +110 -0
- data/docs/rfcs/0004-image-name-truncation-off-by-one.md +108 -0
- data/docs/rfcs/0005-os-dispatch-replace-windows-heuristic.md +117 -0
- data/docs/rfcs/0006-configurable-windows-bucket.md +96 -0
- data/docs/rfcs/0007-logging-quiet-and-typos.md +121 -0
- data/docs/rfcs/0008-namespace-platform-classes.md +110 -0
- data/docs/rfcs/0009-bolt-log-formatter-cleanup.md +111 -0
- data/docs/rfcs/0010-dead-code-cleanup.md +83 -0
- data/docs/rfcs/0011-provisioner-factory-consistency.md +89 -0
- data/docs/rfcs/README.md +34 -0
- data/lib/cem_acpt/cli.rb +10 -1
- data/lib/cem_acpt/config/cem_acpt.rb +4 -1
- data/lib/cem_acpt/image_builder/errors.rb +24 -0
- data/lib/cem_acpt/image_builder/provision_commands.rb +15 -3
- data/lib/cem_acpt/image_builder.rb +29 -2
- data/lib/cem_acpt/image_name_builder.rb +8 -1
- data/lib/cem_acpt/platform/gcp.rb +112 -106
- data/lib/cem_acpt/platform.rb +21 -19
- data/lib/cem_acpt/provision/terraform/linux.rb +1 -1
- data/lib/cem_acpt/provision/terraform/os_data.rb +23 -0
- data/lib/cem_acpt/provision/terraform/windows.rb +7 -1
- data/lib/cem_acpt/provision/terraform.rb +20 -16
- data/lib/cem_acpt/test_runner/log_formatter/bolt_summary_results_formatter.rb +2 -1
- data/lib/cem_acpt/test_runner/log_formatter.rb +0 -1
- data/lib/cem_acpt/test_runner.rb +21 -8
- data/lib/cem_acpt/utils/winrm_runner.rb +4 -3
- data/lib/cem_acpt/utils.rb +0 -12
- data/lib/cem_acpt/version.rb +1 -1
- data/lib/cem_acpt.rb +19 -7
- data/lib/terraform/gcp/linux/main.tf +6 -1
- data/lib/terraform/image/gcp/linux/main.tf +8 -1
- data/specifications/CEM-6713.md +165 -0
- data/specifications/CEM-6714.md +271 -0
- data/specifications/CEM-6715.md +133 -0
- data/specifications/CEM-6716.md +160 -0
- data/specifications/CEM-6717.md +239 -0
- data/specifications/CEM-6718.md +120 -0
- data/specifications/CEM-6719.md +173 -0
- metadata +26 -11
- data/.claude/settings.local.json +0 -7
- data/lib/cem_acpt/action_result.rb +0 -91
- data/lib/cem_acpt/puppet_helpers.rb +0 -38
- data/lib/cem_acpt/test_runner/log_formatter/bolt_error_formatter.rb +0 -65
- data/lib/cem_acpt/test_runner/log_formatter/bolt_output_formatter.rb +0 -54
data/lib/cem_acpt/test_runner.rb
CHANGED
|
@@ -61,8 +61,13 @@ module CemAcpt
|
|
|
61
61
|
@provisioned = true
|
|
62
62
|
logger.info('CemAcpt::TestRunner') { 'Provisioned test nodes...' }
|
|
63
63
|
logger.debug('CemAcpt::TestRunner') { "Instance names and IPs: #{@instance_names_ips}" }
|
|
64
|
-
#
|
|
65
|
-
|
|
64
|
+
# Classify every test in the run by OS family rather than substring-matching the first test name.
|
|
65
|
+
windows_tests, linux_tests = config.get('tests').partition do |t|
|
|
66
|
+
CemAcpt::Provision::OsData.os_family_for(t) == :windows
|
|
67
|
+
end
|
|
68
|
+
unless windows_tests.empty?
|
|
69
|
+
raise 'Mixed Windows and Linux tests in one run are not supported' unless linux_tests.empty?
|
|
70
|
+
|
|
66
71
|
logger.info('CemAcpt') { 'Running on windows nodes...' }
|
|
67
72
|
upload_module_to_bucket
|
|
68
73
|
|
|
@@ -71,7 +76,7 @@ module CemAcpt
|
|
|
71
76
|
# instance_names_ips. It contains the username, password, and ip of the
|
|
72
77
|
# windows node, as well as the test name that will be run on that node.
|
|
73
78
|
login_info = CemAcpt::Utils.get_windows_login_info(k, v)
|
|
74
|
-
win_node = CemAcpt::Utils::WinRMRunner::WinNode.new(login_info, @run_data[:win_remote_module_name])
|
|
79
|
+
win_node = CemAcpt::Utils::WinRMRunner::WinNode.new(login_info, @run_data[:win_remote_module_name], windows_bucket_uri)
|
|
75
80
|
win_node.run
|
|
76
81
|
end
|
|
77
82
|
end
|
|
@@ -203,9 +208,9 @@ module CemAcpt
|
|
|
203
208
|
@bolt_test_runner = CemAcpt::Bolt::TestRunner.new(config, run_data: @run_data)
|
|
204
209
|
@bolt_test_runner.setup!
|
|
205
210
|
rescue CemAcpt::ShellCommandNotFoundError => e
|
|
206
|
-
logger.
|
|
207
|
-
logger.
|
|
208
|
-
CemAcpt::Actions.config.
|
|
211
|
+
logger.warn('CemAcpt::TestRunner') { e.message }
|
|
212
|
+
logger.warn('CemAcpt::TestRunner') { 'Bolt binary not found on PATH; skipping :bolt action.' }
|
|
213
|
+
CemAcpt::Actions.config.except = (CemAcpt::Actions.config.except + [:bolt]).uniq
|
|
209
214
|
return
|
|
210
215
|
end
|
|
211
216
|
return unless @bolt_test_runner.tests.to_a.empty?
|
|
@@ -378,14 +383,22 @@ module CemAcpt
|
|
|
378
383
|
logger.error { result.log_formatter.results.join("\n") }
|
|
379
384
|
end
|
|
380
385
|
|
|
386
|
+
# Returns the configured Windows GCS bucket URI (e.g. "gs://win_cem_acpt").
|
|
387
|
+
# Raises if the bucket is unset or empty so we fail before invoking gcloud.
|
|
388
|
+
def windows_bucket_uri
|
|
389
|
+
bucket = config.get('platform.gcp.windows_bucket')
|
|
390
|
+
raise 'platform.gcp.windows_bucket is not configured' if bucket.nil? || bucket.to_s.empty?
|
|
391
|
+
"gs://#{bucket}"
|
|
392
|
+
end
|
|
393
|
+
|
|
381
394
|
# Upload the cem_windows module to the bucket if we're testing the cem_windows module
|
|
382
395
|
# This should only be done once per cem_acpt run. It's important to update the module_package_path
|
|
383
396
|
# in the run_data to reflect the new module path if we do end up changing the module name
|
|
384
397
|
def upload_module_to_bucket
|
|
385
398
|
@run_data[:win_remote_module_name] = SecureRandom.uuid << File.split(@run_data[:module_package_path]).last
|
|
386
|
-
@run_data[:win_remote_module_path] = File.join(
|
|
399
|
+
@run_data[:win_remote_module_path] = File.join(windows_bucket_uri, @run_data[:win_remote_module_name])
|
|
387
400
|
# Upload the module from the local host to the bucket
|
|
388
|
-
logger.info('CemAcpt') { "Uploading #{@run_data[:
|
|
401
|
+
logger.info('CemAcpt') { "Uploading #{@run_data[:module_package_path]} to #{@run_data[:win_remote_module_path]}..." }
|
|
389
402
|
CemAcpt::Utils::Shell.run_cmd("gcloud storage cp #{@run_data[:module_package_path]} #{@run_data[:win_remote_module_path]}")
|
|
390
403
|
logger.debug('CemAcpt') { 'Successfully uploaded module' }
|
|
391
404
|
end
|
|
@@ -36,11 +36,12 @@ module CemAcpt
|
|
|
36
36
|
class WinNode
|
|
37
37
|
include CemAcpt::Logging
|
|
38
38
|
|
|
39
|
-
attr_reader :mod_name
|
|
39
|
+
attr_reader :mod_name, :bucket_uri
|
|
40
40
|
|
|
41
|
-
def initialize(login_info, mod_name)
|
|
41
|
+
def initialize(login_info, mod_name, bucket_uri)
|
|
42
42
|
@login_info = login_info
|
|
43
43
|
@mod_name = mod_name
|
|
44
|
+
@bucket_uri = bucket_uri
|
|
44
45
|
end
|
|
45
46
|
|
|
46
47
|
def run_winrm
|
|
@@ -80,7 +81,7 @@ module CemAcpt
|
|
|
80
81
|
logger.debug('CemAcpt') { 'Creating cem_acpt directory and downloading necessary files...' }
|
|
81
82
|
winrm_runner.run("mkdir C:\\cem_acpt")
|
|
82
83
|
# Download the cem_windows module from the bucket to cem_acpt directory
|
|
83
|
-
winrm_runner.run("gcloud storage cp
|
|
84
|
+
winrm_runner.run("gcloud storage cp #{@bucket_uri}/#{@mod_name} C:\\cem_acpt")
|
|
84
85
|
# Install the cem_windows module
|
|
85
86
|
logger.info('CemAcpt') { 'Installing cem_windows module...' }
|
|
86
87
|
winrm_runner.run("Start-Process -FilePath 'C:\\Program Files\\Puppet Labs\\Puppet\\bin\\puppet.bat' -ArgumentList 'module install C:\\cem_acpt\\#{@mod_name}' -Wait -NoNewWindow")
|
data/lib/cem_acpt/utils.rb
CHANGED
|
@@ -13,18 +13,6 @@ module CemAcpt
|
|
|
13
13
|
class << self
|
|
14
14
|
include CemAcpt::Logging
|
|
15
15
|
|
|
16
|
-
# This is method currently unused, see lib/cem_acpt/utils/puppet.rb for details.
|
|
17
|
-
def package_win_module(module_dir)
|
|
18
|
-
# Path to the package file
|
|
19
|
-
package_file = File.join(module_dir, 'puppetlabs-cem_windows.tar.gz')
|
|
20
|
-
|
|
21
|
-
# Remove the old package file if it exists
|
|
22
|
-
FileUtils.rm_f(package_file)
|
|
23
|
-
`cd #{module_dir} && touch puppetlabs-cem_windows.tar.gz && tar -czf puppetlabs-cem_windows.tar.gz --exclude=puppetlabs-cem_windows.tar.gz *`
|
|
24
|
-
logger.info('CemAcpt') { "Windows module packaged at #{package_file}" }
|
|
25
|
-
package_file
|
|
26
|
-
end
|
|
27
|
-
|
|
28
16
|
def reset_password_readiness_polling(instance_name)
|
|
29
17
|
attempts = 0
|
|
30
18
|
last_error = nil
|
data/lib/cem_acpt/version.rb
CHANGED
data/lib/cem_acpt.rb
CHANGED
|
@@ -56,26 +56,38 @@ module CemAcpt
|
|
|
56
56
|
CemAcpt::TestRunner::Runner.new(@config)
|
|
57
57
|
end
|
|
58
58
|
|
|
59
|
+
# Build the logger destinations from `--quiet`, `--log-file`, and CI mode.
|
|
60
|
+
# See docs/rfcs/0007-logging-quiet-and-typos.md for the matrix.
|
|
59
61
|
def initialize_logger!
|
|
60
62
|
raise 'Config must be loaded before logger can be initialized' if config.nil? || config.empty?
|
|
61
63
|
|
|
62
64
|
log_formatter = config.get('log_format')&.to_sym || :text
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
65
|
+
quiet = !!config.get('quiet')
|
|
66
|
+
log_file = config.get('log_file')
|
|
67
|
+
|
|
68
|
+
logdevs = []
|
|
69
|
+
logdevs << $stdout unless quiet
|
|
70
|
+
logdevs << log_file if log_file
|
|
71
|
+
|
|
72
|
+
ci_override = false
|
|
70
73
|
if config.ci_mode? && !logdevs.include?($stdout)
|
|
71
74
|
logdevs << $stdout
|
|
75
|
+
ci_override = quiet
|
|
72
76
|
end
|
|
77
|
+
|
|
78
|
+
if logdevs.empty?
|
|
79
|
+
raise '--quiet without --log-file would silence all output; pass --log-file or drop --quiet'
|
|
80
|
+
end
|
|
81
|
+
|
|
73
82
|
new_log = new_logger(
|
|
74
83
|
*logdevs,
|
|
75
84
|
level: config.get('log_level'),
|
|
76
85
|
formatter: log_formatter,
|
|
77
86
|
)
|
|
78
87
|
new_log.set_verbose(!!config.get('verbose'))
|
|
88
|
+
if ci_override
|
|
89
|
+
new_log.warn('CemAcpt') { '--quiet is overridden in CI mode; logging to stdout for ::group:: support' }
|
|
90
|
+
end
|
|
79
91
|
new_log
|
|
80
92
|
end
|
|
81
93
|
|
|
@@ -189,7 +189,12 @@ resource "google_compute_instance" "acpt-test-node" {
|
|
|
189
189
|
}
|
|
190
190
|
|
|
191
191
|
metadata = {
|
|
192
|
-
"
|
|
192
|
+
# Keep OSLogin disabled on test instances. The metadata "ssh-keys" entry
|
|
193
|
+
# below is what cem_acpt relies on for SSH auth (terraform's remote-exec
|
|
194
|
+
# provisioner connects with the ephemeral key generated per run). With
|
|
195
|
+
# enable-oslogin=TRUE, GCP ignores the metadata "ssh-keys" entry, and we
|
|
196
|
+
# currently have no plumbing for OSLogin key registration.
|
|
197
|
+
"enable-oslogin" = "FALSE"
|
|
193
198
|
"ssh-keys" = "${var.username}:${file(var.public_key)}"
|
|
194
199
|
"cem-acpt-test" = each.value.test_name
|
|
195
200
|
}
|
|
@@ -116,7 +116,14 @@ resource "google_compute_instance" "acpt-test-node" {
|
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
metadata = {
|
|
119
|
-
|
|
119
|
+
# Keep OSLogin disabled on the throwaway build instance. The metadata
|
|
120
|
+
# "ssh-keys" entry below is what terraform's remote-exec provisioner
|
|
121
|
+
# uses for SSH auth (with the ephemeral key generated per run). With
|
|
122
|
+
# enable-oslogin=TRUE, GCP ignores the metadata "ssh-keys" entry, and
|
|
123
|
+
# we have no plumbing for OSLogin key registration. The build instance
|
|
124
|
+
# is destroyed immediately after the disk is snapshotted, so disabling
|
|
125
|
+
# OSLogin here only affects the throwaway VM, not the resulting image.
|
|
126
|
+
"enable-oslogin" = "FALSE"
|
|
120
127
|
"ssh-keys" = "${var.username}:${file(var.public_key)}"
|
|
121
128
|
"for-image-family" = each.value.image_family
|
|
122
129
|
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# CEM-6713 — Fix image-name truncation off-by-one
|
|
2
|
+
|
|
3
|
+
## Background
|
|
4
|
+
|
|
5
|
+
`ImageBuilder::TerraformBuilder#image_name_from_image_family`
|
|
6
|
+
(`lib/cem_acpt/image_builder.rb:114-116`) currently does:
|
|
7
|
+
|
|
8
|
+
```ruby
|
|
9
|
+
def image_name_from_image_family(image_family)
|
|
10
|
+
"#{image_family}-v#{@start_time.to_i}"[0..64]
|
|
11
|
+
end
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
`String#[0..64]` is **inclusive** on both ends and returns up to 65
|
|
15
|
+
characters. GCE image names are limited to 63 characters per RFC 1035
|
|
16
|
+
label rules, and must match `[a-z]([-a-z0-9]*[a-z0-9])?`. So the
|
|
17
|
+
current code:
|
|
18
|
+
|
|
19
|
+
1. Allows names of up to 65 chars, which GCE rejects with no clear
|
|
20
|
+
error path back to the user.
|
|
21
|
+
2. When it does truncate, the cut can land mid-timestamp or on the
|
|
22
|
+
`-v` separator, allowing two consecutive builds in the same family
|
|
23
|
+
to collide on the truncated name.
|
|
24
|
+
|
|
25
|
+
This work is the implementation half of
|
|
26
|
+
[RFC 0004](../docs/rfcs/0004-image-name-truncation-off-by-one.md).
|
|
27
|
+
|
|
28
|
+
## Functional behavior
|
|
29
|
+
|
|
30
|
+
`image_name_from_image_family` will produce a string that is
|
|
31
|
+
GCE-name-safe by construction:
|
|
32
|
+
|
|
33
|
+
- **Length ≤ 63 characters.**
|
|
34
|
+
- **Suffix `-v<unix_timestamp>` is preserved verbatim.** Two
|
|
35
|
+
consecutive builds in the same family always differ in the
|
|
36
|
+
timestamp portion, regardless of how long the family is.
|
|
37
|
+
- **No trailing dash.** When the prefix has to be clipped and the
|
|
38
|
+
clip lands on a `-`, trailing dashes are stripped before the suffix
|
|
39
|
+
is appended.
|
|
40
|
+
|
|
41
|
+
### Method signature (unchanged)
|
|
42
|
+
|
|
43
|
+
```ruby
|
|
44
|
+
# @param image_family [String] the GCE image family
|
|
45
|
+
# @return [String] the GCE-safe image name, ≤ 63 chars,
|
|
46
|
+
# with the timestamp suffix preserved
|
|
47
|
+
def image_name_from_image_family(image_family)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Implementation sketch
|
|
51
|
+
|
|
52
|
+
```ruby
|
|
53
|
+
GCE_IMAGE_NAME_MAX = 63
|
|
54
|
+
|
|
55
|
+
def image_name_from_image_family(image_family)
|
|
56
|
+
suffix = "-v#{@start_time.to_i}"
|
|
57
|
+
full = "#{image_family}#{suffix}"
|
|
58
|
+
return full if full.length <= GCE_IMAGE_NAME_MAX
|
|
59
|
+
|
|
60
|
+
prefix = image_family[0, GCE_IMAGE_NAME_MAX - suffix.length]
|
|
61
|
+
prefix.sub(/-+\z/, '') + suffix
|
|
62
|
+
end
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
The constant lives on `TerraformBuilder` next to
|
|
66
|
+
`DEFAULT_PLAN_NAME` / `DEFAULT_VARS_FILE`.
|
|
67
|
+
|
|
68
|
+
## Input / output contract
|
|
69
|
+
|
|
70
|
+
| Input `image_family` length | `@start_time.to_i` length | Expected output |
|
|
71
|
+
| --------------------------- | ------------------------- | ----------------------------------------- |
|
|
72
|
+
| 5 (`rhel9`) | 10 | `"rhel9-v<ts>"` (17 chars, unchanged) |
|
|
73
|
+
| Any value ≤ 63 chars total | — | unchanged |
|
|
74
|
+
| 64 chars total | — | family clipped by 1, suffix intact |
|
|
75
|
+
| Family clip ends in `-` | — | trailing `-` stripped, suffix intact |
|
|
76
|
+
|
|
77
|
+
`@start_time` is a `Time` set at the start of `run`. Callers
|
|
78
|
+
(`run` → `create_image_from_disk`) already only invoke this method
|
|
79
|
+
during a build, so `@start_time` is non-nil when this method runs.
|
|
80
|
+
|
|
81
|
+
## Edge cases
|
|
82
|
+
|
|
83
|
+
1. **Short family** — `image_family.length + suffix.length <= 63`:
|
|
84
|
+
return concatenation unchanged.
|
|
85
|
+
2. **Exactly 63 chars** — returned unchanged (no truncation).
|
|
86
|
+
3. **64 chars (one over)** — clip the family to fit; suffix is
|
|
87
|
+
preserved.
|
|
88
|
+
4. **Family ends in dashes after clipping** — strip *all* trailing
|
|
89
|
+
dashes from the clipped prefix before appending the suffix. (The
|
|
90
|
+
suffix begins with `-v…` so the result still has exactly one `-`
|
|
91
|
+
between the clipped family and the `v`.)
|
|
92
|
+
5. **Family long enough that clipping reduces it to nothing** — not a
|
|
93
|
+
realistic case (`@start_time.to_i` is 10 digits, suffix is 12
|
|
94
|
+
chars, leaving 51 chars of family room), but the implementation
|
|
95
|
+
tolerates it: `String#[0, 0]` returns `""`, `sub` is a no-op, and
|
|
96
|
+
the result is just the suffix. We are not adding explicit
|
|
97
|
+
handling for this; if it ever fires, GCE will reject (name must
|
|
98
|
+
start with a letter) and the failure surfaces normally.
|
|
99
|
+
|
|
100
|
+
## Constraints / invariants
|
|
101
|
+
|
|
102
|
+
- The suffix `-v<ts>` must always appear literally at the end of the
|
|
103
|
+
returned name.
|
|
104
|
+
- The returned name's length is always ≤ 63.
|
|
105
|
+
- The returned name never ends in `-`.
|
|
106
|
+
- No other call sites in the codebase grep for `image_name_from_image_family`,
|
|
107
|
+
so the contract change is local to this method.
|
|
108
|
+
|
|
109
|
+
## Error handling
|
|
110
|
+
|
|
111
|
+
No new exceptions. The method is pure-string, no I/O, no Terraform
|
|
112
|
+
or GCE calls. Existing callers (`create_image_from_disk`) handle
|
|
113
|
+
GCE errors at their own layer.
|
|
114
|
+
|
|
115
|
+
## Tests
|
|
116
|
+
|
|
117
|
+
Add to `spec/cem_acpt/image_builder_spec.rb`:
|
|
118
|
+
|
|
119
|
+
- A `describe '#image_name_from_image_family'` block that builds a
|
|
120
|
+
`TerraformBuilder` instance, sets `@start_time` via
|
|
121
|
+
`instance_variable_set`, and exercises:
|
|
122
|
+
1. Short family: result equals `"<family>-v<ts>"` and is unchanged.
|
|
123
|
+
2. Exact 63-char total: result unchanged, length 63.
|
|
124
|
+
3. 64-char total (one over): result is exactly 63 chars, ends in
|
|
125
|
+
the full `-v<ts>` suffix, family portion is one shorter.
|
|
126
|
+
4. Prefix-ending-in-dash: family chosen so the clip lands on `-`
|
|
127
|
+
(or `--`); result has no trailing dash before the suffix and
|
|
128
|
+
length ≤ 63.
|
|
129
|
+
|
|
130
|
+
Tests use a fixed `@start_time` (e.g.
|
|
131
|
+
`Time.utc(2026, 5, 4, 12, 0, 0)`) so timestamp digits are
|
|
132
|
+
deterministic.
|
|
133
|
+
|
|
134
|
+
## Documentation
|
|
135
|
+
|
|
136
|
+
- Update `docs/ARCHITECTURE.md` §12 — replace the off-by-one note
|
|
137
|
+
(current lines 757-762) with a description of the new bounded
|
|
138
|
+
behavior, referencing `GCE_IMAGE_NAME_MAX` and the suffix-preserving
|
|
139
|
+
rule.
|
|
140
|
+
- Update `docs/rfcs/0004-image-name-truncation-off-by-one.md` to
|
|
141
|
+
flip status from `Proposed` to `Implemented` and append an
|
|
142
|
+
"Implementation" note linking to the commit / PR.
|
|
143
|
+
|
|
144
|
+
## Non-goals
|
|
145
|
+
|
|
146
|
+
- Refusing to build when the family alone exceeds 63 chars
|
|
147
|
+
(alternative listed in the RFC) — out of scope.
|
|
148
|
+
- Hashing-based naming — out of scope.
|
|
149
|
+
- Refactoring the `TerraformBuilder` ↔ `Provision::Terraform`
|
|
150
|
+
duplication — out of scope; that is its own follow-up.
|
|
151
|
+
- Adding validation for the `[a-z]([-a-z0-9]*[a-z0-9])?` regex —
|
|
152
|
+
the existing code does not validate the family characters, and
|
|
153
|
+
this ticket is scoped to the length / off-by-one issue.
|
|
154
|
+
|
|
155
|
+
## Acceptance criteria
|
|
156
|
+
|
|
157
|
+
- [ ] `image_name_from_image_family` enforces a 63-char cap.
|
|
158
|
+
- [ ] Generated names never end in `-`.
|
|
159
|
+
- [ ] The `-v<ts>` suffix is preserved intact.
|
|
160
|
+
- [ ] RSpec coverage for: short, exact-63, 64-overflow,
|
|
161
|
+
prefix-ending-in-dash.
|
|
162
|
+
- [ ] `bundle exec rake spec` passes.
|
|
163
|
+
- [ ] `rubocop` is clean on touched files.
|
|
164
|
+
- [ ] `docs/ARCHITECTURE.md` §12 updated.
|
|
165
|
+
- [ ] RFC 0004 status updated to `Implemented`.
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
# CEM-6714 — Replace `tests.first.include?('windows')` with explicit OS dispatch
|
|
2
|
+
|
|
3
|
+
## Background
|
|
4
|
+
|
|
5
|
+
`TestRunner::Runner#run` (`lib/cem_acpt/test_runner.rb:65`) currently
|
|
6
|
+
decides whether to take the Windows branch with:
|
|
7
|
+
|
|
8
|
+
```ruby
|
|
9
|
+
if config.get('tests').first.include? 'windows'
|
|
10
|
+
upload_module_to_bucket
|
|
11
|
+
@instance_names_ips.each { |k, v| ... WinRMRunner ... }
|
|
12
|
+
end
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
This is a substring match against an arbitrary test name. It:
|
|
16
|
+
|
|
17
|
+
1. Misroutes any test whose name happens to contain the substring
|
|
18
|
+
`windows` (e.g. `cis_rhel-8_firewalld_windowserver_2`).
|
|
19
|
+
2. Only inspects the first test, so a mixed-OS run silently
|
|
20
|
+
mishandles every test after the first.
|
|
21
|
+
3. Couples the runner's OS dispatch to test-name spelling.
|
|
22
|
+
4. Duplicates logic already owned by `Provision::OsData`. The
|
|
23
|
+
provisioner already classifies the run via
|
|
24
|
+
`Linux.use_for?` / `Windows.use_for?` (`lib/cem_acpt/provision/terraform.rb:184-188`).
|
|
25
|
+
|
|
26
|
+
This work is the implementation half of
|
|
27
|
+
[RFC 0005](../docs/rfcs/0005-os-dispatch-replace-windows-heuristic.md).
|
|
28
|
+
|
|
29
|
+
## Functional behavior
|
|
30
|
+
|
|
31
|
+
Add a single classification helper to `Provision::OsData`, and have
|
|
32
|
+
the runner consume it.
|
|
33
|
+
|
|
34
|
+
### `Provision::OsData.os_family_for(test_name)`
|
|
35
|
+
|
|
36
|
+
A new module-level method on `CemAcpt::Provision::OsData`:
|
|
37
|
+
|
|
38
|
+
```ruby
|
|
39
|
+
# @param test_name [String]
|
|
40
|
+
# @return [Symbol] :linux or :windows
|
|
41
|
+
# @raise [CemAcpt::Provision::OsData::UnknownOsFamilyError] if the
|
|
42
|
+
# test_name does not match any known Linux or Windows OS / version.
|
|
43
|
+
def self.os_family_for(test_name)
|
|
44
|
+
return :windows if CemAcpt::Provision::Windows.use_for?(test_name)
|
|
45
|
+
return :linux if CemAcpt::Provision::Linux.use_for?(test_name)
|
|
46
|
+
|
|
47
|
+
raise UnknownOsFamilyError, ...
|
|
48
|
+
end
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`UnknownOsFamilyError < StandardError` is defined inside `OsData` and
|
|
52
|
+
its message names the test_name plus the valid OS/version sets — same
|
|
53
|
+
information that `Provision::Terraform#new_backend` raises today, so
|
|
54
|
+
preflight failures are at least as informative as today's
|
|
55
|
+
provisioner-time failures.
|
|
56
|
+
|
|
57
|
+
`Provision::Terraform#new_backend` will be refactored to delegate to
|
|
58
|
+
`os_family_for` so the two callers cannot drift. The error class
|
|
59
|
+
raised there changes from `ArgumentError` to
|
|
60
|
+
`OsData::UnknownOsFamilyError`. No callers of `new_backend` rescue
|
|
61
|
+
`ArgumentError` specifically (verified by grep), so this is a
|
|
62
|
+
behavior-preserving narrowing.
|
|
63
|
+
|
|
64
|
+
### Runner change
|
|
65
|
+
|
|
66
|
+
`TestRunner::Runner#run` replaces the `if … include? 'windows'`
|
|
67
|
+
block with:
|
|
68
|
+
|
|
69
|
+
```ruby
|
|
70
|
+
windows_tests, linux_tests = config.get('tests').partition do |t|
|
|
71
|
+
CemAcpt::Provision::OsData.os_family_for(t) == :windows
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
unless windows_tests.empty?
|
|
75
|
+
raise 'Mixed Windows and Linux tests in one run are not supported' \
|
|
76
|
+
unless linux_tests.empty?
|
|
77
|
+
|
|
78
|
+
logger.info('CemAcpt') { 'Running on windows nodes...' }
|
|
79
|
+
upload_module_to_bucket
|
|
80
|
+
@instance_names_ips.each do |k, v|
|
|
81
|
+
login_info = CemAcpt::Utils.get_windows_login_info(k, v)
|
|
82
|
+
win_node = CemAcpt::Utils::WinRMRunner::WinNode.new(
|
|
83
|
+
login_info, @run_data[:win_remote_module_name],
|
|
84
|
+
)
|
|
85
|
+
win_node.run
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Two behavior consequences:
|
|
91
|
+
|
|
92
|
+
- A test name that doesn't match either Linux or Windows now fails
|
|
93
|
+
fast at preflight (via `os_family_for`) instead of later in
|
|
94
|
+
`Provision::Terraform#new_backend`. RFC 0005 calls this out as
|
|
95
|
+
desired.
|
|
96
|
+
- A mixed-OS `tests:` list now raises a clear error instead of
|
|
97
|
+
silently treating tests after the first as Linux. This is the
|
|
98
|
+
desired behavior.
|
|
99
|
+
|
|
100
|
+
## Input / output contract
|
|
101
|
+
|
|
102
|
+
### `OsData.os_family_for`
|
|
103
|
+
|
|
104
|
+
| Input `test_name` | Output |
|
|
105
|
+
|--------------------------------------------|---------------------------------|
|
|
106
|
+
| `'cis_windows-2022_…'` | `:windows` |
|
|
107
|
+
| `'cis_rhel-8_firewalld_server_2'` | `:linux` |
|
|
108
|
+
| `'cis_rhel-8_firewalld_windowserver_2'` | `:linux` (substring trap fixed) |
|
|
109
|
+
| `'cis_freebsd-14_…'` (unknown OS) | raises `UnknownOsFamilyError` |
|
|
110
|
+
| `'malformed name with no version'` | raises `UnknownOsFamilyError` |
|
|
111
|
+
|
|
112
|
+
The method does no I/O and is pure with respect to `Linux.valid_names`,
|
|
113
|
+
`Linux.valid_versions`, `Windows.valid_names`, `Windows.valid_versions`.
|
|
114
|
+
|
|
115
|
+
### Runner
|
|
116
|
+
|
|
117
|
+
| `tests:` list | Behavior |
|
|
118
|
+
|----------------------------------------------|---------------------------------------------------------|
|
|
119
|
+
| All Linux test names | Skip Windows branch, run as today. |
|
|
120
|
+
| All Windows test names | Take Windows branch, upload module + WinRM each instance.|
|
|
121
|
+
| Mixed Linux + Windows | Raise `'Mixed Windows and Linux tests in one run are not supported'` from `#run`; `rescue StandardError` records it as a result and `clean_up` runs as today. |
|
|
122
|
+
| Any test name not matching any known OS | Raises `OsData::UnknownOsFamilyError`; same rescue path.|
|
|
123
|
+
|
|
124
|
+
## Constraints / invariants
|
|
125
|
+
|
|
126
|
+
- The dispatch decision in `TestRunner::Runner` and
|
|
127
|
+
`Provision::Terraform` MUST come from the same source of truth
|
|
128
|
+
(`OsData.os_family_for`).
|
|
129
|
+
- `OsData.os_family_for` MUST NOT do substring matching against the
|
|
130
|
+
test name; it goes through `use_for?`, which already anchors with
|
|
131
|
+
the `^prefix_osname-version` regex.
|
|
132
|
+
- The runner MUST classify every test name in the list, not just the
|
|
133
|
+
first.
|
|
134
|
+
- Both new and pre-existing failure modes route through the runner's
|
|
135
|
+
existing `rescue StandardError` and `ensure clean_up`, so resource
|
|
136
|
+
cleanup is unchanged.
|
|
137
|
+
|
|
138
|
+
## Error handling
|
|
139
|
+
|
|
140
|
+
- `OsData::UnknownOsFamilyError < StandardError` — raised by
|
|
141
|
+
`os_family_for` when neither `Linux.use_for?` nor `Windows.use_for?`
|
|
142
|
+
matches. Message format:
|
|
143
|
+
|
|
144
|
+
```
|
|
145
|
+
Cannot determine OS family for test name: <test_name>.
|
|
146
|
+
Known OSes: linux=[...], windows=[...].
|
|
147
|
+
Known versions: linux=[...], windows=[...].
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
- `Provision::Terraform#new_backend` is refactored to:
|
|
151
|
+
|
|
152
|
+
```ruby
|
|
153
|
+
case CemAcpt::Provision::OsData.os_family_for(test_name)
|
|
154
|
+
when :linux
|
|
155
|
+
CemAcpt::Provision::Linux.new(@config, @provision_data)
|
|
156
|
+
when :windows
|
|
157
|
+
CemAcpt::Provision::Windows.new(@config, @provision_data)
|
|
158
|
+
end
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
An unknown test name will surface
|
|
162
|
+
`OsData::UnknownOsFamilyError` instead of `ArgumentError`. No
|
|
163
|
+
call sites in `lib/`, `exe/`, or `spec/` rescue `ArgumentError`
|
|
164
|
+
from this method, so this is a safe narrowing.
|
|
165
|
+
|
|
166
|
+
- Mixed-OS run — runner raises a `RuntimeError` (plain
|
|
167
|
+
`raise '…'`) with a message that names the constraint. It is
|
|
168
|
+
caught by the existing `rescue StandardError => e` in `#run`, so
|
|
169
|
+
resource cleanup runs and the error is reported via the results
|
|
170
|
+
queue.
|
|
171
|
+
|
|
172
|
+
## Non-goals
|
|
173
|
+
|
|
174
|
+
- **Per-test OS dispatch** in a single `cem_acpt` invocation (i.e.
|
|
175
|
+
actually supporting a mixed list). RFC 0005 explicitly defers this
|
|
176
|
+
to a follow-up; we are only closing the obvious correctness gap.
|
|
177
|
+
- Renaming or moving `Provision::Linux` / `Provision::Windows` /
|
|
178
|
+
`OsData`, or restructuring the provisioner.
|
|
179
|
+
- Touching `Utils::Puppet::ModulePackageBuilder` (which uses
|
|
180
|
+
`metadata['name'].include?('windows')` for a different purpose:
|
|
181
|
+
module packaging on the local machine, not test routing).
|
|
182
|
+
- Touching `spec/cem_acpt/test_runner_spec.rb`'s
|
|
183
|
+
`module_name.include?('windows')` (test fixture matcher, not
|
|
184
|
+
production logic).
|
|
185
|
+
|
|
186
|
+
## Tests
|
|
187
|
+
|
|
188
|
+
### New file: `spec/cem_acpt/provision/terraform/os_data_spec.rb`
|
|
189
|
+
|
|
190
|
+
`describe CemAcpt::Provision::OsData` covering `.os_family_for`:
|
|
191
|
+
|
|
192
|
+
1. Returns `:linux` for a representative Linux test name
|
|
193
|
+
(`'cis_rhel-8_firewalld_server_2'`).
|
|
194
|
+
2. Returns `:windows` for a representative Windows test name
|
|
195
|
+
(`'cis_windows-2022_firewall_server_2'`).
|
|
196
|
+
3. Returns `:linux`, not `:windows`, for the substring-trap case
|
|
197
|
+
(`'cis_rhel-8_firewalld_windowserver_2'`).
|
|
198
|
+
4. Raises `UnknownOsFamilyError` for an unrecognized OS
|
|
199
|
+
(`'cis_freebsd-14_…'`).
|
|
200
|
+
5. Raises `UnknownOsFamilyError` for a name that doesn't match the
|
|
201
|
+
`^\w+_(\w+)-(\d+).*` regex at all.
|
|
202
|
+
|
|
203
|
+
The spec doesn't need a Config or run_data — `os_family_for` is a
|
|
204
|
+
pure module method.
|
|
205
|
+
|
|
206
|
+
### Update: `spec/cem_acpt/test_runner_spec.rb`
|
|
207
|
+
|
|
208
|
+
Add a context that constructs a Runner with a mixed-OS `tests:`
|
|
209
|
+
list and asserts the run surfaces the mixed-OS error. The cleanest
|
|
210
|
+
way to test this without spinning up the full provisioning chain is
|
|
211
|
+
to call the partition logic via the same path `#run` uses — which
|
|
212
|
+
means we let the existing `pre_provision_test_nodes` /
|
|
213
|
+
`provision_test_nodes` / `provisioner_output` stubs from the
|
|
214
|
+
"successfully runs a test" test fire, then assert that
|
|
215
|
+
`@results.to_a.last` is the mixed-OS error.
|
|
216
|
+
|
|
217
|
+
Concretely, add inside the existing `'with a Runner object'`
|
|
218
|
+
context:
|
|
219
|
+
|
|
220
|
+
```ruby
|
|
221
|
+
it 'records an error when tests mixes Windows and Linux' do
|
|
222
|
+
mixed_config = CemAcpt::Config::CemAcpt.new(
|
|
223
|
+
opts: { 'tests' => ['cis_windows-2022_firewall_server_2',
|
|
224
|
+
'cis_rhel-8_firewalld_server_2'] },
|
|
225
|
+
load_user_config: false,
|
|
226
|
+
)
|
|
227
|
+
runner = CemAcpt::TestRunner::Runner.new(mixed_config)
|
|
228
|
+
allow(runner).to receive(:pre_provision_test_nodes).and_return(true)
|
|
229
|
+
allow(runner).to receive(:provision_test_nodes).and_return(true)
|
|
230
|
+
allow(runner).to receive(:provisioner_output).and_return(instance_names_ips)
|
|
231
|
+
allow(runner).to receive(:clean_up).and_return(true)
|
|
232
|
+
allow(runner).to receive(:process_test_results).and_return(true)
|
|
233
|
+
results = runner.run
|
|
234
|
+
expect(results.last).to be_a(StandardError)
|
|
235
|
+
expect(results.last.message).to match(/Mixed Windows and Linux/)
|
|
236
|
+
end
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
This relies on `Runner#run`'s `rescue StandardError => e; @results << e`
|
|
240
|
+
behavior, which already exists.
|
|
241
|
+
|
|
242
|
+
## Documentation
|
|
243
|
+
|
|
244
|
+
- `docs/ARCHITECTURE.md` §4 — replace the `if tests.first.include?('windows')`
|
|
245
|
+
bullet in the lifecycle diagram with text that describes
|
|
246
|
+
`OsData.os_family_for`-driven partition + the mixed-OS guard.
|
|
247
|
+
- `docs/ARCHITECTURE.md` §13 — replace the
|
|
248
|
+
"After Terraform reports back IPs, the runner branches on
|
|
249
|
+
`tests.first.include?('windows')`" sentence with the new dispatch
|
|
250
|
+
description.
|
|
251
|
+
- `docs/rfcs/0005-os-dispatch-replace-windows-heuristic.md` — flip
|
|
252
|
+
status from `Proposed` to `Implemented` and add an
|
|
253
|
+
"Implementation" note linking the commit / PR.
|
|
254
|
+
|
|
255
|
+
## Acceptance criteria
|
|
256
|
+
|
|
257
|
+
- [ ] `Provision::OsData.os_family_for` exists with `:linux` /
|
|
258
|
+
`:windows` return values and raises `UnknownOsFamilyError` for
|
|
259
|
+
unknown names.
|
|
260
|
+
- [ ] `lib/cem_acpt/test_runner.rb` no longer references
|
|
261
|
+
`tests.first.include?` (or any other substring-on-test-name
|
|
262
|
+
heuristic) for OS dispatch.
|
|
263
|
+
- [ ] `Provision::Terraform#new_backend` consumes
|
|
264
|
+
`OsData.os_family_for` rather than calling
|
|
265
|
+
`Linux.use_for?` / `Windows.use_for?` directly.
|
|
266
|
+
- [ ] RSpec coverage for `os_family_for` (5 cases above) passes.
|
|
267
|
+
- [ ] RSpec coverage for the runner's mixed-OS error passes.
|
|
268
|
+
- [ ] `docs/ARCHITECTURE.md` §4 and §13 updated.
|
|
269
|
+
- [ ] RFC 0005 status flipped to `Implemented`.
|
|
270
|
+
- [ ] `bundle exec rake spec` passes.
|
|
271
|
+
- [ ] `rubocop` is clean on touched files.
|