ace-lab 0.9.0 → 0.13.1
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/CHANGELOG.md +399 -102
- data/README.md +66 -100
- data/docs/usage.md +563 -592
- data/lib/ace/lab/atoms/install_schema.rb +444 -0
- data/lib/ace/lab/cli/commands/install.rb +161 -0
- data/lib/ace/lab/cli/commands/prepare.rb +120 -0
- data/lib/ace/lab/cli/commands/project_add.rb +101 -0
- data/lib/ace/lab/cli/commands/status.rb +109 -0
- data/lib/ace/lab/cli/commands/verify.rb +115 -0
- data/lib/ace/lab/cli.rb +21 -23
- data/lib/ace/lab/install_manifest_path.rb +23 -0
- data/lib/ace/lab/models/status_report.rb +38 -0
- data/lib/ace/lab/molecules/account_provisioner.rb +196 -0
- data/lib/ace/lab/molecules/config_state_classifier.rb +145 -0
- data/lib/ace/lab/molecules/directory_provisioner.rb +208 -0
- data/lib/ace/lab/molecules/gemset_provisioner.rb +222 -0
- data/lib/ace/lab/molecules/grant_provisioner.rb +250 -0
- data/lib/ace/lab/molecules/grant_resolver.rb +34 -23
- data/lib/ace/lab/molecules/hitl_routes.rb +263 -0
- data/lib/ace/lab/molecules/install_manifest_loader.rb +64 -0
- data/lib/ace/lab/molecules/key_provisioner.rb +736 -0
- data/lib/ace/lab/molecules/lab_state_scanner.rb +55 -0
- data/lib/ace/lab/molecules/overseer_provisioner.rb +188 -0
- data/lib/ace/lab/molecules/project_registry.rb +251 -0
- data/lib/ace/lab/molecules/repository_provisioner.rb +141 -0
- data/lib/ace/lab/molecules/role_verifier.rb +223 -0
- data/lib/ace/lab/organisms/install.rb +218 -0
- data/lib/ace/lab/organisms/prepare.rb +170 -0
- data/lib/ace/lab/organisms/project_add.rb +170 -0
- data/lib/ace/lab/organisms/status_service.rb +70 -0
- data/lib/ace/lab/organisms/verify.rb +836 -0
- data/lib/ace/lab/version.rb +1 -1
- data/lib/ace/lab.rb +34 -6
- metadata +35 -108
- data/lib/ace/lab/atoms/protected_publication_input.rb +0 -50
- data/lib/ace/lab/atoms/protected_workspace_prune_input.rb +0 -114
- data/lib/ace/lab/atoms/protected_workspace_prune_preview.rb +0 -86
- data/lib/ace/lab/atoms/service_input.rb +0 -92
- data/lib/ace/lab/cli/commands/assignment.rb +0 -92
- data/lib/ace/lab/cli/commands/command_execute.rb +0 -28
- data/lib/ace/lab/cli/commands/run.rb +0 -65
- data/lib/ace/lab/cli/commands/service.rb +0 -240
- data/lib/ace/lab/models/service_receipt.rb +0 -18
- data/lib/ace/lab/molecules/command_owner_admission.rb +0 -183
- data/lib/ace/lab/molecules/command_policy.rb +0 -158
- data/lib/ace/lab/molecules/hitl_authorizer.rb +0 -63
- data/lib/ace/lab/molecules/ordinary_admission_resolver.rb +0 -131
- data/lib/ace/lab/molecules/ordinary_listener.rb +0 -262
- data/lib/ace/lab/molecules/ordinary_merge_read.rb +0 -45
- data/lib/ace/lab/molecules/ordinary_native_read.rb +0 -106
- data/lib/ace/lab/molecules/ordinary_owner_admission.rb +0 -152
- data/lib/ace/lab/molecules/ordinary_owner_config.rb +0 -232
- data/lib/ace/lab/molecules/ordinary_receiver_config.rb +0 -94
- data/lib/ace/lab/molecules/ordinary_socket.rb +0 -126
- data/lib/ace/lab/molecules/ordinary_worktree_read.rb +0 -216
- data/lib/ace/lab/molecules/protected_cleanup_owner_admission.rb +0 -163
- data/lib/ace/lab/molecules/protected_cleanup_owner_client.rb +0 -165
- data/lib/ace/lab/molecules/protected_cleanup_owner_identity.rb +0 -239
- data/lib/ace/lab/molecules/protected_publication_publisher.rb +0 -110
- data/lib/ace/lab/molecules/protected_service_handler.rb +0 -43
- data/lib/ace/lab/molecules/protected_service_policy.rb +0 -156
- data/lib/ace/lab/molecules/service_executor.rb +0 -99
- data/lib/ace/lab/molecules/service_policy.rb +0 -289
- data/lib/ace/lab/organisms/authority_composition.rb +0 -87
- data/lib/ace/lab/organisms/command_executor.rb +0 -220
- data/lib/ace/lab/organisms/command_run_client.rb +0 -131
- data/lib/ace/lab/organisms/ordinary_assignment_client.rb +0 -120
- data/lib/ace/lab/organisms/ordinary_authority_composition.rb +0 -153
- data/lib/ace/lab/organisms/ordinary_merge_receiver.rb +0 -679
- data/lib/ace/lab/organisms/ordinary_merge_request.rb +0 -456
- data/lib/ace/lab/organisms/protected_cleanup_owner.rb +0 -361
- data/lib/ace/lab/organisms/protected_service_client.rb +0 -129
- data/lib/ace/lab/organisms/protected_service_listener.rb +0 -181
- data/lib/ace/lab/organisms/protected_service_publication.rb +0 -268
- data/lib/ace/lab/organisms/protected_service_receiver.rb +0 -267
- data/lib/ace/lab/organisms/protected_service_recovery.rb +0 -185
- data/lib/ace/lab/organisms/protected_service_request.rb +0 -128
- data/lib/ace/lab/organisms/protected_service_worker.rb +0 -89
- data/lib/ace/lab/organisms/service_request_service.rb +0 -249
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "ace/support/cli"
|
|
5
|
+
require "ace/core"
|
|
6
|
+
require_relative "../../organisms/prepare"
|
|
7
|
+
|
|
8
|
+
module Ace
|
|
9
|
+
module Lab
|
|
10
|
+
module CLI
|
|
11
|
+
module Commands
|
|
12
|
+
class Prepare < Ace::Support::Cli::Command
|
|
13
|
+
include Ace::Support::Cli::Base
|
|
14
|
+
|
|
15
|
+
# One shared section-namer map: the table renderer and the
|
|
16
|
+
# failure summary must name items identically.
|
|
17
|
+
SECTION_NAMERS = {
|
|
18
|
+
"accounts" => ->(item) { item["username"] },
|
|
19
|
+
"directories" => ->(item) { item["path"] },
|
|
20
|
+
"keys" => ->(item) { "#{item['account']}:#{item['path']}" },
|
|
21
|
+
"roles" => ->(item) { item["id"] },
|
|
22
|
+
"gems" => ->(item) { "#{item['name']} #{item['version']}" }
|
|
23
|
+
}.freeze
|
|
24
|
+
|
|
25
|
+
# "SHA256:" plus the first 20 base64 characters is enough to
|
|
26
|
+
# eyeball-match a key without ballooning the table row.
|
|
27
|
+
FINGERPRINT_WIDTH = 27
|
|
28
|
+
|
|
29
|
+
desc "Prepare accounts, directories, permissions, keys, roles and the declared " \
|
|
30
|
+
"gem set from the trusted install manifest"
|
|
31
|
+
|
|
32
|
+
example [
|
|
33
|
+
"--dry-run # Predict all sections without mutating",
|
|
34
|
+
"--format json # Machine-readable report"
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
option :dry_run, type: :boolean, desc: "Predict changes without applying them"
|
|
38
|
+
option :format, type: :string, desc: "Output format (table, json; default: table)"
|
|
39
|
+
option :quiet, type: :boolean, aliases: %w[-q], desc: "Suppress output"
|
|
40
|
+
|
|
41
|
+
def initialize(prepare: nil)
|
|
42
|
+
super()
|
|
43
|
+
@prepare = prepare
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def call(**options)
|
|
47
|
+
format = options[:format] || "table"
|
|
48
|
+
unless %w[table json].include?(format)
|
|
49
|
+
raise Ace::Support::Cli::Error,
|
|
50
|
+
"--format \"#{format}\" is not supported; table or json"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
organism = @prepare || Organisms::Prepare.new
|
|
54
|
+
report = organism.call(dry_run: options[:dry_run] || false)
|
|
55
|
+
render(report, format: format, quiet: options[:quiet])
|
|
56
|
+
|
|
57
|
+
# A wet run with failed items never exits 0: the report renders
|
|
58
|
+
# first, then the failure list names each item explicitly.
|
|
59
|
+
# Dry-run conflicts are predicted failures, not execution
|
|
60
|
+
# failures — exit 0 so callers can script a preview.
|
|
61
|
+
unless report["ok"] || report["dry_run"]
|
|
62
|
+
raise Ace::Support::Cli::Error, failure_summary(report)
|
|
63
|
+
end
|
|
64
|
+
rescue Ace::Lab::InvalidConfigurationError => e
|
|
65
|
+
raise Ace::Support::Cli::Error, e.message
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
def render(report, format:, quiet:)
|
|
71
|
+
return if quiet
|
|
72
|
+
|
|
73
|
+
puts format == "json" ? JSON.generate(report) : render_table(report)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def render_table(report)
|
|
77
|
+
lines = ["Machine: #{report['machine']} manifest: #{report['manifest']}" \
|
|
78
|
+
"#{" (dry run)" if report['dry_run']}"]
|
|
79
|
+
SECTION_NAMERS.each do |section, namer|
|
|
80
|
+
lines << "#{section.capitalize}:"
|
|
81
|
+
items = report[section]
|
|
82
|
+
if items.empty?
|
|
83
|
+
lines << " (none declared)"
|
|
84
|
+
else
|
|
85
|
+
items.each do |item|
|
|
86
|
+
detail = detail_for(section, item)
|
|
87
|
+
lines << " #{namer.call(item).to_s.ljust(34)} #{item['status']}#{detail}"
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
lines << "Result: #{report['ok'] ? 'ok' : 'FAILED'}"
|
|
92
|
+
lines.join("\n")
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Key rows render a truncated public fingerprint (never key
|
|
96
|
+
# material); everything else appends the message when present.
|
|
97
|
+
def detail_for(section, item)
|
|
98
|
+
parts = []
|
|
99
|
+
if section == "keys" && item["fingerprint"]
|
|
100
|
+
fingerprint = item["fingerprint"]
|
|
101
|
+
parts << (fingerprint.length <= FINGERPRINT_WIDTH ? fingerprint :
|
|
102
|
+
"#{fingerprint[0, FINGERPRINT_WIDTH]}…")
|
|
103
|
+
end
|
|
104
|
+
parts << item["message"] if item["message"]
|
|
105
|
+
parts.empty? ? "" : " #{parts.join(' ')}"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def failure_summary(report)
|
|
109
|
+
failures = SECTION_NAMERS.flat_map do |section, namer|
|
|
110
|
+
report[section]
|
|
111
|
+
.select { |item| item["status"] == "failed" }
|
|
112
|
+
.map { |item| "#{section}/#{namer.call(item)}: #{item['message']}" }
|
|
113
|
+
end
|
|
114
|
+
"prepare failed (#{failures.size} item(s)): #{failures.join('; ')}"
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "ace/support/cli"
|
|
5
|
+
require "ace/core"
|
|
6
|
+
require_relative "../../organisms/project_add"
|
|
7
|
+
|
|
8
|
+
module Ace
|
|
9
|
+
module Lab
|
|
10
|
+
module CLI
|
|
11
|
+
module Commands
|
|
12
|
+
class ProjectAdd < Ace::Support::Cli::Command
|
|
13
|
+
include Ace::Support::Cli::Base
|
|
14
|
+
|
|
15
|
+
SECTION_NAMERS = {
|
|
16
|
+
"repository" => ->(item) { item["path"] || item["location"] },
|
|
17
|
+
"overseer" => ->(item) { item["dir"] || item["location"] },
|
|
18
|
+
"registry" => ->(item) { item["path"] || item["location"] }
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
desc "Register a project and make it ready for team work " \
|
|
22
|
+
"(repository, private _overseer, lab config registry)"
|
|
23
|
+
|
|
24
|
+
example [
|
|
25
|
+
"atlas --path /Users/ace/lab/atlas --remote git@forge:atlas/atlas.git",
|
|
26
|
+
"atlas --path /Users/ace/lab/atlas --dry-run # Predict without mutating",
|
|
27
|
+
"atlas --path /Users/ace/lab/atlas --format json"
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
argument :id, type: :string, required: true, desc: "Project id (topology id)"
|
|
31
|
+
|
|
32
|
+
option :path, type: :string, required: true, desc: "Absolute project path"
|
|
33
|
+
option :remote, type: :string, desc: "Origin remote URL (git clone source)"
|
|
34
|
+
option :label, type: :string, desc: "Display label recorded in the registry"
|
|
35
|
+
option :dry_run, type: :boolean, desc: "Predict changes without applying them"
|
|
36
|
+
option :format, type: :string, desc: "Output format (table, json; default: table)"
|
|
37
|
+
option :quiet, type: :boolean, aliases: %w[-q], desc: "Suppress output"
|
|
38
|
+
|
|
39
|
+
def initialize(project_add: nil)
|
|
40
|
+
super()
|
|
41
|
+
@project_add = project_add
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def call(**options)
|
|
45
|
+
format = options[:format] || "table"
|
|
46
|
+
unless %w[table json].include?(format)
|
|
47
|
+
raise Ace::Support::Cli::Error,
|
|
48
|
+
"--format \"#{format}\" is not supported; table or json"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
path = File.expand_path(options[:path])
|
|
52
|
+
organism = @project_add || Organisms::ProjectAdd.new
|
|
53
|
+
report = organism.call(
|
|
54
|
+
id: options[:id], path: path, remote: options[:remote],
|
|
55
|
+
label: options[:label], dry_run: options[:dry_run] || false
|
|
56
|
+
)
|
|
57
|
+
render(report, format: format, quiet: options[:quiet])
|
|
58
|
+
|
|
59
|
+
unless report["ok"] || report["dry_run"]
|
|
60
|
+
raise Ace::Support::Cli::Error, failure_summary(report)
|
|
61
|
+
end
|
|
62
|
+
rescue Ace::Lab::InvalidConfigurationError => e
|
|
63
|
+
raise Ace::Support::Cli::Error, e.message
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def render(report, format:, quiet:)
|
|
69
|
+
return if quiet
|
|
70
|
+
|
|
71
|
+
puts format == "json" ? JSON.generate(report) : render_table(report)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def render_table(report)
|
|
75
|
+
lines = ["Project: #{report['project']} path: #{report['path']}" \
|
|
76
|
+
"#{" remote: #{report['remote']}" if report['remote']}" \
|
|
77
|
+
"#{" (dry run)" if report['dry_run']}"]
|
|
78
|
+
SECTION_NAMERS.each do |section, namer|
|
|
79
|
+
lines << "#{section.capitalize}:"
|
|
80
|
+
item = report[section]
|
|
81
|
+
detail = item["message"] ? " #{item['message']}" : ""
|
|
82
|
+
lines << " #{namer.call(item).to_s.ljust(34)} #{item['status']}#{detail}"
|
|
83
|
+
end
|
|
84
|
+
lines << "Result: #{report['ok'] ? 'ok' : 'FAILED'}"
|
|
85
|
+
lines.join("\n")
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def failure_summary(report)
|
|
89
|
+
failures = SECTION_NAMERS.map do |section, namer|
|
|
90
|
+
item = report[section]
|
|
91
|
+
next unless item["status"] == "failed"
|
|
92
|
+
|
|
93
|
+
"#{section}/#{namer.call(item)}: #{item['message']}"
|
|
94
|
+
end.compact
|
|
95
|
+
"project add failed (#{failures.size} step(s)): #{failures.join('; ')}"
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "ace/support/cli"
|
|
5
|
+
require "ace/core"
|
|
6
|
+
require_relative "support"
|
|
7
|
+
|
|
8
|
+
module Ace
|
|
9
|
+
module Lab
|
|
10
|
+
module CLI
|
|
11
|
+
module Commands
|
|
12
|
+
class Status < Ace::Support::Cli::Command
|
|
13
|
+
include Ace::Support::Cli::Base
|
|
14
|
+
include Runtime
|
|
15
|
+
|
|
16
|
+
desc <<~DESC.strip
|
|
17
|
+
Show lab configuration and local file state (read-only)
|
|
18
|
+
|
|
19
|
+
Distinguishes a valid empty set, missing configuration, and a
|
|
20
|
+
source error. Reads files only: no services are started and no
|
|
21
|
+
installer is required. The enriched worktree view stays
|
|
22
|
+
ace-git-worktree list.
|
|
23
|
+
DESC
|
|
24
|
+
|
|
25
|
+
example ["--format json # Full report as JSON"]
|
|
26
|
+
|
|
27
|
+
option :format, type: :string, default: "table", desc: "Output format: table or json"
|
|
28
|
+
option :quiet, type: :boolean, aliases: %w[-q], desc: "Suppress output"
|
|
29
|
+
|
|
30
|
+
def initialize(status_service: nil)
|
|
31
|
+
@status_service = status_service
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def call(**options)
|
|
35
|
+
format = options[:format] || "table"
|
|
36
|
+
cli_error("Invalid format: #{format} (use table or json)") unless %w[table json].include?(format)
|
|
37
|
+
|
|
38
|
+
report = status_service.report
|
|
39
|
+
unless options[:quiet]
|
|
40
|
+
puts(format == "json" ? JSON.generate(report.to_h) : render_table(report))
|
|
41
|
+
end
|
|
42
|
+
# Full report first; source failures then fail the command
|
|
43
|
+
unless report.ok?
|
|
44
|
+
raise Ace::Support::Cli::Error, "lab status reported source errors: #{first_error(report)}"
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def status_service
|
|
51
|
+
@status_service ||= Organisms::StatusService.from_config
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def first_error(report)
|
|
55
|
+
report.config[:error] ||
|
|
56
|
+
report.runtime_config[:error] ||
|
|
57
|
+
report.state.find { |location| location[:state] == :error }&.dig(:note) ||
|
|
58
|
+
"unreadable state location"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def render_table(report)
|
|
62
|
+
[config_line(report), runtime_line(report), "Local state (root: #{report.root}):"]
|
|
63
|
+
.concat(report.state.map { |location| location_line(location) })
|
|
64
|
+
.join("\n")
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def config_line(report)
|
|
68
|
+
config = report.config
|
|
69
|
+
case config[:state]
|
|
70
|
+
when :configured
|
|
71
|
+
"Lab configuration: configured — #{config[:source_path]} " \
|
|
72
|
+
"(topology: #{config[:topology][:projects]} project(s), " \
|
|
73
|
+
"#{config[:topology][:agents]} agent(s), #{config[:topology][:services]} service(s))"
|
|
74
|
+
when :defaults_only
|
|
75
|
+
"Lab configuration: missing (defaults only; no .ace/lab/config.yml or ~/.ace/lab/config.yml)"
|
|
76
|
+
else
|
|
77
|
+
"Lab configuration: invalid — #{config[:error]}"
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def runtime_line(report)
|
|
82
|
+
runtime = report.runtime_config
|
|
83
|
+
case runtime[:state]
|
|
84
|
+
when :valid
|
|
85
|
+
origin = runtime[:path] || runtime[:source]
|
|
86
|
+
"Runtime configuration: #{runtime[:runtime] || '(value unset)'} (#{origin})"
|
|
87
|
+
when :invalid
|
|
88
|
+
"Runtime configuration: invalid — #{runtime[:error]}"
|
|
89
|
+
else
|
|
90
|
+
"Runtime configuration: none (no config file, ACE_RUNTIME unset)"
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def location_line(location)
|
|
95
|
+
state =
|
|
96
|
+
case location[:state]
|
|
97
|
+
when :present then format("present (%s)", location[:entries])
|
|
98
|
+
when :prepared then "prepared"
|
|
99
|
+
when :error then "ERROR"
|
|
100
|
+
else "not present"
|
|
101
|
+
end
|
|
102
|
+
note = location[:note] ? " (#{location[:note]})" : ""
|
|
103
|
+
format(" %-12s %-16s %s%s", location[:key], state, location[:path], note)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "ace/support/cli"
|
|
5
|
+
require "ace/core"
|
|
6
|
+
require_relative "../../organisms/verify"
|
|
7
|
+
|
|
8
|
+
module Ace
|
|
9
|
+
module Lab
|
|
10
|
+
module CLI
|
|
11
|
+
module Commands
|
|
12
|
+
class Verify < Ace::Support::Cli::Command
|
|
13
|
+
include Ace::Support::Cli::Base
|
|
14
|
+
|
|
15
|
+
SECTION_LABELS = {
|
|
16
|
+
"accounts" => "account", "directories" => "directory", "keys" => "account",
|
|
17
|
+
"roles" => "id", "repositories" => "project", "grants" => "principal",
|
|
18
|
+
"registry" => "project", "overseer" => "project",
|
|
19
|
+
"adapters" => "adapter", "tools" => "tool", "gems" => "gem"
|
|
20
|
+
}.freeze
|
|
21
|
+
|
|
22
|
+
desc "Verify the installed lab's readiness under the target accounts (read-only): " \
|
|
23
|
+
"accounts, directories, key pairs, role instructions, repository origins and " \
|
|
24
|
+
"target-executor access, grant-based project visibility, private _overseer " \
|
|
25
|
+
"folders, configured communication/HITL adapters, required tools, declared gem set"
|
|
26
|
+
|
|
27
|
+
example [
|
|
28
|
+
"--format json # Machine-readable report for the overseer / next tool",
|
|
29
|
+
" # exit 0 only when every check passes or is unknown"
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
option :format, type: :string, desc: "Output format (table, json; default: table)"
|
|
33
|
+
option :quiet, type: :boolean, aliases: %w[-q], desc: "Suppress output"
|
|
34
|
+
|
|
35
|
+
def initialize(verifier: nil)
|
|
36
|
+
super()
|
|
37
|
+
@verifier = verifier
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def call(**options)
|
|
41
|
+
format = options[:format] || "table"
|
|
42
|
+
unless %w[table json].include?(format)
|
|
43
|
+
raise Ace::Support::Cli::Error,
|
|
44
|
+
"--format \"#{format}\" is not supported; table or json"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
report = (@verifier || Organisms::Verify.new).call
|
|
48
|
+
render(report, format: format, quiet: options[:quiet])
|
|
49
|
+
|
|
50
|
+
# A not-ready lab never exits 0: the report renders first
|
|
51
|
+
# (failure beside healthy data — both visible), then the
|
|
52
|
+
# failure list names every failed check concretely.
|
|
53
|
+
unless report["ready"]
|
|
54
|
+
raise Ace::Support::Cli::Error, failure_summary(report)
|
|
55
|
+
end
|
|
56
|
+
rescue Ace::Lab::InvalidConfigurationError => e
|
|
57
|
+
raise Ace::Support::Cli::Error, e.message
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def render(report, format:, quiet:)
|
|
63
|
+
return if quiet
|
|
64
|
+
|
|
65
|
+
puts format == "json" ? JSON.generate(report) : render_table(report)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def render_table(report)
|
|
69
|
+
counts = report["counts"]
|
|
70
|
+
lines = ["Machine: #{report['machine']} manifest: #{report['manifest']}"]
|
|
71
|
+
report["checks"].each do |section, checks|
|
|
72
|
+
lines << "#{section.capitalize}:"
|
|
73
|
+
if checks.empty?
|
|
74
|
+
lines << " (none declared)"
|
|
75
|
+
next
|
|
76
|
+
end
|
|
77
|
+
checks.each do |check|
|
|
78
|
+
label = check[SECTION_LABELS.fetch(section, "reason")] || check["path"] || section
|
|
79
|
+
detail = detail_of(check)
|
|
80
|
+
lines << " #{label.to_s.ljust(34)} #{check['status'].upcase}#{detail}"
|
|
81
|
+
lines << " #{check['reason']}" if check["reason"]
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
lines << "Checks: #{counts['pass']} pass, #{counts['fail']} failed, " \
|
|
85
|
+
"#{counts['unknown']} unknown"
|
|
86
|
+
lines << "Result: #{report['ready'] ? 'ready' : 'NOT READY'}"
|
|
87
|
+
lines.join("\n")
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def detail_of(check)
|
|
91
|
+
parts = []
|
|
92
|
+
parts << "uid #{check['uid']}" if check.key?("uid")
|
|
93
|
+
parts << "shell #{check['shell']}" if check["shell"]
|
|
94
|
+
parts << "fingerprint #{check['fingerprint']}" if check["fingerprint"]
|
|
95
|
+
parts << "origin #{check['origin']}" if check["origin"]
|
|
96
|
+
parts << "projects #{check['projects'].join(', ')}" if check["projects"].is_a?(Array)
|
|
97
|
+
parts.empty? ? "" : " (#{parts.join(', ')})"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def failure_summary(report)
|
|
101
|
+
failures = report["checks"].flat_map do |section, checks|
|
|
102
|
+
checks.select { |check| check["status"] == "fail" }
|
|
103
|
+
.map { |check| "#{section}/#{check_name(check, section)}: #{check['reason']}" }
|
|
104
|
+
end
|
|
105
|
+
"lab is not ready (#{failures.size} failed check(s)): #{failures.join('; ')}"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def check_name(check, section)
|
|
109
|
+
check[SECTION_LABELS.fetch(section, "reason")] || check["path"] || section
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
data/lib/ace/lab/cli.rb
CHANGED
|
@@ -8,10 +8,11 @@ require_relative "cli/commands/agents"
|
|
|
8
8
|
require_relative "cli/commands/services"
|
|
9
9
|
require_relative "cli/commands/resolve"
|
|
10
10
|
require_relative "cli/commands/route"
|
|
11
|
-
require_relative "cli/commands/
|
|
12
|
-
require_relative "cli/commands/
|
|
13
|
-
require_relative "cli/commands/
|
|
14
|
-
require_relative "cli/commands/
|
|
11
|
+
require_relative "cli/commands/status"
|
|
12
|
+
require_relative "cli/commands/prepare"
|
|
13
|
+
require_relative "cli/commands/project_add"
|
|
14
|
+
require_relative "cli/commands/install"
|
|
15
|
+
require_relative "cli/commands/verify"
|
|
15
16
|
|
|
16
17
|
module Ace
|
|
17
18
|
module Lab
|
|
@@ -23,26 +24,28 @@ module Ace
|
|
|
23
24
|
|
|
24
25
|
# Application commands with descriptions (for help output)
|
|
25
26
|
REGISTERED_COMMANDS = [
|
|
26
|
-
["run", "Run one explicitly authorized project command"],
|
|
27
|
-
["run-status", "Inspect an original command execution without running it"],
|
|
28
27
|
["projects", "List lab projects visible to the verified caller"],
|
|
29
28
|
["agents", "List lab agents in a project by stable ID"],
|
|
30
29
|
["services", "List lab services in a project by stable ID"],
|
|
31
30
|
["resolve", "Resolve one lab entry by exact stable ID"],
|
|
32
31
|
["route", "Select a configured capable service in a project"],
|
|
33
|
-
["
|
|
34
|
-
["
|
|
35
|
-
["
|
|
36
|
-
["
|
|
37
|
-
["
|
|
38
|
-
["assignment request", "Send one retained operation to the installed ordinary Assign owner"]
|
|
32
|
+
["status", "Show lab configuration and local file state (read-only)"],
|
|
33
|
+
["prepare", "Prepare accounts, directories, permissions, keys and roles from the trusted install manifest"],
|
|
34
|
+
["project add", "Register a project and make it ready for team work (repository, _overseer, lab config)"],
|
|
35
|
+
["install", "Perform the full new-lab installation from the trusted install manifest"],
|
|
36
|
+
["verify", "Verify the installed lab's readiness under the target accounts (read-only)"]
|
|
39
37
|
].freeze
|
|
40
38
|
|
|
41
39
|
HELP_EXAMPLES = [
|
|
42
40
|
"ace-lab projects --format json",
|
|
43
41
|
"ace-lab agents --project atlas --format json",
|
|
44
42
|
"ace-lab resolve --id atlas-planner --format json",
|
|
45
|
-
"ace-lab route --project atlas --capability search --format json"
|
|
43
|
+
"ace-lab route --project atlas --capability search --format json",
|
|
44
|
+
"ace-lab status --format json",
|
|
45
|
+
"ace-lab prepare --dry-run",
|
|
46
|
+
"ace-lab project add atlas --path /Users/ace/lab/atlas --remote git@forge:atlas/atlas.git",
|
|
47
|
+
"ace-lab install --dry-run",
|
|
48
|
+
"ace-lab verify --format json"
|
|
46
49
|
].freeze
|
|
47
50
|
|
|
48
51
|
# Start the CLI
|
|
@@ -50,7 +53,6 @@ module Ace
|
|
|
50
53
|
# @param args [Array<String>] Command-line arguments
|
|
51
54
|
# @return [Integer] Exit code (0 for success, non-zero for failure)
|
|
52
55
|
def self.start(args)
|
|
53
|
-
return CLI::Commands::Run.invoke(args.drop(1)) if args.first == "run" && !(args.take(args.index("--") || args.size).include?("--help") || args.take(args.index("--") || args.size).include?("-h"))
|
|
54
56
|
Ace::Support::Cli::Runner.new(self).call(args: args)
|
|
55
57
|
end
|
|
56
58
|
|
|
@@ -60,15 +62,11 @@ module Ace
|
|
|
60
62
|
register "services", CLI::Commands::Services.new
|
|
61
63
|
register "resolve", CLI::Commands::Resolve.new
|
|
62
64
|
register "route", CLI::Commands::Route.new
|
|
63
|
-
register "
|
|
64
|
-
register "
|
|
65
|
-
register "
|
|
66
|
-
register "
|
|
67
|
-
register "
|
|
68
|
-
register "command execute", CLI::Commands::CommandExecute.new
|
|
69
|
-
register "assignment prepare", CLI::Commands::Assignment::Prepare.new
|
|
70
|
-
register "assignment drive", CLI::Commands::Assignment::Drive.new
|
|
71
|
-
register "assignment request", CLI::Commands::Assignment::Request.new
|
|
65
|
+
register "status", CLI::Commands::Status.new
|
|
66
|
+
register "prepare", CLI::Commands::Prepare.new
|
|
67
|
+
register "project add", CLI::Commands::ProjectAdd.new
|
|
68
|
+
register "install", CLI::Commands::Install.new
|
|
69
|
+
register "verify", CLI::Commands::Verify.new
|
|
72
70
|
|
|
73
71
|
# Register version command
|
|
74
72
|
version_cmd = Ace::Support::Cli::VersionCommand.build(
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ace
|
|
4
|
+
module Lab
|
|
5
|
+
# The trusted install manifest is a deployment document at a FIXED path
|
|
6
|
+
# — never in the caller-writable configuration cascade and never at a
|
|
7
|
+
# caller-selected location. It must be root-owned and not
|
|
8
|
+
# group/world-writable along its whole path, exactly like
|
|
9
|
+
# AUTHORIZATION_PATH; InstallManifestLoader verifies through
|
|
10
|
+
# GrantResolver.read_verified and fails closed. A caller-writable
|
|
11
|
+
# manifest could provision accounts, directories and keys, i.e.
|
|
12
|
+
# manufacture privileges.
|
|
13
|
+
INSTALL_MANIFEST_PATH = "/etc/lab/ace-lab/install.yml"
|
|
14
|
+
|
|
15
|
+
# Path of the trusted install manifest. A method (not a bare constant
|
|
16
|
+
# reference) so tests stub the seam without any caller-controllable
|
|
17
|
+
# production override; the CLI deliberately exposes no --manifest flag.
|
|
18
|
+
# @return [String]
|
|
19
|
+
def self.install_manifest_path
|
|
20
|
+
INSTALL_MANIFEST_PATH
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ace
|
|
4
|
+
module Lab
|
|
5
|
+
module Models
|
|
6
|
+
# Immutable value object carrying the assembled status report.
|
|
7
|
+
# ok? is false exactly when a configuration source is invalid or a
|
|
8
|
+
# state location failed to read — an empty-but-valid lab is ok.
|
|
9
|
+
class StatusReport
|
|
10
|
+
attr_reader :config, :runtime_config, :state, :root
|
|
11
|
+
|
|
12
|
+
def initialize(config:, runtime_config:, state:, root:)
|
|
13
|
+
@config = config.freeze
|
|
14
|
+
@runtime_config = runtime_config.freeze
|
|
15
|
+
@state = state.map(&:freeze).freeze
|
|
16
|
+
@root = root
|
|
17
|
+
freeze
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def ok?
|
|
21
|
+
@config[:state] != :invalid &&
|
|
22
|
+
@runtime_config[:state] != :invalid &&
|
|
23
|
+
@state.none? { |location| location[:state] == :error }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def to_h
|
|
27
|
+
{
|
|
28
|
+
config: @config,
|
|
29
|
+
runtime_config: @runtime_config,
|
|
30
|
+
state: @state,
|
|
31
|
+
root: @root,
|
|
32
|
+
ok: ok?
|
|
33
|
+
}
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|