Skip to content

Commit cfa0537

Browse files
authored
Merge pull request #16 from code0-tech/add-support-for-reticulum-builds-with-version-overrides
Add command to trigger reticulum build with version overrides
2 parents 0f9e61c + fb91d2d commit cfa0537

8 files changed

Lines changed: 253 additions & 43 deletions

File tree

lib/pyxis/commands/components.rb

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,28 @@ def info
1919
result
2020
end
2121

22+
desc 'build', 'Start a reticulum build with specific versions'
23+
method_option :aquila_sha, desc: 'Commit SHA of aquila to build', type: :string
24+
method_option :draco_sha, desc: 'Commit SHA of draco to build', type: :string
25+
method_option :sagittarius_sha, desc: 'Commit SHA of sagittarius to build', type: :string
26+
method_option :sculptor_sha, desc: 'Commit SHA of sculptor to build', type: :string
27+
method_option :taurus_sha, desc: 'Commit SHA of taurus to build', type: :string
28+
def build
29+
version_overrides = {
30+
aquila: options[:aquila_sha],
31+
draco: options[:draco_sha],
32+
sagittarius: options[:sagittarius_sha],
33+
sculptor: options[:sculptor_sha],
34+
taurus: options[:taurus_sha],
35+
}.compact
36+
37+
pipeline = Pyxis::Services::CreateReticulumBuildService.new(version_overrides).execute
38+
39+
raise Pyxis::MessageError, 'Failed to create pipeline' if pipeline.nil?
40+
41+
"Created reticulum build at #{pipeline.web_url}"
42+
end
43+
2244
desc 'update', 'Update a component in reticulum'
2345
method_option :component, aliases: '-c', desc: 'The component to update', required: true, type: :string
2446
def update

lib/pyxis/dry_run_enforcer.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
module Pyxis
44
module DryRunEnforcer
55
class FaradayBlocker < Faraday::Middleware
6-
DryRunError = Class.new(StandardError)
6+
DryRunError = Class.new(Pyxis::Error)
77
include ::SemanticLogger::Loggable
88

99
def on_request(env)

lib/pyxis/generic_faraday.rb

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# frozen_string_literal: true
2+
3+
module Pyxis
4+
class GenericFaraday
5+
include SemanticLogger::Loggable
6+
7+
def self.create(options)
8+
faraday = Faraday.new(options)
9+
faraday.use Pyxis::DryRunEnforcer::FaradayBlocker
10+
faraday.use Pyxis::Logger::FaradayLogger
11+
12+
enhance_faraday(faraday)
13+
14+
faraday
15+
end
16+
17+
def self.enhance_faraday(faraday)
18+
%i[get post put patch delete].each do |method|
19+
faraday.define_singleton_method(:"#{method}_json") do |*args, **kwargs|
20+
response = faraday.send(method, *args, **kwargs)
21+
json = response.body.blank? ? nil : JSON.parse(response.body)
22+
23+
Thor::CoreExt::HashWithIndifferentAccess.new(
24+
{
25+
body: json.is_a?(Hash) ? Thor::CoreExt::HashWithIndifferentAccess.new(json) : json,
26+
response: response,
27+
}
28+
)
29+
end
30+
end
31+
end
32+
end
33+
end

lib/pyxis/github_client.rb

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,18 +31,26 @@ def without_auto_pagination(octokit)
3131
octokit.instance_variable_set(:@auto_paginate, current_auto_paginate)
3232
end
3333

34-
private
35-
36-
def create_octokit(instance)
37-
logger.info('Creating octokit client', instance: instance)
34+
def create_installation_access_token(instance, options = {})
3835
config = CLIENT_CONFIGS[instance]
3936
global_client = Octokit::Client.new(bearer_token: create_jwt(config[:private_key_location], config[:app_id]))
4037
logger.debug('Created JWT for client', app: global_client.app.slug)
4138
installation_token = Pyxis::GlobalStatus.with_faraday_dry_run_bypass do
42-
global_client.create_app_installation_access_token(config[:installation_id])
39+
global_client.create_app_installation_access_token(config[:installation_id], options)
4340
end
41+
4442
logger.debug('Created app installation access token',
4543
installation_token: installation_token.to_h.except(:token))
44+
45+
installation_token
46+
end
47+
48+
private
49+
50+
def create_octokit(instance)
51+
logger.info('Creating octokit client', instance: instance)
52+
53+
installation_token = create_installation_access_token(instance)
4654
Octokit::Client.new(bearer_token: installation_token[:token])
4755
end
4856

