Skip to content

Commit db17644

Browse files
authored
Merge pull request #366 from miharp/feat/sbom-component-tables
Source agent and OpenBolt component tables from SBOMs
2 parents 9b1e5dd + 076dd52 commit db17644

4 files changed

Lines changed: 100 additions & 112 deletions

File tree

Rakefile

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,8 +135,10 @@ namespace :references do
135135
# its own OPENBOLT_SERIES / OPENBOLT_MIN_RELEASE rather than the generic
136136
# SERIES / MIN_RELEASE. That keeps an OpenVox 9.x regeneration (SERIES=9.) from
137137
# leaking into OpenBolt, where it would resolve no 9.x releases and abort.
138+
# The floor is 5.3.0 because OpenBolt SBOMs begin there; 5.1.0/5.2.0 predate the
139+
# SBOMs and would only be skipped.
138140
series = ENV.fetch('OPENBOLT_SERIES', '5.')
139-
min_version = ENV.fetch('OPENBOLT_MIN_RELEASE', '5.1.0')
141+
min_version = ENV.fetch('OPENBOLT_MIN_RELEASE', '5.3.0')
140142
path = ENV.fetch('OPENBOLT_VERSIONS_DATA', '_data/openbolt_release_contents.yml')
141143
rows = PuppetReferences::OpenboltReleaseTable.write_data_file(series:, min_version:, path:)
142144
puts "Wrote #{rows.size} releases to #{path}"
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,21 @@
11
---
22
- release: 5.6.0
3-
openvox: "~> 8.0"
3+
openvox: 8.28.0
44
ruby: 3.2.11
55
openssl: 3.0.21
66
r10k: 5.0.3
77
- release: 5.5.0
8-
openvox: "~> 8.0"
8+
openvox: 8.26.2
99
ruby: 3.2.11
1010
openssl: 3.0.20
1111
r10k: 5.0.3
1212
- release: 5.4.0
13-
openvox: "~> 8.0"
13+
openvox: 8.25.0
1414
ruby: 3.2.10
1515
openssl: 3.0.19
1616
r10k: 5.0.2
1717
- release: 5.3.0
18-
openvox: "~> 8.0"
18+
openvox: 8.24.2
1919
ruby: 3.2.9
2020
openssl: 3.0.18
2121
r10k: 5.0.2

docs/_openvox_8x/component_versions.md

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ title: "Component versions in recent OpenVox releases"
99
[openbolt_apply]: /openbolt/latest/applying_manifest_blocks.html
1010
[server_install_pre]: /openvox-server/latest/install_pre.html
1111
[openvoxdb_postgres]: /openvoxdb/latest/configure_postgres.html
12+
[sbom_tools]: https://github.com/OpenVoxProject/openvox-sbom-tools
1213

1314
{% assign nav_key = page.nav %}
1415

@@ -25,10 +26,11 @@ on its own 5.x line). There is no single "OpenVox platform version" that pins al
2526
of them at once, so each component is shown in its own table, keyed by that
2627
component's release.
2728

28-
The bundled-component columns are **generated** from upstream component pins, so
29-
they don't drift. Columns that aren't bundled or pinned anywhere (Java and
30-
PostgreSQL) are supported-version requirements maintained by hand; see the note
31-
under each table.
29+
The bundled-component columns are **generated** from upstream release metadata (the
30+
per-release SBOMs published by [openvox-sbom-tools][sbom_tools] for the agent and
31+
OpenBolt, and component pins for the server), so they don't drift. Columns that
32+
aren't bundled or pinned anywhere (Java and PostgreSQL) are supported-version
33+
requirements maintained by hand; see the note under each table.
3234

3335
<!-- markdownlint-disable MD055 MD056 -->
3436

@@ -92,12 +94,12 @@ separately, for example with the `puppet/r10k` module or a `gem install`.
9294
{% for r in site.data.openbolt_release_contents %}| {{ r.release }} | {{ r.openvox }} | {{ r.ruby }} | {{ r.openssl }} | {{ r.r10k }} |
9395
{% endfor %}
9496

