kitsune-kit 0.4.0 → 0.5.0
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 +117 -2
- data/CONTRIBUTING.md +83 -0
- data/README.md +108 -136
- data/SECURITY.md +25 -0
- data/bin/kit +4 -1
- data/docs/architecture/decisions/0001-product-boundary.md +23 -0
- data/docs/architecture/decisions/0002-core-and-interfaces.md +23 -0
- data/docs/architecture/decisions/0003-configuration-state-and-secrets.md +23 -0
- data/docs/architecture/decisions/0004-supported-platforms.md +19 -0
- data/docs/architecture/decisions/0005-operation-semantics.md +19 -0
- data/docs/architecture/decisions/0006-tui-backend.md +21 -0
- data/docs/architecture/provider-adapters.md +49 -0
- data/docs/architecture.md +89 -0
- data/docs/commands.md +210 -0
- data/docs/configuration.md +210 -0
- data/docs/getting-started.md +133 -0
- data/docs/providers/digitalocean.md +52 -0
- data/docs/releasing.md +43 -0
- data/docs/roadmap.md +46 -0
- data/docs/security-audit.md +130 -0
- data/docs/security.md +116 -0
- data/docs/services/postgres.md +78 -0
- data/docs/services/redis.md +53 -0
- data/docs/testing.md +200 -0
- data/docs/troubleshooting.md +123 -0
- data/docs/tui.md +57 -0
- data/lib/kitsune/kit/adapters/confirming_host_key_verifier.rb +42 -0
- data/lib/kitsune/kit/adapters/digitalocean_provider.rb +217 -0
- data/lib/kitsune/kit/adapters/fake_clock.rb +34 -0
- data/lib/kitsune/kit/adapters/fake_provider.rb +107 -0
- data/lib/kitsune/kit/adapters/fake_reporter.rb +34 -0
- data/lib/kitsune/kit/adapters/fake_secret_store.rb +35 -0
- data/lib/kitsune/kit/adapters/fake_state_store.rb +91 -0
- data/lib/kitsune/kit/adapters/fake_transport.rb +56 -0
- data/lib/kitsune/kit/adapters/net_ssh_transport.rb +155 -0
- data/lib/kitsune/kit/adapters/provider.rb +34 -0
- data/lib/kitsune/kit/adapters/transport.rb +22 -0
- data/lib/kitsune/kit/adapters/transport_factory.rb +116 -0
- data/lib/kitsune/kit/application.rb +170 -0
- data/lib/kitsune/kit/cancellation.rb +21 -0
- data/lib/kitsune/kit/cli.rb +802 -64
- data/lib/kitsune/kit/clock.rb +11 -0
- data/lib/kitsune/kit/configuration.rb +493 -0
- data/lib/kitsune/kit/errors.rb +98 -0
- data/lib/kitsune/kit/events.rb +55 -0
- data/lib/kitsune/kit/operations/ensure_dns_records.rb +165 -0
- data/lib/kitsune/kit/operations/ensure_server.rb +135 -0
- data/lib/kitsune/kit/operations/ensure_service.rb +285 -0
- data/lib/kitsune/kit/operations/remote_script.rb +214 -0
- data/lib/kitsune/kit/operations/service_backup.rb +71 -0
- data/lib/kitsune/kit/operations/service_files.rb +127 -0
- data/lib/kitsune/kit/operations/service_firewall.rb +190 -0
- data/lib/kitsune/kit/operations/service_state.rb +59 -0
- data/lib/kitsune/kit/plan.rb +72 -0
- data/lib/kitsune/kit/reporters/human.rb +89 -0
- data/lib/kitsune/kit/reporters/json.rb +65 -0
- data/lib/kitsune/kit/reporters/reporter.rb +11 -0
- data/lib/kitsune/kit/result.rb +28 -0
- data/lib/kitsune/kit/run_journal.rb +102 -0
- data/lib/kitsune/kit/run_logger.rb +37 -0
- data/lib/kitsune/kit/scripts/docker.sh +137 -0
- data/lib/kitsune/kit/scripts/firewall.sh +205 -0
- data/lib/kitsune/kit/scripts/metrics.sh +54 -0
- data/lib/kitsune/kit/scripts/ssh.sh +95 -0
- data/lib/kitsune/kit/scripts/swap.sh +86 -0
- data/lib/kitsune/kit/scripts/unattended.sh +81 -0
- data/lib/kitsune/kit/scripts/user.sh +114 -0
- data/lib/kitsune/kit/secret_filter.rb +54 -0
- data/lib/kitsune/kit/secret_store.rb +32 -0
- data/lib/kitsune/kit/secret_stores/store.rb +12 -0
- data/lib/kitsune/kit/service_compose.rb +72 -0
- data/lib/kitsune/kit/state_store.rb +158 -0
- data/lib/kitsune/kit/state_stores/store.rb +15 -0
- data/lib/kitsune/kit/tui/actions.rb +72 -0
- data/lib/kitsune/kit/tui/application.rb +35 -0
- data/lib/kitsune/kit/tui/controller.rb +162 -0
- data/lib/kitsune/kit/tui/renderer.rb +134 -0
- data/lib/kitsune/kit/tui/state.rb +18 -0
- data/lib/kitsune/kit/tui/store.rb +88 -0
- data/lib/kitsune/kit/tui/terminal.rb +95 -0
- data/lib/kitsune/kit/version.rb +1 -1
- data/lib/kitsune/kit/workflows/apply_plan.rb +145 -0
- data/lib/kitsune/kit/workflows/base.rb +31 -0
- data/lib/kitsune/kit/workflows/build_plan.rb +33 -0
- data/lib/kitsune/kit/workflows/destroy_server.rb +84 -0
- data/lib/kitsune/kit/workflows/doctor.rb +225 -0
- data/lib/kitsune/kit/workflows/environment_selection.rb +70 -0
- data/lib/kitsune/kit/workflows/import_server.rb +100 -0
- data/lib/kitsune/kit/workflows/initialize_project.rb +121 -0
- data/lib/kitsune/kit/workflows/inspect_environment.rb +44 -0
- data/lib/kitsune/kit/workflows/rollback.rb +54 -0
- data/lib/kitsune/kit/workflows/support_bundle.rb +82 -0
- data/lib/kitsune/kit.rb +41 -2
- metadata +122 -79
- data/.rspec +0 -3
- data/Rakefile +0 -8
- data/kitsune-kit-logo.jpg +0 -0
- data/lib/kitsune/blueprints/.env.template +0 -31
- data/lib/kitsune/blueprints/docker/postgres.yml +0 -27
- data/lib/kitsune/blueprints/docker/redis.yml +0 -23
- data/lib/kitsune/blueprints/kit.env.template +0 -1
- data/lib/kitsune/kit/ansi_color.rb +0 -78
- data/lib/kitsune/kit/commands/bootstrap.rb +0 -134
- data/lib/kitsune/kit/commands/bootstrap_docker.rb +0 -66
- data/lib/kitsune/kit/commands/dns.rb +0 -112
- data/lib/kitsune/kit/commands/init.rb +0 -148
- data/lib/kitsune/kit/commands/install_docker_engine.rb +0 -146
- data/lib/kitsune/kit/commands/postinstall_docker.rb +0 -142
- data/lib/kitsune/kit/commands/provision.rb +0 -43
- data/lib/kitsune/kit/commands/setup_do_metrics.rb +0 -123
- data/lib/kitsune/kit/commands/setup_docker_prereqs.rb +0 -151
- data/lib/kitsune/kit/commands/setup_firewall.rb +0 -132
- data/lib/kitsune/kit/commands/setup_postgres_docker.rb +0 -246
- data/lib/kitsune/kit/commands/setup_redis_docker.rb +0 -241
- data/lib/kitsune/kit/commands/setup_swap.rb +0 -151
- data/lib/kitsune/kit/commands/setup_unattended.rb +0 -132
- data/lib/kitsune/kit/commands/setup_user.rb +0 -189
- data/lib/kitsune/kit/commands/ssh.rb +0 -46
- data/lib/kitsune/kit/commands/switch_env.rb +0 -42
- data/lib/kitsune/kit/defaults.rb +0 -91
- data/lib/kitsune/kit/env_loader.rb +0 -41
- data/lib/kitsune/kit/options_builder.rb +0 -26
- data/lib/kitsune/kit/provisioner.rb +0 -107
- data/sig/kitsune/kit.rbs +0 -6
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kitsune
|
|
4
|
+
module Kit
|
|
5
|
+
class Change < Data.define(:resource, :action, :summary, :details, :destructive)
|
|
6
|
+
ACTIONS = %w[create update delete no_change].freeze
|
|
7
|
+
|
|
8
|
+
def initialize(resource:, action:, summary:, details: {}, destructive: false)
|
|
9
|
+
raise ArgumentError, "unknown action: #{action}" unless ACTIONS.include?(action.to_s)
|
|
10
|
+
|
|
11
|
+
super(
|
|
12
|
+
resource: resource.to_s,
|
|
13
|
+
action: action.to_s,
|
|
14
|
+
summary: summary.to_s,
|
|
15
|
+
details: details.freeze,
|
|
16
|
+
destructive: !!destructive
|
|
17
|
+
)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def changed? = action != "no_change"
|
|
21
|
+
def destructive? = destructive
|
|
22
|
+
|
|
23
|
+
def to_h
|
|
24
|
+
{
|
|
25
|
+
resource: resource,
|
|
26
|
+
action: action,
|
|
27
|
+
summary: summary,
|
|
28
|
+
details: details,
|
|
29
|
+
destructive: destructive
|
|
30
|
+
}
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.from_h(value)
|
|
34
|
+
new(
|
|
35
|
+
resource: value.fetch("resource"),
|
|
36
|
+
action: value.fetch("action"),
|
|
37
|
+
summary: value.fetch("summary"),
|
|
38
|
+
details: value.fetch("details", {}),
|
|
39
|
+
destructive: value.fetch("destructive", false)
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
class Plan < Data.define(:environment, :changes)
|
|
45
|
+
def initialize(environment:, changes:)
|
|
46
|
+
super(environment: environment.to_s, changes: changes.freeze)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def changed? = changes.any?(&:changed?)
|
|
50
|
+
def destructive? = changes.any?(&:destructive)
|
|
51
|
+
def changed_count = changes.count(&:changed?)
|
|
52
|
+
|
|
53
|
+
def counts
|
|
54
|
+
changes.each_with_object(Hash.new(0)) { |change, total| total[change.action] += 1 }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def to_h
|
|
58
|
+
{
|
|
59
|
+
environment: environment,
|
|
60
|
+
changed: changed?,
|
|
61
|
+
destructive: destructive?,
|
|
62
|
+
counts: counts,
|
|
63
|
+
changes: changes.map(&:to_h)
|
|
64
|
+
}
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def self.from_h(environment:, changes:)
|
|
68
|
+
new(environment: environment, changes: changes.map { |change| Change.from_h(change) })
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "reporter"
|
|
4
|
+
|
|
5
|
+
module Kitsune
|
|
6
|
+
module Kit
|
|
7
|
+
module Reporters
|
|
8
|
+
class Human < Reporter
|
|
9
|
+
SYMBOLS = {
|
|
10
|
+
"run_started" => "START",
|
|
11
|
+
"plan_built" => "PLAN",
|
|
12
|
+
"operation_started" => "RUN",
|
|
13
|
+
"operation_progressed" => "PROGRESS",
|
|
14
|
+
"operation_succeeded" => "OK",
|
|
15
|
+
"operation_skipped" => "SKIP",
|
|
16
|
+
"operation_failed" => "FAIL",
|
|
17
|
+
"warning_emitted" => "WARN",
|
|
18
|
+
"run_finished" => "DONE"
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
def initialize(output: $stdout, error: $stderr, color: nil, quiet: false, verbose: false,
|
|
22
|
+
secret_filter: SecretFilter.new)
|
|
23
|
+
super()
|
|
24
|
+
@output = output
|
|
25
|
+
@error = error
|
|
26
|
+
@color = color.nil? ? output.tty? && !ENV.key?("NO_COLOR") : color
|
|
27
|
+
@secret_filter = secret_filter
|
|
28
|
+
@quiet = quiet
|
|
29
|
+
@verbose = verbose
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def handle(event)
|
|
33
|
+
return if @quiet && !%w[operation_failed warning_emitted run_finished].include?(event.type)
|
|
34
|
+
|
|
35
|
+
data = @secret_filter.filter(event.data)
|
|
36
|
+
stream = %w[operation_failed warning_emitted].include?(event.type) ? @error : @output
|
|
37
|
+
message = message_for(event.type, data)
|
|
38
|
+
stream.puts("#{label(event.type)} #{message}") if message
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def label(type)
|
|
44
|
+
value = "[#{SYMBOLS.fetch(type, type.upcase)}]"
|
|
45
|
+
return value unless @color
|
|
46
|
+
|
|
47
|
+
code = case type
|
|
48
|
+
when "operation_succeeded", "run_finished" then 32
|
|
49
|
+
when "operation_failed" then 31
|
|
50
|
+
when "warning_emitted" then 33
|
|
51
|
+
else 36
|
|
52
|
+
end
|
|
53
|
+
"\e[#{code}m#{value}\e[0m"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def message_for(type, data)
|
|
57
|
+
method = "message_#{type}"
|
|
58
|
+
send(method, data) if respond_to?(method, true)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def message_run_started(data) = "#{data[:command]} for #{data[:environment]}"
|
|
62
|
+
def message_plan_built(data) = "#{data[:changed_count]} change(s), #{data[:unchanged_count]} unchanged"
|
|
63
|
+
|
|
64
|
+
def message_operation_started(data)
|
|
65
|
+
progress = data[:index] && data[:total] ? "[#{data[:index]}/#{data[:total]}] " : ""
|
|
66
|
+
"#{progress}#{data[:summary]}"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def message_operation_succeeded(data)
|
|
70
|
+
duration = data[:duration_ms] ? " (#{data[:duration_ms]}ms)" : ""
|
|
71
|
+
"#{data[:summary]}#{duration}"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def message_operation_progressed(data)
|
|
75
|
+
"#{data[:summary]}: #{data[:percent]}%" if @verbose
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def message_operation_failed(data) = "#{data[:summary]}: #{data[:message]}"
|
|
79
|
+
|
|
80
|
+
def message_operation_skipped(data)
|
|
81
|
+
"#{data[:summary]} (#{data[:reason]})" if @verbose
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def message_warning_emitted(data) = data[:message]
|
|
85
|
+
def message_run_finished(data) = "#{data[:status]} in #{data[:duration_ms]}ms"
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require_relative "reporter"
|
|
5
|
+
|
|
6
|
+
module Kitsune
|
|
7
|
+
module Kit
|
|
8
|
+
module Reporters
|
|
9
|
+
class Json < Reporter
|
|
10
|
+
attr_reader :events
|
|
11
|
+
|
|
12
|
+
def initialize(output: $stdout, secret_filter: SecretFilter.new)
|
|
13
|
+
super()
|
|
14
|
+
@output = output
|
|
15
|
+
@secret_filter = secret_filter
|
|
16
|
+
@events = []
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def handle(event)
|
|
20
|
+
@events << @secret_filter.filter(event.to_h)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def flush(result: nil, command: nil, environment: nil)
|
|
24
|
+
started = event("run_started", reverse: true)
|
|
25
|
+
finished = event("run_finished", reverse: true)
|
|
26
|
+
payload = {
|
|
27
|
+
schema_version: 1,
|
|
28
|
+
command: command || started&.dig(:data, :command),
|
|
29
|
+
environment: environment || started&.dig(:data, :environment),
|
|
30
|
+
run_id: started&.dig(:run_id) || result&.metadata&.dig(:run_id),
|
|
31
|
+
status: (result&.status || status_from_events).to_s,
|
|
32
|
+
duration_ms: finished&.dig(:data, :duration_ms),
|
|
33
|
+
result: @secret_filter.filter(serialize(result&.value)),
|
|
34
|
+
warnings: @secret_filter.filter(result&.warnings || []),
|
|
35
|
+
events: @events
|
|
36
|
+
}
|
|
37
|
+
@output.puts(::JSON.generate(payload))
|
|
38
|
+
payload
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def event(type, reverse: false)
|
|
44
|
+
source = reverse ? @events.reverse_each : @events.each
|
|
45
|
+
source.find { |item| item[:type] == type }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def status_from_events
|
|
49
|
+
@events.reverse_each do |event|
|
|
50
|
+
return event.dig(:data, :status) if event[:type] == "run_finished"
|
|
51
|
+
end
|
|
52
|
+
"unknown"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def serialize(value)
|
|
56
|
+
return value.map { |item| serialize(item) } if value.is_a?(Array)
|
|
57
|
+
return value.to_h { |key, item| [key, serialize(item)] } if value.is_a?(Hash)
|
|
58
|
+
return serialize(value.to_h) if value.respond_to?(:to_h)
|
|
59
|
+
|
|
60
|
+
value
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kitsune
|
|
4
|
+
module Kit
|
|
5
|
+
class Result < Data.define(:status, :value, :warnings, :metadata)
|
|
6
|
+
STATUSES = %i[success failure cancelled].freeze
|
|
7
|
+
|
|
8
|
+
def initialize(status:, value: nil, warnings: [], metadata: {})
|
|
9
|
+
raise ArgumentError, "unknown status: #{status}" unless STATUSES.include?(status.to_sym)
|
|
10
|
+
|
|
11
|
+
super(
|
|
12
|
+
status: status.to_sym,
|
|
13
|
+
value: value,
|
|
14
|
+
warnings: warnings.freeze,
|
|
15
|
+
metadata: metadata.freeze
|
|
16
|
+
)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def success? = status == :success
|
|
20
|
+
def failure? = status == :failure
|
|
21
|
+
def cancelled? = status == :cancelled
|
|
22
|
+
|
|
23
|
+
def self.success(value = nil, warnings: [], metadata: {})
|
|
24
|
+
new(status: :success, value: value, warnings: warnings, metadata: metadata)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "time"
|
|
5
|
+
require_relative "errors"
|
|
6
|
+
|
|
7
|
+
module Kitsune
|
|
8
|
+
module Kit
|
|
9
|
+
class RunJournal
|
|
10
|
+
RESUMABLE_STATUSES = %w[running failure cancelled].freeze
|
|
11
|
+
|
|
12
|
+
def initialize(state_store:, environment:, run_id:, clock: -> { Time.now.utc })
|
|
13
|
+
@state_store = state_store
|
|
14
|
+
@environment = environment
|
|
15
|
+
@run_id = run_id
|
|
16
|
+
@clock = clock
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def start(changes:, resumed_from: nil)
|
|
20
|
+
update_run do |run|
|
|
21
|
+
run.merge!(
|
|
22
|
+
"status" => "running",
|
|
23
|
+
"started_at" => timestamp,
|
|
24
|
+
"finished_at" => nil,
|
|
25
|
+
"resumed_from" => resumed_from,
|
|
26
|
+
"changes" => normalize(changes),
|
|
27
|
+
"steps" => {}
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def record_step(resource, status, error: nil)
|
|
33
|
+
update_run do |run|
|
|
34
|
+
run["steps"] ||= {}
|
|
35
|
+
run["steps"][resource] = {
|
|
36
|
+
"status" => status,
|
|
37
|
+
"error" => error,
|
|
38
|
+
"updated_at" => timestamp
|
|
39
|
+
}
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def finish(status, error: nil)
|
|
44
|
+
update_run do |run|
|
|
45
|
+
run["status"] = status
|
|
46
|
+
run["error"] = error
|
|
47
|
+
run["finished_at"] = timestamp
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def resumable(requested_id = nil)
|
|
52
|
+
runs = state_store.read(environment)["runs"]
|
|
53
|
+
selected_id = requested_id || runs.keys.reverse.find { |id| resumable_status?(runs.fetch(id)) }
|
|
54
|
+
raise no_resumable_run_error unless selected_id
|
|
55
|
+
|
|
56
|
+
run = runs[selected_id]
|
|
57
|
+
raise Errors::ConfigurationError, "run not found: #{selected_id}" unless run
|
|
58
|
+
|
|
59
|
+
validate_resumable!(selected_id, run)
|
|
60
|
+
[selected_id, run]
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
attr_reader :state_store, :environment, :run_id, :clock
|
|
66
|
+
|
|
67
|
+
def update_run
|
|
68
|
+
return unless state_store
|
|
69
|
+
|
|
70
|
+
state_store.update(environment) do |state|
|
|
71
|
+
run = state["runs"][run_id] ||= {}
|
|
72
|
+
yield(run)
|
|
73
|
+
state
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def validate_resumable!(selected_id, run)
|
|
78
|
+
unless resumable_status?(run)
|
|
79
|
+
raise Errors::ConfigurationError.new(
|
|
80
|
+
"run #{selected_id} cannot be resumed because its status is #{run['status']}",
|
|
81
|
+
hint: "Resume a failed, cancelled, or interrupted run."
|
|
82
|
+
)
|
|
83
|
+
end
|
|
84
|
+
return if run["changes"].is_a?(Array) && run["steps"].is_a?(Hash)
|
|
85
|
+
|
|
86
|
+
raise Errors::ConfigurationError, "run #{selected_id} does not contain a resumable plan"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def resumable_status?(run) = RESUMABLE_STATUSES.include?(run["status"])
|
|
90
|
+
|
|
91
|
+
def no_resumable_run_error
|
|
92
|
+
Errors::ConfigurationError.new(
|
|
93
|
+
"there is no run available to resume",
|
|
94
|
+
hint: "Run `kit apply` first or provide the ID of a failed run."
|
|
95
|
+
)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def timestamp = clock.call.iso8601(6)
|
|
99
|
+
def normalize(value) = JSON.parse(JSON.generate(value))
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "json"
|
|
5
|
+
require "pathname"
|
|
6
|
+
|
|
7
|
+
module Kitsune
|
|
8
|
+
module Kit
|
|
9
|
+
class RunLogger
|
|
10
|
+
MAX_FILES = 20
|
|
11
|
+
|
|
12
|
+
attr_reader :path
|
|
13
|
+
|
|
14
|
+
def initialize(root:, run_id:, secret_filter: SecretFilter.new, clock: Clock.new)
|
|
15
|
+
directory = Pathname(root).expand_path.join(".kitsune/logs")
|
|
16
|
+
FileUtils.mkdir_p(directory, mode: 0o700)
|
|
17
|
+
@path = directory.join("#{clock.now.strftime('%Y%m%dT%H%M%S')}-#{run_id}.jsonl")
|
|
18
|
+
@secret_filter = secret_filter
|
|
19
|
+
FileUtils.touch(@path)
|
|
20
|
+
File.chmod(0o600, @path)
|
|
21
|
+
rotate(directory)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def handle(event)
|
|
25
|
+
File.open(path, "a", 0o600) do |file|
|
|
26
|
+
file.puts(JSON.generate(@secret_filter.filter(event.to_h)))
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def rotate(directory)
|
|
33
|
+
directory.glob("*.jsonl").sort_by(&:mtime).reverse.drop(MAX_FILES).each(&:delete)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -Eeuo pipefail
|
|
3
|
+
|
|
4
|
+
action="${1:?action is required}"
|
|
5
|
+
user="${2:?user is required}"
|
|
6
|
+
backup_dir="/var/lib/kitsune/backups/docker"
|
|
7
|
+
keyring="/etc/apt/keyrings/docker.asc"
|
|
8
|
+
repository="/etc/apt/sources.list.d/docker.sources"
|
|
9
|
+
packages=(docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin)
|
|
10
|
+
prerequisites=(ca-certificates curl)
|
|
11
|
+
conflicting_packages=(docker.io docker-compose docker-compose-v2 podman-docker containerd runc)
|
|
12
|
+
|
|
13
|
+
refuse_conflicting_packages() {
|
|
14
|
+
local installed=()
|
|
15
|
+
local package
|
|
16
|
+
for package in "${conflicting_packages[@]}"; do
|
|
17
|
+
if dpkg-query -W -f='${Status}' "${package}" 2>/dev/null | grep -q "ok installed"; then
|
|
18
|
+
installed+=("${package}")
|
|
19
|
+
fi
|
|
20
|
+
done
|
|
21
|
+
if (( ${#installed[@]} > 0 )); then
|
|
22
|
+
printf 'conflicting Docker packages are installed: %s\n' "${installed[*]}" >&2
|
|
23
|
+
echo "Remove or migrate them explicitly before Kitsune Kit installs Docker CE." >&2
|
|
24
|
+
exit 3
|
|
25
|
+
fi
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
snapshot() {
|
|
29
|
+
[[ -f "${backup_dir}/snapshot-complete" ]] && return
|
|
30
|
+
: > "${backup_dir}/packages-before"
|
|
31
|
+
for package in "${prerequisites[@]}" "${packages[@]}"; do
|
|
32
|
+
if dpkg-query -W -f='${Status}' "${package}" 2>/dev/null | grep -q "ok installed"; then
|
|
33
|
+
printf '%s\n' "${package}" >> "${backup_dir}/packages-before"
|
|
34
|
+
fi
|
|
35
|
+
done
|
|
36
|
+
systemctl is-enabled --quiet docker && touch "${backup_dir}/enabled-before"
|
|
37
|
+
systemctl is-active --quiet docker && touch "${backup_dir}/active-before"
|
|
38
|
+
touch "${backup_dir}/snapshot-complete"
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
apply() {
|
|
42
|
+
refuse_conflicting_packages
|
|
43
|
+
install -d -m 0700 "${backup_dir}"
|
|
44
|
+
snapshot
|
|
45
|
+
apt-get update
|
|
46
|
+
DEBIAN_FRONTEND=noninteractive apt-get install --yes "${prerequisites[@]}"
|
|
47
|
+
install -d -m 0755 /etc/apt/keyrings
|
|
48
|
+
if [[ ! -f "${keyring}" ]]; then
|
|
49
|
+
temporary_key="$(mktemp)"
|
|
50
|
+
trap 'rm -f "${temporary_key}"' EXIT
|
|
51
|
+
curl --fail --silent --show-error --location \
|
|
52
|
+
https://download.docker.com/linux/ubuntu/gpg --output "${temporary_key}"
|
|
53
|
+
test -s "${temporary_key}"
|
|
54
|
+
install -o root -g root -m 0644 "${temporary_key}" "${keyring}"
|
|
55
|
+
rm -f "${temporary_key}"
|
|
56
|
+
trap - EXIT
|
|
57
|
+
touch "${backup_dir}/key-created"
|
|
58
|
+
fi
|
|
59
|
+
if [[ ! -f "${repository}" ]]; then
|
|
60
|
+
codename="$(sed -n 's/^UBUNTU_CODENAME=//p' /etc/os-release | tr -d '\"')"
|
|
61
|
+
if [[ -z "${codename}" ]]; then
|
|
62
|
+
codename="$(sed -n 's/^VERSION_CODENAME=//p' /etc/os-release | tr -d '\"')"
|
|
63
|
+
fi
|
|
64
|
+
[[ -n "${codename}" ]] || { echo "cannot determine Ubuntu codename" >&2; exit 1; }
|
|
65
|
+
temporary_repository="$(mktemp)"
|
|
66
|
+
trap 'rm -f "${temporary_repository}"' EXIT
|
|
67
|
+
cat > "${temporary_repository}" <<EOF
|
|
68
|
+
Types: deb
|
|
69
|
+
URIs: https://download.docker.com/linux/ubuntu
|
|
70
|
+
Suites: ${codename}
|
|
71
|
+
Components: stable
|
|
72
|
+
Signed-By: ${keyring}
|
|
73
|
+
EOF
|
|
74
|
+
install -o root -g root -m 0644 "${temporary_repository}" "${repository}"
|
|
75
|
+
rm -f "${temporary_repository}"
|
|
76
|
+
trap - EXIT
|
|
77
|
+
touch "${backup_dir}/repository-created"
|
|
78
|
+
fi
|
|
79
|
+
apt-get update
|
|
80
|
+
DEBIAN_FRONTEND=noninteractive apt-get install --yes "${packages[@]}"
|
|
81
|
+
systemctl enable --now docker
|
|
82
|
+
if ! id -nG "${user}" | tr ' ' '\n' | grep -Fxq docker; then
|
|
83
|
+
usermod -aG docker "${user}"
|
|
84
|
+
touch "${backup_dir}/group-added"
|
|
85
|
+
fi
|
|
86
|
+
if ! docker network inspect kitsune-private >/dev/null 2>&1; then
|
|
87
|
+
docker network create --driver bridge kitsune-private >/dev/null
|
|
88
|
+
touch "${backup_dir}/network-created"
|
|
89
|
+
fi
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
verify() {
|
|
93
|
+
systemctl is-active --quiet docker
|
|
94
|
+
docker version >/dev/null
|
|
95
|
+
docker compose version >/dev/null
|
|
96
|
+
id -nG "${user}" | tr ' ' '\n' | grep -Fxq docker
|
|
97
|
+
docker network inspect kitsune-private >/dev/null
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
rollback() {
|
|
101
|
+
if [[ -f "${backup_dir}/group-added" ]]; then
|
|
102
|
+
gpasswd -d "${user}" docker || true
|
|
103
|
+
fi
|
|
104
|
+
if [[ -f "${backup_dir}/network-created" ]]; then
|
|
105
|
+
docker network rm kitsune-private || true
|
|
106
|
+
fi
|
|
107
|
+
for package in "${packages[@]}"; do
|
|
108
|
+
if ! grep -Fxq "${package}" "${backup_dir}/packages-before" 2>/dev/null; then
|
|
109
|
+
apt-get remove --yes "${package}" || true
|
|
110
|
+
fi
|
|
111
|
+
done
|
|
112
|
+
for package in "${prerequisites[@]}"; do
|
|
113
|
+
if ! grep -Fxq "${package}" "${backup_dir}/packages-before" 2>/dev/null; then
|
|
114
|
+
apt-get remove --yes "${package}" || true
|
|
115
|
+
fi
|
|
116
|
+
done
|
|
117
|
+
if [[ -f "${backup_dir}/enabled-before" ]]; then
|
|
118
|
+
systemctl enable docker || true
|
|
119
|
+
else
|
|
120
|
+
systemctl disable docker || true
|
|
121
|
+
fi
|
|
122
|
+
if [[ -f "${backup_dir}/active-before" ]]; then
|
|
123
|
+
systemctl start docker || true
|
|
124
|
+
else
|
|
125
|
+
systemctl stop docker || true
|
|
126
|
+
fi
|
|
127
|
+
[[ -f "${backup_dir}/repository-created" ]] && rm -f "${repository}"
|
|
128
|
+
[[ -f "${backup_dir}/key-created" ]] && rm -f "${keyring}"
|
|
129
|
+
rm -rf "${backup_dir}"
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
case "${action}" in
|
|
133
|
+
apply) apply ;;
|
|
134
|
+
verify) verify ;;
|
|
135
|
+
rollback) rollback ;;
|
|
136
|
+
*) echo "unknown action: ${action}" >&2; exit 2 ;;
|
|
137
|
+
esac
|