diff --git a/Gemfile.lock b/Gemfile.lock index b49b42a..7bd508e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - envirobly (1.9.0) + envirobly (1.10.0) activesupport (~> 8.0) aws-sdk-s3 (~> 1.182) concurrent-ruby (~> 1.3) diff --git a/lib/envirobly/cli/main.rb b/lib/envirobly/cli/main.rb index 8bfbf75..78ba6d0 100644 --- a/lib/envirobly/cli/main.rb +++ b/lib/envirobly/cli/main.rb @@ -36,14 +36,14 @@ def set_default_region Envirobly::Defaults::Region.new(shell:).require_value end - desc "validate", "Validates config" - def validate + desc "validate", "Validates config (for given environ)" + def validate(environ_name = nil) Envirobly::AccessToken.new(shell:).require! config = Envirobly::Config.new api = Envirobly::Api.new - params = { validation: { configs: config.configs } } + params = { validation: { config: config.merge(environ_name).to_yaml } } api.validate_shape params say "Config is valid #{green_check}" diff --git a/lib/envirobly/config.rb b/lib/envirobly/config.rb index 2900ed8..4568c81 100644 --- a/lib/envirobly/config.rb +++ b/lib/envirobly/config.rb @@ -1,26 +1,62 @@ # frozen_string_literal: true -class Envirobly::Config - DIR = ".envirobly" - BASE = "deploy.yml" - OVERRIDES_PATTERN = /deploy\.([a-z0-9\-_]+)\.yml/i +module Envirobly + class Config + DIR = ".envirobly" + BASE = "deploy.yml" + OVERRIDES_PATTERN = /deploy\.([a-z0-9\-_]+)\.yml/i - def initialize(dir = DIR) - @dir = Pathname.new dir - end + attr_reader :errors - def configs - Dir.entries(@dir).map do |file| - path = File.join(@dir, file) + def initialize(dir = DIR) + @dir = Pathname.new dir + @errors = [] + end - next unless File.file?(path) && config_file?(file) + def configs + Dir.entries(@dir).map do |file| + path = File.join(@dir, file) - [ "#{DIR}/#{file}", ERB.new(File.read(path)).result ] - end.compact.to_h - end + next unless File.file?(path) && config_file?(file) + + [ "#{DIR}/#{file}", File.read(path) ] + end.compact.to_h + end + + def merge(environ_name = nil) + path = Pathname.new(DIR).join(BASE).to_s + yaml = configs.fetch(path) + result = parse yaml, path - private - def config_file?(file) - file == BASE || file.match?(OVERRIDES_PATTERN) + if environ_name.present? + override_path = Pathname.new(DIR).join("deploy.#{environ_name}.yml").to_s + + if configs.key?(override_path) + other_yaml = configs.fetch(override_path) + override = parse other_yaml, override_path + result = result.deep_merge(override) if override.is_a?(Hash) + end + end + + @errors.empty? ? result : nil end + + private + def config_file?(file) + file == BASE || file.match?(OVERRIDES_PATTERN) + end + + def parse(content, path) + begin + yaml = ERB.new(content).result + rescue Exception => e + @errors << { message: e.message, path: } + return + end + + YAML.safe_load yaml, aliases: true, permitted_classes: [ Secret ] + rescue Psych::Exception => e + @errors << { message: e.message, path: } + end + end end diff --git a/lib/envirobly/deployment.rb b/lib/envirobly/deployment.rb index 88bbc88..db74e09 100644 --- a/lib/envirobly/deployment.rb +++ b/lib/envirobly/deployment.rb @@ -6,7 +6,7 @@ module Envirobly class Deployment include Colorize - attr_reader :params + attr_reader :params, :shell def initialize(environ_name:, commit:, account_id:, project_name:, project_id:, region:, shell:) @commit = commit @@ -53,19 +53,40 @@ def initialize(environ_name:, commit:, account_id:, project_name:, project_id:, commit_time: @commit.time, commit_message: @commit.message, object_tree_checksum: @commit.object_tree_checksum, - configs: @config.configs + config: @config.merge(@environ_name).to_yaml } } end def perform(dry_run:) + if dry_run + shell.say "This is a dry run, nothing will be deployed.", :green + end + + # TODO: Replace with shell puts [ "Deploying commit", yellow(@commit.short_ref), faint("→"), green(@environ_name) ].join(" ") puts + # TODO: Multiline indent puts " #{@commit.message}" puts if dry_run - puts YAML.dump(@params) + puts green("Config:") + puts @params[:deployment][:config] + + shell.say + shell.say "Targeting:", :green + + targets_and_values = [ + [ "Account ID", @params[:account_id].to_s ], + [ "Project ID", @params[:project_id].to_s ], + [ "Region", @params[:region] ], + [ "Project Name", @params[:project_name] ], + [ "Environ Name", @params[:deployment][:environ_name] ] + ] + + shell.print_table targets_and_values, borders: true + return end diff --git a/lib/envirobly/secret.rb b/lib/envirobly/secret.rb new file mode 100644 index 0000000..55c2b8c --- /dev/null +++ b/lib/envirobly/secret.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Envirobly + class Secret + attr_reader :plain + + def initialize(value) + @plain = value + end + + def init_with(coder) + @plain = coder.scalar + end + + def encode_with(coder) + coder.scalar = @plain + end + + def to_s + "[SECRET]" + end + + def ==(other) + other.is_a?(Secret) && other.plain == plain + end + end +end + +YAML.add_tag("!secret", Envirobly::Secret) diff --git a/lib/envirobly/version.rb b/lib/envirobly/version.rb index 2f4fae9..9dacd60 100644 --- a/lib/envirobly/version.rb +++ b/lib/envirobly/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Envirobly - VERSION = "1.9.0" + VERSION = "1.10.0" end diff --git a/test/envirobly/config_test.rb b/test/envirobly/config_test.rb index 5df1bce..b8eb89a 100644 --- a/test/envirobly/config_test.rb +++ b/test/envirobly/config_test.rb @@ -12,4 +12,71 @@ class Envirobly::ConfigTest < ActiveSupport::TestCase } assert_equal expected, config.configs end + + test "merge without override" do + config = Envirobly::Config.new("test/fixtures/configs") + expected = { + "x" => { + "shared_env" => { + "SECRET" => Envirobly::Secret.new("mySecret") + } + }, + "services" => { + "app" => { + "public" => true, + "env" => { + "SECRET" => Envirobly::Secret.new("mySecret"), + "APP_ENV" => "production" + } + } + } + } + + assert_equal expected, config.merge + assert_empty config.errors + + assert_equal expected, config.merge("xyz"), "This override doens't exist" + assert_empty config.errors + end + + test "merge with environ override" do + config = Envirobly::Config.new("test/fixtures/configs") + expected = { + "x" => { + "shared_env" => { + "SECRET" => Envirobly::Secret.new("mySecret") + } + }, + "services" => { + "app" => { + "public" => false, + "instance_type" => "t4g.micro", + "env" => { + "SECRET" => Envirobly::Secret.new("mySecret"), + "APP_ENV" => "staging", + "STAGING_VAR" => "abcd" + } + } + } + } + assert_equal expected, config.merge("staging") + assert_empty config.errors + end + + test "invalid YAML" do + config = Envirobly::Config.new("test/fixtures/invalid_configs") + assert_nil config.merge + assert_equal 1, config.errors.size + assert_equal "(): did not find expected node content while parsing a flow node at line 2 column 3", config.errors.first[:message] + assert_equal ".envirobly/deploy.yml", config.errors.first[:path] + end + + test "invalid ERB" do + config = Envirobly::Config.new("test/fixtures/invalid_configs") + assert_nil config.merge("erb-error") + assert_equal 2, config.errors.size + assert_equal ".envirobly/deploy.yml", config.errors.first[:path] + assert_equal ".envirobly/deploy.erb-error.yml", config.errors.second[:path] + assert_equal "uncaught throw \"ERB error\"", config.errors.second[:message] + end end diff --git a/test/envirobly/secret_test.rb b/test/envirobly/secret_test.rb new file mode 100644 index 0000000..3dd70d4 --- /dev/null +++ b/test/envirobly/secret_test.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require "test_helper" + +module Envirobly + class SecretTest < ActiveSupport::TestCase + test "equality" do + secret1 = Secret.new("a") + secret2 = Secret.new("a") + assert_equal secret1, secret2 + + secret2 = Secret.new("b") + assert_not_equal secret1, secret2 + end + + test "dump" do + yaml = { + secret: Secret.new("hello") + }.to_yaml + assert_equal "---\n:secret: !secret hello\n", yaml + end + end +end diff --git a/test/fixtures/configs/deploy.staging.yml b/test/fixtures/configs/deploy.staging.yml index 6aab550..17783fb 100644 --- a/test/fixtures/configs/deploy.staging.yml +++ b/test/fixtures/configs/deploy.staging.yml @@ -1,3 +1,7 @@ services: app: + public: <%= "false" if true %> instance_type: t4g.micro + env: + APP_ENV: staging + STAGING_VAR: abcd diff --git a/test/fixtures/configs/deploy.yml b/test/fixtures/configs/deploy.yml index 7ee3535..f5629e8 100644 --- a/test/fixtures/configs/deploy.yml +++ b/test/fixtures/configs/deploy.yml @@ -1,5 +1,10 @@ -project: https://envirobly.com/projects/1 +x: + shared_env: &shared_env + SECRET: !secret mySecret services: app: - public: true + public: <%= "true" %> + env: + <<: *shared_env + APP_ENV: production diff --git a/test/fixtures/configs/dignity/deploy.yml b/test/fixtures/configs/dignity/deploy.yml new file mode 100644 index 0000000..e69de29 diff --git a/test/fixtures/configs/ignored.txt b/test/fixtures/configs/ignored.txt new file mode 100644 index 0000000..1e69222 --- /dev/null +++ b/test/fixtures/configs/ignored.txt @@ -0,0 +1 @@ +This file is ignored by Config class. diff --git a/test/fixtures/configs/kindness/deploy.production.yml b/test/fixtures/configs/kindness/deploy.production.yml new file mode 100644 index 0000000..e69de29 diff --git a/test/fixtures/configs/nirvana/deploy.staging.yml b/test/fixtures/configs/nirvana/deploy.staging.yml new file mode 100644 index 0000000..e69de29 diff --git a/test/fixtures/configs/nirvana/deploy.yml b/test/fixtures/configs/nirvana/deploy.yml new file mode 100644 index 0000000..56605ba --- /dev/null +++ b/test/fixtures/configs/nirvana/deploy.yml @@ -0,0 +1,3 @@ +services: + app: + public: false diff --git a/test/fixtures/invalid_configs/deploy.erb-error.yml b/test/fixtures/invalid_configs/deploy.erb-error.yml new file mode 100644 index 0000000..76a7083 --- /dev/null +++ b/test/fixtures/invalid_configs/deploy.erb-error.yml @@ -0,0 +1 @@ +services: <% throw "ERB error" %> diff --git a/test/fixtures/invalid_configs/deploy.yml b/test/fixtures/invalid_configs/deploy.yml new file mode 100644 index 0000000..c789edd --- /dev/null +++ b/test/fixtures/invalid_configs/deploy.yml @@ -0,0 +1,2 @@ +invalid_yaml: { + -