# frozen_string_literal: true require 'yaml' require 'fileutils' require 'json' CONFIG_FILE = 'config/1password-secrets.yml' class SecretsFetcher def run system('which op > /dev/null') || abort('op is not installed. Please install it (e.g. brew install 1password-cli).') abort("Config file #{CONFIG_FILE} not found.") unless File.exist?(CONFIG_FILE) config = YAML.load_file(CONFIG_FILE).deep_symbolize_keys config[:items].each do |item| elaborate_item(item) end end private def elaborate_item(item) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize private_link = item[:private_link] if private_link.nil? warn 'Private link not found in your config file.' return elsif private_link.includes?('share.1password.com') warn 'Please use a private link, not a share link.' return end item_id = extract_item_id(private_link) item_json = get_item(item_id) return if item_json.nil? item[:env_variables].each do |env_variable| process_env_variable(env_variable[:name], item_id, item_json) end item[:files].each do |file| process_file(file[:folder] || item[:default_folder], file[:name], item_json, item_id) end end def process_env_variable(name, item_id, item_json) env_json = item_json['fields'].find { |field| field['label'] == name } if env_json.nil? warn "Field `#{name}` not found in item #{item_id}. Check the field name in your #{CONFIG_FILE} file." else value = env_json['value'] update_or_add_variable('.env', name, /^#{name}=/, "#{name}=#{value}") update_or_add_variable('config/application.yml', name, /^#{name}:.*$/, "#{name}: \"#{value}\"") end end def update_or_add_variable(file_path, name, pattern, replacement) # rubocop:disable Metrics/MethodLength return unless File.exist?(file_path) file_contents = File.read(file_path) updated_contents = file_contents if file_contents.match(pattern) if ask("Do you want to update the value of #{name} in #{file_path}? (y/n)", 'y') == 'y' updated_contents = file_contents.gsub(pattern, replacement) end else puts "Added environment variable #{name} to #{file_path}" updated_contents = "#{file_contents}#{replacement}\n" end File.write(file_path, updated_contents) end def get_item(item_id) output = execute_command(command: "op item get #{item_id} --format json", success_message: '', error_message: "Error fetching item #{item_id}." \ "Check `private_link` in your #{CONFIG_FILE} file.") JSON.parse(output) if output end def execute_command(command:, success_message:, error_message:) output = `#{command}` if $CHILD_STATUS.success? puts success_message unless success_message.empty? output else warn error_message end end def extract_item_id(private_link) /&i=([^&]+)/.match(private_link)[1] end def process_file(output_folder, filename, item, item_id) FileUtils.mkdir_p(output_folder) output_path = File.join(output_folder, filename) vault_id = item.dig('vault', 'id') download_file_from_item(item['files'], vault_id, item_id, filename, output_path) puts "\n\n" end def download_file_from_item(files, vault_id, item_id, filename, output_path) item_file = files&.find { |tmp_file_config| tmp_file_config['name'].eql?(filename) } if item_file.nil? warn "File `#{filename}` not found in item #{item_id}. Check the file name in your #{CONFIG_FILE} file." return end execute_command(command: "op read \"op://#{vault_id}/#{item_id}/#{item_file['id']}\" --out-file #{output_path}", success_message: "Successfully fetched #{filename} and saved to #{output_path}", error_message: "Error fetching #{filename}.") end end