lib/pyxis/gitlab_client.rb

Lines changed: 38 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -27,30 +27,48 @@ def self.create_client(instance)
2727
'Private-Token': File.read(client_config[:private_token]),
2828
},
2929
}
30-
faraday = Faraday.new(options)
31-
faraday.use Pyxis::DryRunEnforcer::FaradayBlocker
32-
faraday.use Pyxis::Logger::FaradayLogger
3330

34-
enhance_faraday(faraday)
31+
GenericFaraday.create(options)
32+
end
33+
34+
def self.paginate_json(client, url, options = {})
35+
response = []
36+
37+
client.get_json(url, options).tap do |page|
38+
response += page.body
39+
while (next_page_link = PageLinks.new(page.response.env.response_headers).next)
40+
page = client.get_json(next_page_link)
41+
response += page.body
42+
end
43+
end
3544

36-
faraday
45+
response
3746
end
3847

39-
def self.enhance_faraday(faraday)
40-
%i[get post put patch delete].each do |method|
41-
faraday.define_singleton_method(:"#{method}_json") do |*args, **kwargs|
42-
response = faraday.send(method, *args, **kwargs)
43-
json = response.body.blank? ? nil : JSON.parse(response.body)
44-
45-
if json.is_a?(Hash)
46-
Thor::CoreExt::HashWithIndifferentAccess.new(
47-
{
48-
body: Thor::CoreExt::HashWithIndifferentAccess.new(json),
49-
response: response,
50-
}
51-
)
52-
else
53-
response
48+
class PageLinks
49+
HEADER_LINK = 'link'
50+
DELIM_LINKS = ','
51+
LINK_REGEX = /<([^>]+)>; rel="([^"]+)"/
52+
METAS = %w[last next first prev].freeze
53+
54+
attr_accessor(*METAS)
55+
56+
def initialize(headers)
57+
link_header = headers[HEADER_LINK]
58+
59+
extract_links(link_header) if link_header && link_header =~ /(next|first|last|prev)/
60+
end
61+
62+
private
63+
64+
def extract_links(header)
65+
header.split(DELIM_LINKS).each do |link|
66+
LINK_REGEX.match(link.strip) do |match|
67+
url = match[1]
68+
meta = match[2]
69+
next if !url || !meta || METAS.index(meta).nil?
70+
71+
send("#{meta}=", url)
5472
end
5573
end
5674
end

lib/pyxis/managed_versioning/component_info.rb

Lines changed: 69 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -17,25 +17,77 @@ def execute
1717
)
1818
return nil if pipeline.response.status == 404
1919

20-
reticulum_sha = pipeline.body.sha
21-
22-
components = {}
23-
24-
Pyxis::Project.components.each do |project_name|
25-
component_project_class = Pyxis::Project.const_get(project_name)
26-
version_file = "versions/#{component_project_class.component_name}"
27-
28-
begin
29-
version_content = GithubClient.octokit.contents(Project::Reticulum.github_path, path: version_file,
30-
ref: reticulum_sha)
31-
version = Base64.decode64(version_content.content)
32-
components[component_project_class.component_name] = version
33-
rescue Octokit::NotFound
34-
logger.warn("Version file not found for #{component_project_class.component_name} at SHA #{reticulum_sha}")
35-
end
20+
jobs = GitlabClient.paginate_json(
21+
GitlabClient.client,
22+
"/api/v4/projects/#{Project::Reticulum.api_gitlab_path}/pipelines/#{build_id}/jobs"
23+
)
24+
25+
manifests = find_manifests(jobs)
26+
27+
components = {
28+
reticulum: pipeline.body.sha,
29+
}
30+
31+
manifests.each do |image|
32+
component = image.first.to_sym
33+
next if components.key?(component)
34+
35+
image_tag = image.length == 1 ? build_id : "#{build_id}-#{image.last}"
36+
37+
components[component] = revision_for("code0-tech/reticulum/ci-builds/#{component}", image_tag)
3638
end
3739

