kitsune-kit 0.5.0 → 0.6.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 +27 -1
- data/README.md +8 -3
- data/docs/architecture/decisions/0007-compose-customization.md +30 -0
- data/docs/architecture.md +5 -1
- data/docs/commands.md +14 -0
- data/docs/configuration.md +16 -0
- data/docs/services/compose.md +164 -0
- data/lib/kitsune/kit/cli.rb +83 -6
- data/lib/kitsune/kit/configuration.rb +96 -12
- data/lib/kitsune/kit/operations/ensure_service.rb +33 -16
- data/lib/kitsune/kit/operations/service_files.rb +14 -10
- data/lib/kitsune/kit/scripts/docker.sh +11 -6
- data/lib/kitsune/kit/scripts/firewall.sh +8 -3
- data/lib/kitsune/kit/scripts/metrics.sh +6 -1
- data/lib/kitsune/kit/scripts/unattended.sh +8 -3
- data/lib/kitsune/kit/service_compose.rb +205 -5
- data/lib/kitsune/kit/version.rb +1 -1
- data/lib/kitsune/kit/workflows/eject_compose.rb +82 -0
- data/lib/kitsune/kit/workflows/initialize_project.rb +8 -0
- data/lib/kitsune/kit.rb +1 -0
- metadata +4 -1
|
@@ -44,8 +44,27 @@ module Kitsune
|
|
|
44
44
|
end
|
|
45
45
|
end
|
|
46
46
|
|
|
47
|
+
class Compose < Data.define(:mode, :file, :allow_unsafe)
|
|
48
|
+
MODES = %w[generated overlay custom].freeze
|
|
49
|
+
|
|
50
|
+
def initialize(mode: "generated", file: nil, allow_unsafe: false, root: Dir.pwd)
|
|
51
|
+
expanded_file = file.to_s.empty? ? nil : Pathname(file.to_s).expand_path(root).to_s
|
|
52
|
+
super(mode: mode.to_s, file: expanded_file, allow_unsafe: boolean(allow_unsafe))
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def boolean(value)
|
|
58
|
+
return value if [true, false].include?(value)
|
|
59
|
+
return true if value.to_s.casecmp("true").zero?
|
|
60
|
+
return false if value.to_s.casecmp("false").zero?
|
|
61
|
+
|
|
62
|
+
raise Errors::ConfigurationError, "expected a boolean, got #{value.inspect}"
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
47
66
|
class Service < Data.define(:enabled, :mode, :host, :image, :publish, :bind, :allowed_cidrs, :port,
|
|
48
|
-
:password_env)
|
|
67
|
+
:password_env, :compose)
|
|
49
68
|
def initialize(**attributes)
|
|
50
69
|
super(
|
|
51
70
|
enabled: boolean(attributes.fetch(:enabled)),
|
|
@@ -56,7 +75,8 @@ module Kitsune
|
|
|
56
75
|
bind: attributes.fetch(:bind).to_s,
|
|
57
76
|
allowed_cidrs: Array(attributes.fetch(:allowed_cidrs)).map(&:to_s).freeze,
|
|
58
77
|
port: Integer(attributes.fetch(:port)),
|
|
59
|
-
password_env: attributes.fetch(:password_env).to_s
|
|
78
|
+
password_env: attributes.fetch(:password_env).to_s,
|
|
79
|
+
compose: attributes.fetch(:compose, Compose.new)
|
|
60
80
|
)
|
|
61
81
|
rescue ArgumentError, TypeError
|
|
62
82
|
raise Errors::ConfigurationError, "service port must be an integer"
|
|
@@ -142,7 +162,8 @@ module Kitsune
|
|
|
142
162
|
"bind" => "127.0.0.1",
|
|
143
163
|
"allowed_cidrs" => [],
|
|
144
164
|
"port" => 5432,
|
|
145
|
-
"password_env" => "POSTGRES_PASSWORD"
|
|
165
|
+
"password_env" => "POSTGRES_PASSWORD",
|
|
166
|
+
"compose" => { "mode" => "generated", "file" => nil, "allow_unsafe" => false }
|
|
146
167
|
},
|
|
147
168
|
"redis" => {
|
|
148
169
|
"enabled" => false,
|
|
@@ -153,7 +174,8 @@ module Kitsune
|
|
|
153
174
|
"bind" => "127.0.0.1",
|
|
154
175
|
"allowed_cidrs" => [],
|
|
155
176
|
"port" => 6379,
|
|
156
|
-
"password_env" => "REDIS_PASSWORD"
|
|
177
|
+
"password_env" => "REDIS_PASSWORD",
|
|
178
|
+
"compose" => { "mode" => "generated", "file" => nil, "allow_unsafe" => false }
|
|
157
179
|
}
|
|
158
180
|
},
|
|
159
181
|
"system" => {
|
|
@@ -185,17 +207,20 @@ module Kitsune
|
|
|
185
207
|
%w[server] => %w[name region size image ssh_key_id tags],
|
|
186
208
|
%w[ssh] => %w[user port key_path allowed_cidrs],
|
|
187
209
|
%w[services] => %w[postgres redis],
|
|
188
|
-
%w[services postgres] => %w[enabled mode host image publish bind allowed_cidrs port password_env],
|
|
189
|
-
%w[services redis] => %w[enabled mode host image publish bind allowed_cidrs port password_env],
|
|
210
|
+
%w[services postgres] => %w[enabled mode host image publish bind allowed_cidrs port password_env compose],
|
|
211
|
+
%w[services redis] => %w[enabled mode host image publish bind allowed_cidrs port password_env compose],
|
|
212
|
+
%w[services postgres compose] => %w[mode file allow_unsafe],
|
|
213
|
+
%w[services redis compose] => %w[mode file allow_unsafe],
|
|
190
214
|
%w[system] => %w[swap_size_gb swap_swappiness unattended_upgrades metrics metrics_installer_sha256],
|
|
191
215
|
%w[dns] => %w[domains ttl]
|
|
192
216
|
}.freeze
|
|
193
217
|
|
|
194
218
|
class Loader
|
|
195
|
-
def initialize(root: Dir.pwd, env: ENV, config_path: nil)
|
|
219
|
+
def initialize(root: Dir.pwd, env: ENV, config_path: nil, validate_secrets: true)
|
|
196
220
|
@root = Pathname(root).expand_path
|
|
197
221
|
@env = env
|
|
198
222
|
@config_path = config_path ? Pathname(config_path).expand_path : @root.join(".kitsune/config.yml")
|
|
223
|
+
@validate_secrets = validate_secrets
|
|
199
224
|
end
|
|
200
225
|
|
|
201
226
|
def load(environment: nil, overrides: {})
|
|
@@ -260,13 +285,13 @@ module Kitsune
|
|
|
260
285
|
server: Server.new(**symbolize(data.fetch("server"))),
|
|
261
286
|
ssh: Ssh.new(**symbolize(data.fetch("ssh"))),
|
|
262
287
|
services: Services.new(
|
|
263
|
-
postgres:
|
|
264
|
-
redis:
|
|
288
|
+
postgres: build_service(data.dig("services", "postgres")),
|
|
289
|
+
redis: build_service(data.dig("services", "redis"))
|
|
265
290
|
),
|
|
266
291
|
system: System.new(**symbolize(data.fetch("system"))),
|
|
267
292
|
dns: Dns.new(**symbolize(data.fetch("dns")))
|
|
268
293
|
)
|
|
269
|
-
Validator.new(config, env: @env).validate!
|
|
294
|
+
Validator.new(config, env: @env, root: @root, validate_secrets: @validate_secrets).validate!
|
|
270
295
|
config
|
|
271
296
|
rescue KeyError => e
|
|
272
297
|
raise Errors::ConfigurationError.new("missing configuration value: #{e.key}",
|
|
@@ -293,6 +318,12 @@ module Kitsune
|
|
|
293
318
|
)
|
|
294
319
|
end
|
|
295
320
|
|
|
321
|
+
def build_service(data)
|
|
322
|
+
attributes = symbolize(data)
|
|
323
|
+
compose = Compose.new(**symbolize(data.fetch("compose")), root: @root)
|
|
324
|
+
Service.new(**attributes, compose: compose)
|
|
325
|
+
end
|
|
326
|
+
|
|
296
327
|
def validate_schema_version!(version)
|
|
297
328
|
return if version == SCHEMA_VERSION
|
|
298
329
|
|
|
@@ -335,9 +366,11 @@ module Kitsune
|
|
|
335
366
|
IMAGE = %r{\A[a-z0-9]+(?:[._/-][a-z0-9]+)*(?::[a-zA-Z0-9][a-zA-Z0-9._-]*)?(?:@sha256:[a-f0-9]{64})?\z}
|
|
336
367
|
INSECURE_SECRETS = %w[password postgres redis changeme change-me secret admin].freeze
|
|
337
368
|
|
|
338
|
-
def initialize(config, env: ENV)
|
|
369
|
+
def initialize(config, env: ENV, root: Dir.pwd, validate_secrets: true)
|
|
339
370
|
@config = config
|
|
340
371
|
@env = env
|
|
372
|
+
@root = Pathname(root).expand_path
|
|
373
|
+
@validate_secrets = validate_secrets
|
|
341
374
|
@errors = []
|
|
342
375
|
end
|
|
343
376
|
|
|
@@ -410,7 +443,58 @@ module Kitsune
|
|
|
410
443
|
else
|
|
411
444
|
validate_managed_service(name, service)
|
|
412
445
|
end
|
|
413
|
-
|
|
446
|
+
validate_compose(name, service)
|
|
447
|
+
validate_service_secret(name, service) if service.enabled && @validate_secrets
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
def validate_compose(name, service)
|
|
451
|
+
compose = service.compose
|
|
452
|
+
return invalid_compose_mode(name) unless Compose::MODES.include?(compose.mode)
|
|
453
|
+
|
|
454
|
+
validate_compose_ownership(name, service)
|
|
455
|
+
return validate_generated_compose(name, compose) if compose.mode == "generated"
|
|
456
|
+
return missing_compose_file(name, compose) unless compose.file
|
|
457
|
+
|
|
458
|
+
validate_compose_file(name, Pathname(compose.file))
|
|
459
|
+
rescue SystemCallError => e
|
|
460
|
+
@errors << "services.#{name}.compose.file cannot be inspected: #{e.class}"
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
def invalid_compose_mode(name)
|
|
464
|
+
@errors << "services.#{name}.compose.mode must be generated, overlay or custom"
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
def validate_compose_ownership(name, service)
|
|
468
|
+
return unless service.mode == "external" && service.compose.mode != "generated"
|
|
469
|
+
|
|
470
|
+
@errors << "services.#{name}.compose is only available in managed mode"
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
def validate_generated_compose(name, compose)
|
|
474
|
+
@errors << "services.#{name}.compose.file must be empty in generated mode" if compose.file
|
|
475
|
+
return unless compose.allow_unsafe
|
|
476
|
+
|
|
477
|
+
@errors << "services.#{name}.compose.allow_unsafe must be false in generated mode"
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
def missing_compose_file(name, compose)
|
|
481
|
+
@errors << "services.#{name}.compose.file is required in #{compose.mode} mode"
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
def validate_compose_file(name, path)
|
|
485
|
+
root_prefix = "#{@root}#{File::SEPARATOR}"
|
|
486
|
+
return @errors << "services.#{name}.compose.file must stay inside the project root" unless
|
|
487
|
+
path.to_s.start_with?(root_prefix)
|
|
488
|
+
|
|
489
|
+
if path.symlink?
|
|
490
|
+
@errors << "services.#{name}.compose.file must not be a symbolic link"
|
|
491
|
+
elsif !path.file?
|
|
492
|
+
@errors << "services.#{name}.compose.file does not exist or is not a regular file: #{path}"
|
|
493
|
+
elsif !path.realpath.to_s.start_with?("#{@root.realpath}#{File::SEPARATOR}")
|
|
494
|
+
@errors << "services.#{name}.compose.file must not resolve outside the project root"
|
|
495
|
+
elsif path.size > 262_144
|
|
496
|
+
@errors << "services.#{name}.compose.file must not exceed 256 KiB"
|
|
497
|
+
end
|
|
414
498
|
end
|
|
415
499
|
|
|
416
500
|
def validate_managed_service(name, service)
|
|
@@ -12,7 +12,8 @@ require_relative "service_state"
|
|
|
12
12
|
module Kitsune
|
|
13
13
|
module Kit
|
|
14
14
|
module Operations
|
|
15
|
-
|
|
15
|
+
# Service lifecycle coordination intentionally lives together so apply and rollback share one transaction model.
|
|
16
|
+
class EnsureService # rubocop:disable Metrics/ClassLength
|
|
16
17
|
TYPES = %w[postgres redis].freeze
|
|
17
18
|
MARKER_DIRECTORY = "/var/lib/kitsune/state"
|
|
18
19
|
|
|
@@ -62,7 +63,11 @@ module Kitsune
|
|
|
62
63
|
transport.execute("mkdir", arguments: ["-p", service_directory], timeout: 10).then do |result|
|
|
63
64
|
raise_remote!(result, "create service directory")
|
|
64
65
|
end
|
|
65
|
-
|
|
66
|
+
@compose.documents.each do |document|
|
|
67
|
+
transport.upload(
|
|
68
|
+
content: document.content, remote_path: files.compose_path(document.filename), mode: "0600"
|
|
69
|
+
)
|
|
70
|
+
end
|
|
66
71
|
transport.upload(content: env_content(password), remote_path: env_path, mode: "0600")
|
|
67
72
|
validate_compose
|
|
68
73
|
start_service
|
|
@@ -79,7 +84,7 @@ module Kitsune
|
|
|
79
84
|
def remove
|
|
80
85
|
return false unless managed?
|
|
81
86
|
|
|
82
|
-
compose("down", "--remove-orphans")
|
|
87
|
+
compose("down", "--remove-orphans", filenames: installed_compose_files)
|
|
83
88
|
firewall.remove
|
|
84
89
|
remove_marker
|
|
85
90
|
@managed_state.record_remove
|
|
@@ -97,7 +102,8 @@ module Kitsune
|
|
|
97
102
|
remove
|
|
98
103
|
if files.restore_previous(previous, marker_path: marker_path)
|
|
99
104
|
firewall.restore(previous["previous_state"] || {})
|
|
100
|
-
start_service
|
|
105
|
+
start_service(filenames: previous.dig("previous_state", "compose_files")) if
|
|
106
|
+
previous.dig("previous_state", "installed") != false
|
|
101
107
|
end
|
|
102
108
|
@managed_state.record_rollback(previous)
|
|
103
109
|
true
|
|
@@ -107,7 +113,7 @@ module Kitsune
|
|
|
107
113
|
previous = managed_state
|
|
108
114
|
return false unless previous
|
|
109
115
|
|
|
110
|
-
compose("down", "--volumes", "--remove-orphans")
|
|
116
|
+
compose("down", "--volumes", "--remove-orphans", filenames: installed_compose_files)
|
|
111
117
|
files.destroy(previous, marker_path: marker_path)
|
|
112
118
|
@managed_state.delete("destroy_data")
|
|
113
119
|
true
|
|
@@ -120,13 +126,12 @@ module Kitsune
|
|
|
120
126
|
def transport = @transport ||= transport_factory.deploy
|
|
121
127
|
def project_name = "kitsune-#{config.environment}-#{@type}"
|
|
122
128
|
def service_directory = files.directory
|
|
123
|
-
def compose_path = files.compose_path
|
|
124
129
|
def env_path = files.env_path
|
|
125
130
|
def marker_path = "#{MARKER_DIRECTORY}/service-#{@type}.sha256"
|
|
126
131
|
|
|
127
132
|
def fingerprint
|
|
128
133
|
@fingerprint ||= Digest::SHA256.hexdigest(
|
|
129
|
-
[
|
|
134
|
+
[@compose.fingerprint, service.password_env, secret_digest, service.publish, service.bind,
|
|
130
135
|
*service.allowed_cidrs].join("\0")
|
|
131
136
|
)
|
|
132
137
|
end
|
|
@@ -147,21 +152,24 @@ module Kitsune
|
|
|
147
152
|
value
|
|
148
153
|
end
|
|
149
154
|
|
|
150
|
-
def compose_content = @compose.content
|
|
151
155
|
def env_content(password) = @compose.env_content(password)
|
|
152
156
|
|
|
153
157
|
def validate_compose = compose("config", "--quiet")
|
|
154
|
-
|
|
158
|
+
|
|
159
|
+
def start_service(filenames: nil)
|
|
160
|
+
compose("up", "--detach", "--wait", "--wait-timeout", "120", filenames: filenames)
|
|
161
|
+
end
|
|
155
162
|
|
|
156
163
|
def verify_service
|
|
157
164
|
result = compose("ps", "--status", "running", "--quiet")
|
|
158
165
|
raise Errors::VerificationError, "#{@type} did not reach running state" if result.stdout.strip.empty?
|
|
159
166
|
end
|
|
160
167
|
|
|
161
|
-
def compose(*arguments)
|
|
168
|
+
def compose(*arguments, filenames: nil)
|
|
162
169
|
result = transport.execute(
|
|
163
170
|
"docker",
|
|
164
|
-
arguments: ["compose", "--project-directory", service_directory,
|
|
171
|
+
arguments: ["compose", "--project-directory", service_directory,
|
|
172
|
+
*compose_file_arguments(filenames), *arguments],
|
|
165
173
|
timeout: 180
|
|
166
174
|
)
|
|
167
175
|
raise_remote!(result, "docker compose #{arguments.first}")
|
|
@@ -176,7 +184,7 @@ module Kitsune
|
|
|
176
184
|
firewall.restore_after_failure(previous_state) if @firewall_changed
|
|
177
185
|
safe_compose_down
|
|
178
186
|
files.restore_after_failure(@backed_up_files)
|
|
179
|
-
start_service unless @backed_up_files.empty?
|
|
187
|
+
start_service(filenames: previous_state&.fetch("compose_files", nil)) unless @backed_up_files.empty?
|
|
180
188
|
end
|
|
181
189
|
|
|
182
190
|
def recover_failed_apply(previous_state, original_error)
|
|
@@ -194,7 +202,7 @@ module Kitsune
|
|
|
194
202
|
def safe_compose_down
|
|
195
203
|
transport.execute(
|
|
196
204
|
"docker",
|
|
197
|
-
arguments: ["compose", "--project-directory", service_directory,
|
|
205
|
+
arguments: ["compose", "--project-directory", service_directory, *compose_file_arguments,
|
|
198
206
|
"down", "--remove-orphans"],
|
|
199
207
|
timeout: 180
|
|
200
208
|
)
|
|
@@ -219,6 +227,8 @@ module Kitsune
|
|
|
219
227
|
{
|
|
220
228
|
"managed" => true, "installed" => true, "fingerprint" => fingerprint, "project" => project_name,
|
|
221
229
|
"directory" => service_directory, "published" => service.publish, "port" => service.port,
|
|
230
|
+
"compose_mode" => @compose.mode, "compose_files" => @compose.filenames,
|
|
231
|
+
"compose_fingerprint" => @compose.fingerprint,
|
|
222
232
|
"firewall_rules_added" => firewall.owned_rules, "firewall_drop_added" => firewall.drop_owned,
|
|
223
233
|
"volume" => "#{project_name}_data",
|
|
224
234
|
"previous_fingerprint" => change.details[:actual_fingerprint] || change.details["actual_fingerprint"],
|
|
@@ -229,11 +239,12 @@ module Kitsune
|
|
|
229
239
|
|
|
230
240
|
def managed? = @managed_state.managed?
|
|
231
241
|
def managed_state = @managed_state.current
|
|
242
|
+
def installed_compose_files = managed_state&.fetch("compose_files", nil)
|
|
232
243
|
|
|
233
244
|
def files
|
|
234
245
|
@files ||= ServiceFiles.new(
|
|
235
246
|
config: config, type: @type, transport: transport, state_store: state_store,
|
|
236
|
-
resource: resource, fingerprint: -> { fingerprint }
|
|
247
|
+
resource: resource, fingerprint: -> { fingerprint }, compose_filenames: -> { @compose.filenames }
|
|
237
248
|
)
|
|
238
249
|
end
|
|
239
250
|
|
|
@@ -246,7 +257,8 @@ module Kitsune
|
|
|
246
257
|
|
|
247
258
|
def backup_operation
|
|
248
259
|
@backup_operation ||= ServiceBackup.new(
|
|
249
|
-
config: config, type: @type, transport: transport,
|
|
260
|
+
config: config, type: @type, transport: transport,
|
|
261
|
+
compose: ->(*arguments) { compose(*arguments, filenames: installed_compose_files) }, clock: @clock
|
|
250
262
|
)
|
|
251
263
|
end
|
|
252
264
|
|
|
@@ -274,10 +286,15 @@ module Kitsune
|
|
|
274
286
|
image: service.image, published: service.publish,
|
|
275
287
|
bind: service.publish ? service.bind : nil, port: service.publish ? service.port : nil,
|
|
276
288
|
fingerprint: fingerprint,
|
|
277
|
-
actual_fingerprint: actual.empty? ? nil : actual
|
|
289
|
+
actual_fingerprint: actual.empty? ? nil : actual,
|
|
290
|
+
compose: @compose.metadata
|
|
278
291
|
}
|
|
279
292
|
end
|
|
280
293
|
|
|
294
|
+
def compose_file_arguments(filenames = nil)
|
|
295
|
+
Array(filenames || @compose.filenames).flat_map { |filename| ["--file", files.compose_path(filename)] }
|
|
296
|
+
end
|
|
297
|
+
|
|
281
298
|
def error_name(error) = error.respond_to?(:code) ? error.code : error.class.name
|
|
282
299
|
end
|
|
283
300
|
end
|
|
@@ -6,23 +6,25 @@ module Kitsune
|
|
|
6
6
|
module Kit
|
|
7
7
|
module Operations
|
|
8
8
|
class ServiceFiles
|
|
9
|
-
def initialize(config:, type:, transport:, state_store:, resource:, fingerprint:)
|
|
9
|
+
def initialize(config:, type:, transport:, state_store:, resource:, fingerprint:, compose_filenames:)
|
|
10
10
|
@config = config
|
|
11
11
|
@type = type
|
|
12
12
|
@transport = transport
|
|
13
13
|
@state_store = state_store
|
|
14
14
|
@resource = resource
|
|
15
15
|
@fingerprint = fingerprint
|
|
16
|
+
@compose_filenames = compose_filenames
|
|
16
17
|
end
|
|
17
18
|
|
|
18
19
|
def directory = "/home/#{config.ssh.user}/.local/share/kitsune/services/#{type}"
|
|
19
|
-
def compose_path = "#{directory}
|
|
20
|
+
def compose_path(filename = "compose.yml") = "#{directory}/#{filename}"
|
|
21
|
+
def compose_paths = compose_filenames.call.map { |filename| compose_path(filename) }
|
|
20
22
|
def env_path = "#{directory}/.env"
|
|
21
23
|
def backup_directory = "/var/lib/kitsune/backups/service-#{type}-#{fingerprint.call[0, 12]}"
|
|
22
24
|
|
|
23
25
|
def ensure_managed_or_absent!(actual_fingerprint)
|
|
24
26
|
return if managed?
|
|
25
|
-
return if actual_fingerprint.empty? &&
|
|
27
|
+
return if actual_fingerprint.empty? && known_paths.none? { |path| exists?(path) }
|
|
26
28
|
|
|
27
29
|
raise Errors::UnsafeOperationError.new(
|
|
28
30
|
"refusing to replace unmanaged #{type} service files",
|
|
@@ -39,17 +41,14 @@ module Kitsune
|
|
|
39
41
|
timeout: 20
|
|
40
42
|
)
|
|
41
43
|
raise_remote!(result, "create service backup directory")
|
|
42
|
-
|
|
44
|
+
known_paths.select { |path| exists?(path) }.tap do |paths|
|
|
43
45
|
paths.each { |path| copy(path, backup_directory) }
|
|
44
46
|
end
|
|
45
47
|
end
|
|
46
48
|
|
|
47
49
|
def restore_after_failure(backed_up_files)
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
else
|
|
51
|
-
backed_up_files.each { |path| copy("#{backup_directory}/#{File.basename(path)}", File.dirname(path)) }
|
|
52
|
-
end
|
|
50
|
+
transport.execute("rm", arguments: ["-f", *known_paths], timeout: 20)
|
|
51
|
+
backed_up_files.each { |path| copy("#{backup_directory}/#{File.basename(path)}", File.dirname(path)) }
|
|
53
52
|
end
|
|
54
53
|
|
|
55
54
|
def restore_previous(previous, marker_path:)
|
|
@@ -75,7 +74,12 @@ module Kitsune
|
|
|
75
74
|
|
|
76
75
|
private
|
|
77
76
|
|
|
78
|
-
attr_reader :config, :type, :transport, :state_store, :resource, :fingerprint
|
|
77
|
+
attr_reader :config, :type, :transport, :state_store, :resource, :fingerprint, :compose_filenames
|
|
78
|
+
|
|
79
|
+
def known_paths
|
|
80
|
+
previous = state_store.read(config.environment).dig("resources", resource, "compose_files") || []
|
|
81
|
+
(compose_paths + previous.map { |filename| compose_path(filename) } + [env_path]).uniq
|
|
82
|
+
end
|
|
79
83
|
|
|
80
84
|
def managed? = !!state_store.read(config.environment).dig("resources", resource, "managed")
|
|
81
85
|
def exists?(path) = transport.execute("test", arguments: ["-e", path], timeout: 10).success?
|
|
@@ -9,6 +9,11 @@ repository="/etc/apt/sources.list.d/docker.sources"
|
|
|
9
9
|
packages=(docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin)
|
|
10
10
|
prerequisites=(ca-certificates curl)
|
|
11
11
|
conflicting_packages=(docker.io docker-compose docker-compose-v2 podman-docker containerd runc)
|
|
12
|
+
apt_options=(-o DPkg::Lock::Timeout=120 -o Acquire::Retries=3)
|
|
13
|
+
|
|
14
|
+
apt_get() {
|
|
15
|
+
DEBIAN_FRONTEND=noninteractive apt-get "${apt_options[@]}" "$@"
|
|
16
|
+
}
|
|
12
17
|
|
|
13
18
|
refuse_conflicting_packages() {
|
|
14
19
|
local installed=()
|
|
@@ -42,8 +47,8 @@ apply() {
|
|
|
42
47
|
refuse_conflicting_packages
|
|
43
48
|
install -d -m 0700 "${backup_dir}"
|
|
44
49
|
snapshot
|
|
45
|
-
|
|
46
|
-
|
|
50
|
+
apt_get update
|
|
51
|
+
apt_get install --yes "${prerequisites[@]}"
|
|
47
52
|
install -d -m 0755 /etc/apt/keyrings
|
|
48
53
|
if [[ ! -f "${keyring}" ]]; then
|
|
49
54
|
temporary_key="$(mktemp)"
|
|
@@ -76,8 +81,8 @@ EOF
|
|
|
76
81
|
trap - EXIT
|
|
77
82
|
touch "${backup_dir}/repository-created"
|
|
78
83
|
fi
|
|
79
|
-
|
|
80
|
-
|
|
84
|
+
apt_get update
|
|
85
|
+
apt_get install --yes "${packages[@]}"
|
|
81
86
|
systemctl enable --now docker
|
|
82
87
|
if ! id -nG "${user}" | tr ' ' '\n' | grep -Fxq docker; then
|
|
83
88
|
usermod -aG docker "${user}"
|
|
@@ -106,12 +111,12 @@ rollback() {
|
|
|
106
111
|
fi
|
|
107
112
|
for package in "${packages[@]}"; do
|
|
108
113
|
if ! grep -Fxq "${package}" "${backup_dir}/packages-before" 2>/dev/null; then
|
|
109
|
-
|
|
114
|
+
apt_get remove --yes "${package}" || true
|
|
110
115
|
fi
|
|
111
116
|
done
|
|
112
117
|
for package in "${prerequisites[@]}"; do
|
|
113
118
|
if ! grep -Fxq "${package}" "${backup_dir}/packages-before" 2>/dev/null; then
|
|
114
|
-
|
|
119
|
+
apt_get remove --yes "${package}" || true
|
|
115
120
|
fi
|
|
116
121
|
done
|
|
117
122
|
if [[ -f "${backup_dir}/enabled-before" ]]; then
|
|
@@ -8,6 +8,11 @@ allowed_cidrs=("$@")
|
|
|
8
8
|
backup_dir="/var/lib/kitsune/backups/firewall"
|
|
9
9
|
added_file="${backup_dir}/rules-added"
|
|
10
10
|
pending_file="${backup_dir}/rules-pending"
|
|
11
|
+
apt_options=(-o DPkg::Lock::Timeout=120 -o Acquire::Retries=3)
|
|
12
|
+
|
|
13
|
+
apt_get() {
|
|
14
|
+
DEBIAN_FRONTEND=noninteractive apt-get "${apt_options[@]}" "$@"
|
|
15
|
+
}
|
|
11
16
|
|
|
12
17
|
validate() {
|
|
13
18
|
[[ "${ssh_port}" =~ ^[0-9]+$ ]] && (( ssh_port >= 1 && ssh_port <= 65535 ))
|
|
@@ -127,8 +132,8 @@ apply() {
|
|
|
127
132
|
touch "${backup_dir}/snapshot-complete"
|
|
128
133
|
fi
|
|
129
134
|
if ! dpkg-query -W -f='${Status}' ufw 2>/dev/null | grep -q "ok installed"; then
|
|
130
|
-
|
|
131
|
-
|
|
135
|
+
apt_get update
|
|
136
|
+
apt_get install --yes ufw
|
|
132
137
|
touch "${backup_dir}/package-created"
|
|
133
138
|
fi
|
|
134
139
|
|
|
@@ -190,7 +195,7 @@ rollback() {
|
|
|
190
195
|
ufw --force disable || true
|
|
191
196
|
fi
|
|
192
197
|
if [[ -f "${backup_dir}/package-created" ]]; then
|
|
193
|
-
|
|
198
|
+
apt_get remove --yes ufw || true
|
|
194
199
|
fi
|
|
195
200
|
rm -rf "${backup_dir}"
|
|
196
201
|
}
|
|
@@ -4,6 +4,11 @@ set -Eeuo pipefail
|
|
|
4
4
|
action="${1:?action is required}"
|
|
5
5
|
expected_sha256="${2:?installer SHA-256 is required}"
|
|
6
6
|
backup_dir="/var/lib/kitsune/backups/metrics"
|
|
7
|
+
apt_options=(-o DPkg::Lock::Timeout=120 -o Acquire::Retries=3)
|
|
8
|
+
|
|
9
|
+
apt_get() {
|
|
10
|
+
DEBIAN_FRONTEND=noninteractive apt-get "${apt_options[@]}" "$@"
|
|
11
|
+
}
|
|
7
12
|
|
|
8
13
|
validate() {
|
|
9
14
|
[[ "${expected_sha256}" =~ ^[a-f0-9]{64}$ ]] || {
|
|
@@ -41,7 +46,7 @@ verify() {
|
|
|
41
46
|
rollback() {
|
|
42
47
|
if [[ -f "${backup_dir}/package-created" ]]; then
|
|
43
48
|
systemctl disable --now do-agent || true
|
|
44
|
-
|
|
49
|
+
apt_get remove --purge --yes do-agent || true
|
|
45
50
|
fi
|
|
46
51
|
rm -rf "${backup_dir}"
|
|
47
52
|
}
|
|
@@ -6,6 +6,11 @@ config_file="/etc/apt/apt.conf.d/20auto-upgrades"
|
|
|
6
6
|
backup_dir="/var/lib/kitsune/backups/unattended"
|
|
7
7
|
backup_file="${backup_dir}/20auto-upgrades.before"
|
|
8
8
|
packages=(unattended-upgrades apt-listchanges)
|
|
9
|
+
apt_options=(-o DPkg::Lock::Timeout=120 -o Acquire::Retries=3)
|
|
10
|
+
|
|
11
|
+
apt_get() {
|
|
12
|
+
DEBIAN_FRONTEND=noninteractive apt-get "${apt_options[@]}" "$@"
|
|
13
|
+
}
|
|
9
14
|
|
|
10
15
|
snapshot() {
|
|
11
16
|
[[ -f "${backup_dir}/snapshot-complete" ]] && return
|
|
@@ -28,8 +33,8 @@ snapshot() {
|
|
|
28
33
|
apply() {
|
|
29
34
|
install -d -m 0700 "${backup_dir}"
|
|
30
35
|
snapshot
|
|
31
|
-
|
|
32
|
-
|
|
36
|
+
apt_get update
|
|
37
|
+
apt_get install --yes "${packages[@]}"
|
|
33
38
|
cat > "${config_file}" <<'EOF'
|
|
34
39
|
APT::Periodic::Update-Package-Lists "1";
|
|
35
40
|
APT::Periodic::Download-Upgradeable-Packages "1";
|
|
@@ -67,7 +72,7 @@ rollback() {
|
|
|
67
72
|
restore_service_state
|
|
68
73
|
for package in "${packages[@]}"; do
|
|
69
74
|
if ! grep -Fxq "${package}" "${backup_dir}/packages-before" 2>/dev/null; then
|
|
70
|
-
|
|
75
|
+
apt_get remove --yes "${package}" || true
|
|
71
76
|
fi
|
|
72
77
|
done
|
|
73
78
|
rm -rf "${backup_dir}"
|