kitsune-kit 0.4.1 → 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.
Files changed (128) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +143 -2
  3. data/CONTRIBUTING.md +83 -0
  4. data/README.md +112 -135
  5. data/SECURITY.md +25 -0
  6. data/bin/kit +4 -1
  7. data/docs/architecture/decisions/0001-product-boundary.md +23 -0
  8. data/docs/architecture/decisions/0002-core-and-interfaces.md +23 -0
  9. data/docs/architecture/decisions/0003-configuration-state-and-secrets.md +23 -0
  10. data/docs/architecture/decisions/0004-supported-platforms.md +19 -0
  11. data/docs/architecture/decisions/0005-operation-semantics.md +19 -0
  12. data/docs/architecture/decisions/0006-tui-backend.md +21 -0
  13. data/docs/architecture/decisions/0007-compose-customization.md +30 -0
  14. data/docs/architecture/provider-adapters.md +49 -0
  15. data/docs/architecture.md +93 -0
  16. data/docs/commands.md +224 -0
  17. data/docs/configuration.md +226 -0
  18. data/docs/getting-started.md +133 -0
  19. data/docs/providers/digitalocean.md +52 -0
  20. data/docs/releasing.md +43 -0
  21. data/docs/roadmap.md +46 -0
  22. data/docs/security-audit.md +130 -0
  23. data/docs/security.md +116 -0
  24. data/docs/services/compose.md +164 -0
  25. data/docs/services/postgres.md +78 -0
  26. data/docs/services/redis.md +53 -0
  27. data/docs/testing.md +200 -0
  28. data/docs/troubleshooting.md +123 -0
  29. data/docs/tui.md +57 -0
  30. data/lib/kitsune/kit/adapters/confirming_host_key_verifier.rb +42 -0
  31. data/lib/kitsune/kit/adapters/digitalocean_provider.rb +217 -0
  32. data/lib/kitsune/kit/adapters/fake_clock.rb +34 -0
  33. data/lib/kitsune/kit/adapters/fake_provider.rb +107 -0
  34. data/lib/kitsune/kit/adapters/fake_reporter.rb +34 -0
  35. data/lib/kitsune/kit/adapters/fake_secret_store.rb +35 -0
  36. data/lib/kitsune/kit/adapters/fake_state_store.rb +91 -0
  37. data/lib/kitsune/kit/adapters/fake_transport.rb +56 -0
  38. data/lib/kitsune/kit/adapters/net_ssh_transport.rb +155 -0
  39. data/lib/kitsune/kit/adapters/provider.rb +34 -0
  40. data/lib/kitsune/kit/adapters/transport.rb +22 -0
  41. data/lib/kitsune/kit/adapters/transport_factory.rb +116 -0
  42. data/lib/kitsune/kit/application.rb +170 -0
  43. data/lib/kitsune/kit/cancellation.rb +21 -0
  44. data/lib/kitsune/kit/cli.rb +879 -64
  45. data/lib/kitsune/kit/clock.rb +11 -0
  46. data/lib/kitsune/kit/configuration.rb +577 -0
  47. data/lib/kitsune/kit/errors.rb +98 -0
  48. data/lib/kitsune/kit/events.rb +55 -0
  49. data/lib/kitsune/kit/operations/ensure_dns_records.rb +165 -0
  50. data/lib/kitsune/kit/operations/ensure_server.rb +135 -0
  51. data/lib/kitsune/kit/operations/ensure_service.rb +302 -0
  52. data/lib/kitsune/kit/operations/remote_script.rb +214 -0
  53. data/lib/kitsune/kit/operations/service_backup.rb +71 -0
  54. data/lib/kitsune/kit/operations/service_files.rb +131 -0
  55. data/lib/kitsune/kit/operations/service_firewall.rb +190 -0
  56. data/lib/kitsune/kit/operations/service_state.rb +59 -0
  57. data/lib/kitsune/kit/plan.rb +72 -0
  58. data/lib/kitsune/kit/reporters/human.rb +89 -0
  59. data/lib/kitsune/kit/reporters/json.rb +65 -0
  60. data/lib/kitsune/kit/reporters/reporter.rb +11 -0
  61. data/lib/kitsune/kit/result.rb +28 -0
  62. data/lib/kitsune/kit/run_journal.rb +102 -0
  63. data/lib/kitsune/kit/run_logger.rb +37 -0
  64. data/lib/kitsune/kit/scripts/docker.sh +142 -0
  65. data/lib/kitsune/kit/scripts/firewall.sh +210 -0
  66. data/lib/kitsune/kit/scripts/metrics.sh +59 -0
  67. data/lib/kitsune/kit/scripts/ssh.sh +95 -0
  68. data/lib/kitsune/kit/scripts/swap.sh +86 -0
  69. data/lib/kitsune/kit/scripts/unattended.sh +86 -0
  70. data/lib/kitsune/kit/scripts/user.sh +114 -0
  71. data/lib/kitsune/kit/secret_filter.rb +54 -0
  72. data/lib/kitsune/kit/secret_store.rb +32 -0
  73. data/lib/kitsune/kit/secret_stores/store.rb +12 -0
  74. data/lib/kitsune/kit/service_compose.rb +272 -0
  75. data/lib/kitsune/kit/state_store.rb +158 -0
  76. data/lib/kitsune/kit/state_stores/store.rb +15 -0
  77. data/lib/kitsune/kit/tui/actions.rb +72 -0
  78. data/lib/kitsune/kit/tui/application.rb +35 -0
  79. data/lib/kitsune/kit/tui/controller.rb +162 -0
  80. data/lib/kitsune/kit/tui/renderer.rb +134 -0
  81. data/lib/kitsune/kit/tui/state.rb +18 -0
  82. data/lib/kitsune/kit/tui/store.rb +88 -0
  83. data/lib/kitsune/kit/tui/terminal.rb +95 -0
  84. data/lib/kitsune/kit/version.rb +1 -1
  85. data/lib/kitsune/kit/workflows/apply_plan.rb +145 -0
  86. data/lib/kitsune/kit/workflows/base.rb +31 -0
  87. data/lib/kitsune/kit/workflows/build_plan.rb +33 -0
  88. data/lib/kitsune/kit/workflows/destroy_server.rb +84 -0
  89. data/lib/kitsune/kit/workflows/doctor.rb +225 -0
  90. data/lib/kitsune/kit/workflows/eject_compose.rb +82 -0
  91. data/lib/kitsune/kit/workflows/environment_selection.rb +70 -0
  92. data/lib/kitsune/kit/workflows/import_server.rb +100 -0
  93. data/lib/kitsune/kit/workflows/initialize_project.rb +129 -0
  94. data/lib/kitsune/kit/workflows/inspect_environment.rb +44 -0
  95. data/lib/kitsune/kit/workflows/rollback.rb +54 -0
  96. data/lib/kitsune/kit/workflows/support_bundle.rb +82 -0
  97. data/lib/kitsune/kit.rb +42 -2
  98. metadata +125 -79
  99. data/.rspec +0 -3
  100. data/Rakefile +0 -8
  101. data/kitsune-kit-logo.jpg +0 -0
  102. data/lib/kitsune/blueprints/.env.template +0 -31
  103. data/lib/kitsune/blueprints/docker/postgres.yml +0 -27
  104. data/lib/kitsune/blueprints/docker/redis.yml +0 -23
  105. data/lib/kitsune/blueprints/kit.env.template +0 -1
  106. data/lib/kitsune/kit/ansi_color.rb +0 -78
  107. data/lib/kitsune/kit/commands/bootstrap.rb +0 -134
  108. data/lib/kitsune/kit/commands/bootstrap_docker.rb +0 -66
  109. data/lib/kitsune/kit/commands/dns.rb +0 -112
  110. data/lib/kitsune/kit/commands/init.rb +0 -148
  111. data/lib/kitsune/kit/commands/install_docker_engine.rb +0 -146
  112. data/lib/kitsune/kit/commands/postinstall_docker.rb +0 -142
  113. data/lib/kitsune/kit/commands/provision.rb +0 -43
  114. data/lib/kitsune/kit/commands/setup_do_metrics.rb +0 -123
  115. data/lib/kitsune/kit/commands/setup_docker_prereqs.rb +0 -151
  116. data/lib/kitsune/kit/commands/setup_firewall.rb +0 -132
  117. data/lib/kitsune/kit/commands/setup_postgres_docker.rb +0 -246
  118. data/lib/kitsune/kit/commands/setup_redis_docker.rb +0 -241
  119. data/lib/kitsune/kit/commands/setup_swap.rb +0 -151
  120. data/lib/kitsune/kit/commands/setup_unattended.rb +0 -132
  121. data/lib/kitsune/kit/commands/setup_user.rb +0 -189
  122. data/lib/kitsune/kit/commands/ssh.rb +0 -46
  123. data/lib/kitsune/kit/commands/switch_env.rb +0 -42
  124. data/lib/kitsune/kit/defaults.rb +0 -91
  125. data/lib/kitsune/kit/env_loader.rb +0 -41
  126. data/lib/kitsune/kit/options_builder.rb +0 -26
  127. data/lib/kitsune/kit/provisioner.rb +0 -107
  128. data/sig/kitsune/kit.rbs +0 -6
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "pathname"
5
+ require_relative "../errors"
6
+
7
+ module Kitsune
8
+ module Kit
9
+ module Workflows
10
+ class InitializeProject
11
+ CONFIG = <<~YAML
12
+ version: 1
13
+ provider:
14
+ name: digitalocean
15
+ token_env: DO_API_TOKEN
16
+
17
+ server:
18
+ name: myapp-development
19
+ region: sfo3
20
+ size: s-1vcpu-1gb
21
+ image: ubuntu-24-04-x64
22
+ ssh_key_id: replace-with-digitalocean-key-id
23
+ tags:
24
+ - kitsune-managed
25
+
26
+ ssh:
27
+ user: deploy
28
+ port: 22
29
+ key_path: ~/.ssh/id_ed25519
30
+ allowed_cidrs: []
31
+
32
+ services:
33
+ postgres:
34
+ enabled: false
35
+ mode: managed
36
+ host:
37
+ image: postgres:17
38
+ publish: false
39
+ bind: 127.0.0.1
40
+ allowed_cidrs: []
41
+ port: 5432
42
+ password_env: POSTGRES_PASSWORD
43
+ compose:
44
+ mode: generated
45
+ file:
46
+ allow_unsafe: false
47
+ redis:
48
+ enabled: false
49
+ mode: managed
50
+ host:
51
+ image: redis:7.2
52
+ publish: false
53
+ bind: 127.0.0.1
54
+ allowed_cidrs: []
55
+ port: 6379
56
+ password_env: REDIS_PASSWORD
57
+ compose:
58
+ mode: generated
59
+ file:
60
+ allow_unsafe: false
61
+
62
+ system:
63
+ swap_size_gb: 2
64
+ swap_swappiness: 10
65
+ unattended_upgrades: true
66
+ metrics: false
67
+ metrics_installer_sha256:
68
+
69
+ dns:
70
+ domains: []
71
+ ttl: 3600
72
+ YAML
73
+
74
+ ENVIRONMENT = <<~YAML
75
+ version: 1
76
+ server:
77
+ name: myapp-development
78
+ YAML
79
+
80
+ def initialize(root: Dir.pwd)
81
+ @root = Pathname(root).expand_path
82
+ end
83
+
84
+ def call(force: false)
85
+ base = @root.join(".kitsune")
86
+ files = {
87
+ base.join("config.yml") => CONFIG,
88
+ base.join("environments/development.yml") => ENVIRONMENT,
89
+ base.join("environment") => "development\n"
90
+ }
91
+ existing = files.keys.select(&:exist?)
92
+ if existing.any? && !force
93
+ raise Errors::UnsafeOperationError.new(
94
+ "Kitsune Kit configuration already exists",
95
+ hint: "Use `kit init --force` only if replacing these files is intentional.",
96
+ context: { files: existing.map(&:to_s) }
97
+ )
98
+ end
99
+
100
+ files.each do |path, content|
101
+ FileUtils.mkdir_p(path.dirname, mode: 0o700)
102
+ path.write(content)
103
+ File.chmod(0o600, path)
104
+ end
105
+ ensure_gitignore
106
+ files.keys.map(&:to_s)
107
+ end
108
+
109
+ private
110
+
111
+ def ensure_gitignore
112
+ path = @root.join(".gitignore")
113
+ lines = path.file? ? path.readlines(chomp: true) : []
114
+ additions = [
115
+ "/.kitsune/state/",
116
+ "/.kitsune/logs/",
117
+ "/.kitsune/support/",
118
+ "/.kitsune/environment",
119
+ "/.kitsune/known_hosts"
120
+ ] - lines
121
+ return if additions.empty?
122
+
123
+ separator = lines.empty? || lines.last.empty? ? "" : "\n"
124
+ File.open(path, "a", 0o644) { |file| file.write("#{separator}#{additions.join("\n")}\n") }
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../result"
4
+ require_relative "base"
5
+
6
+ module Kitsune
7
+ module Kit
8
+ module Workflows
9
+ EnvironmentStatus = Data.define(:environment, :server, :managed_resources, :last_operations) do
10
+ def to_h
11
+ {
12
+ environment: environment,
13
+ server: server&.to_h,
14
+ managed_resources: managed_resources,
15
+ last_operations: last_operations
16
+ }
17
+ end
18
+ end
19
+
20
+ class InspectEnvironment < Base
21
+ def initialize(config:, provider:, state_store:, event_bus: Events::NullBus.new, clock: Clock.new)
22
+ super(config: config, event_bus: event_bus, clock: clock)
23
+ @provider = provider
24
+ @state_store = state_store
25
+ end
26
+
27
+ def call
28
+ started_at = monotonic_time
29
+ emit("run_started", command: "inspect", environment: config.environment)
30
+ state = @state_store.read(config.environment)
31
+ server = @provider.find_server(name: config.server.name, tags: config.server.tags)
32
+ value = EnvironmentStatus.new(
33
+ environment: config.environment,
34
+ server: server,
35
+ managed_resources: state["resources"],
36
+ last_operations: state["operations"].last(20)
37
+ )
38
+ emit("run_finished", status: "success", duration_ms: elapsed_ms(started_at))
39
+ Result.success(value, metadata: { run_id: run_id })
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../result"
4
+ require_relative "base"
5
+
6
+ module Kitsune
7
+ module Kit
8
+ module Workflows
9
+ class Rollback < Base
10
+ def initialize(config:, operations:, state_store: nil, event_bus: Events::NullBus.new, clock: Clock.new)
11
+ super(config: config, event_bus: event_bus, clock: clock)
12
+ @operations = operations
13
+ @state_store = state_store
14
+ end
15
+
16
+ def call
17
+ return call_unlocked unless @state_store
18
+
19
+ @state_store.with_execution_lock(config.environment) { call_unlocked }
20
+ end
21
+
22
+ private
23
+
24
+ def call_unlocked
25
+ started_at = monotonic_time
26
+ emit("run_started", command: "rollback", environment: config.environment)
27
+ rolled_back = @operations.reverse.filter_map { |operation| rollback_operation(operation) }
28
+ emit("run_finished", status: "success", duration_ms: elapsed_ms(started_at))
29
+ Result.success(rolled_back, metadata: { run_id: run_id })
30
+ rescue StandardError
31
+ emit("run_finished", status: "failure", duration_ms: elapsed_ms(started_at))
32
+ raise
33
+ end
34
+
35
+ def rollback_operation(operation)
36
+ return unless operation.respond_to?(:rollback)
37
+
38
+ started_at = monotonic_time
39
+ emit("operation_started", resource: operation.resource, summary: "Rollback #{operation.resource}",
40
+ action: "rollback")
41
+ changed = operation.rollback
42
+ emit(
43
+ "operation_succeeded",
44
+ resource: operation.resource,
45
+ summary: changed ? "Rolled back #{operation.resource}" : "#{operation.resource} was not managed",
46
+ action: "rollback",
47
+ duration_ms: elapsed_ms(started_at)
48
+ )
49
+ operation.resource if changed
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "json"
5
+ require "pathname"
6
+ require "rbconfig"
7
+ require "securerandom"
8
+ require "time"
9
+
10
+ module Kitsune
11
+ module Kit
12
+ module Workflows
13
+ class SupportBundle
14
+ MAX_LOG_FILES = 5
15
+ MAX_LOG_LINES = 500
16
+
17
+ def initialize(root:, config:, state_store:, doctor:, secret_filter: SecretFilter.new,
18
+ clock: -> { Time.now.utc })
19
+ @root = Pathname(root).expand_path
20
+ @config = config
21
+ @state_store = state_store
22
+ @doctor = doctor
23
+ @secret_filter = secret_filter
24
+ @clock = clock
25
+ end
26
+
27
+ def call
28
+ directory = root.join(".kitsune/support")
29
+ FileUtils.mkdir_p(directory, mode: 0o700)
30
+ path = directory.join("#{clock.call.strftime('%Y%m%dT%H%M%S')}-#{SecureRandom.hex(4)}.json")
31
+ path.write(JSON.pretty_generate(secret_filter.filter(payload)) << "\n")
32
+ File.chmod(0o600, path)
33
+ path.to_s
34
+ end
35
+
36
+ private
37
+
38
+ attr_reader :root, :config, :state_store, :doctor, :secret_filter, :clock
39
+
40
+ def payload
41
+ {
42
+ schema_version: 1,
43
+ generated_at: clock.call.iso8601(6),
44
+ kitsune_version: VERSION,
45
+ ruby: { version: RUBY_VERSION, platform: RUBY_PLATFORM, host_os: RbConfig::CONFIG["host_os"] },
46
+ configuration: serialize(config),
47
+ state: state_store.read(config.environment),
48
+ doctor: doctor.call.value.map(&:to_h),
49
+ logs: selected_logs
50
+ }
51
+ end
52
+
53
+ def selected_logs
54
+ directory = root.join(".kitsune/logs")
55
+ return [] unless directory.directory?
56
+
57
+ directory.glob("*.jsonl").sort_by(&:mtime).last(MAX_LOG_FILES).map do |path|
58
+ { file: path.basename.to_s, events: read_log(path) }
59
+ end
60
+ end
61
+
62
+ def read_log(path)
63
+ path.readlines.last(MAX_LOG_LINES).filter_map do |line|
64
+ JSON.parse(line)
65
+ rescue JSON::ParserError
66
+ { "type" => "invalid_log_line", "message" => "A local log line could not be parsed." }
67
+ end
68
+ rescue SystemCallError => e
69
+ [{ "type" => "log_read_error", "message" => e.class.name }]
70
+ end
71
+
72
+ def serialize(value)
73
+ return value.members.to_h { |member| [member, serialize(value.public_send(member))] } if value.is_a?(Data)
74
+ return value.to_h { |key, item| [key, serialize(item)] } if value.is_a?(Hash)
75
+ return value.map { |item| serialize(item) } if value.is_a?(Array)
76
+
77
+ value
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
data/lib/kitsune/kit.rb CHANGED
@@ -1,10 +1,50 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "kit/version"
4
+ require_relative "kit/errors"
5
+ require_relative "kit/events"
6
+ require_relative "kit/result"
7
+ require_relative "kit/plan"
8
+ require_relative "kit/secret_filter"
9
+ require_relative "kit/secret_store"
10
+ require_relative "kit/clock"
11
+ require_relative "kit/cancellation"
12
+ require_relative "kit/run_logger"
13
+ require_relative "kit/run_journal"
14
+ require_relative "kit/service_compose"
15
+ require_relative "kit/configuration"
16
+ require_relative "kit/state_store"
17
+ require_relative "kit/adapters/fake_state_store"
18
+ require_relative "kit/adapters/fake_reporter"
19
+ require_relative "kit/adapters/fake_secret_store"
20
+ require_relative "kit/adapters/fake_clock"
21
+ require_relative "kit/application"
22
+ require_relative "kit/operations/ensure_dns_records"
23
+ require_relative "kit/operations/remote_script"
24
+ require_relative "kit/operations/ensure_service"
25
+ require_relative "kit/reporters/human"
26
+ require_relative "kit/reporters/json"
27
+ require_relative "kit/workflows/build_plan"
28
+ require_relative "kit/workflows/apply_plan"
29
+ require_relative "kit/workflows/doctor"
30
+ require_relative "kit/workflows/initialize_project"
31
+ require_relative "kit/workflows/eject_compose"
32
+ require_relative "kit/workflows/inspect_environment"
33
+ require_relative "kit/workflows/rollback"
34
+ require_relative "kit/workflows/destroy_server"
35
+ require_relative "kit/workflows/import_server"
36
+ require_relative "kit/workflows/environment_selection"
37
+ require_relative "kit/workflows/support_bundle"
38
+ require_relative "kit/tui/state"
39
+ require_relative "kit/tui/store"
40
+ require_relative "kit/tui/renderer"
41
+ require_relative "kit/tui/terminal"
42
+ require_relative "kit/tui/actions"
43
+ require_relative "kit/tui/controller"
44
+ require_relative "kit/tui/application"
4
45
 
5
46
  module Kitsune
6
47
  module Kit
7
- class Error < StandardError; end
8
- # Your code goes here...
48
+ Error = Errors::Error
9
49
  end
10
50
  end