95-
> **The OpenVox column is a requirement, not a pinned version.** OpenBolt bundles
96-
> OpenVox for [`bolt apply`][openbolt_apply], declared in its gemspec as a range
97-
> (`~> 8.0`); the exact version is resolved at build time (for example, OpenBolt
98-
> 5.6.0 bundles OpenVox 8.28.0). For `bolt apply`, OpenBolt compiles the catalog
99-
> with this bundled OpenVox and installs the `openvox-agent` package on targets via
100-
> `apply_prep` — so you don't install OpenVox separately to use it.
97+
> **The OpenVox column is the bundled version, not something you install.** OpenBolt
98+
> bundles OpenVox for [`bolt apply`][openbolt_apply]; its gemspec declares a range
99+
> (`~> 8.0`) and the version shown here is the exact one resolved into the package at
100+
> build time. For `bolt apply`, OpenBolt compiles the catalog with this bundled
101+
> OpenVox and installs the `openvox-agent` package on targets via `apply_prep`, so
102+
> you don't install OpenVox separately to use it.
101103
102104
<!-- markdownlint-enable MD055 MD056 -->
103105

lib/puppet_references/release_tables.rb

Lines changed: 81 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,19 @@ module PuppetReferences
1010
# Base for the component "release contents" tables rendered on the OpenVox
1111
# component-versions page. Each subclass names the upstream repo whose releases
1212
# drive the table and resolves one row per stable release from authoritative,
13-
# structured component pins, so the tables are never hand-maintained. The shared
14-
# GitHub releases/contents plumbing lives here so the per-table classes stay small.
13+
# structured metadata, so the tables are never hand-maintained. The shared
14+
# GitHub releases/contents plumbing lives here so the per-table classes stay
15+
# small. Agent and OpenBolt rows come from openvox-sbom-tools SBOMs (see
16+
# SbomReleaseTable); server and OpenVoxDB rows are still resolved from upstream
17+
# pins until their SBOMs land.
1518
#
1619
# Authenticates with ENV GITHUB_TOKEN/GH_TOKEN, falling back to `gh auth token`
17-
# for local runs. A release whose component layout can't be resolved (e.g. very
18-
# old tags that predate these pins) is skipped with a warning; transient API
20+
# for local runs. A release whose components can't be resolved (e.g. a tag that
21+
# predates the data this table reads) is skipped with a warning; transient API
1922
# failures raise so a bad run never overwrites the committed data file.
2023
class ReleaseTable
2124
API_ROOT = 'https://api.github.com'
2225
USER_AGENT = 'openvox-docs-release-table'
23-
RUNTIME_REPO = 'OpenVoxProject/puppet-runtime'
2426

2527
# A resolved component value must look like a dotted version. This screens out
2628
# boundary releases that pin a component by git SHA or whose layout yields a
@@ -96,24 +98,6 @@ def release_tags(series:, min_version:)
9698
.reverse
9799
end
98100

