keysloth 0.1.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 +7 -0
- data/.keyslothrc.example +14 -0
- data/.rspec +4 -0
- data/.rubocop.yml +98 -0
- data/.yardopts +8 -0
- data/CHANGELOG.md +45 -0
- data/Makefile +193 -0
- data/README.md +448 -0
- data/Rakefile +99 -0
- data/bin/keysloth +16 -0
- data/keysloth.gemspec +51 -0
- data/lib/keysloth/cli.rb +468 -0
- data/lib/keysloth/config.rb +129 -0
- data/lib/keysloth/crypto.rb +304 -0
- data/lib/keysloth/errors.rb +41 -0
- data/lib/keysloth/file_manager.rb +450 -0
- data/lib/keysloth/git_manager.rb +394 -0
- data/lib/keysloth/logger.rb +172 -0
- data/lib/keysloth/version.rb +6 -0
- data/lib/keysloth.rb +275 -0
- data/task/cr.md +59 -0
- data/task/plan.md +169 -0
- data/task/ragged_removing.md +103 -0
- data/task/task.md +43 -0
- data/task/test_plan.md +266 -0
- metadata +174 -0
data/lib/keysloth/cli.rb
ADDED
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'thor'
|
|
4
|
+
|
|
5
|
+
module KeySloth
|
|
6
|
+
# Интерфейс командной строки для KeySloth
|
|
7
|
+
#
|
|
8
|
+
# Предоставляет команды pull, push и help для управления секретами.
|
|
9
|
+
# Использует Thor для парсинга аргументов командной строки.
|
|
10
|
+
#
|
|
11
|
+
# @example Использование CLI
|
|
12
|
+
# # Получение секретов
|
|
13
|
+
# keysloth pull --repo git@github.com:company/secrets.git --password secret
|
|
14
|
+
#
|
|
15
|
+
# # Отправка секретов
|
|
16
|
+
# keysloth push --repo git@github.com:company/secrets.git --password secret
|
|
17
|
+
#
|
|
18
|
+
# # Справка
|
|
19
|
+
# keysloth help
|
|
20
|
+
#
|
|
21
|
+
# @author KeySloth Team
|
|
22
|
+
# @since 0.1.0
|
|
23
|
+
class CLI < Thor
|
|
24
|
+
class_option :verbose, type: :boolean, aliases: '-v',
|
|
25
|
+
desc: 'Включить подробное логирование (DEBUG уровень)'
|
|
26
|
+
class_option :quiet, type: :boolean, aliases: '-q',
|
|
27
|
+
desc: 'Тихий режим (только ошибки)'
|
|
28
|
+
class_option :config, type: :string, aliases: '-c',
|
|
29
|
+
desc: 'Путь к файлу конфигурации .keyslothrc'
|
|
30
|
+
|
|
31
|
+
desc 'pull', 'Получить и расшифровать секреты из Git репозитория'
|
|
32
|
+
long_desc <<~DESC
|
|
33
|
+
Команда pull клонирует указанный Git репозиторий, получает зашифрованные файлы
|
|
34
|
+
из указанной ветки, расшифровывает их с использованием предоставленного пароля
|
|
35
|
+
и сохраняет в локальную директорию.
|
|
36
|
+
|
|
37
|
+
Поддерживаемые типы файлов секретов:
|
|
38
|
+
- .cer (сертификаты)
|
|
39
|
+
- .p12 (PKCS#12 сертификаты)
|
|
40
|
+
- .mobileprovisioning (профили подготовки iOS)
|
|
41
|
+
- .json (конфигурационные файлы)
|
|
42
|
+
|
|
43
|
+
Перед выполнением операции автоматически создается резервная копия
|
|
44
|
+
существующей локальной директории с секретами.
|
|
45
|
+
DESC
|
|
46
|
+
option :repo, type: :string, aliases: '-r',
|
|
47
|
+
desc: 'URL Git репозитория (SSH: git@github.com:user/repo.git)'
|
|
48
|
+
option :branch, type: :string, default: 'main', aliases: '-b',
|
|
49
|
+
desc: 'Ветка репозитория для получения секретов'
|
|
50
|
+
option :password, type: :string, required: true, aliases: '-p',
|
|
51
|
+
desc: 'Пароль для расшифровки секретов'
|
|
52
|
+
option :path, type: :string, default: './secrets', aliases: '-d',
|
|
53
|
+
desc: 'Локальный путь для сохранения расшифрованных секретов'
|
|
54
|
+
def pull
|
|
55
|
+
setup_logger
|
|
56
|
+
|
|
57
|
+
logger.info('=== Начинаем операцию получения секретов ===')
|
|
58
|
+
|
|
59
|
+
begin
|
|
60
|
+
KeySloth.pull(
|
|
61
|
+
repo_url: options[:repo],
|
|
62
|
+
branch: options[:branch],
|
|
63
|
+
password: options[:password],
|
|
64
|
+
local_path: options[:path],
|
|
65
|
+
config_file: options[:config]
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
logger.info('=== Операция получения секретов завершена успешно ===')
|
|
69
|
+
rescue KeySloth::KeySlothError => e
|
|
70
|
+
logger.error("Ошибка выполнения операции: #{e.message}")
|
|
71
|
+
exit 1
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
desc 'push', 'Зашифровать и отправить секреты в Git репозиторий'
|
|
76
|
+
long_desc <<~DESC
|
|
77
|
+
Команда push читает файлы секретов из локальной директории, шифрует их
|
|
78
|
+
с использованием предоставленного пароля и отправляет в указанный
|
|
79
|
+
Git репозиторий в указанную ветку.
|
|
80
|
+
|
|
81
|
+
Перед отправкой выполняется проверка актуальности удаленной ветки.
|
|
82
|
+
При обнаружении конфликтов операция прекращается с детальным сообщением
|
|
83
|
+
об ошибке.
|
|
84
|
+
|
|
85
|
+
Поддерживаемые типы файлов секретов:
|
|
86
|
+
- .cer (сертификаты)
|
|
87
|
+
- .p12 (PKCS#12 сертификаты)#{' '}
|
|
88
|
+
- .mobileprovisioning (профили подготовки iOS)
|
|
89
|
+
- .json (конфигурационные файлы)
|
|
90
|
+
DESC
|
|
91
|
+
option :repo, type: :string, aliases: '-r',
|
|
92
|
+
desc: 'URL Git репозитория (SSH: git@github.com:user/repo.git)'
|
|
93
|
+
option :branch, type: :string, default: 'main', aliases: '-b',
|
|
94
|
+
desc: 'Ветка репозитория для отправки секретов'
|
|
95
|
+
option :password, type: :string, required: true, aliases: '-p',
|
|
96
|
+
desc: 'Пароль для шифрования секретов'
|
|
97
|
+
option :path, type: :string, default: './secrets', aliases: '-d',
|
|
98
|
+
desc: 'Локальный путь с секретами для шифрования'
|
|
99
|
+
option :message, type: :string, aliases: '-m',
|
|
100
|
+
desc: 'Сообщение коммита (опционально)'
|
|
101
|
+
def push
|
|
102
|
+
setup_logger
|
|
103
|
+
|
|
104
|
+
logger.info('=== Начинаем операцию отправки секретов ===')
|
|
105
|
+
|
|
106
|
+
begin
|
|
107
|
+
KeySloth.push(
|
|
108
|
+
repo_url: options[:repo],
|
|
109
|
+
branch: options[:branch],
|
|
110
|
+
password: options[:password],
|
|
111
|
+
local_path: options[:path],
|
|
112
|
+
config_file: options[:config],
|
|
113
|
+
commit_message: options[:message]
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
logger.info('=== Операция отправки секретов завершена успешно ===')
|
|
117
|
+
rescue KeySloth::KeySlothError => e
|
|
118
|
+
logger.error("Ошибка выполнения операции: #{e.message}")
|
|
119
|
+
exit 1
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
desc 'status', 'Проверить состояние локальных секретов'
|
|
124
|
+
long_desc <<~DESC
|
|
125
|
+
Команда status показывает информацию о состоянии локальных секретов:
|
|
126
|
+
- Количество найденных файлов секретов
|
|
127
|
+
- Список файлов с их размерами
|
|
128
|
+
- Информацию о доступных резервных копиях
|
|
129
|
+
- Проверку целостности файлов
|
|
130
|
+
DESC
|
|
131
|
+
option :path, type: :string, default: './secrets', aliases: '-d',
|
|
132
|
+
desc: 'Локальный путь с секретами для проверки'
|
|
133
|
+
def status
|
|
134
|
+
setup_logger
|
|
135
|
+
|
|
136
|
+
logger.info('=== Проверяем состояние локальных секретов ===')
|
|
137
|
+
|
|
138
|
+
begin
|
|
139
|
+
file_manager = FileManager.new(logger)
|
|
140
|
+
|
|
141
|
+
unless file_manager.directory_exists?(options[:path])
|
|
142
|
+
logger.warn("Директория секретов не существует: #{options[:path]}")
|
|
143
|
+
return
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Собираем файлы секретов
|
|
147
|
+
secret_files = file_manager.collect_secret_files(options[:path])
|
|
148
|
+
|
|
149
|
+
if secret_files.empty?
|
|
150
|
+
logger.info('Файлы секретов не найдены')
|
|
151
|
+
return
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
logger.info("Найдено #{secret_files.size} файлов секретов:")
|
|
155
|
+
|
|
156
|
+
secret_files.each do |file_path|
|
|
157
|
+
size = File.size(file_path)
|
|
158
|
+
relative_path = file_manager.get_relative_path(file_path, options[:path])
|
|
159
|
+
integrity = file_manager.verify_file_integrity(file_path) ? '✓' : '✗'
|
|
160
|
+
|
|
161
|
+
logger.info(" #{integrity} #{relative_path} (#{size} байт)")
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Показываем информацию о backup'ах
|
|
165
|
+
backups = file_manager.list_backups(options[:path])
|
|
166
|
+
if backups.any?
|
|
167
|
+
logger.info("\nДоступные резервные копии:")
|
|
168
|
+
backups.each do |backup_path|
|
|
169
|
+
backup_time = File.basename(backup_path).match(/_(\d{8}_\d{6})$/)&.captures&.first
|
|
170
|
+
if backup_time
|
|
171
|
+
formatted_time = Time.strptime(backup_time,
|
|
172
|
+
'%Y%m%d_%H%M%S').strftime('%Y-%m-%d %H:%M:%S')
|
|
173
|
+
logger.info(" #{File.basename(backup_path)} (#{formatted_time})")
|
|
174
|
+
else
|
|
175
|
+
logger.info(" #{File.basename(backup_path)}")
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
else
|
|
179
|
+
logger.info("\nРезервные копии не найдены")
|
|
180
|
+
end
|
|
181
|
+
rescue KeySloth::KeySlothError => e
|
|
182
|
+
logger.error("Ошибка проверки состояния: #{e.message}")
|
|
183
|
+
exit 1
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
desc 'restore BACKUP_PATH', 'Восстановить секреты из резервной копии'
|
|
188
|
+
long_desc <<~DESC
|
|
189
|
+
Команда restore восстанавливает локальные секреты из указанной
|
|
190
|
+
резервной копии. Используйте команду 'status' для просмотра
|
|
191
|
+
доступных резервных копий.
|
|
192
|
+
DESC
|
|
193
|
+
option :path, type: :string, default: './secrets', aliases: '-d',
|
|
194
|
+
desc: 'Локальный путь для восстановления секретов'
|
|
195
|
+
def restore(backup_path)
|
|
196
|
+
setup_logger
|
|
197
|
+
|
|
198
|
+
logger.info('=== Восстанавливаем секреты из резервной копии ===')
|
|
199
|
+
|
|
200
|
+
begin
|
|
201
|
+
file_manager = FileManager.new(logger)
|
|
202
|
+
file_manager.restore_from_backup(backup_path, options[:path])
|
|
203
|
+
|
|
204
|
+
logger.info('=== Восстановление завершено успешно ===')
|
|
205
|
+
rescue KeySloth::KeySlothError => e
|
|
206
|
+
logger.error("Ошибка восстановления: #{e.message}")
|
|
207
|
+
exit 1
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
desc 'validate', 'Проверить целостность файлов секретов'
|
|
212
|
+
long_desc <<~DESC
|
|
213
|
+
Команда validate выполняет проверку целостности всех файлов секретов
|
|
214
|
+
в указанной директории:
|
|
215
|
+
- Проверяет доступность файлов для чтения
|
|
216
|
+
- Проверяет что файлы не пустые
|
|
217
|
+
- Валидирует структуру файлов секретов
|
|
218
|
+
- Выводит детальный отчет о состоянии каждого файла
|
|
219
|
+
DESC
|
|
220
|
+
option :path, type: :string, default: './secrets', aliases: '-d',
|
|
221
|
+
desc: 'Локальный путь с секретами для проверки'
|
|
222
|
+
def validate
|
|
223
|
+
setup_logger
|
|
224
|
+
|
|
225
|
+
logger.info('=== Проверяем целостность файлов секретов ===')
|
|
226
|
+
|
|
227
|
+
begin
|
|
228
|
+
file_manager = FileManager.new(logger)
|
|
229
|
+
|
|
230
|
+
unless file_manager.directory_exists?(options[:path])
|
|
231
|
+
logger.error("Директория секретов не существует: #{options[:path]}")
|
|
232
|
+
exit 1
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# Собираем файлы секретов
|
|
236
|
+
secret_files = file_manager.collect_secret_files(options[:path])
|
|
237
|
+
|
|
238
|
+
if secret_files.empty?
|
|
239
|
+
logger.warn('Файлы секретов не найдены')
|
|
240
|
+
return
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
logger.info("Проверяем #{secret_files.size} файлов секретов...")
|
|
244
|
+
|
|
245
|
+
valid_files = 0
|
|
246
|
+
invalid_files = 0
|
|
247
|
+
|
|
248
|
+
secret_files.each do |file_path|
|
|
249
|
+
relative_path = file_manager.get_relative_path(file_path, options[:path])
|
|
250
|
+
|
|
251
|
+
if file_manager.verify_file_integrity(file_path)
|
|
252
|
+
logger.info(" ✓ #{relative_path} - файл корректен")
|
|
253
|
+
valid_files += 1
|
|
254
|
+
else
|
|
255
|
+
logger.error(" ✗ #{relative_path} - файл поврежден или недоступен")
|
|
256
|
+
invalid_files += 1
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# Итоговый отчет
|
|
261
|
+
logger.info("\n=== Результаты проверки ===")
|
|
262
|
+
logger.info("Корректных файлов: #{valid_files}")
|
|
263
|
+
logger.info("Поврежденных файлов: #{invalid_files}")
|
|
264
|
+
|
|
265
|
+
if invalid_files.positive?
|
|
266
|
+
logger.error("Обнаружены поврежденные файлы! Рекомендуется восстановление из backup'а.")
|
|
267
|
+
exit 1
|
|
268
|
+
else
|
|
269
|
+
logger.info('Все файлы секретов прошли проверку целостности.')
|
|
270
|
+
end
|
|
271
|
+
rescue KeySloth::KeySlothError => e
|
|
272
|
+
logger.error("Ошибка проверки целостности: #{e.message}")
|
|
273
|
+
exit 1
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
desc 'init', 'Инициализировать новый проект с KeySloth'
|
|
278
|
+
long_desc <<~DESC
|
|
279
|
+
Команда init выполняет первичную настройку KeySloth в проекте:
|
|
280
|
+
- Создает файл конфигурации .keyslothrc
|
|
281
|
+
- Создает локальную директорию для секретов
|
|
282
|
+
- Добавляет директорию секретов в .gitignore
|
|
283
|
+
- Создает примеры конфигурации
|
|
284
|
+
|
|
285
|
+
После выполнения команды можно использовать pull и push для работы с секретами.
|
|
286
|
+
DESC
|
|
287
|
+
option :repo, type: :string, required: true, aliases: '-r',
|
|
288
|
+
desc: 'URL Git репозитория для хранения секретов'
|
|
289
|
+
option :branch, type: :string, default: 'main', aliases: '-b',
|
|
290
|
+
desc: 'Ветка репозитория для секретов'
|
|
291
|
+
option :path, type: :string, default: './secrets', aliases: '-d',
|
|
292
|
+
desc: 'Локальный путь для секретов'
|
|
293
|
+
option :force, type: :boolean, aliases: '-f',
|
|
294
|
+
desc: 'Перезаписать существующие файлы конфигурации'
|
|
295
|
+
def init
|
|
296
|
+
setup_logger
|
|
297
|
+
|
|
298
|
+
logger.info('=== Инициализируем KeySloth проект ===')
|
|
299
|
+
|
|
300
|
+
begin
|
|
301
|
+
file_manager = FileManager.new(logger)
|
|
302
|
+
|
|
303
|
+
config_path = '.keyslothrc'
|
|
304
|
+
gitignore_path = '.gitignore'
|
|
305
|
+
secrets_path = options[:path]
|
|
306
|
+
|
|
307
|
+
# Проверяем существование конфигурации
|
|
308
|
+
if File.exist?(config_path) && !options[:force]
|
|
309
|
+
logger.error("Файл #{config_path} уже существует. Используйте --force для перезаписи.")
|
|
310
|
+
exit 1
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
# Создаем конфигурационный файл
|
|
314
|
+
config_content = <<~YAML
|
|
315
|
+
# Конфигурация KeySloth
|
|
316
|
+
repo_url: "#{options[:repo]}"
|
|
317
|
+
branch: "#{options[:branch]}"
|
|
318
|
+
local_path: "#{secrets_path}"
|
|
319
|
+
backup_count: 3
|
|
320
|
+
YAML
|
|
321
|
+
|
|
322
|
+
File.write(config_path, config_content)
|
|
323
|
+
logger.info("Создан файл конфигурации: #{config_path}")
|
|
324
|
+
|
|
325
|
+
# Создаем директорию для секретов
|
|
326
|
+
file_manager.ensure_directory(secrets_path)
|
|
327
|
+
logger.info("Создана директория для секретов: #{secrets_path}")
|
|
328
|
+
|
|
329
|
+
# Обновляем .gitignore
|
|
330
|
+
gitignore_entry = "\n# KeySloth секреты\n#{secrets_path}/\n.keyslothrc\n"
|
|
331
|
+
|
|
332
|
+
if File.exist?(gitignore_path)
|
|
333
|
+
gitignore_content = File.read(gitignore_path)
|
|
334
|
+
if gitignore_content.include?(secrets_path)
|
|
335
|
+
logger.info('Файл .gitignore уже содержит правила для KeySloth')
|
|
336
|
+
else
|
|
337
|
+
File.write(gitignore_path, gitignore_content + gitignore_entry)
|
|
338
|
+
logger.info('Обновлен файл .gitignore')
|
|
339
|
+
end
|
|
340
|
+
else
|
|
341
|
+
File.write(gitignore_path, gitignore_entry.strip)
|
|
342
|
+
logger.info('Создан файл .gitignore')
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
# Создаем README для директории секретов
|
|
346
|
+
readme_path = File.join(secrets_path, 'README.md')
|
|
347
|
+
readme_content = <<~MARKDOWN
|
|
348
|
+
# Секреты проекта
|
|
349
|
+
|
|
350
|
+
Эта директория содержит зашифрованные секреты проекта, управляемые KeySloth.
|
|
351
|
+
|
|
352
|
+
## Использование
|
|
353
|
+
|
|
354
|
+
### Получение секретов
|
|
355
|
+
```bash
|
|
356
|
+
keysloth pull
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
### Отправка секретов
|
|
360
|
+
```bash
|
|
361
|
+
keysloth push
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
### Проверка состояния
|
|
365
|
+
```bash
|
|
366
|
+
keysloth status
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
## Поддерживаемые типы файлов
|
|
370
|
+
- `.cer` - сертификаты
|
|
371
|
+
- `.p12` - PKCS#12 сертификаты
|
|
372
|
+
- `.mobileprovisioning` - профили подготовки iOS
|
|
373
|
+
- `.json` - конфигурационные файлы
|
|
374
|
+
|
|
375
|
+
**ВНИМАНИЕ:** Никогда не коммитьте эту директорию в Git!
|
|
376
|
+
MARKDOWN
|
|
377
|
+
|
|
378
|
+
File.write(readme_path, readme_content)
|
|
379
|
+
logger.info("Создан файл справки: #{readme_path}")
|
|
380
|
+
|
|
381
|
+
logger.info("\n=== Инициализация завершена успешно ===")
|
|
382
|
+
logger.info('Следующие шаги:')
|
|
383
|
+
logger.info("1. Поместите ваши файлы секретов в #{secrets_path}/")
|
|
384
|
+
logger.info("2. Используйте 'keysloth push' для первой отправки секретов")
|
|
385
|
+
logger.info('3. Поделитесь паролем шифрования с командой')
|
|
386
|
+
rescue KeySloth::KeySlothError => e
|
|
387
|
+
logger.error("Ошибка инициализации: #{e.message}")
|
|
388
|
+
exit 1
|
|
389
|
+
rescue StandardError => e
|
|
390
|
+
logger.error("Неожиданная ошибка: #{e.message}")
|
|
391
|
+
exit 1
|
|
392
|
+
end
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
desc 'version', 'Показать версию KeySloth'
|
|
396
|
+
def version
|
|
397
|
+
puts "KeySloth версия #{KeySloth::VERSION}"
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
# Переопределяем help для улучшенного вывода
|
|
401
|
+
desc 'help [COMMAND]', 'Показать справку по командам'
|
|
402
|
+
def help(command = nil)
|
|
403
|
+
if command
|
|
404
|
+
super
|
|
405
|
+
else
|
|
406
|
+
puts <<~HELP
|
|
407
|
+
KeySloth v#{KeySloth::VERSION} - Управление зашифрованными секретами в Git репозиториях
|
|
408
|
+
|
|
409
|
+
ИСПОЛЬЗОВАНИЕ:
|
|
410
|
+
keysloth COMMAND [OPTIONS]
|
|
411
|
+
|
|
412
|
+
КОМАНДЫ:
|
|
413
|
+
init Инициализировать новый проект с KeySloth
|
|
414
|
+
pull Получить и расшифровать секреты из Git репозитория
|
|
415
|
+
push Зашифровать и отправить секреты в Git репозиторий#{' '}
|
|
416
|
+
status Проверить состояние локальных секретов
|
|
417
|
+
validate Проверить целостность файлов секретов
|
|
418
|
+
restore Восстановить секреты из резервной копии
|
|
419
|
+
version Показать версию KeySloth
|
|
420
|
+
help Показать эту справку
|
|
421
|
+
|
|
422
|
+
ГЛОБАЛЬНЫЕ ОПЦИИ:
|
|
423
|
+
-v, --verbose Подробное логирование (DEBUG уровень)
|
|
424
|
+
-q, --quiet Тихий режим (только ошибки)
|
|
425
|
+
-c, --config Путь к файлу конфигурации .keyslothrc
|
|
426
|
+
|
|
427
|
+
ПРИМЕРЫ:
|
|
428
|
+
# Инициализировать новый проект
|
|
429
|
+
keysloth init -r git@github.com:company/secrets.git
|
|
430
|
+
|
|
431
|
+
# Получить секреты из репозитория
|
|
432
|
+
keysloth pull -r git@github.com:company/secrets.git -p mypassword
|
|
433
|
+
|
|
434
|
+
# Отправить секреты в репозиторий
|
|
435
|
+
keysloth push -r git@github.com:company/secrets.git -p mypassword -m "Update certificates"
|
|
436
|
+
|
|
437
|
+
# Проверить состояние и целостность
|
|
438
|
+
keysloth status
|
|
439
|
+
keysloth validate
|
|
440
|
+
|
|
441
|
+
Для подробной справки по команде используйте: keysloth help COMMAND
|
|
442
|
+
HELP
|
|
443
|
+
end
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
private
|
|
447
|
+
|
|
448
|
+
# Настраивает логгер в соответствии с параметрами командной строки
|
|
449
|
+
def setup_logger
|
|
450
|
+
level = if options[:verbose]
|
|
451
|
+
:debug
|
|
452
|
+
elsif options[:quiet]
|
|
453
|
+
:error
|
|
454
|
+
else
|
|
455
|
+
:info
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
@logger = Logger.new(level: level)
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
# Возвращает настроенный логгер
|
|
462
|
+
#
|
|
463
|
+
# @return [KeySloth::Logger] Настроенный логгер
|
|
464
|
+
def logger
|
|
465
|
+
@logger ||= Logger.new
|
|
466
|
+
end
|
|
467
|
+
end
|
|
468
|
+
end
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'yaml'
|
|
4
|
+
require 'pathname'
|
|
5
|
+
|
|
6
|
+
module KeySloth
|
|
7
|
+
# Класс для управления конфигурацией KeySloth
|
|
8
|
+
#
|
|
9
|
+
# Поддерживает загрузку конфигурации из YAML файла .keyslothrc.
|
|
10
|
+
# Параметры командной строки имеют приоритет над файлом конфигурации.
|
|
11
|
+
#
|
|
12
|
+
# @example Структура файла .keyslothrc
|
|
13
|
+
# repo_url: "git@github.com:company/secrets.git"
|
|
14
|
+
# branch: "main"
|
|
15
|
+
# local_path: "./secrets"
|
|
16
|
+
# backup_count: 3
|
|
17
|
+
#
|
|
18
|
+
# @example Использование
|
|
19
|
+
# config = KeySloth::Config.load('.keyslothrc')
|
|
20
|
+
# merged = config.merge(repo_url: 'override_url')
|
|
21
|
+
#
|
|
22
|
+
# @author KeySloth Team
|
|
23
|
+
# @since 0.1.0
|
|
24
|
+
class Config
|
|
25
|
+
# Значения конфигурации по умолчанию
|
|
26
|
+
DEFAULT_CONFIG = {
|
|
27
|
+
branch: 'main',
|
|
28
|
+
backup_count: 3,
|
|
29
|
+
local_path: './secrets'
|
|
30
|
+
}.freeze
|
|
31
|
+
|
|
32
|
+
# Инициализация объекта конфигурации
|
|
33
|
+
#
|
|
34
|
+
# @param config_hash [Hash] Хеш с параметрами конфигурации
|
|
35
|
+
def initialize(config_hash = {})
|
|
36
|
+
@config = DEFAULT_CONFIG.merge(symbolize_keys(config_hash))
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Загружает конфигурацию из файла
|
|
40
|
+
#
|
|
41
|
+
# @param config_file [String, nil] Путь к файлу конфигурации
|
|
42
|
+
# @return [Config] Объект конфигурации
|
|
43
|
+
# @raise [ConfigurationError] при ошибках чтения файла конфигурации
|
|
44
|
+
def self.load(config_file = nil)
|
|
45
|
+
config_path = resolve_config_path(config_file)
|
|
46
|
+
|
|
47
|
+
if config_path && File.exist?(config_path)
|
|
48
|
+
begin
|
|
49
|
+
yaml_content = YAML.load_file(config_path)
|
|
50
|
+
new(yaml_content || {})
|
|
51
|
+
rescue StandardError => e
|
|
52
|
+
raise ConfigurationError, "Ошибка чтения конфигурации из #{config_path}: #{e.message}", e
|
|
53
|
+
end
|
|
54
|
+
else
|
|
55
|
+
new
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Объединяет текущую конфигурацию с новыми параметрами
|
|
60
|
+
#
|
|
61
|
+
# @param overrides [Hash] Параметры для переопределения
|
|
62
|
+
# @return [Hash] Объединенная конфигурация
|
|
63
|
+
def merge(overrides = {})
|
|
64
|
+
@config.merge(symbolize_keys(overrides))
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Возвращает значение конфигурации по ключу
|
|
68
|
+
#
|
|
69
|
+
# @param key [Symbol, String] Ключ конфигурации
|
|
70
|
+
# @return [Object] Значение конфигурации
|
|
71
|
+
def [](key)
|
|
72
|
+
@config[key.to_sym]
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Устанавливает значение конфигурации
|
|
76
|
+
#
|
|
77
|
+
# @param key [Symbol, String] Ключ конфигурации
|
|
78
|
+
# @param value [Object] Значение конфигурации
|
|
79
|
+
def []=(key, value)
|
|
80
|
+
@config[key.to_sym] = value
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Возвращает все параметры конфигурации
|
|
84
|
+
#
|
|
85
|
+
# @return [Hash] Хеш конфигурации
|
|
86
|
+
def to_h
|
|
87
|
+
@config.dup
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Валидирует обязательные параметры конфигурации
|
|
91
|
+
#
|
|
92
|
+
# @param required_keys [Array<Symbol>] Список обязательных ключей
|
|
93
|
+
# @raise [ValidationError] при отсутствии обязательных параметров
|
|
94
|
+
def validate!(required_keys = [])
|
|
95
|
+
missing_keys = required_keys.select { |key| @config[key].nil? || @config[key].to_s.empty? }
|
|
96
|
+
|
|
97
|
+
unless missing_keys.empty?
|
|
98
|
+
raise ValidationError, "Отсутствуют обязательные параметры: #{missing_keys.join(', ')}"
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
private
|
|
103
|
+
|
|
104
|
+
# Преобразует строковые ключи в символы
|
|
105
|
+
#
|
|
106
|
+
# @param hash [Hash] Исходный хеш
|
|
107
|
+
# @return [Hash] Хеш с символьными ключами
|
|
108
|
+
def symbolize_keys(hash)
|
|
109
|
+
return {} unless hash.is_a?(Hash)
|
|
110
|
+
|
|
111
|
+
hash.transform_keys(&:to_sym)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Определяет путь к файлу конфигурации
|
|
115
|
+
#
|
|
116
|
+
# @param config_file [String, nil] Явно указанный путь к файлу
|
|
117
|
+
# @return [String, nil] Путь к файлу конфигурации или nil
|
|
118
|
+
def self.resolve_config_path(config_file)
|
|
119
|
+
return config_file if config_file
|
|
120
|
+
|
|
121
|
+
# Ищем .keyslothrc в текущей директории и домашней директории
|
|
122
|
+
['.keyslothrc', File.expand_path('~/.keyslothrc')].find do |path|
|
|
123
|
+
File.exist?(path)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
private_class_method :resolve_config_path
|
|
128
|
+
end
|
|
129
|
+
end
|