38-
components
40+
components.compact
41+
end
42+
43+
private
44+
45+
def ghcr_client
46+
@ghcr_client ||= GenericFaraday.create({ url: 'https://ghcr.io' })
47+
end
48+
49+
def token_for(image)
50+
response = ghcr_client.get_json(
51+
'token',
52+
{
53+
scope: "repository:#{image}:pull",
54+
service: 'ghcr.io',
55+
}
56+
)
57+
58+
logger.warn('Failed to retrieve token', image: image) unless response.response.status == 200
59+
60+
response.body.token
61+
end
62+
63+
def revision_for(image, tag)
64+
token = token_for(image)
65+
66+
response = ghcr_client.get_json(
67+
"v2/#{image}/manifests/#{tag}",
68+
{},
69+
{
70+
Authorization: "Bearer #{token}",
71+
Accept: 'application/vnd.oci.image.index.v1+json',
72+
}
73+
)
74+
75+
logger.warn('Failed to retrieve tag for image', image: image, tag: tag) unless response.response.status == 200
76+
77+
response.body.annotations&.[]('org.opencontainers.image.version')
78+
end
79+
80+
def find_manifests(jobs)
81+
jobs.map { |job| job['name'] }
82+
.select { |job| job.start_with?('manifest:') }
83+
.map { |job| job.delete_prefix('manifest:') }
84+
.sort
85+
.map { |job| job.split(': ') }
86+
.map do |image|
87+
next image if image.length == 1
88+
89+
[image.first, image.last.delete_prefix('[').delete_suffix(']')]
90+
end
3991
end
4092
end
4193
end

lib/pyxis/project/base.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ def github_path
1616
paths[:github]
1717
end
1818

19+
def github_repository_name
20+
github_path.split('/')[1]
21+
end
22+
1923
def api_gitlab_path
2024
paths[:gitlab].gsub('/', '%2F')
2125
end
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# frozen_string_literal: true
2+
3+
module Pyxis
4+
module Services
5+
class CreateReticulumBuildService
6+
InvalidVersionOverride = Class.new(Pyxis::Error)
7+
8+
include SemanticLogger::Loggable
9+
10+
attr_reader :version_overrides
11+
12+
def initialize(version_overrides)
13+
@version_overrides = version_overrides
14+
end
15+
16+
def execute
17+
logger.info('Creating build with version overrides', version_overrides: version_overrides)
18+
19+
version_overrides.each_pair do |component, version|
20+
validate_override!(component, version)
21+
end
22+
23+
pipeline = GitlabClient.client.post_json(
24+
"/api/v4/projects/#{Project::Reticulum.api_gitlab_path}/pipeline",
25+
{
26+
ref: Project::Reticulum.default_branch,
27+
variables: version_override_variables + token_variable,
28+
}
29+
)
30+
31+
pipeline.body if pipeline.response.status == 201
32+
end
33+
34+
private
35+
36+
def validate_override!(component, version)
37+
project = Pyxis::Project.const_get(component.capitalize)
38+
39+
begin
40+
GithubClient.octokit.tag(project.github_path, version)
41+
rescue Octokit::UnprocessableEntity, Octokit::NotFound
42+
begin
43+
GithubClient.octokit.commit(project.github_path, version)
44+
rescue Octokit::UnprocessableEntity, Octokit::NotFound
45+
raise InvalidVersionOverride, "Invalid version '#{version}' for component '#{component}'"
46+
end
47+
end
48+
end
49+
50+
def version_override_variables
51+
variables = []
52+
53+
version_overrides.each_pair do |component, version|
54+
variables << {
55+
key: "OVERRIDE_#{component}_VERSION",
56+
value: version,
57+
}
58+
end
59+
60+
variables
61+
end
62+
63+
def token_variable
64+
[
65+
{
66+
key: 'C0_GH_TOKEN',
67+
value: File.read(ENV.fetch('PYXIS_GH_RETICULUM_PUBLISH_TOKEN')),
68+
}
69+
]
70+
end
71+
end
72+
end
73+
end

0 commit comments

Comments
 (0)