99-
# Files under puppet-runtime's configs/components at a given runtime version.
100-
def runtime_files(runtime_ver)
101-
list_dir(RUNTIME_REPO, 'configs/components', runtime_ver)
102-
end
103-
104-
# Component version from puppet-runtime's <name>.json (.version) or the vanagon
105-
# <name>.rb DSL. Recent components pin in JSON; older ones use the .rb DSL.
106-
def component_version(pattern, runtime_ver, files)
107-
if (json = files.find { |n| n.match?(/\A#{pattern}\.json\z/) })
108-
return json_at(RUNTIME_REPO, "configs/components/#{json}", runtime_ver)['version']
109-
end
110-
111-
rb = files.find { |n| n.match?(/\A#{pattern}\.rb\z/) }
112-
raise NotFound, "component /#{pattern}/ in runtime #{runtime_ver}" unless rb
113-
114-
raw(RUNTIME_REPO, "configs/components/#{rb}", runtime_ver)[/pkg\.version\s+['"]([^'"]+)['"]/, 1]
115-
end
116-
117101
def json_at(repo, path, ref)
118102
JSON.parse(raw(repo, path, ref))
119103
end
@@ -122,11 +106,6 @@ def raw(repo, path, ref)
122106
api_get("/repos/#{repo}/contents/#{path}?ref=#{ref}", accept: 'application/vnd.github.raw')
123107
end
124108

125-
def list_dir(repo, path, ref)
126-
body = api_get("/repos/#{repo}/contents/#{path}?ref=#{ref}", accept: 'application/vnd.github+json')
127-
JSON.parse(body).map { |entry| entry['name'] }
128-
end
129-
130109
def api_get(path, accept:)
131110
uri = URI("#{API_ROOT}#{path}")
132111
req = Net::HTTP::Get.new(uri)
@@ -147,42 +126,95 @@ def token
147126
end
148127
end
149128

150-
# openvox-agent: bundled OpenFact plus Ruby/OpenSSL/curl from the agent runtime.
151-
#
152-
# Only components actually bundled by the agent runtime project (agent-runtime-8.x)
153-
# are reported. r10k is intentionally absent: its pin exists in puppet-runtime, but
154-
# it is bundled by openbolt-runtime, not the agent, so it does not ship in
155-
# openvox-agent (see OpenboltReleaseTable).
156-
class AgentReleaseTable < ReleaseTable
129+
# Base for tables sourced from openvox-sbom-tools CycloneDX SBOMs rather than
130+
# scraping upstream pkg-DSL. Each release ships an SBOM committed at
131+
# lib/openvox/sbom-tools/sbom/<package>_<version>.cdx.json whose `.components[]`
132+
# carry already-resolved `{name, version}` pairs; a subclass maps the component
133+
# names it cares about to the table's columns. A release whose SBOM has not been
134+
# committed yet is skipped (NotFound), so the table tracks SBOM availability:
135+
# a newly tagged release appears once its SBOM lands upstream.
136+
class SbomReleaseTable < ReleaseTable
137+
SBOM_REPO = 'OpenVoxProject/openvox-sbom-tools'
138+
SBOM_DIR = 'lib/openvox/sbom-tools/sbom'
139+
140+
# SBOM package name; the file for a release is "<package>_<tag>.cdx.json".
141+
def sbom_package
142+
raise NotImplementedError, "#{self.class} must define #sbom_package"
143+
end
144+
145+
# Ordered { column => component-name } map pulled from each release's SBOM.
146+
# Insertion order sets the column order of the generated row.
147+
def columns
148+
raise NotImplementedError, "#{self.class} must define #columns"
149+
end
150+
151+
def row_for(tag, _cache)
152+
resolved = sbom_components(tag)
153+
row = { 'release' => tag }
154+
columns.each do |column, name|
155+
version = resolved[name]
156+
raise NotFound, "component #{name.inspect} in #{sbom_package} #{tag} SBOM" unless version
157+
158+
row[column] = version
159+
end
160+
row
161+
end
162+
163+
private
164+
165+
# { component-name => version } for every component in a release's SBOM. The
166+
# underlying GitHub contents fetch raises NotFound when the SBOM file is absent
167+
# (a release without a committed SBOM), which skips the release.
168+
def sbom_components(tag)
169+
sbom = json_at(SBOM_REPO, "#{SBOM_DIR}/#{sbom_package}_#{tag}.cdx.json", 'main')
170+
sbom.fetch('components').to_h { |component| [component['name'], component['version']] }
171+
end
172+
end
173+
174+
# openvox-agent: bundled OpenFact plus Ruby/OpenSSL/curl from the agent runtime,
175+
# read from the openvox-agent SBOM. The SBOM lists both `openssl` and
176+
# `openssl-fips`; we report the `openssl` runtime version to match the column.
177+
class AgentReleaseTable < SbomReleaseTable
157178
REPO = 'OpenVoxProject/openvox'
158179

159180
def repo
160181
REPO
161182
end
162183

163-
def row_for(tag, cache)
164-
openfact = json_at(REPO, 'packaging/configs/components/openfact.json', tag)['ref'].delete_prefix('refs/tags/')
165-
runtime = json_at(REPO, 'packaging/configs/components/puppet-runtime.json', tag)['version']
166-
rt = (cache[runtime] ||= runtime_versions(runtime))
167-
{ 'release' => tag, 'openfact' => openfact, 'ruby' => rt[:ruby], 'openssl' => rt[:openssl], 'curl' => rt[:curl] }
184+
def sbom_package
185+
'openvox-agent'
168186
end
169187

170-
private
188+
def columns
189+
{ 'openfact' => 'openfact', 'ruby' => 'ruby', 'openssl' => 'openssl', 'curl' => 'curl' }
190+
end
191+
end
171192

172-
def runtime_versions(runtime_ver)
173-
files = runtime_files(runtime_ver)
174-
{
175-
ruby: component_version('ruby-\d+\.\d+', runtime_ver, files),
176-
openssl: component_version('openssl-\d+\.\d+', runtime_ver, files),
177-
curl: component_version('curl', runtime_ver, files),
178-
}
193+
# OpenBolt ships on its own 5.x line and bundles its own runtime (Ruby/OpenSSL
194+
# plus r10k) along with OpenVox itself (for `bolt apply`). The SBOM reports the
195+
# exact bundled OpenVox version resolved at build time, so the `openvox` column
196+
# is the resolved version rather than the gemspec requirement range.
197+
class OpenboltReleaseTable < SbomReleaseTable
198+
REPO = 'OpenVoxProject/openbolt'
199+
200+
def repo
201+
REPO
202+
end
203+
204+
def sbom_package
205+
'openbolt'
206+
end
207+
208+
def columns
209+
{ 'openvox' => 'openvox', 'ruby' => 'ruby', 'openssl' => 'openssl', 'r10k' => 'r10k' }
179210
end
180211
end
181212

182213
# openvox-server: bundled JRuby, resolved through the server's pinned
183214
# jruby-utils -> jruby-deps "9.4.12.1-3" (the trailing "-N" packaging suffix is
184215
# stripped). Java is a supported requirement, not a pin, so it is hand-maintained
185-
# on the docs page rather than resolved here.
216+
# on the docs page rather than resolved here. Stays on the upstream-pin scraper
217+
# until an openvox-server SBOM is published.
186218
class ServerReleaseTable < ReleaseTable
187219
SERVER_REPO = 'OpenVoxProject/openvox-server'
188220
JRUBY_UTILS_REPO = 'OpenVoxProject/jruby-utils'
@@ -226,52 +258,4 @@ def row_for(tag, _cache)
226258
{ 'release' => tag }
227259
end
228260
end
229-
230-
# OpenBolt ships on its own 5.x line, independent of the OpenVox major, and
231-
# bundles its own runtime (Ruby/OpenSSL plus r10k). r10k is bundled here, not by
232-
# the agent, which is why it appears on the OpenBolt table. OpenBolt also bundles
233-
# OpenVox itself (for `bolt apply`); its gemspec declares the requirement as a
234-
# range (e.g. "~> 8.0") and the exact version is resolved at build time, so the
235-
# table shows the declared requirement rather than a resolved version.
236-
class OpenboltReleaseTable < ReleaseTable
237-
REPO = 'OpenVoxProject/openbolt'
238-
239-
def repo
240-
REPO
241-
end
242-
243-
def row_for(tag, cache)
244-
runtime = json_at(REPO, 'packaging/configs/components/puppet-runtime.json', tag)['version']
245-
rt = (cache[runtime] ||= runtime_versions(runtime))
246-
{
247-
'release' => tag, 'openvox' => openvox_requirement(tag),
248-
'ruby' => rt[:ruby], 'openssl' => rt[:openssl], 'r10k' => rt[:r10k],
249-
}
250-
end
251-
252-
private
253-
254-
# "openvox" is a dependency requirement (e.g. "~> 8.0"), not a resolved version.
255-
def freeform_fields
256-
%w[openvox]
257-
end
258-
259-
# OpenBolt's bundled-OpenVox requirement, from its gemspec.
260-
def openvox_requirement(tag)
261-
gemspec = raw(REPO, 'openbolt.gemspec', tag)
262-
req = gemspec[/add_dependency\s+["']openvox["']\s*,\s*["']([^"']+)["']/, 1]
263-
raise NotFound, "openvox dependency in openbolt.gemspec@#{tag}" unless req
264-
265-
req
266-
end
267-
268-
def runtime_versions(runtime_ver)
269-
files = runtime_files(runtime_ver)
270-
{
271-
ruby: component_version('ruby-\d+\.\d+', runtime_ver, files),
272-
openssl: component_version('openssl-\d+\.\d+', runtime_ver, files),
273-
r10k: component_version('rubygem-r10k', runtime_ver, files),
274-
}
275-
end
276-
end
277261
end

0 commit comments

Comments
 (0)