From 32e87903c58e52e81f0bbbde4b6ba430f2a68ffc Mon Sep 17 00:00:00 2001 From: Zakir Dzhamaliddinov Date: Fri, 30 Aug 2024 21:49:53 +1200 Subject: [PATCH 01/16] Generate Terraform config from gvc & identity templates --- docs/commands.md | 2 +- lib/command/terraform/generate.rb | 19 +++- lib/core/terraform_config/dsl.rb | 6 +- lib/core/terraform_config/generator.rb | 77 ++++++++++++++++ lib/core/terraform_config/gvc.rb | 57 ++++++++++++ lib/core/terraform_config/identity.rb | 27 ++++++ lib/cpflow.rb | 2 +- lib/patches/string.rb | 4 + spec/command/terraform/generate_spec.rb | 14 +-- spec/core/terraform_config/generator_spec.rb | 92 ++++++++++++++++++++ spec/core/terraform_config/gvc_spec.rb | 52 +++++++++++ spec/core/terraform_config/identity_spec.rb | 36 ++++++++ 12 files changed, 378 insertions(+), 10 deletions(-) create mode 100644 lib/core/terraform_config/generator.rb create mode 100644 lib/core/terraform_config/gvc.rb create mode 100644 lib/core/terraform_config/identity.rb create mode 100644 spec/core/terraform_config/generator_spec.rb create mode 100644 spec/core/terraform_config/gvc_spec.rb create mode 100644 spec/core/terraform_config/identity_spec.rb diff --git a/docs/commands.md b/docs/commands.md index 14fa758a..c6a1ac75 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -449,7 +449,7 @@ cpflow setup-app -a $APP_NAME - Generates terraform configuration files based on `controlplane.yml` and `templates/` config ```sh -cpflow terraform generate +cpflow terraform generate -a $APP_NAME ``` ### `version` diff --git a/lib/command/terraform/generate.rb b/lib/command/terraform/generate.rb index 1ee8be21..164d665f 100644 --- a/lib/command/terraform/generate.rb +++ b/lib/command/terraform/generate.rb @@ -5,15 +5,26 @@ module Terraform class Generate < Base SUBCOMMAND_NAME = "terraform" NAME = "generate" + OPTIONS = [ + app_option(required: true) + ].freeze DESCRIPTION = "Generates terraform configuration files" LONG_DESCRIPTION = <<~DESC - Generates terraform configuration files based on `controlplane.yml` and `templates/` config DESC WITH_INFO_HEADER = false - VALIDATIONS = [].freeze def call File.write(terraform_dir.join("providers.tf"), cpln_provider.to_tf) + + templates.each do |template| + generator = TerraformConfig::Generator.new(config: config, template: template) + + # TODO: Delete line below after all template kinds are supported + next unless %w[gvc identity].include?(template["kind"]) + + File.write(terraform_dir.join(generator.filename), generator.tf_config.to_tf, mode: "a+") + end end private @@ -22,8 +33,14 @@ def cpln_provider TerraformConfig::RequiredProvider.new("cpln", source: "controlplane-com/cpln", version: "~> 1.0") end + def templates + parser = TemplateParser.new(self) + parser.parse(Dir["#{parser.template_dir}/*.yml"]) + end + def terraform_dir @terraform_dir ||= Cpflow.root_path.join("terraform").tap do |path| + FileUtils.rm_rf(path) FileUtils.mkdir_p(path) end end diff --git a/lib/core/terraform_config/dsl.rb b/lib/core/terraform_config/dsl.rb index 68b9baac..3f231628 100644 --- a/lib/core/terraform_config/dsl.rb +++ b/lib/core/terraform_config/dsl.rb @@ -4,6 +4,8 @@ module TerraformConfig module Dsl extend Forwardable + REFERENCE_PATTERN = /^(var|locals|cpln_\w+)\./.freeze + def_delegators :current_context, :put, :output def block(name, *labels) @@ -44,7 +46,7 @@ def tf_value(value) end def expression?(value) - value.start_with?("var.") || value.start_with?("locals.") + value.match?(REFERENCE_PATTERN) end def block_declaration(name, labels) @@ -62,7 +64,7 @@ def initialize end def put(content, indent: 0) - @output += content.indent(indent) + @output += content.to_s.indent(indent) end end diff --git a/lib/core/terraform_config/generator.rb b/lib/core/terraform_config/generator.rb new file mode 100644 index 00000000..84264959 --- /dev/null +++ b/lib/core/terraform_config/generator.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +module TerraformConfig + class Generator + attr_reader :config, :template + + def initialize(config:, template:) + @config = config + @template = template + end + + def filename + case template["kind"] + when "gvc" + "gvc.tf" + when "identity" + "identities.tf" + else + raise "Unsupported template kind - #{template['kind']}" + end + end + + def tf_config + case template["kind"] + when "gvc" + gvc_config + when "identity" + identity_config + else + raise "Unsupported template kind - #{template['kind']}" + end + end + + private + + # rubocop:disable Metrics/MethodLength + def gvc_config + pull_secrets = template.dig("spec", "pullSecretLinks")&.map do |secret_link| + secret_name = secret_link.split("/").last + "cpln_secret.#{secret_name}.name" + end + + load_balancer = template.dig("spec", "loadBalancer") + + TerraformConfig::Gvc.new( + name: template["name"], + description: template["description"], + tags: template["tags"], + domain: template.dig("spec", "domain"), + env: env, + pull_secrets: pull_secrets, + locations: locations, + load_balancer: load_balancer + ) + end + # rubocop:enable Metrics/MethodLength + + def identity_config + TerraformConfig::Identity.new( + gvc: "cpln_gvc.#{config.app}.name", # GVC name matches application name + name: template["name"], + description: template["description"], + tags: template["tags"] + ) + end + + def env + template.dig("spec", "env").to_h { |env_var| [env_var["name"], env_var["value"]] } + end + + def locations + template.dig("spec", "staticPlacement", "locationLinks")&.map do |location_link| + location_link.split("/").last + end + end + end +end diff --git a/lib/core/terraform_config/gvc.rb b/lib/core/terraform_config/gvc.rb new file mode 100644 index 00000000..401e2791 --- /dev/null +++ b/lib/core/terraform_config/gvc.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +module TerraformConfig + class Gvc < Base + attr_reader :name, :description, :tags, :domain, :locations, :pull_secrets, :env, :load_balancer + + # rubocop:disable Metrics/ParameterLists + def initialize( + name:, + description: nil, + tags: nil, + domain: nil, + locations: nil, + pull_secrets: nil, + env: nil, + load_balancer: nil + ) + super() + + @name = name + @description = description + @tags = tags + @domain = domain + @locations = locations + @pull_secrets = pull_secrets + @env = env + @load_balancer = load_balancer&.transform_keys { |k| k.to_s.underscore.to_sym } + end + # rubocop:enable Metrics/ParameterLists + + def to_tf + block :resource, :cpln_gvc, name do + argument :name, name + argument :description, description, optional: true + argument :tags, tags, optional: true + + argument :domain, domain, optional: true + argument :locations, locations, optional: true + argument :pull_secrets, pull_secrets, optional: true + argument :env, env, optional: true + + load_balancer_tf + end + end + + private + + def load_balancer_tf + return if load_balancer.nil? + + block :load_balancer do + argument :dedicated, load_balancer.fetch(:dedicated) + argument :trusted_proxies, load_balancer.fetch(:trusted_proxies, nil), optional: true + end + end + end +end diff --git a/lib/core/terraform_config/identity.rb b/lib/core/terraform_config/identity.rb new file mode 100644 index 00000000..a211ff46 --- /dev/null +++ b/lib/core/terraform_config/identity.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module TerraformConfig + class Identity < Base + attr_reader :gvc, :name, :description, :tags + + def initialize(gvc:, name:, description: nil, tags: nil) + super() + + @gvc = gvc + @name = name + @description = description + @tags = tags + end + + def to_tf + block :resource, :cpln_identity, name do + argument :gvc, gvc + + argument :name, name + argument :description, description, optional: true + + argument :tags, tags, optional: true + end + end + end +end diff --git a/lib/cpflow.rb b/lib/cpflow.rb index 5ac95cfb..4b352784 100644 --- a/lib/cpflow.rb +++ b/lib/cpflow.rb @@ -235,7 +235,7 @@ def self.klass_for(subcommand_name) long_desc(long_description) command_options.each do |option| - params = process_option_params(option[:params]) + params = Cpflow::Cli.process_option_params(option[:params]) method_option(option[:name], **params) end end diff --git a/lib/patches/string.rb b/lib/patches/string.rb index 53b12e99..15508cb3 100644 --- a/lib/patches/string.rb +++ b/lib/patches/string.rb @@ -17,5 +17,9 @@ def indent!(amount, indent_string = nil, indent_empty_lines = false) def unindent gsub(/^#{scan(/^[ \t]+(?=\S)/).min}/, "") end + + def underscore + gsub(/(.)([A-Z])/, '\1_\2').downcase + end end # rubocop:enable Style/OptionalBooleanParameter, Lint/UnderscorePrefixedVariableName diff --git a/spec/command/terraform/generate_spec.rb b/spec/command/terraform/generate_spec.rb index 5c163ef2..4c42dff9 100644 --- a/spec/command/terraform/generate_spec.rb +++ b/spec/command/terraform/generate_spec.rb @@ -9,6 +9,8 @@ TERRAFORM_CONFIG_DIR_PATH = GENERATOR_PLAYGROUND_PATH.join("terraform") describe Command::Terraform::Generate do + let!(:app) { dummy_test_app } + before do FileUtils.rm_r(GENERATOR_PLAYGROUND_PATH) if Dir.exist?(GENERATOR_PLAYGROUND_PATH) FileUtils.mkdir_p GENERATOR_PLAYGROUND_PATH @@ -20,11 +22,13 @@ FileUtils.rm_r GENERATOR_PLAYGROUND_PATH end - it "generates terraform config files" do - providers_config_file_path = TERRAFORM_CONFIG_DIR_PATH.join("providers.tf") + it "generates terraform config files", :aggregate_failures do + config_file_paths = %w[providers.tf gvc.tf identities.tf].map do |config_file_path| + TERRAFORM_CONFIG_DIR_PATH.join(config_file_path) + end - expect(providers_config_file_path).not_to exist - run_cpflow_command(described_class::SUBCOMMAND_NAME, described_class::NAME) - expect(providers_config_file_path).to exist + config_file_paths.each { |config_file_path| expect(config_file_path).not_to exist } + run_cpflow_command(described_class::SUBCOMMAND_NAME, described_class::NAME, "-a", app) + expect(config_file_paths).to all(exist) end end diff --git a/spec/core/terraform_config/generator_spec.rb b/spec/core/terraform_config/generator_spec.rb new file mode 100644 index 00000000..9a57dc8c --- /dev/null +++ b/spec/core/terraform_config/generator_spec.rb @@ -0,0 +1,92 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe TerraformConfig::Generator do + let(:generator) { described_class.new(config: config, template: template) } + + let(:config) { instance_double(Config, org: "org-name", app: "app-name") } + + context "when template's kind is gvc" do + let(:template) do + { + "kind" => "gvc", + "name" => config.app, + "description" => "description", + "tags" => { "tag1" => "tag1_value", "tag2" => "tag2_value" }, + "spec" => { + "domain" => "app.example.com", + "env" => [ + { + "name" => "DATABASE_URL", + "value" => "postgres://the_user:the_password@postgres.#{config.app}.cpln.local:5432/#{config.app}" + }, + { + "name" => "RAILS_ENV", + "value" => "production" + }, + { + "name" => "RAILS_SERVE_STATIC_FILES", + "value" => "true" + } + ], + "staticPlacement" => { + "locationLinks" => ["/org/#{config.org}/location/aws-us-east-2"] + }, + "pullSecretLinks" => ["/org/#{config.org}/secret/some-secret"], + "loadBalancer" => { + "dedicated" => true, + "trustedProxies" => 1 + } + } + } + end + + it "generates correct terraform config and filename for it", :aggregate_failures do + tf_config = generator.tf_config + expect(tf_config).to be_an_instance_of(TerraformConfig::Gvc) + + expect(tf_config.name).to eq(config.app) + expect(tf_config.description).to eq("description") + expect(tf_config.tags).to eq("tag1" => "tag1_value", "tag2" => "tag2_value") + + expect(tf_config.domain).to eq("app.example.com") + expect(tf_config.locations).to eq(["aws-us-east-2"]) + expect(tf_config.pull_secrets).to eq(["cpln_secret.some-secret.name"]) + expect(tf_config.env).to eq( + { + "DATABASE_URL" => "postgres://the_user:the_password@postgres.#{config.app}.cpln.local:5432/#{config.app}", + "RAILS_ENV" => "production", + "RAILS_SERVE_STATIC_FILES" => "true" + } + ) + expect(tf_config.load_balancer).to eq({ dedicated: true, trusted_proxies: 1 }) + + tf_filename = generator.filename + expect(tf_filename).to eq("gvc.tf") + end + end + + context "when template's kind is identity" do + let(:template) do + { + "kind" => "identity", + "name" => "identity-name", + "description" => "description", + "tags" => { "tag1" => "tag1_value", "tag2" => "tag2_value" } + } + end + + it "generates correct terraform config and filename for it", :aggregate_failures do + tf_config = generator.tf_config + expect(tf_config).to be_an_instance_of(TerraformConfig::Identity) + + expect(tf_config.name).to eq("identity-name") + expect(tf_config.description).to eq("description") + expect(tf_config.tags).to eq("tag1" => "tag1_value", "tag2" => "tag2_value") + + tf_filename = generator.filename + expect(tf_filename).to eq("identities.tf") + end + end +end diff --git a/spec/core/terraform_config/gvc_spec.rb b/spec/core/terraform_config/gvc_spec.rb new file mode 100644 index 00000000..2050914d --- /dev/null +++ b/spec/core/terraform_config/gvc_spec.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe TerraformConfig::Gvc do + let(:config) { described_class.new(**options) } + + describe "#to_tf" do + subject(:generated) { config.to_tf } + + context "with required and optional args" do + let(:options) do + { + name: "gvc-name", + description: "gvc description", + domain: "app.example.com", + env: { "var1" => "value", "var2" => 1 }, + tags: { "tag1" => "tag_value", "tag2" => true }, + locations: %w[aws-us-east-1 aws-us-east-2], + pull_secrets: ["cpln_secret.docker.name"], + load_balancer: { "dedicated" => true, "trusted_proxies" => 1 } + } + end + + it "generates correct config" do + expect(generated).to eq( + <<~EXPECTED + resource "cpln_gvc" "gvc-name" { + name = "gvc-name" + description = "gvc description" + tags = { + tag1 = "tag_value" + tag2 = true + } + domain = "app.example.com" + locations = ["aws-us-east-1", "aws-us-east-2"] + pull_secrets = ["cpln_secret.docker.name"] + env = { + var1 = "value" + var2 = 1 + } + load_balancer { + dedicated = true + trusted_proxies = 1 + } + } + EXPECTED + ) + end + end + end +end diff --git a/spec/core/terraform_config/identity_spec.rb b/spec/core/terraform_config/identity_spec.rb new file mode 100644 index 00000000..ca39eeac --- /dev/null +++ b/spec/core/terraform_config/identity_spec.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe TerraformConfig::Identity do + let(:config) { described_class.new(**options) } + + describe "#to_tf" do + subject(:generated) { config.to_tf } + + let(:options) do + { + gvc: "cpln_gvc.some-gvc.name", + name: "identity-name", + description: "identity description", + tags: { "tag1" => "true", "tag2" => "false" } + } + end + + it "generates correct config" do + expect(generated).to eq( + <<~EXPECTED + resource "cpln_identity" "identity-name" { + gvc = cpln_gvc.some-gvc.name + name = "identity-name" + description = "identity description" + tags = { + tag1 = "true" + tag2 = "false" + } + } + EXPECTED + ) + end + end +end From 2630c3cac27f5dade5ab57cd8e18aa2569c1f56a Mon Sep 17 00:00:00 2001 From: Zakir Dzhamaliddinov Date: Thu, 3 Oct 2024 15:37:12 +0400 Subject: [PATCH 02/16] Add dir option & make app option non-required --- docs/commands.md | 2 +- lib/command/base.rb | 13 +++++++ lib/command/terraform/generate.rb | 45 ++++++++++++++++++++----- spec/command/terraform/generate_spec.rb | 17 +++++----- 4 files changed, 60 insertions(+), 17 deletions(-) diff --git a/docs/commands.md b/docs/commands.md index c6a1ac75..14fa758a 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -449,7 +449,7 @@ cpflow setup-app -a $APP_NAME - Generates terraform configuration files based on `controlplane.yml` and `templates/` config ```sh -cpflow terraform generate -a $APP_NAME +cpflow terraform generate ``` ### `version` diff --git a/lib/command/base.rb b/lib/command/base.rb index 38dedff1..a7c788d6 100644 --- a/lib/command/base.rb +++ b/lib/command/base.rb @@ -454,6 +454,19 @@ def self.add_app_identity_option(required: false) } } end + + def self.dir_option(required: false) + { + name: :dir, + params: { + banner: "DIR", + desc: "Output directory", + type: :string, + required: required + } + } + end + # rubocop:enable Metrics/MethodLength def self.all_options diff --git a/lib/command/terraform/generate.rb b/lib/command/terraform/generate.rb index 164d665f..99459a09 100644 --- a/lib/command/terraform/generate.rb +++ b/lib/command/terraform/generate.rb @@ -6,7 +6,8 @@ class Generate < Base SUBCOMMAND_NAME = "terraform" NAME = "generate" OPTIONS = [ - app_option(required: true) + app_option(required: false), + dir_option(required: false) ].freeze DESCRIPTION = "Generates terraform configuration files" LONG_DESCRIPTION = <<~DESC @@ -15,7 +16,32 @@ class Generate < Base WITH_INFO_HEADER = false def call + generate_common_configs + generate_app_configs + end + + private + + def generate_common_configs + cpln_provider = TerraformConfig::RequiredProvider.new( + "cpln", + source: "controlplane-com/cpln", + version: "~> 1.0" + ) + File.write(terraform_dir.join("providers.tf"), cpln_provider.to_tf) + end + + def generate_app_configs + Array(config.app || config.apps.keys).each do |app| + generate_app_config(app.to_s) + end + end + + def generate_app_config(app) + config.class.define_method(:app) { app } + + terraform_app_dir = recreate_terraform_app_dir(app) templates.each do |template| generator = TerraformConfig::Generator.new(config: config, template: template) @@ -23,14 +49,17 @@ def call # TODO: Delete line below after all template kinds are supported next unless %w[gvc identity].include?(template["kind"]) - File.write(terraform_dir.join(generator.filename), generator.tf_config.to_tf, mode: "a+") + File.write(terraform_app_dir.join(generator.filename), generator.tf_config.to_tf, mode: "a+") end end - private + def recreate_terraform_app_dir(app_path) + full_path = terraform_dir.join(app_path) + + FileUtils.rm_rf(full_path) + FileUtils.mkdir_p(full_path) - def cpln_provider - TerraformConfig::RequiredProvider.new("cpln", source: "controlplane-com/cpln", version: "~> 1.0") + full_path end def templates @@ -39,9 +68,9 @@ def templates end def terraform_dir - @terraform_dir ||= Cpflow.root_path.join("terraform").tap do |path| - FileUtils.rm_rf(path) - FileUtils.mkdir_p(path) + @terraform_dir ||= begin + full_path = config.options.fetch(:dir, Cpflow.root_path.join("terraform")) + Pathname.new(full_path).tap { |path| FileUtils.mkdir_p(path) } end end end diff --git a/spec/command/terraform/generate_spec.rb b/spec/command/terraform/generate_spec.rb index 4c42dff9..6c0676da 100644 --- a/spec/command/terraform/generate_spec.rb +++ b/spec/command/terraform/generate_spec.rb @@ -12,23 +12,24 @@ let!(:app) { dummy_test_app } before do - FileUtils.rm_r(GENERATOR_PLAYGROUND_PATH) if Dir.exist?(GENERATOR_PLAYGROUND_PATH) - FileUtils.mkdir_p GENERATOR_PLAYGROUND_PATH - + FileUtils.rm_rf(GENERATOR_PLAYGROUND_PATH) + FileUtils.mkdir_p(GENERATOR_PLAYGROUND_PATH) allow(Cpflow).to receive(:root_path).and_return(GENERATOR_PLAYGROUND_PATH) end after do - FileUtils.rm_r GENERATOR_PLAYGROUND_PATH + FileUtils.rm_rf GENERATOR_PLAYGROUND_PATH end it "generates terraform config files", :aggregate_failures do - config_file_paths = %w[providers.tf gvc.tf identities.tf].map do |config_file_path| - TERRAFORM_CONFIG_DIR_PATH.join(config_file_path) - end - config_file_paths.each { |config_file_path| expect(config_file_path).not_to exist } run_cpflow_command(described_class::SUBCOMMAND_NAME, described_class::NAME, "-a", app) expect(config_file_paths).to all(exist) end + + def config_file_paths + [TERRAFORM_CONFIG_DIR_PATH.join("providers.tf")] + %w[gvc.tf identities.tf].map do |config_file_path| + TERRAFORM_CONFIG_DIR_PATH.join(app, config_file_path) + end + end end From c051fd2def9667b9271e2a89515ad6a7016f0727 Mon Sep 17 00:00:00 2001 From: Zakir Dzhamaliddinov Date: Thu, 3 Oct 2024 16:37:16 +0400 Subject: [PATCH 03/16] Coderabbitai review fixes --- lib/command/terraform/generate.rb | 12 +++++- spec/command/terraform/generate_spec.rb | 52 ++++++++++++++++++++++++- 2 files changed, 61 insertions(+), 3 deletions(-) diff --git a/lib/command/terraform/generate.rb b/lib/command/terraform/generate.rb index 99459a09..d55eb140 100644 --- a/lib/command/terraform/generate.rb +++ b/lib/command/terraform/generate.rb @@ -64,7 +64,17 @@ def recreate_terraform_app_dir(app_path) def templates parser = TemplateParser.new(self) - parser.parse(Dir["#{parser.template_dir}/*.yml"]) + template_files = Dir["#{parser.template_dir}/*.yml"] + + if template_files.empty? + Shell.warn "No templates found in #{parser.template_dir}" + return [] + end + + parser.parse(template_files) + rescue StandardError => e + Shell.warn "Error parsing templates: #{e.message}" + [] end def terraform_dir diff --git a/spec/command/terraform/generate_spec.rb b/spec/command/terraform/generate_spec.rb index 6c0676da..636db525 100644 --- a/spec/command/terraform/generate_spec.rb +++ b/spec/command/terraform/generate_spec.rb @@ -9,6 +9,8 @@ TERRAFORM_CONFIG_DIR_PATH = GENERATOR_PLAYGROUND_PATH.join("terraform") describe Command::Terraform::Generate do + subject(:result) { run_cpflow_command(described_class::SUBCOMMAND_NAME, described_class::NAME, "-a", app) } + let!(:app) { dummy_test_app } before do @@ -23,12 +25,58 @@ it "generates terraform config files", :aggregate_failures do config_file_paths.each { |config_file_path| expect(config_file_path).not_to exist } - run_cpflow_command(described_class::SUBCOMMAND_NAME, described_class::NAME, "-a", app) + + expect(result[:status]).to eq(0) + expect(config_file_paths).to all(exist) end + context "when templates folder is empty" do + let(:template_dir) { "non-existing-folder" } + + before do + # rubocop:disable RSpec/AnyInstance + allow_any_instance_of(TemplateParser).to receive(:template_dir).and_return(template_dir) + # rubocop:enable RSpec/AnyInstance + end + + it "generates only common config files" do + config_file_paths.each { |config_file_path| expect(config_file_path).not_to exist } + + expect(result[:stderr]).to include("No templates found in #{template_dir}") + + expect(common_config_files).to all(exist) + app_config_files.each { |config_file_path| expect(config_file_path).not_to exist } + end + end + + context "when template parsing fails" do + before do + # rubocop:disable RSpec/AnyInstance + allow_any_instance_of(TemplateParser).to receive(:parse).and_raise("error") + # rubocop:enable RSpec/AnyInstance + end + + it "generates only common config files" do + config_file_paths.each { |config_file_path| expect(config_file_path).not_to exist } + + expect(result[:stderr]).to include("Error parsing templates: error") + + expect(common_config_files).to all(exist) + app_config_files.each { |config_file_path| expect(config_file_path).not_to exist } + end + end + def config_file_paths - [TERRAFORM_CONFIG_DIR_PATH.join("providers.tf")] + %w[gvc.tf identities.tf].map do |config_file_path| + common_config_files + app_config_files + end + + def common_config_files + [TERRAFORM_CONFIG_DIR_PATH.join("providers.tf")] + end + + def app_config_files + %w[gvc.tf identities.tf].map do |config_file_path| TERRAFORM_CONFIG_DIR_PATH.join(app, config_file_path) end end From 1a87e3ef5d09cc987e1d2d59103e0eec10c95222 Mon Sep 17 00:00:00 2001 From: Zakir Dzhamaliddinov Date: Fri, 4 Oct 2024 13:18:28 +0400 Subject: [PATCH 04/16] Review fixes - guard FileUtils.rm_rf method call --- lib/command/terraform/generate.rb | 10 +++++++++- spec/command/terraform/generate_spec.rb | 25 ++++++++++++++++++++++++- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/lib/command/terraform/generate.rb b/lib/command/terraform/generate.rb index d55eb140..ece33c70 100644 --- a/lib/command/terraform/generate.rb +++ b/lib/command/terraform/generate.rb @@ -56,6 +56,10 @@ def generate_app_config(app) def recreate_terraform_app_dir(app_path) full_path = terraform_dir.join(app_path) + unless File.expand_path(full_path).include?(Cpflow.root_path.to_s) + Shell.abort("Directory to save terraform configuration files cannot be outside of current directory") + end + FileUtils.rm_rf(full_path) FileUtils.mkdir_p(full_path) @@ -80,7 +84,11 @@ def templates def terraform_dir @terraform_dir ||= begin full_path = config.options.fetch(:dir, Cpflow.root_path.join("terraform")) - Pathname.new(full_path).tap { |path| FileUtils.mkdir_p(path) } + Pathname.new(full_path).tap do |path| + FileUtils.mkdir_p(path) + rescue StandardError => e + Shell.abort("Invalid directory: #{e.message}") + end end end end diff --git a/spec/command/terraform/generate_spec.rb b/spec/command/terraform/generate_spec.rb index 636db525..f93bbda1 100644 --- a/spec/command/terraform/generate_spec.rb +++ b/spec/command/terraform/generate_spec.rb @@ -9,9 +9,10 @@ TERRAFORM_CONFIG_DIR_PATH = GENERATOR_PLAYGROUND_PATH.join("terraform") describe Command::Terraform::Generate do - subject(:result) { run_cpflow_command(described_class::SUBCOMMAND_NAME, described_class::NAME, "-a", app) } + subject(:result) { run_cpflow_command(described_class::SUBCOMMAND_NAME, described_class::NAME, *options) } let!(:app) { dummy_test_app } + let(:options) { ["-a", app] } before do FileUtils.rm_rf(GENERATOR_PLAYGROUND_PATH) @@ -67,6 +68,28 @@ end end + context "when --dir option is outside of project dir" do + let(:options) { ["-a", app, "--dir", GEM_TEMP_PATH.join("path-outside-of-project").to_s] } + + it "aborts command execution" do + expect(result[:status]).to eq(ExitCode::ERROR_DEFAULT) + expect(result[:stderr]).to include( + "Directory to save terraform configuration files cannot be outside of current directory" + ) + end + end + + context "when terraform config directory creation fails" do + before do + allow(FileUtils).to receive(:mkdir_p).and_raise("error") + end + + it "aborts command execution" do + expect(result[:status]).to eq(ExitCode::ERROR_DEFAULT) + expect(result[:stderr]).to include("error") + end + end + def config_file_paths common_config_files + app_config_files end From 152452ac5b6ec6c73dd2a4df592d54727326d5a0 Mon Sep 17 00:00:00 2001 From: Zakir Dzhamaliddinov Date: Thu, 10 Oct 2024 13:08:48 +0300 Subject: [PATCH 05/16] Support terraform config generation from secret template --- lib/command/terraform/generate.rb | 2 +- lib/core/terraform_config/dsl.rb | 4 +- lib/core/terraform_config/generator.rb | 14 + lib/core/terraform_config/gvc.rb | 2 +- lib/core/terraform_config/secret.rb | 104 ++++ lib/patches/hash.rb | 17 + lib/patches/string.rb | 3 +- spec/command/terraform/generate_spec.rb | 2 +- spec/core/terraform_config/generator_spec.rb | 34 + spec/core/terraform_config/secret_spec.rb | 580 ++++++++++++++++++ spec/dummy/.controlplane/templates/secret.yml | 6 + spec/pathces/hash_spec.rb | 25 + 12 files changed, 788 insertions(+), 5 deletions(-) create mode 100644 lib/core/terraform_config/secret.rb create mode 100644 lib/patches/hash.rb create mode 100644 spec/core/terraform_config/secret_spec.rb create mode 100644 spec/dummy/.controlplane/templates/secret.yml create mode 100644 spec/pathces/hash_spec.rb diff --git a/lib/command/terraform/generate.rb b/lib/command/terraform/generate.rb index ece33c70..e5b5c83b 100644 --- a/lib/command/terraform/generate.rb +++ b/lib/command/terraform/generate.rb @@ -47,7 +47,7 @@ def generate_app_config(app) generator = TerraformConfig::Generator.new(config: config, template: template) # TODO: Delete line below after all template kinds are supported - next unless %w[gvc identity].include?(template["kind"]) + next unless %w[gvc identity secret].include?(template["kind"]) File.write(terraform_app_dir.join(generator.filename), generator.tf_config.to_tf, mode: "a+") end diff --git a/lib/core/terraform_config/dsl.rb b/lib/core/terraform_config/dsl.rb index 3f231628..e2284e70 100644 --- a/lib/core/terraform_config/dsl.rb +++ b/lib/core/terraform_config/dsl.rb @@ -39,7 +39,9 @@ def tf_value(value) case value when String - expression?(value) ? value : "\"#{value}\"" + return value if expression?(value) + + value.include?("\n") ? "EOF\n#{value.strip.indent(2)}\nEOF" : "\"#{value}\"" else value end diff --git a/lib/core/terraform_config/generator.rb b/lib/core/terraform_config/generator.rb index 84264959..a5f9f42e 100644 --- a/lib/core/terraform_config/generator.rb +++ b/lib/core/terraform_config/generator.rb @@ -13,6 +13,8 @@ def filename case template["kind"] when "gvc" "gvc.tf" + when "secret" + "secrets.tf" when "identity" "identities.tf" else @@ -26,6 +28,8 @@ def tf_config gvc_config when "identity" identity_config + when "secret" + secret_config else raise "Unsupported template kind - #{template['kind']}" end @@ -64,6 +68,16 @@ def identity_config ) end + def secret_config + TerraformConfig::Secret.new( + name: template["name"], + description: template["description"], + type: template["type"], + data: template["data"], + tags: template["tags"] + ) + end + def env template.dig("spec", "env").to_h { |env_var| [env_var["name"], env_var["value"]] } end diff --git a/lib/core/terraform_config/gvc.rb b/lib/core/terraform_config/gvc.rb index 401e2791..3142562c 100644 --- a/lib/core/terraform_config/gvc.rb +++ b/lib/core/terraform_config/gvc.rb @@ -24,7 +24,7 @@ def initialize( @locations = locations @pull_secrets = pull_secrets @env = env - @load_balancer = load_balancer&.transform_keys { |k| k.to_s.underscore.to_sym } + @load_balancer = load_balancer&.underscore_keys&.symbolize_keys end # rubocop:enable Metrics/ParameterLists diff --git a/lib/core/terraform_config/secret.rb b/lib/core/terraform_config/secret.rb new file mode 100644 index 00000000..cc757d8c --- /dev/null +++ b/lib/core/terraform_config/secret.rb @@ -0,0 +1,104 @@ +# frozen_string_literal: true + +module TerraformConfig + class Secret < Base + attr_reader :name, :type, :data, :description, :tags + + def initialize(name:, type:, data:, description: nil, tags: nil) + super() + + @name = name + @type = type + @data = data.is_a?(Hash) ? data.underscore_keys.symbolize_keys : data + @description = description + @tags = tags + end + + def to_tf + block :resource, :cpln_secret, name do + argument :name, name + argument :description, description, optional: true + argument :tags, tags, optional: true + + secret_data + end + end + + private + + def secret_data + case type + when "azure-sdk", "dictionary", "docker", "gcp" + argument type.underscore, data, optional: true + when "azure-connector", "aws", "ecr", "keypair", "nats-account", "opaque", "tls", "userpass" + send("#{type.underscore}_tf") + else + raise "Invalid secret type given - #{type}" + end + end + + def aws_tf + block :aws do + argument :secret_key, data.fetch(:secret_key) + argument :access_key, data.fetch(:access_key) + argument :role_arn, data.fetch(:role_arn, nil), optional: true + argument :external_id, data.fetch(:external_id, nil), optional: true + end + end + + def azure_connector_tf + block :azure_connector do + argument :url, data.fetch(:url) + argument :code, data.fetch(:code) + end + end + + def ecr_tf + block :ecr do + argument :secret_key, data.fetch(:secret_key) + argument :access_key, data.fetch(:access_key) + argument :repos, data.fetch(:repos) + argument :role_arn, data.fetch(:role_arn, nil), optional: true + argument :external_id, data.fetch(:external_id, nil), optional: true + end + end + + def keypair_tf + block :keypair do + argument :secret_key, data.fetch(:secret_key) + argument :public_key, data.fetch(:public_key, nil), optional: true + argument :passphrase, data.fetch(:passphrase, nil), optional: true + end + end + + def nats_account_tf + block :nats_account do + argument :account_id, data.fetch(:account_id) + argument :private_key, data.fetch(:private_key) + end + end + + def opaque_tf + block :opaque do + argument :payload, data.fetch(:payload) + argument :encoding, data.fetch(:encoding, nil), optional: true + end + end + + def tls_tf + block :tls do + argument :key, data.fetch(:key) + argument :cert, data.fetch(:cert) + argument :chain, data.fetch(:chain, nil), optional: true + end + end + + def userpass_tf + block :userpass do + argument :username, data.fetch(:username) + argument :password, data.fetch(:password) + argument :encoding, data.fetch(:encoding, nil), optional: true + end + end + end +end diff --git a/lib/patches/hash.rb b/lib/patches/hash.rb new file mode 100644 index 00000000..80e2b0a4 --- /dev/null +++ b/lib/patches/hash.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class Hash + # Copied from Rails + def symbolize_keys + transform_keys { |key| key.to_sym rescue key } # rubocop:disable Style/RescueModifier + end + + def underscore_keys + transform_keys do |key| + underscored = key.to_s.underscore + key.is_a?(Symbol) ? underscored.to_sym : underscored + rescue StandardError + key + end + end +end diff --git a/lib/patches/string.rb b/lib/patches/string.rb index 15508cb3..bfc35d62 100644 --- a/lib/patches/string.rb +++ b/lib/patches/string.rb @@ -18,8 +18,9 @@ def unindent gsub(/^#{scan(/^[ \t]+(?=\S)/).min}/, "") end + # Copied from Rails def underscore - gsub(/(.)([A-Z])/, '\1_\2').downcase + gsub("::", "/").gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2').gsub(/([a-z\d])([A-Z])/, '\1_\2').tr("-", "_").downcase end end # rubocop:enable Style/OptionalBooleanParameter, Lint/UnderscorePrefixedVariableName diff --git a/spec/command/terraform/generate_spec.rb b/spec/command/terraform/generate_spec.rb index f93bbda1..6d049129 100644 --- a/spec/command/terraform/generate_spec.rb +++ b/spec/command/terraform/generate_spec.rb @@ -99,7 +99,7 @@ def common_config_files end def app_config_files - %w[gvc.tf identities.tf].map do |config_file_path| + %w[gvc.tf identities.tf secrets.tf].map do |config_file_path| TERRAFORM_CONFIG_DIR_PATH.join(app, config_file_path) end end diff --git a/spec/core/terraform_config/generator_spec.rb b/spec/core/terraform_config/generator_spec.rb index 9a57dc8c..3d8ad8df 100644 --- a/spec/core/terraform_config/generator_spec.rb +++ b/spec/core/terraform_config/generator_spec.rb @@ -7,6 +7,15 @@ let(:config) { instance_double(Config, org: "org-name", app: "app-name") } + context "when template's kind is unsupported" do + let(:template) { { "kind" => "invalid" } } + + it "does not generate terraform config or filename for it", :aggregate_failures do + expect { generator.tf_config }.to raise_error("Unsupported template kind - #{template['kind']}") + expect { generator.filename }.to raise_error("Unsupported template kind - #{template['kind']}") + end + end + context "when template's kind is gvc" do let(:template) do { @@ -89,4 +98,29 @@ expect(tf_filename).to eq("identities.tf") end end + + context "when template's kind is secret" do + let(:template) do + { + "kind" => "secret", + "type" => "dictionary", + "name" => "secret-name", + "description" => "description", + "tags" => { "tag1" => "tag1_value", "tag2" => "tag2_value" }, + "data" => { "key1" => "key1_value", "key2" => "key2_value2" } + } + end + + it "generates correct terraform config and filename for it", :aggregate_failures do + tf_config = generator.tf_config + expect(tf_config).to be_an_instance_of(TerraformConfig::Secret) + + expect(tf_config.name).to eq("secret-name") + expect(tf_config.description).to eq("description") + expect(tf_config.tags).to eq("tag1" => "tag1_value", "tag2" => "tag2_value") + + tf_filename = generator.filename + expect(tf_filename).to eq("secrets.tf") + end + end end diff --git a/spec/core/terraform_config/secret_spec.rb b/spec/core/terraform_config/secret_spec.rb new file mode 100644 index 00000000..2320e92b --- /dev/null +++ b/spec/core/terraform_config/secret_spec.rb @@ -0,0 +1,580 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe TerraformConfig::Secret do + let(:config) { described_class.new(**base_options.merge(type: type, data: data)) } + + let(:base_options) do + { + name: "some-secret", + description: "secret description", + tags: { "tag1" => "some-tag-1", "tag2" => "some-tag-2" } + } + end + + describe "#to_tf" do + subject(:generated) { config.to_tf } + + context "with aws secret type" do + let(:type) { "aws" } + let(:data) do + { + "accessKey" => "AKIAIOSFODNN7EXAMPLE", + "secretKey" => "secret", + "roleArn" => "arn:awskey", + "externalId" => "123" + } + end + + it "generates correct config" do + expect(generated).to eq( + <<~EXPECTED + resource "cpln_secret" "#{base_options.fetch(:name)}" { + name = "#{base_options.fetch(:name)}" + description = "#{base_options.fetch(:description)}" + tags = { + tag1 = "some-tag-1" + tag2 = "some-tag-2" + } + aws { + secret_key = "#{data.fetch('secretKey')}" + access_key = "#{data.fetch('accessKey')}" + role_arn = "#{data.fetch('roleArn')}" + external_id = "#{data.fetch('externalId')}" + } + } + EXPECTED + ) + end + end + + context "with azure-connector secret type" do + let(:type) { "azure-connector" } + let(:data) do + { + "url" => "https://azure-connector-url.com", + "code" => "123" + } + end + + it "generates correct config" do + expect(generated).to eq( + <<~EXPECTED + resource "cpln_secret" "#{base_options.fetch(:name)}" { + name = "#{base_options.fetch(:name)}" + description = "#{base_options.fetch(:description)}" + tags = { + tag1 = "some-tag-1" + tag2 = "some-tag-2" + } + azure_connector { + url = "#{data.fetch('url')}" + code = "#{data.fetch('code')}" + } + } + EXPECTED + ) + end + end + + context "with azure-sdk secret type" do + let(:type) { "azure-sdk" } + let(:data) do + { + subscriptionId: "2cd2674e-4f89-4a1f-b420-7a1361b46ef7", + tenantId: "292f5674-78b0-488b-9ff8-6d30d77f38d9", + clientId: "649746ce-d862-49d5-a5eb-7d5aad90f54e", + clientSecret: "CONFIDENTIAL" + }.to_json + end + + it "generates correct config" do + expect(generated).to eq( + <<~EXPECTED + resource "cpln_secret" "#{base_options.fetch(:name)}" { + name = "#{base_options.fetch(:name)}" + description = "#{base_options.fetch(:description)}" + tags = { + tag1 = "some-tag-1" + tag2 = "some-tag-2" + } + azure_sdk = "#{data}" + } + EXPECTED + ) + end + end + + context "with dictionary secret type" do + let(:type) { "dictionary" } + let(:data) do + { + "key1" => "value1", + "key2" => "value2" + } + end + + it "generates correct config" do + expect(generated).to eq( + <<~EXPECTED + resource "cpln_secret" "#{base_options.fetch(:name)}" { + name = "#{base_options.fetch(:name)}" + description = "#{base_options.fetch(:description)}" + tags = { + tag1 = "some-tag-1" + tag2 = "some-tag-2" + } + dictionary = { + key1 = "value1" + key2 = "value2" + } + } + EXPECTED + ) + end + end + + context "with docker secret type" do + let(:type) { "docker" } + let(:data) do + { + auths: { + "registry-server": { + username: "username", + password: "password", + email: "email", + auth: "CONFIDENTIAL" + } + } + }.to_json + end + + it "generates correct config" do + expect(generated).to eq( + <<~EXPECTED + resource "cpln_secret" "#{base_options.fetch(:name)}" { + name = "#{base_options.fetch(:name)}" + description = "#{base_options.fetch(:description)}" + tags = { + tag1 = "some-tag-1" + tag2 = "some-tag-2" + } + docker = "#{data}" + } + EXPECTED + ) + end + end + + context "with ecr secret type" do + let(:type) { "ecr" } + let(:data) do + { + "accessKey" => "AKIAIOSFODNN7EXAMPLE", + "secretKey" => "secret", + "repos" => ["015716931765.dkr.ecr.us-west-2.amazonaws.com/cpln-test"], + "roleArn" => "arn:awskey", + "externalId" => "123" + } + end + + it "generates correct config" do + expect(generated).to eq( + <<~EXPECTED + resource "cpln_secret" "#{base_options.fetch(:name)}" { + name = "#{base_options.fetch(:name)}" + description = "#{base_options.fetch(:description)}" + tags = { + tag1 = "some-tag-1" + tag2 = "some-tag-2" + } + ecr { + secret_key = "#{data.fetch('secretKey')}" + access_key = "#{data.fetch('accessKey')}" + repos = #{data.fetch('repos')} + role_arn = "#{data.fetch('roleArn')}" + external_id = "#{data.fetch('externalId')}" + } + } + EXPECTED + ) + end + end + + context "with gcp secret type" do + let(:type) { "gcp" } + let(:data) do + { + "type" => "gcp", + "project_id" => "cpln12345", + "private_key_id" => "pvt_key", + "private_key" => "key", + "client_email" => "support@cpln.io", + "client_id" => "12744", + "auth_uri" => "cloud.google.com", + "token_uri" => "token.cloud.google.com", + "auth_provider_x509_cert_url" => "cert.google.com", + "client_x509_cert_url" => "cert.google.com" + }.to_json + end + + it "generates correct config" do + expect(generated).to eq( + <<~EXPECTED + resource "cpln_secret" "#{base_options.fetch(:name)}" { + name = "#{base_options.fetch(:name)}" + description = "#{base_options.fetch(:description)}" + tags = { + tag1 = "some-tag-1" + tag2 = "some-tag-2" + } + gcp = "#{data}" + } + EXPECTED + ) + end + end + + context "with keypair secret type" do + let(:type) { "keypair" } + let(:data) do + { + "secretKey" => <<~PRIVATE_KEY, + -----BEGIN RSA PRIVATE KEY----- + Proc-Type: 4,ENCRYPTED + DEK-Info: DES-EDE3-CBC,9A26BB15304B18E7 + + ZdBgMExsvIJEsIFDMQ02xh4nDnhXEGUNu7LiWIZjn9WS6QB2jApyOFOBWmp0lK6L + dIJ+Mb8wMeHtkiKS6ZbYeea8M29kwEejZRnKl1Wq0EFycdwbONtbcbjzF+tQGEBT + gQQgkY7wjDWl8HwjFEA+NUuitzi6uI2xWlQpFdUrmqJAZCbxNFa0aM8nW6jnitvP + 616ps3HjLnWCjoyqS4hWxiWmt+VE3KruPnUVVV7bWlzc6jnoZcSaeqeaoQrNKguH + te2iBIMdY/uldb7Ik2Kxr2+kBRmV4YNkp1EelNi/m39VcoUHJLk1jLldzuINhbi2 + IRqYZe4EEMSYdb3TkSosXa64Sz7jMBz5AxlA0n78FKlB9G5FAxaXcVYNQIlvzCbw + uXPbQd/UYKUuEI1Yn8OmGBN5xcOdgWz8hfyxA2Hq1tmo1XN6snavGe7TKbZd70N+ + 1yFbclB2T1z8fPcLwUZUxOl4g2DoMMHIzCSPaIe/otT8389k4H6hEulLis4lW0p3 + qopL5kdpxmSGgXsX6q6CUFb/0cw9HskNT3zbzKLx2MzjFCo93IB07UxPwkCD2kb1 + sLKMcpTC8a0vLaTVNYgDX7wW/YjBrCokaqk0z1whuN6iSReOtvmu5ybrq1Ksg8UQ + yvCSScM/+muKi+gbEOskQs4Ph3ZLHqAX3/XYoyBcFnPNxVHTIa5Dcju6h5gl1/uY + 6tkRsHDr0Lzy8pd6jjf/ApPf9ypCuxKUO1q8PzPg2E4bmEFxc8zOB2NLvfPgFrUR + 0Sbkapv/6x6nNRw75cu69c5we/atip6wst8J1MSU0fTqb6bZ3TF2pDyNEOkdkvoZ + YZ0r3hUytdT0pImoDLKoyy17mtHLLApzHyIgmR3cqtSt07ncmC5lyEBcZBrQXMa8 + aZeOr8iUWQE/q+4BvoxeKsOD6ttKuFnrgl0rmMnYQsSyLJOPizrU4L1d1HMIKswm + iW+Rg7xlWmQg95m8XEWTjAb3tuNz/tGXC7Qa88HvC7YfyG69yM61oPsT83YnxcBT + C/X67lSFTYguFa3HgDZpjGq7Hc/Q7nhaoqNMEs01O6jbcmrue8IIa2FH1tTwPN0W + D7JefjCQjEghue2mjc0fovOGe9A9jvWf+gJHF3vRtFa67uQiQxge9zUzpHyVNpOj + Ve0y0HvibNTd6TSCArctJpIcwpjO3MTT5LBJ1p/8v4b4+knEKD2c69jumNbKGbWr + Wjq39M/MGNUO5SbZMO3gFCt6fgtXkOktH9pJ9iOQpYKgl7QTe2qQygfWkIm0EZRN + 6EaQdNNKgENWicpKyKQ4BxoY1LYAHFHJ95VisLf3KmmOF5MwajADZQT/yth3gvht + xx21b9iudcgq/CRccSvfIPIWZKi6oaqNIXK+E3DQd40TUopLsBWzacTZn9maSZtW + RyAY1TkRn1qDR2soyhBcihrX5PZ83jnOlM3XTdfF1784g8zB9ooDnK7mUKueH1W3 + hWFADMUF7uaBbo5EZ9sE+dFPzWPJLhu2j67a1iHmByqEvFY64lzq7VwwU/GE8JdA + 85oEkhg1ZEPJp3OYTQfPI/CC/2fc93Exf6wmaXuss8AHehuGcKQniOZmFOKOBprv + -----END RSA PRIVATE KEY----- + PRIVATE_KEY + "publicKey" => <<~PUBLIC_KEY, + -----BEGIN PUBLIC KEY----- + MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwrVyExI0uvRmwCAKFHiv + baAcPMcKJDa6f6TtaVo2p8jyfEhVwDTmR3FUrDDZAjh0Q8G/Up8Ob3+IJafNymCO + BhUKou+8ie7guqsbU9JrT0Zos1k/pd0aVfnAR0EpW3es/7fdkWUszU0uweeEj22m + XMlLplnqqoYOGAhuNMqGsZwBr36Bxq9EeB2O79QsAFDNkPVg7xIaYKn32j69o0Zr + ryYI8xqOYYy5Dw6CX+++YYLYiR/PkLYJTVAsxXeqyltCfb3Iv7vN5HrfoYBhndr3 + NxBPkcIJZeh3Z+QzfJ5U+bB5fP/aOsEk5bPbtLzylj2KnOOM/ZxXJtOcu0xtJLd3 + XwIDAQAB + -----END PUBLIC KEY----- + PUBLIC_KEY + "passphrase" => "cpln" + } + end + + it "generates correct config" do + expect(generated).to eq( + <<~EXPECTED + resource "cpln_secret" "#{base_options.fetch(:name)}" { + name = "#{base_options.fetch(:name)}" + description = "#{base_options.fetch(:description)}" + tags = { + tag1 = "some-tag-1" + tag2 = "some-tag-2" + } + keypair { + secret_key = EOF + -----BEGIN RSA PRIVATE KEY----- + Proc-Type: 4,ENCRYPTED + DEK-Info: DES-EDE3-CBC,9A26BB15304B18E7 + + ZdBgMExsvIJEsIFDMQ02xh4nDnhXEGUNu7LiWIZjn9WS6QB2jApyOFOBWmp0lK6L + dIJ+Mb8wMeHtkiKS6ZbYeea8M29kwEejZRnKl1Wq0EFycdwbONtbcbjzF+tQGEBT + gQQgkY7wjDWl8HwjFEA+NUuitzi6uI2xWlQpFdUrmqJAZCbxNFa0aM8nW6jnitvP + 616ps3HjLnWCjoyqS4hWxiWmt+VE3KruPnUVVV7bWlzc6jnoZcSaeqeaoQrNKguH + te2iBIMdY/uldb7Ik2Kxr2+kBRmV4YNkp1EelNi/m39VcoUHJLk1jLldzuINhbi2 + IRqYZe4EEMSYdb3TkSosXa64Sz7jMBz5AxlA0n78FKlB9G5FAxaXcVYNQIlvzCbw + uXPbQd/UYKUuEI1Yn8OmGBN5xcOdgWz8hfyxA2Hq1tmo1XN6snavGe7TKbZd70N+ + 1yFbclB2T1z8fPcLwUZUxOl4g2DoMMHIzCSPaIe/otT8389k4H6hEulLis4lW0p3 + qopL5kdpxmSGgXsX6q6CUFb/0cw9HskNT3zbzKLx2MzjFCo93IB07UxPwkCD2kb1 + sLKMcpTC8a0vLaTVNYgDX7wW/YjBrCokaqk0z1whuN6iSReOtvmu5ybrq1Ksg8UQ + yvCSScM/+muKi+gbEOskQs4Ph3ZLHqAX3/XYoyBcFnPNxVHTIa5Dcju6h5gl1/uY + 6tkRsHDr0Lzy8pd6jjf/ApPf9ypCuxKUO1q8PzPg2E4bmEFxc8zOB2NLvfPgFrUR + 0Sbkapv/6x6nNRw75cu69c5we/atip6wst8J1MSU0fTqb6bZ3TF2pDyNEOkdkvoZ + YZ0r3hUytdT0pImoDLKoyy17mtHLLApzHyIgmR3cqtSt07ncmC5lyEBcZBrQXMa8 + aZeOr8iUWQE/q+4BvoxeKsOD6ttKuFnrgl0rmMnYQsSyLJOPizrU4L1d1HMIKswm + iW+Rg7xlWmQg95m8XEWTjAb3tuNz/tGXC7Qa88HvC7YfyG69yM61oPsT83YnxcBT + C/X67lSFTYguFa3HgDZpjGq7Hc/Q7nhaoqNMEs01O6jbcmrue8IIa2FH1tTwPN0W + D7JefjCQjEghue2mjc0fovOGe9A9jvWf+gJHF3vRtFa67uQiQxge9zUzpHyVNpOj + Ve0y0HvibNTd6TSCArctJpIcwpjO3MTT5LBJ1p/8v4b4+knEKD2c69jumNbKGbWr + Wjq39M/MGNUO5SbZMO3gFCt6fgtXkOktH9pJ9iOQpYKgl7QTe2qQygfWkIm0EZRN + 6EaQdNNKgENWicpKyKQ4BxoY1LYAHFHJ95VisLf3KmmOF5MwajADZQT/yth3gvht + xx21b9iudcgq/CRccSvfIPIWZKi6oaqNIXK+E3DQd40TUopLsBWzacTZn9maSZtW + RyAY1TkRn1qDR2soyhBcihrX5PZ83jnOlM3XTdfF1784g8zB9ooDnK7mUKueH1W3 + hWFADMUF7uaBbo5EZ9sE+dFPzWPJLhu2j67a1iHmByqEvFY64lzq7VwwU/GE8JdA + 85oEkhg1ZEPJp3OYTQfPI/CC/2fc93Exf6wmaXuss8AHehuGcKQniOZmFOKOBprv + -----END RSA PRIVATE KEY----- + EOF + public_key = EOF + -----BEGIN PUBLIC KEY----- + MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwrVyExI0uvRmwCAKFHiv + baAcPMcKJDa6f6TtaVo2p8jyfEhVwDTmR3FUrDDZAjh0Q8G/Up8Ob3+IJafNymCO + BhUKou+8ie7guqsbU9JrT0Zos1k/pd0aVfnAR0EpW3es/7fdkWUszU0uweeEj22m + XMlLplnqqoYOGAhuNMqGsZwBr36Bxq9EeB2O79QsAFDNkPVg7xIaYKn32j69o0Zr + ryYI8xqOYYy5Dw6CX+++YYLYiR/PkLYJTVAsxXeqyltCfb3Iv7vN5HrfoYBhndr3 + NxBPkcIJZeh3Z+QzfJ5U+bB5fP/aOsEk5bPbtLzylj2KnOOM/ZxXJtOcu0xtJLd3 + XwIDAQAB + -----END PUBLIC KEY----- + EOF + passphrase = "#{data.fetch('passphrase')}" + } + } + EXPECTED + ) + end + end + + context "with nats-account secret type" do + let(:type) { "nats-account" } + let(:data) do + { + "accountId" => "AB7JJPKAYKNQOKRKIOS5UCCLALTUAAXCC7FR2QGC4V5UFCAKW4EBIFVZ", + "privateKey" => "SAABRA7OGVHKARDQLUQ6THIABW5PMOHJVPSOPTWZRP4WD5LPVOLGTU6ONQ" + } + end + + it "generates correct config" do + expect(generated).to eq( + <<~EXPECTED + resource "cpln_secret" "#{base_options.fetch(:name)}" { + name = "#{base_options.fetch(:name)}" + description = "#{base_options.fetch(:description)}" + tags = { + tag1 = "some-tag-1" + tag2 = "some-tag-2" + } + nats_account { + account_id = "#{data.fetch('accountId')}" + private_key = "#{data.fetch('privateKey')}" + } + } + EXPECTED + ) + end + end + + context "with opaque secret type" do + let(:type) { "opaque" } + let(:data) do + { + "payload" => "payload", + "encoding" => "plain" + } + end + + it "generates correct config" do + expect(generated).to eq( + <<~EXPECTED + resource "cpln_secret" "#{base_options.fetch(:name)}" { + name = "#{base_options.fetch(:name)}" + description = "#{base_options.fetch(:description)}" + tags = { + tag1 = "some-tag-1" + tag2 = "some-tag-2" + } + opaque { + payload = "#{data.fetch('payload')}" + encoding = "#{data.fetch('encoding')}" + } + } + EXPECTED + ) + end + end + + context "with tls secret type" do + let(:type) { "tls" } + let(:data) do + { + "key" => <<~PRIVATE_KEY, + -----BEGIN PRIVATE KEY----- + MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDBzN2jRf9ouoF4 + XG0eUxcc4f1sP8vhW1fQXjun3cl0RsN4jRdOyTKWcls1yAxlOkwFod8d6HND9OvN + rsl7U4iJIEcJL6vTqHY7jTGXQkd9yPONMpMXYE8Dsiqtk0deoOab7fafYcvq1iWn + pvg157mJ/u9qdyU+1h8DncES30FkPsG8TsIsjx94JkTJeMmEJxtws4dfuoCk88IN + bBHLjxBQgwTu0vgMxN34b5z+esHraetDN2fqxSoTOeIlyFzeS+kwG3GK4I1hUQBi + L2TeDrnEY6qP/ZoGuyyVnsT/6pHY/BTAcH3Rgeqose7mqBT+7zlxDfHYHceuNB/l + jq0e1j69AgMBAAECggEAPGhrPZV4A2D/MlE9AhLMRYh7wd4w4tHiEWUOG0kank/g + Zhc0iK5WQmbq31y34GXHhInsThpCs5AIYFh3HSXwjS2udsKRQKxmDjH4nzldp2uX + 3w9Aoiy29GP4wZoCyRBGUZxfH1cQhOazXgrBm6vbPZRldD4nMer0R+BIamWEsIYD + YjDj1pT0noLUSeqoLmGxSQ4DNIBQVZB/T8ziMcEzl6bhprT0QrapJSyD2CtA8tH1 + Z8cyhmyE0CUvSkV4K2ecvVukWBJvrAYc6euPAnkS5LJrQotI5+3jJO2QawOlL6Uw + rFWBpgBrCgbzquMRpDCQ/J9/GDYaZjim4YdonboBgQKBgQD7jx3CVnG4LDz198am + spmPwKCW1ke6PhlG7zf3YR00xg9vPBYiy4obb1Jg6em1wr+iZ0dEt8fimeZXewBf + LzlrR8T1Or0eLzfbn+GlLIKGKhn2pKB/i1iolkfIonchqXRk9WNx+PzjgUqiYWRC + /1tH2BsODlVrzKL2lnbWKNIFdQKBgQDFOLedpMeYemLhrsU1TXGt1xTxAbWvOCyt + vig/huyz4SQENXyu3ImPzxIxpTHxKhUaXo/qFXn0jhqnf0LfWI4nbQUbkivb5BPr + KY9aj7XwwsY4MXW5C12Qi0lIwHOWCmfzvyS7TCMqnQb7sT4Mjmm4ydEbiI1TjlFJ + D/RFxzcDKQKBgQCehPcJyZNrrWTU0sh5rz4ZWhdYNbuJXyxqiMBJwQa4hL6hJ8oD + LyPeWe4daAmAIjLEUjSU1wK8hqKiKb54PLgAJH+20MbvyG14lm2Iul2d0dX+mIsT + FGpQAjNF+Sr9KV1RaVi7L12ct5KidKDLn0KUKVgTKXEmtxNSNEq6dYqzKQKBgDI8 + zljzvnwSwNloIYgAYDK+FPGHU/Z8QrVHOQ1lmyn+8aO41DfeqZPeVW4b/GrII3QC + HnqsWdJ32EZOXoRyFFPqq2BojY+Hu6MthPy2msvncYKi5q/qOz00nchQbaEMqYon + aH3lWRfjxAGdFocwR7HwhrmSwR1FpWMNE1Yq9tJxAoGBANc0nZSy5ZlTiMWdRrTt + gFc9N/jz8OL6qLrJtX2Axyv7Vv8H/gbDg4olLR+Io38M0S1WwEHsaIJLIvJ6msjl + /LlseAW6oiO6jzhWEr0VQSLkuJn45hG/uy7t19SDuNR7W5NuEr0YbWd6fZEpR7RR + S1hFKnRRcrVqA+HjWnZ//BGi + -----END PRIVATE KEY----- + PRIVATE_KEY + "cert" => <<~CERTIFICATE, + -----BEGIN CERTIFICATE----- + MIID+zCCAuOgAwIBAgIUEwBv3WQkP7dIiEIxyj+Wi1STz7QwDQYJKoZIhvcNAQEL + BQAwgYwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRQwEgYDVQQH + DAtMb3MgQW5nZWxlczENMAsGA1UECgwEQ1BMTjERMA8GA1UECwwIQ1BMTi1PUkcx + EDAOBgNVBAMMB2NwbG4uaW8xHjAcBgkqhkiG9w0BCQEWD3N1cHBvcnRAY3Bsbi5p + bzAeFw0yMDEwMTQxNzI4MDhaFw0zMDEwMTIxNzI4MDhaMIGMMQswCQYDVQQGEwJV + UzETMBEGA1UECAwKQ2FsaWZvcm5pYTEUMBIGA1UEBwwLTG9zIEFuZ2VsZXMxDTAL + BgNVBAoMBENQTE4xETAPBgNVBAsMCENQTE4tT1JHMRAwDgYDVQQDDAdjcGxuLmlv + MR4wHAYJKoZIhvcNAQkBFg9zdXBwb3J0QGNwbG4uaW8wggEiMA0GCSqGSIb3DQEB + AQUAA4IBDwAwggEKAoIBAQDBzN2jRf9ouoF4XG0eUxcc4f1sP8vhW1fQXjun3cl0 + RsN4jRdOyTKWcls1yAxlOkwFod8d6HND9OvNrsl7U4iJIEcJL6vTqHY7jTGXQkd9 + yPONMpMXYE8Dsiqtk0deoOab7fafYcvq1iWnpvg157mJ/u9qdyU+1h8DncES30Fk + PsG8TsIsjx94JkTJeMmEJxtws4dfuoCk88INbBHLjxBQgwTu0vgMxN34b5z+esHr + aetDN2fqxSoTOeIlyFzeS+kwG3GK4I1hUQBiL2TeDrnEY6qP/ZoGuyyVnsT/6pHY + /BTAcH3Rgeqose7mqBT+7zlxDfHYHceuNB/ljq0e1j69AgMBAAGjUzBRMB0GA1Ud + DgQWBBRxncC/8RRio/S9Ly8tKFS7WnTcNTAfBgNVHSMEGDAWgBRxncC/8RRio/S9 + Ly8tKFS7WnTcNTAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAr + sDZQj4K47fW6JkJbxlzZ1hd7IX6cQhI/DRIdTGR1u0kM1RtZoS0UtV5qsYV/g/S4 + ChuB/aIARyTWvHKDhcT3bRGHLnoZJ8pLlQh4nEfO07SRhyeNiO4qmWM9az0nP5qD + wAXpLpmYIairzAgY7QXbk5wXbTrXli3mz14VaNoqN4s7iyLtHn5TGAXc12aMwo7M + 5yn/RGxoWQoJqSQKc9nf909cR81AVCdG1dFcp7u8Ud1pTtlmiU9ZJ/YOXDCT/1hZ + YxoeotDBBOIao3Ym/3351somMoQ7Lz6hRWvG0WhDIsCXvth4XSxRkZFXgjWNuhdD + u2ZCis/EwXsqRJPkIPnL + -----END CERTIFICATE----- + CERTIFICATE + "chain" => "None. The above key and certificate were self-signed." + } + end + + it "generates correct config" do + expect(generated).to eq( + <<~EXPECTED + resource "cpln_secret" "#{base_options.fetch(:name)}" { + name = "#{base_options.fetch(:name)}" + description = "#{base_options.fetch(:description)}" + tags = { + tag1 = "some-tag-1" + tag2 = "some-tag-2" + } + tls { + key = EOF + -----BEGIN PRIVATE KEY----- + MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDBzN2jRf9ouoF4 + XG0eUxcc4f1sP8vhW1fQXjun3cl0RsN4jRdOyTKWcls1yAxlOkwFod8d6HND9OvN + rsl7U4iJIEcJL6vTqHY7jTGXQkd9yPONMpMXYE8Dsiqtk0deoOab7fafYcvq1iWn + pvg157mJ/u9qdyU+1h8DncES30FkPsG8TsIsjx94JkTJeMmEJxtws4dfuoCk88IN + bBHLjxBQgwTu0vgMxN34b5z+esHraetDN2fqxSoTOeIlyFzeS+kwG3GK4I1hUQBi + L2TeDrnEY6qP/ZoGuyyVnsT/6pHY/BTAcH3Rgeqose7mqBT+7zlxDfHYHceuNB/l + jq0e1j69AgMBAAECggEAPGhrPZV4A2D/MlE9AhLMRYh7wd4w4tHiEWUOG0kank/g + Zhc0iK5WQmbq31y34GXHhInsThpCs5AIYFh3HSXwjS2udsKRQKxmDjH4nzldp2uX + 3w9Aoiy29GP4wZoCyRBGUZxfH1cQhOazXgrBm6vbPZRldD4nMer0R+BIamWEsIYD + YjDj1pT0noLUSeqoLmGxSQ4DNIBQVZB/T8ziMcEzl6bhprT0QrapJSyD2CtA8tH1 + Z8cyhmyE0CUvSkV4K2ecvVukWBJvrAYc6euPAnkS5LJrQotI5+3jJO2QawOlL6Uw + rFWBpgBrCgbzquMRpDCQ/J9/GDYaZjim4YdonboBgQKBgQD7jx3CVnG4LDz198am + spmPwKCW1ke6PhlG7zf3YR00xg9vPBYiy4obb1Jg6em1wr+iZ0dEt8fimeZXewBf + LzlrR8T1Or0eLzfbn+GlLIKGKhn2pKB/i1iolkfIonchqXRk9WNx+PzjgUqiYWRC + /1tH2BsODlVrzKL2lnbWKNIFdQKBgQDFOLedpMeYemLhrsU1TXGt1xTxAbWvOCyt + vig/huyz4SQENXyu3ImPzxIxpTHxKhUaXo/qFXn0jhqnf0LfWI4nbQUbkivb5BPr + KY9aj7XwwsY4MXW5C12Qi0lIwHOWCmfzvyS7TCMqnQb7sT4Mjmm4ydEbiI1TjlFJ + D/RFxzcDKQKBgQCehPcJyZNrrWTU0sh5rz4ZWhdYNbuJXyxqiMBJwQa4hL6hJ8oD + LyPeWe4daAmAIjLEUjSU1wK8hqKiKb54PLgAJH+20MbvyG14lm2Iul2d0dX+mIsT + FGpQAjNF+Sr9KV1RaVi7L12ct5KidKDLn0KUKVgTKXEmtxNSNEq6dYqzKQKBgDI8 + zljzvnwSwNloIYgAYDK+FPGHU/Z8QrVHOQ1lmyn+8aO41DfeqZPeVW4b/GrII3QC + HnqsWdJ32EZOXoRyFFPqq2BojY+Hu6MthPy2msvncYKi5q/qOz00nchQbaEMqYon + aH3lWRfjxAGdFocwR7HwhrmSwR1FpWMNE1Yq9tJxAoGBANc0nZSy5ZlTiMWdRrTt + gFc9N/jz8OL6qLrJtX2Axyv7Vv8H/gbDg4olLR+Io38M0S1WwEHsaIJLIvJ6msjl + /LlseAW6oiO6jzhWEr0VQSLkuJn45hG/uy7t19SDuNR7W5NuEr0YbWd6fZEpR7RR + S1hFKnRRcrVqA+HjWnZ//BGi + -----END PRIVATE KEY----- + EOF + cert = EOF + -----BEGIN CERTIFICATE----- + MIID+zCCAuOgAwIBAgIUEwBv3WQkP7dIiEIxyj+Wi1STz7QwDQYJKoZIhvcNAQEL + BQAwgYwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRQwEgYDVQQH + DAtMb3MgQW5nZWxlczENMAsGA1UECgwEQ1BMTjERMA8GA1UECwwIQ1BMTi1PUkcx + EDAOBgNVBAMMB2NwbG4uaW8xHjAcBgkqhkiG9w0BCQEWD3N1cHBvcnRAY3Bsbi5p + bzAeFw0yMDEwMTQxNzI4MDhaFw0zMDEwMTIxNzI4MDhaMIGMMQswCQYDVQQGEwJV + UzETMBEGA1UECAwKQ2FsaWZvcm5pYTEUMBIGA1UEBwwLTG9zIEFuZ2VsZXMxDTAL + BgNVBAoMBENQTE4xETAPBgNVBAsMCENQTE4tT1JHMRAwDgYDVQQDDAdjcGxuLmlv + MR4wHAYJKoZIhvcNAQkBFg9zdXBwb3J0QGNwbG4uaW8wggEiMA0GCSqGSIb3DQEB + AQUAA4IBDwAwggEKAoIBAQDBzN2jRf9ouoF4XG0eUxcc4f1sP8vhW1fQXjun3cl0 + RsN4jRdOyTKWcls1yAxlOkwFod8d6HND9OvNrsl7U4iJIEcJL6vTqHY7jTGXQkd9 + yPONMpMXYE8Dsiqtk0deoOab7fafYcvq1iWnpvg157mJ/u9qdyU+1h8DncES30Fk + PsG8TsIsjx94JkTJeMmEJxtws4dfuoCk88INbBHLjxBQgwTu0vgMxN34b5z+esHr + aetDN2fqxSoTOeIlyFzeS+kwG3GK4I1hUQBiL2TeDrnEY6qP/ZoGuyyVnsT/6pHY + /BTAcH3Rgeqose7mqBT+7zlxDfHYHceuNB/ljq0e1j69AgMBAAGjUzBRMB0GA1Ud + DgQWBBRxncC/8RRio/S9Ly8tKFS7WnTcNTAfBgNVHSMEGDAWgBRxncC/8RRio/S9 + Ly8tKFS7WnTcNTAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAr + sDZQj4K47fW6JkJbxlzZ1hd7IX6cQhI/DRIdTGR1u0kM1RtZoS0UtV5qsYV/g/S4 + ChuB/aIARyTWvHKDhcT3bRGHLnoZJ8pLlQh4nEfO07SRhyeNiO4qmWM9az0nP5qD + wAXpLpmYIairzAgY7QXbk5wXbTrXli3mz14VaNoqN4s7iyLtHn5TGAXc12aMwo7M + 5yn/RGxoWQoJqSQKc9nf909cR81AVCdG1dFcp7u8Ud1pTtlmiU9ZJ/YOXDCT/1hZ + YxoeotDBBOIao3Ym/3351somMoQ7Lz6hRWvG0WhDIsCXvth4XSxRkZFXgjWNuhdD + u2ZCis/EwXsqRJPkIPnL + -----END CERTIFICATE----- + EOF + chain = "#{data.fetch('chain')}" + } + } + EXPECTED + ) + end + end + + context "with userpass secret type" do + let(:type) { "userpass" } + let(:data) do + { + "username" => "cpln_username", + "password" => "cpln_password", + "encoding" => "base64" + } + end + + it "generates correct config" do + expect(generated).to eq( + <<~EXPECTED + resource "cpln_secret" "#{base_options.fetch(:name)}" { + name = "#{base_options.fetch(:name)}" + description = "#{base_options.fetch(:description)}" + tags = { + tag1 = "some-tag-1" + tag2 = "some-tag-2" + } + userpass { + username = "#{data.fetch('username')}" + password = "#{data.fetch('password')}" + encoding = "#{data.fetch('encoding')}" + } + } + EXPECTED + ) + end + end + end +end diff --git a/spec/dummy/.controlplane/templates/secret.yml b/spec/dummy/.controlplane/templates/secret.yml new file mode 100644 index 00000000..a1b91218 --- /dev/null +++ b/spec/dummy/.controlplane/templates/secret.yml @@ -0,0 +1,6 @@ +kind: secret +name: dictionary-type-secret +type: dictionary +data: + password: the_password + username: the_user diff --git a/spec/pathces/hash_spec.rb b/spec/pathces/hash_spec.rb new file mode 100644 index 00000000..9a4b20cb --- /dev/null +++ b/spec/pathces/hash_spec.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe Hash do + describe "#underscore_keys" do + subject(:underscored_keys_hash) { hash.underscore_keys } + + context "with string keys" do + let(:hash) { { "camelCaseKey" => "value", "snake_case_key" => "value" } } + + it "returns underscored string keys" do + expect(underscored_keys_hash).to eq("camel_case_key" => "value", "snake_case_key" => "value") + end + end + + context "with symbol keys" do + let(:hash) { { camelCaseKey: "value", snake_case_key: "value" } } + + it "returns underscored symbol keys" do + expect(underscored_keys_hash).to eq(camel_case_key: "value", snake_case_key: "value") + end + end + end +end From e1d0440f8c0a2c39867416b89ebeafcaeff16f96 Mon Sep 17 00:00:00 2001 From: Zakir Dzhamaliddinov Date: Thu, 10 Oct 2024 18:20:36 +0300 Subject: [PATCH 06/16] coderabbitai review fixes --- lib/core/terraform_config/dsl.rb | 13 +- lib/core/terraform_config/secret.rb | 30 ++- spec/core/terraform_config/secret_spec.rb | 229 +++------------------- spec/pathces/hash_spec.rb | 60 +++++- 4 files changed, 119 insertions(+), 213 deletions(-) diff --git a/lib/core/terraform_config/dsl.rb b/lib/core/terraform_config/dsl.rb index e2284e70..1789d76b 100644 --- a/lib/core/terraform_config/dsl.rb +++ b/lib/core/terraform_config/dsl.rb @@ -34,17 +34,14 @@ def argument(name, value, optional: false) private - def tf_value(value) + def tf_value(value, heredoc_delimiter: "EOF", multiline_indent: 2) value = value.to_s if value.is_a?(Symbol) - case value - when String - return value if expression?(value) + return value unless value.is_a?(String) + return value if expression?(value) + return "\"#{value}\"" unless value.include?("\n") - value.include?("\n") ? "EOF\n#{value.strip.indent(2)}\nEOF" : "\"#{value}\"" - else - value - end + "#{heredoc_delimiter}\n#{value.indent(multiline_indent)}\n#{heredoc_delimiter}" end def expression?(value) diff --git a/lib/core/terraform_config/secret.rb b/lib/core/terraform_config/secret.rb index cc757d8c..b32ebb7b 100644 --- a/lib/core/terraform_config/secret.rb +++ b/lib/core/terraform_config/secret.rb @@ -1,7 +1,20 @@ # frozen_string_literal: true module TerraformConfig + # rubocop:disable Metrics/ClassLength class Secret < Base + REQUIRED_DATA_KEYS = { + "aws" => %i[secret_key access_key], + "azure-connector" => %i[url code], + "ecr" => %i[secret_key access_key repos], + "keypair" => %i[secret_key], + "nats-account" => %i[account_id private_key], + "opaque" => %i[payload], + "tls" => %i[key cert], + "userpass" => %i[username password], + "dictionary" => [] + }.freeze + attr_reader :name, :type, :data, :description, :tags def initialize(name:, type:, data:, description: nil, tags: nil) @@ -9,9 +22,9 @@ def initialize(name:, type:, data:, description: nil, tags: nil) @name = name @type = type - @data = data.is_a?(Hash) ? data.underscore_keys.symbolize_keys : data @description = description @tags = tags + @data = prepare_data(type: type, data: data) end def to_tf @@ -26,6 +39,20 @@ def to_tf private + def prepare_data(type:, data:) + return data unless data.is_a?(Hash) + + data.underscore_keys.symbolize_keys.tap do |prepared_data| + validate_required_data_keys!(type: type, data: prepared_data) + end + end + + def validate_required_data_keys!(type:, data:) + required = REQUIRED_DATA_KEYS[type] + missing_keys = required - data.keys + raise ArgumentError, "Missing required data keys for #{type}: #{missing_keys.join(', ')}" if missing_keys.any? + end + def secret_data case type when "azure-sdk", "dictionary", "docker", "gcp" @@ -101,4 +128,5 @@ def userpass_tf end end end + # rubocop:enable Metrics/ClassLength end diff --git a/spec/core/terraform_config/secret_spec.rb b/spec/core/terraform_config/secret_spec.rb index 2320e92b..4dfec4db 100644 --- a/spec/core/terraform_config/secret_spec.rb +++ b/spec/core/terraform_config/secret_spec.rb @@ -20,8 +20,8 @@ let(:type) { "aws" } let(:data) do { - "accessKey" => "AKIAIOSFODNN7EXAMPLE", - "secretKey" => "secret", + "accessKey" => "FAKE_AWS_ACCESS_KEY", + "secretKey" => "FAKE_AWS_SECRET_KEY", "roleArn" => "arn:awskey", "externalId" => "123" } @@ -85,7 +85,7 @@ subscriptionId: "2cd2674e-4f89-4a1f-b420-7a1361b46ef7", tenantId: "292f5674-78b0-488b-9ff8-6d30d77f38d9", clientId: "649746ce-d862-49d5-a5eb-7d5aad90f54e", - clientSecret: "CONFIDENTIAL" + clientSecret: "FAKE_CLIENT_SECRET" }.to_json end @@ -144,7 +144,7 @@ username: "username", password: "password", email: "email", - auth: "CONFIDENTIAL" + auth: "FAKE_AUTH" } } }.to_json @@ -171,8 +171,8 @@ let(:type) { "ecr" } let(:data) do { - "accessKey" => "AKIAIOSFODNN7EXAMPLE", - "secretKey" => "secret", + "accessKey" => "FAKE_ECR_ACCESS_KEY", + "secretKey" => "FAKE_ECR_SECRET_KEY", "repos" => ["015716931765.dkr.ecr.us-west-2.amazonaws.com/cpln-test"], "roleArn" => "arn:awskey", "externalId" => "123" @@ -240,49 +240,8 @@ let(:type) { "keypair" } let(:data) do { - "secretKey" => <<~PRIVATE_KEY, - -----BEGIN RSA PRIVATE KEY----- - Proc-Type: 4,ENCRYPTED - DEK-Info: DES-EDE3-CBC,9A26BB15304B18E7 - - ZdBgMExsvIJEsIFDMQ02xh4nDnhXEGUNu7LiWIZjn9WS6QB2jApyOFOBWmp0lK6L - dIJ+Mb8wMeHtkiKS6ZbYeea8M29kwEejZRnKl1Wq0EFycdwbONtbcbjzF+tQGEBT - gQQgkY7wjDWl8HwjFEA+NUuitzi6uI2xWlQpFdUrmqJAZCbxNFa0aM8nW6jnitvP - 616ps3HjLnWCjoyqS4hWxiWmt+VE3KruPnUVVV7bWlzc6jnoZcSaeqeaoQrNKguH - te2iBIMdY/uldb7Ik2Kxr2+kBRmV4YNkp1EelNi/m39VcoUHJLk1jLldzuINhbi2 - IRqYZe4EEMSYdb3TkSosXa64Sz7jMBz5AxlA0n78FKlB9G5FAxaXcVYNQIlvzCbw - uXPbQd/UYKUuEI1Yn8OmGBN5xcOdgWz8hfyxA2Hq1tmo1XN6snavGe7TKbZd70N+ - 1yFbclB2T1z8fPcLwUZUxOl4g2DoMMHIzCSPaIe/otT8389k4H6hEulLis4lW0p3 - qopL5kdpxmSGgXsX6q6CUFb/0cw9HskNT3zbzKLx2MzjFCo93IB07UxPwkCD2kb1 - sLKMcpTC8a0vLaTVNYgDX7wW/YjBrCokaqk0z1whuN6iSReOtvmu5ybrq1Ksg8UQ - yvCSScM/+muKi+gbEOskQs4Ph3ZLHqAX3/XYoyBcFnPNxVHTIa5Dcju6h5gl1/uY - 6tkRsHDr0Lzy8pd6jjf/ApPf9ypCuxKUO1q8PzPg2E4bmEFxc8zOB2NLvfPgFrUR - 0Sbkapv/6x6nNRw75cu69c5we/atip6wst8J1MSU0fTqb6bZ3TF2pDyNEOkdkvoZ - YZ0r3hUytdT0pImoDLKoyy17mtHLLApzHyIgmR3cqtSt07ncmC5lyEBcZBrQXMa8 - aZeOr8iUWQE/q+4BvoxeKsOD6ttKuFnrgl0rmMnYQsSyLJOPizrU4L1d1HMIKswm - iW+Rg7xlWmQg95m8XEWTjAb3tuNz/tGXC7Qa88HvC7YfyG69yM61oPsT83YnxcBT - C/X67lSFTYguFa3HgDZpjGq7Hc/Q7nhaoqNMEs01O6jbcmrue8IIa2FH1tTwPN0W - D7JefjCQjEghue2mjc0fovOGe9A9jvWf+gJHF3vRtFa67uQiQxge9zUzpHyVNpOj - Ve0y0HvibNTd6TSCArctJpIcwpjO3MTT5LBJ1p/8v4b4+knEKD2c69jumNbKGbWr - Wjq39M/MGNUO5SbZMO3gFCt6fgtXkOktH9pJ9iOQpYKgl7QTe2qQygfWkIm0EZRN - 6EaQdNNKgENWicpKyKQ4BxoY1LYAHFHJ95VisLf3KmmOF5MwajADZQT/yth3gvht - xx21b9iudcgq/CRccSvfIPIWZKi6oaqNIXK+E3DQd40TUopLsBWzacTZn9maSZtW - RyAY1TkRn1qDR2soyhBcihrX5PZ83jnOlM3XTdfF1784g8zB9ooDnK7mUKueH1W3 - hWFADMUF7uaBbo5EZ9sE+dFPzWPJLhu2j67a1iHmByqEvFY64lzq7VwwU/GE8JdA - 85oEkhg1ZEPJp3OYTQfPI/CC/2fc93Exf6wmaXuss8AHehuGcKQniOZmFOKOBprv - -----END RSA PRIVATE KEY----- - PRIVATE_KEY - "publicKey" => <<~PUBLIC_KEY, - -----BEGIN PUBLIC KEY----- - MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwrVyExI0uvRmwCAKFHiv - baAcPMcKJDa6f6TtaVo2p8jyfEhVwDTmR3FUrDDZAjh0Q8G/Up8Ob3+IJafNymCO - BhUKou+8ie7guqsbU9JrT0Zos1k/pd0aVfnAR0EpW3es/7fdkWUszU0uweeEj22m - XMlLplnqqoYOGAhuNMqGsZwBr36Bxq9EeB2O79QsAFDNkPVg7xIaYKn32j69o0Zr - ryYI8xqOYYy5Dw6CX+++YYLYiR/PkLYJTVAsxXeqyltCfb3Iv7vN5HrfoYBhndr3 - NxBPkcIJZeh3Z+QzfJ5U+bB5fP/aOsEk5bPbtLzylj2KnOOM/ZxXJtOcu0xtJLd3 - XwIDAQAB - -----END PUBLIC KEY----- - PUBLIC_KEY + "secretKey" => "<<\nPRIVATE\n_KEY\n_CONTENT\n>>", + "publicKey" => "<<\nPUBLIC\n_KEY\n_CONTENT\n>>", "passphrase" => "cpln" } end @@ -299,47 +258,18 @@ } keypair { secret_key = EOF - -----BEGIN RSA PRIVATE KEY----- - Proc-Type: 4,ENCRYPTED - DEK-Info: DES-EDE3-CBC,9A26BB15304B18E7 - - ZdBgMExsvIJEsIFDMQ02xh4nDnhXEGUNu7LiWIZjn9WS6QB2jApyOFOBWmp0lK6L - dIJ+Mb8wMeHtkiKS6ZbYeea8M29kwEejZRnKl1Wq0EFycdwbONtbcbjzF+tQGEBT - gQQgkY7wjDWl8HwjFEA+NUuitzi6uI2xWlQpFdUrmqJAZCbxNFa0aM8nW6jnitvP - 616ps3HjLnWCjoyqS4hWxiWmt+VE3KruPnUVVV7bWlzc6jnoZcSaeqeaoQrNKguH - te2iBIMdY/uldb7Ik2Kxr2+kBRmV4YNkp1EelNi/m39VcoUHJLk1jLldzuINhbi2 - IRqYZe4EEMSYdb3TkSosXa64Sz7jMBz5AxlA0n78FKlB9G5FAxaXcVYNQIlvzCbw - uXPbQd/UYKUuEI1Yn8OmGBN5xcOdgWz8hfyxA2Hq1tmo1XN6snavGe7TKbZd70N+ - 1yFbclB2T1z8fPcLwUZUxOl4g2DoMMHIzCSPaIe/otT8389k4H6hEulLis4lW0p3 - qopL5kdpxmSGgXsX6q6CUFb/0cw9HskNT3zbzKLx2MzjFCo93IB07UxPwkCD2kb1 - sLKMcpTC8a0vLaTVNYgDX7wW/YjBrCokaqk0z1whuN6iSReOtvmu5ybrq1Ksg8UQ - yvCSScM/+muKi+gbEOskQs4Ph3ZLHqAX3/XYoyBcFnPNxVHTIa5Dcju6h5gl1/uY - 6tkRsHDr0Lzy8pd6jjf/ApPf9ypCuxKUO1q8PzPg2E4bmEFxc8zOB2NLvfPgFrUR - 0Sbkapv/6x6nNRw75cu69c5we/atip6wst8J1MSU0fTqb6bZ3TF2pDyNEOkdkvoZ - YZ0r3hUytdT0pImoDLKoyy17mtHLLApzHyIgmR3cqtSt07ncmC5lyEBcZBrQXMa8 - aZeOr8iUWQE/q+4BvoxeKsOD6ttKuFnrgl0rmMnYQsSyLJOPizrU4L1d1HMIKswm - iW+Rg7xlWmQg95m8XEWTjAb3tuNz/tGXC7Qa88HvC7YfyG69yM61oPsT83YnxcBT - C/X67lSFTYguFa3HgDZpjGq7Hc/Q7nhaoqNMEs01O6jbcmrue8IIa2FH1tTwPN0W - D7JefjCQjEghue2mjc0fovOGe9A9jvWf+gJHF3vRtFa67uQiQxge9zUzpHyVNpOj - Ve0y0HvibNTd6TSCArctJpIcwpjO3MTT5LBJ1p/8v4b4+knEKD2c69jumNbKGbWr - Wjq39M/MGNUO5SbZMO3gFCt6fgtXkOktH9pJ9iOQpYKgl7QTe2qQygfWkIm0EZRN - 6EaQdNNKgENWicpKyKQ4BxoY1LYAHFHJ95VisLf3KmmOF5MwajADZQT/yth3gvht - xx21b9iudcgq/CRccSvfIPIWZKi6oaqNIXK+E3DQd40TUopLsBWzacTZn9maSZtW - RyAY1TkRn1qDR2soyhBcihrX5PZ83jnOlM3XTdfF1784g8zB9ooDnK7mUKueH1W3 - hWFADMUF7uaBbo5EZ9sE+dFPzWPJLhu2j67a1iHmByqEvFY64lzq7VwwU/GE8JdA - 85oEkhg1ZEPJp3OYTQfPI/CC/2fc93Exf6wmaXuss8AHehuGcKQniOZmFOKOBprv - -----END RSA PRIVATE KEY----- + << + PRIVATE + _KEY + _CONTENT + >> EOF public_key = EOF - -----BEGIN PUBLIC KEY----- - MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwrVyExI0uvRmwCAKFHiv - baAcPMcKJDa6f6TtaVo2p8jyfEhVwDTmR3FUrDDZAjh0Q8G/Up8Ob3+IJafNymCO - BhUKou+8ie7guqsbU9JrT0Zos1k/pd0aVfnAR0EpW3es/7fdkWUszU0uweeEj22m - XMlLplnqqoYOGAhuNMqGsZwBr36Bxq9EeB2O79QsAFDNkPVg7xIaYKn32j69o0Zr - ryYI8xqOYYy5Dw6CX+++YYLYiR/PkLYJTVAsxXeqyltCfb3Iv7vN5HrfoYBhndr3 - NxBPkcIJZeh3Z+QzfJ5U+bB5fP/aOsEk5bPbtLzylj2KnOOM/ZxXJtOcu0xtJLd3 - XwIDAQAB - -----END PUBLIC KEY----- + << + PUBLIC + _KEY + _CONTENT + >> EOF passphrase = "#{data.fetch('passphrase')}" } @@ -353,8 +283,8 @@ let(:type) { "nats-account" } let(:data) do { - "accountId" => "AB7JJPKAYKNQOKRKIOS5UCCLALTUAAXCC7FR2QGC4V5UFCAKW4EBIFVZ", - "privateKey" => "SAABRA7OGVHKARDQLUQ6THIABW5PMOHJVPSOPTWZRP4WD5LPVOLGTU6ONQ" + "accountId" => "FAKE_ACCOUNT_ID", + "privateKey" => "FAKE_PRIVATE_KEY" } end @@ -411,62 +341,8 @@ let(:type) { "tls" } let(:data) do { - "key" => <<~PRIVATE_KEY, - -----BEGIN PRIVATE KEY----- - MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDBzN2jRf9ouoF4 - XG0eUxcc4f1sP8vhW1fQXjun3cl0RsN4jRdOyTKWcls1yAxlOkwFod8d6HND9OvN - rsl7U4iJIEcJL6vTqHY7jTGXQkd9yPONMpMXYE8Dsiqtk0deoOab7fafYcvq1iWn - pvg157mJ/u9qdyU+1h8DncES30FkPsG8TsIsjx94JkTJeMmEJxtws4dfuoCk88IN - bBHLjxBQgwTu0vgMxN34b5z+esHraetDN2fqxSoTOeIlyFzeS+kwG3GK4I1hUQBi - L2TeDrnEY6qP/ZoGuyyVnsT/6pHY/BTAcH3Rgeqose7mqBT+7zlxDfHYHceuNB/l - jq0e1j69AgMBAAECggEAPGhrPZV4A2D/MlE9AhLMRYh7wd4w4tHiEWUOG0kank/g - Zhc0iK5WQmbq31y34GXHhInsThpCs5AIYFh3HSXwjS2udsKRQKxmDjH4nzldp2uX - 3w9Aoiy29GP4wZoCyRBGUZxfH1cQhOazXgrBm6vbPZRldD4nMer0R+BIamWEsIYD - YjDj1pT0noLUSeqoLmGxSQ4DNIBQVZB/T8ziMcEzl6bhprT0QrapJSyD2CtA8tH1 - Z8cyhmyE0CUvSkV4K2ecvVukWBJvrAYc6euPAnkS5LJrQotI5+3jJO2QawOlL6Uw - rFWBpgBrCgbzquMRpDCQ/J9/GDYaZjim4YdonboBgQKBgQD7jx3CVnG4LDz198am - spmPwKCW1ke6PhlG7zf3YR00xg9vPBYiy4obb1Jg6em1wr+iZ0dEt8fimeZXewBf - LzlrR8T1Or0eLzfbn+GlLIKGKhn2pKB/i1iolkfIonchqXRk9WNx+PzjgUqiYWRC - /1tH2BsODlVrzKL2lnbWKNIFdQKBgQDFOLedpMeYemLhrsU1TXGt1xTxAbWvOCyt - vig/huyz4SQENXyu3ImPzxIxpTHxKhUaXo/qFXn0jhqnf0LfWI4nbQUbkivb5BPr - KY9aj7XwwsY4MXW5C12Qi0lIwHOWCmfzvyS7TCMqnQb7sT4Mjmm4ydEbiI1TjlFJ - D/RFxzcDKQKBgQCehPcJyZNrrWTU0sh5rz4ZWhdYNbuJXyxqiMBJwQa4hL6hJ8oD - LyPeWe4daAmAIjLEUjSU1wK8hqKiKb54PLgAJH+20MbvyG14lm2Iul2d0dX+mIsT - FGpQAjNF+Sr9KV1RaVi7L12ct5KidKDLn0KUKVgTKXEmtxNSNEq6dYqzKQKBgDI8 - zljzvnwSwNloIYgAYDK+FPGHU/Z8QrVHOQ1lmyn+8aO41DfeqZPeVW4b/GrII3QC - HnqsWdJ32EZOXoRyFFPqq2BojY+Hu6MthPy2msvncYKi5q/qOz00nchQbaEMqYon - aH3lWRfjxAGdFocwR7HwhrmSwR1FpWMNE1Yq9tJxAoGBANc0nZSy5ZlTiMWdRrTt - gFc9N/jz8OL6qLrJtX2Axyv7Vv8H/gbDg4olLR+Io38M0S1WwEHsaIJLIvJ6msjl - /LlseAW6oiO6jzhWEr0VQSLkuJn45hG/uy7t19SDuNR7W5NuEr0YbWd6fZEpR7RR - S1hFKnRRcrVqA+HjWnZ//BGi - -----END PRIVATE KEY----- - PRIVATE_KEY - "cert" => <<~CERTIFICATE, - -----BEGIN CERTIFICATE----- - MIID+zCCAuOgAwIBAgIUEwBv3WQkP7dIiEIxyj+Wi1STz7QwDQYJKoZIhvcNAQEL - BQAwgYwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRQwEgYDVQQH - DAtMb3MgQW5nZWxlczENMAsGA1UECgwEQ1BMTjERMA8GA1UECwwIQ1BMTi1PUkcx - EDAOBgNVBAMMB2NwbG4uaW8xHjAcBgkqhkiG9w0BCQEWD3N1cHBvcnRAY3Bsbi5p - bzAeFw0yMDEwMTQxNzI4MDhaFw0zMDEwMTIxNzI4MDhaMIGMMQswCQYDVQQGEwJV - UzETMBEGA1UECAwKQ2FsaWZvcm5pYTEUMBIGA1UEBwwLTG9zIEFuZ2VsZXMxDTAL - BgNVBAoMBENQTE4xETAPBgNVBAsMCENQTE4tT1JHMRAwDgYDVQQDDAdjcGxuLmlv - MR4wHAYJKoZIhvcNAQkBFg9zdXBwb3J0QGNwbG4uaW8wggEiMA0GCSqGSIb3DQEB - AQUAA4IBDwAwggEKAoIBAQDBzN2jRf9ouoF4XG0eUxcc4f1sP8vhW1fQXjun3cl0 - RsN4jRdOyTKWcls1yAxlOkwFod8d6HND9OvNrsl7U4iJIEcJL6vTqHY7jTGXQkd9 - yPONMpMXYE8Dsiqtk0deoOab7fafYcvq1iWnpvg157mJ/u9qdyU+1h8DncES30Fk - PsG8TsIsjx94JkTJeMmEJxtws4dfuoCk88INbBHLjxBQgwTu0vgMxN34b5z+esHr - aetDN2fqxSoTOeIlyFzeS+kwG3GK4I1hUQBiL2TeDrnEY6qP/ZoGuyyVnsT/6pHY - /BTAcH3Rgeqose7mqBT+7zlxDfHYHceuNB/ljq0e1j69AgMBAAGjUzBRMB0GA1Ud - DgQWBBRxncC/8RRio/S9Ly8tKFS7WnTcNTAfBgNVHSMEGDAWgBRxncC/8RRio/S9 - Ly8tKFS7WnTcNTAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAr - sDZQj4K47fW6JkJbxlzZ1hd7IX6cQhI/DRIdTGR1u0kM1RtZoS0UtV5qsYV/g/S4 - ChuB/aIARyTWvHKDhcT3bRGHLnoZJ8pLlQh4nEfO07SRhyeNiO4qmWM9az0nP5qD - wAXpLpmYIairzAgY7QXbk5wXbTrXli3mz14VaNoqN4s7iyLtHn5TGAXc12aMwo7M - 5yn/RGxoWQoJqSQKc9nf909cR81AVCdG1dFcp7u8Ud1pTtlmiU9ZJ/YOXDCT/1hZ - YxoeotDBBOIao3Ym/3351somMoQ7Lz6hRWvG0WhDIsCXvth4XSxRkZFXgjWNuhdD - u2ZCis/EwXsqRJPkIPnL - -----END CERTIFICATE----- - CERTIFICATE + "key" => "<<\nPRIVATE\n_KEY\n_CONTENT\n>>", + "cert" => "<<\nCERTIFICATE\n_CONTENT\n>>", "chain" => "None. The above key and certificate were self-signed." } end @@ -483,60 +359,17 @@ } tls { key = EOF - -----BEGIN PRIVATE KEY----- - MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDBzN2jRf9ouoF4 - XG0eUxcc4f1sP8vhW1fQXjun3cl0RsN4jRdOyTKWcls1yAxlOkwFod8d6HND9OvN - rsl7U4iJIEcJL6vTqHY7jTGXQkd9yPONMpMXYE8Dsiqtk0deoOab7fafYcvq1iWn - pvg157mJ/u9qdyU+1h8DncES30FkPsG8TsIsjx94JkTJeMmEJxtws4dfuoCk88IN - bBHLjxBQgwTu0vgMxN34b5z+esHraetDN2fqxSoTOeIlyFzeS+kwG3GK4I1hUQBi - L2TeDrnEY6qP/ZoGuyyVnsT/6pHY/BTAcH3Rgeqose7mqBT+7zlxDfHYHceuNB/l - jq0e1j69AgMBAAECggEAPGhrPZV4A2D/MlE9AhLMRYh7wd4w4tHiEWUOG0kank/g - Zhc0iK5WQmbq31y34GXHhInsThpCs5AIYFh3HSXwjS2udsKRQKxmDjH4nzldp2uX - 3w9Aoiy29GP4wZoCyRBGUZxfH1cQhOazXgrBm6vbPZRldD4nMer0R+BIamWEsIYD - YjDj1pT0noLUSeqoLmGxSQ4DNIBQVZB/T8ziMcEzl6bhprT0QrapJSyD2CtA8tH1 - Z8cyhmyE0CUvSkV4K2ecvVukWBJvrAYc6euPAnkS5LJrQotI5+3jJO2QawOlL6Uw - rFWBpgBrCgbzquMRpDCQ/J9/GDYaZjim4YdonboBgQKBgQD7jx3CVnG4LDz198am - spmPwKCW1ke6PhlG7zf3YR00xg9vPBYiy4obb1Jg6em1wr+iZ0dEt8fimeZXewBf - LzlrR8T1Or0eLzfbn+GlLIKGKhn2pKB/i1iolkfIonchqXRk9WNx+PzjgUqiYWRC - /1tH2BsODlVrzKL2lnbWKNIFdQKBgQDFOLedpMeYemLhrsU1TXGt1xTxAbWvOCyt - vig/huyz4SQENXyu3ImPzxIxpTHxKhUaXo/qFXn0jhqnf0LfWI4nbQUbkivb5BPr - KY9aj7XwwsY4MXW5C12Qi0lIwHOWCmfzvyS7TCMqnQb7sT4Mjmm4ydEbiI1TjlFJ - D/RFxzcDKQKBgQCehPcJyZNrrWTU0sh5rz4ZWhdYNbuJXyxqiMBJwQa4hL6hJ8oD - LyPeWe4daAmAIjLEUjSU1wK8hqKiKb54PLgAJH+20MbvyG14lm2Iul2d0dX+mIsT - FGpQAjNF+Sr9KV1RaVi7L12ct5KidKDLn0KUKVgTKXEmtxNSNEq6dYqzKQKBgDI8 - zljzvnwSwNloIYgAYDK+FPGHU/Z8QrVHOQ1lmyn+8aO41DfeqZPeVW4b/GrII3QC - HnqsWdJ32EZOXoRyFFPqq2BojY+Hu6MthPy2msvncYKi5q/qOz00nchQbaEMqYon - aH3lWRfjxAGdFocwR7HwhrmSwR1FpWMNE1Yq9tJxAoGBANc0nZSy5ZlTiMWdRrTt - gFc9N/jz8OL6qLrJtX2Axyv7Vv8H/gbDg4olLR+Io38M0S1WwEHsaIJLIvJ6msjl - /LlseAW6oiO6jzhWEr0VQSLkuJn45hG/uy7t19SDuNR7W5NuEr0YbWd6fZEpR7RR - S1hFKnRRcrVqA+HjWnZ//BGi - -----END PRIVATE KEY----- + << + PRIVATE + _KEY + _CONTENT + >> EOF cert = EOF - -----BEGIN CERTIFICATE----- - MIID+zCCAuOgAwIBAgIUEwBv3WQkP7dIiEIxyj+Wi1STz7QwDQYJKoZIhvcNAQEL - BQAwgYwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRQwEgYDVQQH - DAtMb3MgQW5nZWxlczENMAsGA1UECgwEQ1BMTjERMA8GA1UECwwIQ1BMTi1PUkcx - EDAOBgNVBAMMB2NwbG4uaW8xHjAcBgkqhkiG9w0BCQEWD3N1cHBvcnRAY3Bsbi5p - bzAeFw0yMDEwMTQxNzI4MDhaFw0zMDEwMTIxNzI4MDhaMIGMMQswCQYDVQQGEwJV - UzETMBEGA1UECAwKQ2FsaWZvcm5pYTEUMBIGA1UEBwwLTG9zIEFuZ2VsZXMxDTAL - BgNVBAoMBENQTE4xETAPBgNVBAsMCENQTE4tT1JHMRAwDgYDVQQDDAdjcGxuLmlv - MR4wHAYJKoZIhvcNAQkBFg9zdXBwb3J0QGNwbG4uaW8wggEiMA0GCSqGSIb3DQEB - AQUAA4IBDwAwggEKAoIBAQDBzN2jRf9ouoF4XG0eUxcc4f1sP8vhW1fQXjun3cl0 - RsN4jRdOyTKWcls1yAxlOkwFod8d6HND9OvNrsl7U4iJIEcJL6vTqHY7jTGXQkd9 - yPONMpMXYE8Dsiqtk0deoOab7fafYcvq1iWnpvg157mJ/u9qdyU+1h8DncES30Fk - PsG8TsIsjx94JkTJeMmEJxtws4dfuoCk88INbBHLjxBQgwTu0vgMxN34b5z+esHr - aetDN2fqxSoTOeIlyFzeS+kwG3GK4I1hUQBiL2TeDrnEY6qP/ZoGuyyVnsT/6pHY - /BTAcH3Rgeqose7mqBT+7zlxDfHYHceuNB/ljq0e1j69AgMBAAGjUzBRMB0GA1Ud - DgQWBBRxncC/8RRio/S9Ly8tKFS7WnTcNTAfBgNVHSMEGDAWgBRxncC/8RRio/S9 - Ly8tKFS7WnTcNTAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAr - sDZQj4K47fW6JkJbxlzZ1hd7IX6cQhI/DRIdTGR1u0kM1RtZoS0UtV5qsYV/g/S4 - ChuB/aIARyTWvHKDhcT3bRGHLnoZJ8pLlQh4nEfO07SRhyeNiO4qmWM9az0nP5qD - wAXpLpmYIairzAgY7QXbk5wXbTrXli3mz14VaNoqN4s7iyLtHn5TGAXc12aMwo7M - 5yn/RGxoWQoJqSQKc9nf909cR81AVCdG1dFcp7u8Ud1pTtlmiU9ZJ/YOXDCT/1hZ - YxoeotDBBOIao3Ym/3351somMoQ7Lz6hRWvG0WhDIsCXvth4XSxRkZFXgjWNuhdD - u2ZCis/EwXsqRJPkIPnL - -----END CERTIFICATE----- + << + CERTIFICATE + _CONTENT + >> EOF chain = "#{data.fetch('chain')}" } diff --git a/spec/pathces/hash_spec.rb b/spec/pathces/hash_spec.rb index 9a4b20cb..6c1216fb 100644 --- a/spec/pathces/hash_spec.rb +++ b/spec/pathces/hash_spec.rb @@ -6,19 +6,67 @@ describe "#underscore_keys" do subject(:underscored_keys_hash) { hash.underscore_keys } + context "with an empty hash" do + let(:hash) { {} } + + it "returns an empty hash" do + expect(underscored_keys_hash).to eq({}) + end + end + + context "with a nested hash" do + let(:hash) { { "outerCamelCase" => { innerCamelCase: "value" } } } + + it "transforms keys only at top level" do + expect(underscored_keys_hash).to eq("outer_camel_case" => { innerCamelCase: "value" }) + end + end + + context "with already underscored keys" do + let(:hash) { { "already_underscored" => "value" } } + + it "leaves underscored keys unchanged" do + expect(underscored_keys_hash).to eq("already_underscored" => "value") + end + end + + context "with keys containing numbers or special characters" do + let(:hash) { { "camelCase123" => "value1", "special@CaseKey" => "value2" } } + + it "correctly transforms keys with numbers or special characters" do + expect(underscored_keys_hash).to eq("camel_case123" => "value1", "special@case_key" => "value2") + end + end + context "with string keys" do - let(:hash) { { "camelCaseKey" => "value", "snake_case_key" => "value" } } + let(:hash) { { "camelCaseKey" => "value1", "snake_case_key" => "value2", "XMLHttpRequest" => "value3" } } + + it "transforms camelCase keys to snake_case" do + expect(underscored_keys_hash["camel_case_key"]).to eq("value1") + end - it "returns underscored string keys" do - expect(underscored_keys_hash).to eq("camel_case_key" => "value", "snake_case_key" => "value") + it "leaves snake_case keys unchanged" do + expect(underscored_keys_hash["snake_case_key"]).to eq("value2") + end + + it "correctly handles keys with multiple uppercase letters" do + expect(underscored_keys_hash["xml_http_request"]).to eq("value3") end end context "with symbol keys" do - let(:hash) { { camelCaseKey: "value", snake_case_key: "value" } } + let(:hash) { { camelCaseKey: "value1", snake_case_key: "value2", XMLHttpRequest: "value3" } } + + it "transforms camelCase symbol keys to snake_case" do + expect(underscored_keys_hash[:camel_case_key]).to eq("value1") + end + + it "leaves snake_case symbol keys unchanged" do + expect(underscored_keys_hash[:snake_case_key]).to eq("value2") + end - it "returns underscored symbol keys" do - expect(underscored_keys_hash).to eq(camel_case_key: "value", snake_case_key: "value") + it "correctly handles symbol keys with multiple uppercase letters" do + expect(underscored_keys_hash[:xml_http_request]).to eq("value3") end end end From a58bcf9d538cc3caabb324b1d0df4372561893b7 Mon Sep 17 00:00:00 2001 From: Zakir Dzhamaliddinov Date: Thu, 10 Oct 2024 18:39:40 +0300 Subject: [PATCH 07/16] coderabbitai review fixes #2 --- lib/core/terraform_config/secret.rb | 32 +++++++++++------------ spec/core/terraform_config/secret_spec.rb | 28 ++++++++++---------- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/lib/core/terraform_config/secret.rb b/lib/core/terraform_config/secret.rb index b32ebb7b..cf203013 100644 --- a/lib/core/terraform_config/secret.rb +++ b/lib/core/terraform_config/secret.rb @@ -65,12 +65,11 @@ def secret_data end def aws_tf - block :aws do - argument :secret_key, data.fetch(:secret_key) - argument :access_key, data.fetch(:access_key) - argument :role_arn, data.fetch(:role_arn, nil), optional: true - argument :external_id, data.fetch(:external_id, nil), optional: true - end + aws_based_tf(:aws) + end + + def ecr_tf + aws_based_tf(:ecr, repos: data.fetch(:repos)) end def azure_connector_tf @@ -80,16 +79,6 @@ def azure_connector_tf end end - def ecr_tf - block :ecr do - argument :secret_key, data.fetch(:secret_key) - argument :access_key, data.fetch(:access_key) - argument :repos, data.fetch(:repos) - argument :role_arn, data.fetch(:role_arn, nil), optional: true - argument :external_id, data.fetch(:external_id, nil), optional: true - end - end - def keypair_tf block :keypair do argument :secret_key, data.fetch(:secret_key) @@ -127,6 +116,17 @@ def userpass_tf argument :encoding, data.fetch(:encoding, nil), optional: true end end + + def aws_based_tf(name, **kwargs) + block name do + argument :secret_key, data.fetch(:secret_key) + argument :access_key, data.fetch(:access_key) + argument :role_arn, data.fetch(:role_arn, nil), optional: true + argument :external_id, data.fetch(:external_id, nil), optional: true + + kwargs.each { |key, value| argument key, value } + end + end end # rubocop:enable Metrics/ClassLength end diff --git a/spec/core/terraform_config/secret_spec.rb b/spec/core/terraform_config/secret_spec.rb index 4dfec4db..673a1c0f 100644 --- a/spec/core/terraform_config/secret_spec.rb +++ b/spec/core/terraform_config/secret_spec.rb @@ -82,9 +82,9 @@ let(:type) { "azure-sdk" } let(:data) do { - subscriptionId: "2cd2674e-4f89-4a1f-b420-7a1361b46ef7", - tenantId: "292f5674-78b0-488b-9ff8-6d30d77f38d9", - clientId: "649746ce-d862-49d5-a5eb-7d5aad90f54e", + subscriptionId: "FAKE_SUBSCRIPTION_ID", + tenantId: "FAKE_TENANT_ID", + clientId: "FAKE_CLIENT_ID", clientSecret: "FAKE_CLIENT_SECRET" }.to_json end @@ -173,7 +173,7 @@ { "accessKey" => "FAKE_ECR_ACCESS_KEY", "secretKey" => "FAKE_ECR_SECRET_KEY", - "repos" => ["015716931765.dkr.ecr.us-west-2.amazonaws.com/cpln-test"], + "repos" => [".dkr.ecr..amazonaws.com/cpln-test"], "roleArn" => "arn:awskey", "externalId" => "123" } @@ -192,9 +192,9 @@ ecr { secret_key = "#{data.fetch('secretKey')}" access_key = "#{data.fetch('accessKey')}" - repos = #{data.fetch('repos')} role_arn = "#{data.fetch('roleArn')}" external_id = "#{data.fetch('externalId')}" + repos = #{data.fetch('repos')} } } EXPECTED @@ -207,15 +207,15 @@ let(:data) do { "type" => "gcp", - "project_id" => "cpln12345", - "private_key_id" => "pvt_key", - "private_key" => "key", - "client_email" => "support@cpln.io", - "client_id" => "12744", - "auth_uri" => "cloud.google.com", - "token_uri" => "token.cloud.google.com", - "auth_provider_x509_cert_url" => "cert.google.com", - "client_x509_cert_url" => "cert.google.com" + "project_id" => "FAKE_PROJECT_ID", + "private_key_id" => "FAKE_PRIVATE_KEY_ID", + "private_key" => "FAKE_PRIVATE_KEY", + "client_email" => "fake-email@example.com", + "client_id" => "FAKE_CLIENT_ID", + "auth_uri" => "https://auth-uri.example.com", + "token_uri" => "https://token-uri.example.com", + "auth_provider_x509_cert_url" => "https://auth-provider-cert-url.example.com", + "client_x509_cert_url" => "https://client-cert-url.example.com" }.to_json end From 4e391a288368ffbd919a9f85399c8e6c21deedd1 Mon Sep 17 00:00:00 2001 From: Zakir Dzhamaliddinov Date: Wed, 16 Oct 2024 11:35:24 +0300 Subject: [PATCH 08/16] Generate Terraform config from policy templates --- .rubocop.yml | 4 + lib/command/terraform/generate.rb | 2 +- lib/core/terraform_config/generator.rb | 52 +++-- lib/core/terraform_config/gvc.rb | 2 +- lib/core/terraform_config/policy.rb | 146 ++++++++++++++ lib/core/terraform_config/secret.rb | 2 +- lib/patches/hash.rb | 29 ++- spec/command/terraform/generate_spec.rb | 2 +- spec/core/terraform_config/generator_spec.rb | 52 +++++ spec/core/terraform_config/policy_spec.rb | 179 ++++++++++++++++++ spec/dummy/.controlplane/templates/policy.yml | 16 ++ spec/pathces/hash_spec.rb | 26 +-- 12 files changed, 480 insertions(+), 32 deletions(-) create mode 100644 lib/core/terraform_config/policy.rb create mode 100644 spec/core/terraform_config/policy_spec.rb create mode 100644 spec/dummy/.controlplane/templates/policy.yml diff --git a/.rubocop.yml b/.rubocop.yml index 29d37195..f1a81d5c 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -20,3 +20,7 @@ RSpec/ExampleLength: RSpec/MultipleExpectations: Enabled: false + +RSpec/NestedGroups: + Enabled: true + Max: 5 diff --git a/lib/command/terraform/generate.rb b/lib/command/terraform/generate.rb index e5b5c83b..21ebbac8 100644 --- a/lib/command/terraform/generate.rb +++ b/lib/command/terraform/generate.rb @@ -47,7 +47,7 @@ def generate_app_config(app) generator = TerraformConfig::Generator.new(config: config, template: template) # TODO: Delete line below after all template kinds are supported - next unless %w[gvc identity secret].include?(template["kind"]) + next unless %w[gvc identity secret policy].include?(template["kind"]) File.write(terraform_app_dir.join(generator.filename), generator.tf_config.to_tf, mode: "a+") end diff --git a/lib/core/terraform_config/generator.rb b/lib/core/terraform_config/generator.rb index a5f9f42e..4e207c0c 100644 --- a/lib/core/terraform_config/generator.rb +++ b/lib/core/terraform_config/generator.rb @@ -9,6 +9,7 @@ def initialize(config:, template:) @template = template end + # rubocop:disable Metrics/MethodLength def filename case template["kind"] when "gvc" @@ -17,22 +18,19 @@ def filename "secrets.tf" when "identity" "identities.tf" + when "policy" + "policies.tf" else raise "Unsupported template kind - #{template['kind']}" end end + # rubocop:enable Metrics/MethodLength def tf_config - case template["kind"] - when "gvc" - gvc_config - when "identity" - identity_config - when "secret" - secret_config - else - raise "Unsupported template kind - #{template['kind']}" - end + method_name = :"#{template['kind']}_config" + raise "Unsupported template kind - #{template['kind']}" unless self.class.private_method_defined?(method_name) + + send(method_name) end private @@ -61,7 +59,7 @@ def gvc_config def identity_config TerraformConfig::Identity.new( - gvc: "cpln_gvc.#{config.app}.name", # GVC name matches application name + gvc: gvc, name: template["name"], description: template["description"], tags: template["tags"] @@ -78,6 +76,38 @@ def secret_config ) end + # rubocop:disable Metrics/MethodLength + def policy_config + # //secret/secret-name -> secret-name + target_links = template["targetLinks"]&.map do |target_link| + target_link.split("/").last + end + + # //group/viewers -> group/viewers + bindings = template["bindings"]&.map do |data| + principal_links = data.delete("principalLinks")&.map { |link| link.delete_prefix("//") } + data.merge("principalLinks" => principal_links) + end + + TerraformConfig::Policy.new( + name: template["name"], + description: template["description"], + tags: template["tags"], + target: template["target"], + target_kind: template["targetKind"], + target_query: template["targetQuery"], + target_links: target_links, + gvc: gvc, + bindings: bindings + ) + end + # rubocop:enable Metrics/MethodLength + + # GVC name matches application name + def gvc + "cpln_gvc.#{config.app}.name" + end + def env template.dig("spec", "env").to_h { |env_var| [env_var["name"], env_var["value"]] } end diff --git a/lib/core/terraform_config/gvc.rb b/lib/core/terraform_config/gvc.rb index 3142562c..355d38a4 100644 --- a/lib/core/terraform_config/gvc.rb +++ b/lib/core/terraform_config/gvc.rb @@ -24,7 +24,7 @@ def initialize( @locations = locations @pull_secrets = pull_secrets @env = env - @load_balancer = load_balancer&.underscore_keys&.symbolize_keys + @load_balancer = load_balancer&.deep_underscore_keys&.deep_symbolize_keys end # rubocop:enable Metrics/ParameterLists diff --git a/lib/core/terraform_config/policy.rb b/lib/core/terraform_config/policy.rb new file mode 100644 index 00000000..c8156293 --- /dev/null +++ b/lib/core/terraform_config/policy.rb @@ -0,0 +1,146 @@ +# frozen_string_literal: true + +module TerraformConfig + # rubocop:disable Metrics/ClassLength + class Policy < Base + TARGET_KINDS = %w[ + agent auditctx cloudaccount domain group gvc identity image ipset kubernetes location + org policy quota secret serviceaccount task user volumeset workload + ].freeze + + GVC_REQUIERD_TARGET_KINDS = %w[identity workload volumeset].freeze + + attr_reader :name, :description, :tags, :target_kind, :gvc, :target, :target_links, :target_query, :bindings + + # rubocop:disable Metrics/ParameterLists, Metrics/MethodLength + def initialize( + name:, + description: nil, + tags: nil, + target_kind: nil, + gvc: nil, + target: nil, + target_links: nil, + target_query: nil, + bindings: nil + ) + super() + + @name = name + @description = description + @tags = tags + + @target_kind = target_kind + validate_target_kind! + + @gvc = gvc + validate_gvc! + + @target = target + @target_links = target_links + + @target_query = target_query&.deep_underscore_keys&.deep_symbolize_keys + @bindings = bindings&.map { |data| data.deep_underscore_keys.deep_symbolize_keys } + end + # rubocop:enable Metrics/ParameterLists, Metrics/MethodLength + + def to_tf + block :resource, :cpln_policy, name do + argument :name, name + + %i[description tags target_kind gvc target target_links].each do |arg_name| + argument arg_name, send(arg_name), optional: true + end + + bindings_tf + target_query_tf + end + end + + private + + def validate_target_kind! + return if target_kind.nil? || TARGET_KINDS.include?(target_kind.to_s) + + raise ArgumentError, "Invalid target kind given - #{target_kind}" + end + + def validate_gvc! + return unless GVC_REQUIERD_TARGET_KINDS.include?(target_kind.to_s) && gvc.nil? + + raise ArgumentError, "`gvc` is required for `#{target_kind}` target kind" + end + + def bindings_tf + return if bindings.nil? + + bindings.each do |binding_data| + block :binding do + argument :permissions, binding_data.fetch(:permissions, nil), optional: true + argument :principal_links, binding_data.fetch(:principal_links, nil), optional: true + end + end + end + + def target_query_tf + return if target_query.nil? + + fetch_type = target_query.fetch(:fetch, nil) + validate_fetch_type!(fetch_type) if fetch_type + + block :target_query do + argument :fetch, fetch_type, optional: true + target_query_spec_tf + end + end + + def validate_fetch_type!(fetch_type) + return if %w[links items].include?(fetch_type.to_s) + + raise ArgumentError, "Invalid fetch type - #{fetch_type}. Should be either `links` or `items`" + end + + def target_query_spec_tf + spec = target_query.fetch(:spec, nil) + return if spec.nil? + + match_type = spec.fetch(:match, nil) + validate_match_type!(match_type) if match_type + + block :spec do + argument :match, match_type, optional: true + + target_query_spec_terms_tf(spec) + end + end + + def validate_match_type!(match_type) + return if %w[all any none].include?(match_type.to_s) + + raise ArgumentError, "Invalid match type - #{match_type}. Should be either `all`, `any` or `none`" + end + + def target_query_spec_terms_tf(spec) + terms = spec.fetch(:terms, nil) + return if terms.nil? + + terms.each do |term| + validate_term!(term) + + block :terms do + %i[op property rel tag value].each do |arg_name| + argument arg_name, term.fetch(arg_name, nil), optional: true + end + end + end + end + + def validate_term!(term) + return unless (%i[property rel tag] & term.keys).count > 1 + + raise ArgumentError, + "`target_query.spec.terms` can contain only one of the following attributes: `property`, `rel`, `tag`." + end + end + # rubocop:enable Metrics/ClassLength +end diff --git a/lib/core/terraform_config/secret.rb b/lib/core/terraform_config/secret.rb index cf203013..ff4c2259 100644 --- a/lib/core/terraform_config/secret.rb +++ b/lib/core/terraform_config/secret.rb @@ -42,7 +42,7 @@ def to_tf def prepare_data(type:, data:) return data unless data.is_a?(Hash) - data.underscore_keys.symbolize_keys.tap do |prepared_data| + data.deep_underscore_keys.deep_symbolize_keys.tap do |prepared_data| validate_required_data_keys!(type: type, data: prepared_data) end end diff --git a/lib/patches/hash.rb b/lib/patches/hash.rb index 80e2b0a4..5d6afea0 100644 --- a/lib/patches/hash.rb +++ b/lib/patches/hash.rb @@ -2,16 +2,37 @@ class Hash # Copied from Rails - def symbolize_keys - transform_keys { |key| key.to_sym rescue key } # rubocop:disable Style/RescueModifier + def deep_symbolize_keys + deep_transform_keys { |key| key.to_sym rescue key } # rubocop:disable Style/RescueModifier end - def underscore_keys - transform_keys do |key| + def deep_underscore_keys + deep_transform_keys do |key| underscored = key.to_s.underscore key.is_a?(Symbol) ? underscored.to_sym : underscored rescue StandardError key end end + + private + + # Copied from Rails + def deep_transform_keys(&block) + deep_transform_keys_in_object(self, &block) + end + + # Copied from Rails + def deep_transform_keys_in_object(object, &block) + case object + when Hash + object.each_with_object(self.class.new) do |(key, value), result| + result[yield(key)] = deep_transform_keys_in_object(value, &block) + end + when Array + object.map { |e| deep_transform_keys_in_object(e, &block) } + else + object + end + end end diff --git a/spec/command/terraform/generate_spec.rb b/spec/command/terraform/generate_spec.rb index 6d049129..917eab18 100644 --- a/spec/command/terraform/generate_spec.rb +++ b/spec/command/terraform/generate_spec.rb @@ -99,7 +99,7 @@ def common_config_files end def app_config_files - %w[gvc.tf identities.tf secrets.tf].map do |config_file_path| + %w[gvc.tf identities.tf secrets.tf policies.tf].map do |config_file_path| TERRAFORM_CONFIG_DIR_PATH.join(app, config_file_path) end end diff --git a/spec/core/terraform_config/generator_spec.rb b/spec/core/terraform_config/generator_spec.rb index 3d8ad8df..3f6cbdc1 100644 --- a/spec/core/terraform_config/generator_spec.rb +++ b/spec/core/terraform_config/generator_spec.rb @@ -123,4 +123,56 @@ expect(tf_filename).to eq("secrets.tf") end end + + context "when template's kind is policy" do + let(:template) do + { + "kind" => "policy", + "name" => "policy-name", + "description" => "policy description", + "tags" => { "tag1" => "tag1_value", "tag2" => "tag2_value" }, + "target" => "all", + "targetKind" => "secret", + "targetLinks" => [ + "//secret/postgres-poc-credentials", + "//secret/postgres-poc-entrypoint-script" + ], + "bindings" => [ + { + "permissions" => %w[reveal view use], + "principalLinks" => %W[//gvc/#{config.app}/identity/postgres-poc-identity] + }, + { + "permissions" => %w[view], + "principalLinks" => %w[user/fake-user@fake-email.com] + } + ] + } + end + + it "generates correct terraform config and filename for it", :aggregate_failures do + tf_config = generator.tf_config + expect(tf_config).to be_an_instance_of(TerraformConfig::Policy) + + expect(tf_config.name).to eq("policy-name") + expect(tf_config.description).to eq("policy description") + expect(tf_config.tags).to eq("tag1" => "tag1_value", "tag2" => "tag2_value") + expect(tf_config.target).to eq("all") + expect(tf_config.target_kind).to eq("secret") + expect(tf_config.target_links).to eq(%w[postgres-poc-credentials postgres-poc-entrypoint-script]) + expect(tf_config.bindings).to contain_exactly( + { + permissions: %w[reveal view use], + principal_links: %W[gvc/#{config.app}/identity/postgres-poc-identity] + }, + { + permissions: %w[view], + principal_links: %w[user/fake-user@fake-email.com] + } + ) + + tf_filename = generator.filename + expect(tf_filename).to eq("policies.tf") + end + end end diff --git a/spec/core/terraform_config/policy_spec.rb b/spec/core/terraform_config/policy_spec.rb new file mode 100644 index 00000000..cac57eed --- /dev/null +++ b/spec/core/terraform_config/policy_spec.rb @@ -0,0 +1,179 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe TerraformConfig::Policy do + let(:config) { described_class.new(**base_options.merge(extra_options)) } + + describe "#to_tf" do + subject(:generated) { config.to_tf } + + let(:base_options) do + { + name: "policy-name", + description: "policy description", + tags: { "tag1" => "true", "tag2" => "false" }, + target_links: ["secret/postgres-poc-credentials", "secret/postgres-poc-entrypoint-script"], + bindings: [ + { + "permissions" => %w[view], + "principalLinks" => [ + "user/fake-user@fake-email.com", + "serviceaccount/FAKE_SERVICE_ACCOUNT_NAME", + "group/FAKE-GROUP" + ] + }, + { + "permissions" => %w[view edit], + "principalLinks" => ["user/fake-admin-user@fake-email.com"] + } + ] + } + end + + let(:extra_options) { {} } + + context "with target query" do + let(:extra_options) do + { + target_kind: "agent", + target_query: { + "kind" => "agent", + "fetch" => fetch_type, + "spec" => { + "match" => match_type, + "terms" => [ + { + "op" => "=", + "tag" => "tag_name", + "value" => "some_tag" + } + ] + } + } + } + end + + let(:fetch_type) { "items" } + let(:match_type) { "all" } + + it "generates correct config" do + expect(generated).to eq( + <<~EXPECTED + resource "cpln_policy" "policy-name" { + name = "policy-name" + description = "policy description" + tags = { + tag1 = "true" + tag2 = "false" + } + target_kind = "agent" + target_links = ["secret/postgres-poc-credentials", "secret/postgres-poc-entrypoint-script"] + binding { + permissions = ["view"] + principal_links = ["user/fake-user@fake-email.com", "serviceaccount/FAKE_SERVICE_ACCOUNT_NAME", "group/FAKE-GROUP"] + } + binding { + permissions = ["view", "edit"] + principal_links = ["user/fake-admin-user@fake-email.com"] + } + target_query { + fetch = "items" + spec { + match = "all" + terms { + op = "=" + tag = "tag_name" + value = "some_tag" + } + } + } + } + EXPECTED + ) + end + + context "when fetch type is invalid" do + let(:fetch_type) { "invalid" } + + it "raises an argument error" do + expect { generated }.to raise_error( + ArgumentError, + "Invalid fetch type - #{fetch_type}. Should be either `links` or `items`" + ) + end + end + + context "when match type is invalid" do + let(:match_type) { "invalid" } + + it "raises an argument error" do + expect { generated }.to raise_error( + ArgumentError, + "Invalid match type - #{match_type}. Should be either `all`, `any` or `none`" + ) + end + end + + context "when term is invalid" do + let(:extra_options) do + { + target_query: { + "spec" => { + "terms" => [ + { + "property" => "id", # extra attribute + "tag" => "tag_name" + } + ] + } + } + } + end + + it "raises an argument error" do + expect { generated }.to raise_error( + ArgumentError, + "`target_query.spec.terms` can contain only one of the following attributes: `property`, `rel`, `tag`." + ) + end + end + end + + context "without target query" do + let(:extra_options) { {} } + + it "generates correct config" do + expect(generated).to eq( + <<~EXPECTED + resource "cpln_policy" "policy-name" { + name = "policy-name" + description = "policy description" + tags = { + tag1 = "true" + tag2 = "false" + } + target_links = ["secret/postgres-poc-credentials", "secret/postgres-poc-entrypoint-script"] + binding { + permissions = ["view"] + principal_links = ["user/fake-user@fake-email.com", "serviceaccount/FAKE_SERVICE_ACCOUNT_NAME", "group/FAKE-GROUP"] + } + binding { + permissions = ["view", "edit"] + principal_links = ["user/fake-admin-user@fake-email.com"] + } + } + EXPECTED + ) + end + end + + context "when gvc is required" do + let(:extra_options) { { target_kind: "identity", gvc: nil } } + + it "raises error if gvc is missing" do + expect { generated }.to raise_error(ArgumentError, "`gvc` is required for `identity` target kind") + end + end + end +end diff --git a/spec/dummy/.controlplane/templates/policy.yml b/spec/dummy/.controlplane/templates/policy.yml new file mode 100644 index 00000000..000b108f --- /dev/null +++ b/spec/dummy/.controlplane/templates/policy.yml @@ -0,0 +1,16 @@ +kind: identity +name: postgres-poc-identity +description: postgres-poc-identity +--- +kind: policy +name: postgres-poc-access +description: postgres-poc-access +bindings: + - permissions: + - view + principalLinks: + - //gvc/{{APP_NAME}}/identity/postgres-poc-identity +targetKind: secret +targetLinks: + - //secret/postgres-poc-credentials + - //secret/postgres-poc-entrypoint-script diff --git a/spec/pathces/hash_spec.rb b/spec/pathces/hash_spec.rb index 6c1216fb..afe7dd07 100644 --- a/spec/pathces/hash_spec.rb +++ b/spec/pathces/hash_spec.rb @@ -3,22 +3,22 @@ require "spec_helper" describe Hash do - describe "#underscore_keys" do - subject(:underscored_keys_hash) { hash.underscore_keys } + describe "#deep_underscore_keys" do + subject(:deep_underscored_keys_hash) { hash.deep_underscore_keys } context "with an empty hash" do let(:hash) { {} } it "returns an empty hash" do - expect(underscored_keys_hash).to eq({}) + expect(deep_underscored_keys_hash).to eq({}) end end context "with a nested hash" do let(:hash) { { "outerCamelCase" => { innerCamelCase: "value" } } } - it "transforms keys only at top level" do - expect(underscored_keys_hash).to eq("outer_camel_case" => { innerCamelCase: "value" }) + it "transforms keys at all levels" do + expect(deep_underscored_keys_hash).to eq("outer_camel_case" => { inner_camel_case: "value" }) end end @@ -26,7 +26,7 @@ let(:hash) { { "already_underscored" => "value" } } it "leaves underscored keys unchanged" do - expect(underscored_keys_hash).to eq("already_underscored" => "value") + expect(deep_underscored_keys_hash).to eq("already_underscored" => "value") end end @@ -34,7 +34,7 @@ let(:hash) { { "camelCase123" => "value1", "special@CaseKey" => "value2" } } it "correctly transforms keys with numbers or special characters" do - expect(underscored_keys_hash).to eq("camel_case123" => "value1", "special@case_key" => "value2") + expect(deep_underscored_keys_hash).to eq("camel_case123" => "value1", "special@case_key" => "value2") end end @@ -42,15 +42,15 @@ let(:hash) { { "camelCaseKey" => "value1", "snake_case_key" => "value2", "XMLHttpRequest" => "value3" } } it "transforms camelCase keys to snake_case" do - expect(underscored_keys_hash["camel_case_key"]).to eq("value1") + expect(deep_underscored_keys_hash["camel_case_key"]).to eq("value1") end it "leaves snake_case keys unchanged" do - expect(underscored_keys_hash["snake_case_key"]).to eq("value2") + expect(deep_underscored_keys_hash["snake_case_key"]).to eq("value2") end it "correctly handles keys with multiple uppercase letters" do - expect(underscored_keys_hash["xml_http_request"]).to eq("value3") + expect(deep_underscored_keys_hash["xml_http_request"]).to eq("value3") end end @@ -58,15 +58,15 @@ let(:hash) { { camelCaseKey: "value1", snake_case_key: "value2", XMLHttpRequest: "value3" } } it "transforms camelCase symbol keys to snake_case" do - expect(underscored_keys_hash[:camel_case_key]).to eq("value1") + expect(deep_underscored_keys_hash[:camel_case_key]).to eq("value1") end it "leaves snake_case symbol keys unchanged" do - expect(underscored_keys_hash[:snake_case_key]).to eq("value2") + expect(deep_underscored_keys_hash[:snake_case_key]).to eq("value2") end it "correctly handles symbol keys with multiple uppercase letters" do - expect(underscored_keys_hash[:xml_http_request]).to eq("value3") + expect(deep_underscored_keys_hash[:xml_http_request]).to eq("value3") end end end From f92501cfa74322ebceb384ba4eecb8fb8668bd7f Mon Sep 17 00:00:00 2001 From: Zakir Dzhamaliddinov Date: Wed, 16 Oct 2024 12:37:41 +0300 Subject: [PATCH 09/16] coderabbitai review fixes --- lib/core/terraform_config/generator.rb | 30 +++++++++++++---------- lib/core/terraform_config/policy.rb | 3 ++- spec/core/terraform_config/policy_spec.rb | 3 ++- 3 files changed, 21 insertions(+), 15 deletions(-) diff --git a/lib/core/terraform_config/generator.rb b/lib/core/terraform_config/generator.rb index 4e207c0c..69bda1ea 100644 --- a/lib/core/terraform_config/generator.rb +++ b/lib/core/terraform_config/generator.rb @@ -78,17 +78,6 @@ def secret_config # rubocop:disable Metrics/MethodLength def policy_config - # //secret/secret-name -> secret-name - target_links = template["targetLinks"]&.map do |target_link| - target_link.split("/").last - end - - # //group/viewers -> group/viewers - bindings = template["bindings"]&.map do |data| - principal_links = data.delete("principalLinks")&.map { |link| link.delete_prefix("//") } - data.merge("principalLinks" => principal_links) - end - TerraformConfig::Policy.new( name: template["name"], description: template["description"], @@ -96,9 +85,9 @@ def policy_config target: template["target"], target_kind: template["targetKind"], target_query: template["targetQuery"], - target_links: target_links, + target_links: policy_target_links, gvc: gvc, - bindings: bindings + bindings: policy_bindings ) end # rubocop:enable Metrics/MethodLength @@ -108,6 +97,21 @@ def gvc "cpln_gvc.#{config.app}.name" end + # //secret/secret-name -> secret-name + def policy_target_links + template["targetLinks"]&.map do |target_link| + target_link.split("/").last + end + end + + # //group/viewers -> group/viewers + def policy_bindings + template["bindings"]&.map do |data| + principal_links = data.delete("principalLinks")&.map { |link| link.delete_prefix("//") } + data.merge("principalLinks" => principal_links) + end + end + def env template.dig("spec", "env").to_h { |env_var| [env_var["name"], env_var["value"]] } end diff --git a/lib/core/terraform_config/policy.rb b/lib/core/terraform_config/policy.rb index c8156293..f9fe7948 100644 --- a/lib/core/terraform_config/policy.rb +++ b/lib/core/terraform_config/policy.rb @@ -139,7 +139,8 @@ def validate_term!(term) return unless (%i[property rel tag] & term.keys).count > 1 raise ArgumentError, - "`target_query.spec.terms` can contain only one of the following attributes: `property`, `rel`, `tag`." + "Each term in `target_query.spec.terms` must contain exactly one of the following attributes: " \ + "`property`, `rel`, or `tag`." end end # rubocop:enable Metrics/ClassLength diff --git a/spec/core/terraform_config/policy_spec.rb b/spec/core/terraform_config/policy_spec.rb index cac57eed..f0b742a1 100644 --- a/spec/core/terraform_config/policy_spec.rb +++ b/spec/core/terraform_config/policy_spec.rb @@ -134,7 +134,8 @@ it "raises an argument error" do expect { generated }.to raise_error( ArgumentError, - "`target_query.spec.terms` can contain only one of the following attributes: `property`, `rel`, `tag`." + "Each term in `target_query.spec.terms` must contain exactly one of the following attributes: " \ + "`property`, `rel`, or `tag`." ) end end From e36c11cf7972dab9f0e81c1addb051bec2b6ab13 Mon Sep 17 00:00:00 2001 From: Zakir Dzhamaliddinov Date: Wed, 16 Oct 2024 13:07:36 +0300 Subject: [PATCH 10/16] Symbolize template keys in TerrafromConfig::Generator --- lib/core/terraform_config/generator.rb | 102 ++++++++----------- spec/core/terraform_config/generator_spec.rb | 8 +- 2 files changed, 48 insertions(+), 62 deletions(-) diff --git a/lib/core/terraform_config/generator.rb b/lib/core/terraform_config/generator.rb index 69bda1ea..b3212c04 100644 --- a/lib/core/terraform_config/generator.rb +++ b/lib/core/terraform_config/generator.rb @@ -6,12 +6,12 @@ class Generator def initialize(config:, template:) @config = config - @template = template + @template = template.deep_underscore_keys.deep_symbolize_keys end # rubocop:disable Metrics/MethodLength def filename - case template["kind"] + case kind when "gvc" "gvc.tf" when "secret" @@ -21,104 +21,90 @@ def filename when "policy" "policies.tf" else - raise "Unsupported template kind - #{template['kind']}" + raise "Unsupported template kind - #{kind}" end end # rubocop:enable Metrics/MethodLength def tf_config - method_name = :"#{template['kind']}_config" - raise "Unsupported template kind - #{template['kind']}" unless self.class.private_method_defined?(method_name) + method_name = :"#{kind}_config" + raise "Unsupported template kind - #{kind}" unless self.class.private_method_defined?(method_name) send(method_name) end private + def kind + @kind ||= template[:kind] + end + # rubocop:disable Metrics/MethodLength def gvc_config - pull_secrets = template.dig("spec", "pullSecretLinks")&.map do |secret_link| - secret_name = secret_link.split("/").last - "cpln_secret.#{secret_name}.name" - end - - load_balancer = template.dig("spec", "loadBalancer") - TerraformConfig::Gvc.new( - name: template["name"], - description: template["description"], - tags: template["tags"], - domain: template.dig("spec", "domain"), - env: env, - pull_secrets: pull_secrets, - locations: locations, - load_balancer: load_balancer + **template + .slice(:name, :description, :tags) + .merge( + env: gvc_env, + pull_secrets: gvc_pull_secrets, + locations: gvc_locations, + domain: template.dig(:spec, :domain), + load_balancer: template.dig(:spec, :load_balancer) + ) ) end # rubocop:enable Metrics/MethodLength def identity_config - TerraformConfig::Identity.new( - gvc: gvc, - name: template["name"], - description: template["description"], - tags: template["tags"] - ) + TerraformConfig::Identity.new(**template.slice(:name, :description, :tags).merge(gvc: gvc)) end def secret_config - TerraformConfig::Secret.new( - name: template["name"], - description: template["description"], - type: template["type"], - data: template["data"], - tags: template["tags"] - ) + TerraformConfig::Secret.new(**template.slice(:name, :description, :type, :data, :tags)) end - # rubocop:disable Metrics/MethodLength def policy_config TerraformConfig::Policy.new( - name: template["name"], - description: template["description"], - tags: template["tags"], - target: template["target"], - target_kind: template["targetKind"], - target_query: template["targetQuery"], - target_links: policy_target_links, - gvc: gvc, - bindings: policy_bindings + **template + .slice(:name, :description, :tags, :target, :target_kind, :target_query) + .merge(gvc: gvc, target_links: policy_target_links, bindings: policy_bindings) ) end - # rubocop:enable Metrics/MethodLength # GVC name matches application name def gvc "cpln_gvc.#{config.app}.name" end - # //secret/secret-name -> secret-name - def policy_target_links - template["targetLinks"]&.map do |target_link| - target_link.split("/").last + def gvc_pull_secrets + template.dig(:spec, :pull_secret_links)&.map do |secret_link| + secret_name = secret_link.split("/").last + "cpln_secret.#{secret_name}.name" end end - # //group/viewers -> group/viewers - def policy_bindings - template["bindings"]&.map do |data| - principal_links = data.delete("principalLinks")&.map { |link| link.delete_prefix("//") } - data.merge("principalLinks" => principal_links) + def gvc_env + template.dig(:spec, :env).to_h { |env_var| [env_var[:name], env_var[:value]] } + end + + def gvc_locations + template.dig(:spec, :static_placement, :location_links)&.map do |location_link| + location_link.split("/").last end end - def env - template.dig("spec", "env").to_h { |env_var| [env_var["name"], env_var["value"]] } + # //secret/secret-name -> secret-name + def policy_target_links + template[:target_links]&.map do |target_link| + target_link.split("/").last + end end - def locations - template.dig("spec", "staticPlacement", "locationLinks")&.map do |location_link| - location_link.split("/").last + # //group/viewers -> group/viewers + def policy_bindings + template[:bindings]&.map do |data| + principal_links = data.delete(:principal_links)&.map { |link| link.delete_prefix("//") } + data.merge(principal_links: principal_links) end end end diff --git a/spec/core/terraform_config/generator_spec.rb b/spec/core/terraform_config/generator_spec.rb index 3f6cbdc1..8034da97 100644 --- a/spec/core/terraform_config/generator_spec.rb +++ b/spec/core/terraform_config/generator_spec.rb @@ -57,7 +57,7 @@ expect(tf_config.name).to eq(config.app) expect(tf_config.description).to eq("description") - expect(tf_config.tags).to eq("tag1" => "tag1_value", "tag2" => "tag2_value") + expect(tf_config.tags).to eq(tag1: "tag1_value", tag2: "tag2_value") expect(tf_config.domain).to eq("app.example.com") expect(tf_config.locations).to eq(["aws-us-east-2"]) @@ -92,7 +92,7 @@ expect(tf_config.name).to eq("identity-name") expect(tf_config.description).to eq("description") - expect(tf_config.tags).to eq("tag1" => "tag1_value", "tag2" => "tag2_value") + expect(tf_config.tags).to eq(tag1: "tag1_value", tag2: "tag2_value") tf_filename = generator.filename expect(tf_filename).to eq("identities.tf") @@ -117,7 +117,7 @@ expect(tf_config.name).to eq("secret-name") expect(tf_config.description).to eq("description") - expect(tf_config.tags).to eq("tag1" => "tag1_value", "tag2" => "tag2_value") + expect(tf_config.tags).to eq(tag1: "tag1_value", tag2: "tag2_value") tf_filename = generator.filename expect(tf_filename).to eq("secrets.tf") @@ -156,7 +156,7 @@ expect(tf_config.name).to eq("policy-name") expect(tf_config.description).to eq("policy description") - expect(tf_config.tags).to eq("tag1" => "tag1_value", "tag2" => "tag2_value") + expect(tf_config.tags).to eq(tag1: "tag1_value", tag2: "tag2_value") expect(tf_config.target).to eq("all") expect(tf_config.target_kind).to eq("secret") expect(tf_config.target_links).to eq(%w[postgres-poc-credentials postgres-poc-entrypoint-script]) From 080f3d6a32d63363e2333c0300385a98414df5da Mon Sep 17 00:00:00 2001 From: Zakir Dzhamaliddinov Date: Wed, 16 Oct 2024 13:07:51 +0300 Subject: [PATCH 11/16] Fix typo --- lib/core/terraform_config/policy.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/core/terraform_config/policy.rb b/lib/core/terraform_config/policy.rb index f9fe7948..09cdbff9 100644 --- a/lib/core/terraform_config/policy.rb +++ b/lib/core/terraform_config/policy.rb @@ -8,7 +8,7 @@ class Policy < Base org policy quota secret serviceaccount task user volumeset workload ].freeze - GVC_REQUIERD_TARGET_KINDS = %w[identity workload volumeset].freeze + GVC_REQUIRED_TARGET_KINDS = %w[identity workload volumeset].freeze attr_reader :name, :description, :tags, :target_kind, :gvc, :target, :target_links, :target_query, :bindings @@ -66,7 +66,7 @@ def validate_target_kind! end def validate_gvc! - return unless GVC_REQUIERD_TARGET_KINDS.include?(target_kind.to_s) && gvc.nil? + return unless GVC_REQUIRED_TARGET_KINDS.include?(target_kind.to_s) && gvc.nil? raise ArgumentError, "`gvc` is required for `#{target_kind}` target kind" end From 2544b47d915dfe6aeb5ec660ca2da6fa4217c31e Mon Sep 17 00:00:00 2001 From: Zakir Dzhamaliddinov Date: Wed, 16 Oct 2024 17:22:21 +0300 Subject: [PATCH 12/16] Generate Terraform config from volumeset templates --- lib/command/terraform/generate.rb | 7 +- lib/core/terraform_config/generator.rb | 116 +++++----- lib/core/terraform_config/volume_set.rb | 139 ++++++++++++ lib/patches/string.rb | 14 ++ spec/command/terraform/generate_spec.rb | 2 +- spec/core/terraform_config/generator_spec.rb | 60 +++++- spec/core/terraform_config/gvc_spec.rb | 2 +- spec/core/terraform_config/volume_set_spec.rb | 201 ++++++++++++++++++ spec/{pathces => patches}/hash_spec.rb | 0 spec/patches/string_spec.rb | 59 +++++ 10 files changed, 534 insertions(+), 66 deletions(-) create mode 100644 lib/core/terraform_config/volume_set.rb create mode 100644 spec/core/terraform_config/volume_set_spec.rb rename spec/{pathces => patches}/hash_spec.rb (100%) create mode 100644 spec/patches/string_spec.rb diff --git a/lib/command/terraform/generate.rb b/lib/command/terraform/generate.rb index 21ebbac8..6e0a4fa6 100644 --- a/lib/command/terraform/generate.rb +++ b/lib/command/terraform/generate.rb @@ -44,11 +44,10 @@ def generate_app_config(app) terraform_app_dir = recreate_terraform_app_dir(app) templates.each do |template| - generator = TerraformConfig::Generator.new(config: config, template: template) - - # TODO: Delete line below after all template kinds are supported - next unless %w[gvc identity secret policy].include?(template["kind"]) + # TODO: Raise error i/o ignoring invalid template kind after all template kinds are supported + next unless TerraformConfig::Generator::SUPPORTED_TEMPLATE_KINDS.include?(template["kind"]) + generator = TerraformConfig::Generator.new(config: config, template: template) File.write(terraform_app_dir.join(generator.filename), generator.tf_config.to_tf, mode: "a+") end end diff --git a/lib/core/terraform_config/generator.rb b/lib/core/terraform_config/generator.rb index b3212c04..11b1762c 100644 --- a/lib/core/terraform_config/generator.rb +++ b/lib/core/terraform_config/generator.rb @@ -2,73 +2,82 @@ module TerraformConfig class Generator + SUPPORTED_TEMPLATE_KINDS = %w[gvc secret identity policy volumeset].freeze + attr_reader :config, :template def initialize(config:, template:) @config = config @template = template.deep_underscore_keys.deep_symbolize_keys + validate_template_kind! end - # rubocop:disable Metrics/MethodLength def filename - case kind - when "gvc" - "gvc.tf" - when "secret" - "secrets.tf" - when "identity" - "identities.tf" - when "policy" - "policies.tf" - else - raise "Unsupported template kind - #{kind}" - end + "#{kind.pluralize}.tf" end - # rubocop:enable Metrics/MethodLength def tf_config - method_name = :"#{kind}_config" - raise "Unsupported template kind - #{kind}" unless self.class.private_method_defined?(method_name) - - send(method_name) + config_class.new(**config_params) end private - def kind - @kind ||= template[:kind] + def validate_template_kind! + return if SUPPORTED_TEMPLATE_KINDS.include?(kind) + + raise ArgumentError, "Unsupported template kind: #{kind}" + end + + def config_class + if kind == "volumeset" + TerraformConfig::VolumeSet + else + TerraformConfig.const_get(kind.capitalize) + end end - # rubocop:disable Metrics/MethodLength - def gvc_config - TerraformConfig::Gvc.new( - **template - .slice(:name, :description, :tags) - .merge( - env: gvc_env, - pull_secrets: gvc_pull_secrets, - locations: gvc_locations, - domain: template.dig(:spec, :domain), - load_balancer: template.dig(:spec, :load_balancer) - ) - ) + def config_params + send("#{kind}_config_params") end - # rubocop:enable Metrics/MethodLength - def identity_config - TerraformConfig::Identity.new(**template.slice(:name, :description, :tags).merge(gvc: gvc)) + def gvc_config_params + template + .slice(:name, :description, :tags) + .merge( + env: gvc_env, + pull_secrets: gvc_pull_secrets, + locations: gvc_locations, + domain: template.dig(:spec, :domain), + load_balancer: template.dig(:spec, :load_balancer) + ) end - def secret_config - TerraformConfig::Secret.new(**template.slice(:name, :description, :type, :data, :tags)) + def identity_config_params + template.slice(:name, :description, :tags).merge(gvc: gvc) end - def policy_config - TerraformConfig::Policy.new( - **template - .slice(:name, :description, :tags, :target, :target_kind, :target_query) - .merge(gvc: gvc, target_links: policy_target_links, bindings: policy_bindings) - ) + def secret_config_params + template.slice(:name, :description, :type, :data, :tags) + end + + def policy_config_params + template + .slice(:name, :description, :tags, :target, :target_kind, :target_query) + .merge(gvc: gvc, target_links: policy_target_links, bindings: policy_bindings) + end + + def volumeset_config_params # rubocop:disable Metrics/MethodLength + template + .slice(:name, :description, :tags) + .merge( + gvc: gvc, + initial_capacity: template.dig(:spec, :initial_capacity), + performance_class: template.dig(:spec, :performance_class), + file_system_type: template.dig(:spec, :file_system_type), + storage_class_suffix: template.dig(:spec, :storage_class_suffix), + snapshots: template.dig(:spec, :snapshots), + autoscaling: template.dig(:spec, :autoscaling) + ) end # GVC name matches application name @@ -77,10 +86,7 @@ def gvc end def gvc_pull_secrets - template.dig(:spec, :pull_secret_links)&.map do |secret_link| - secret_name = secret_link.split("/").last - "cpln_secret.#{secret_name}.name" - end + template.dig(:spec, :pull_secret_links)&.map { |secret_link| "cpln_secret.#{secret_link.split('/').last}.name" } end def gvc_env @@ -88,24 +94,22 @@ def gvc_env end def gvc_locations - template.dig(:spec, :static_placement, :location_links)&.map do |location_link| - location_link.split("/").last - end + template.dig(:spec, :static_placement, :location_links)&.map { |location_link| location_link.split("/").last } end - # //secret/secret-name -> secret-name def policy_target_links - template[:target_links]&.map do |target_link| - target_link.split("/").last - end + template[:target_links]&.map { |target_link| target_link.split("/").last } end - # //group/viewers -> group/viewers def policy_bindings template[:bindings]&.map do |data| principal_links = data.delete(:principal_links)&.map { |link| link.delete_prefix("//") } data.merge(principal_links: principal_links) end end + + def kind + @kind ||= template[:kind] + end end end diff --git a/lib/core/terraform_config/volume_set.rb b/lib/core/terraform_config/volume_set.rb new file mode 100644 index 00000000..01e28cef --- /dev/null +++ b/lib/core/terraform_config/volume_set.rb @@ -0,0 +1,139 @@ +# frozen_string_literal: true + +module TerraformConfig + class VolumeSet < Base # rubocop:disable Metrics/ClassLength + PERFORMANCE_CLASSES = %w[general-purpose-ssd high-throughput-ssd].freeze + FILE_SYSTEM_TYPES = %w[xfs ext4].freeze + MIN_CAPACITY = 10 + MIN_SCALING_FACTOR = 1.1 + + attr_reader :gvc, :name, :initial_capacity, :performance_class, :file_system_type, + :storage_class_suffix, :description, :tags, :snapshots, :autoscaling + + def initialize( # rubocop:disable Metrics/ParameterLists, Metrics/MethodLength + gvc:, + name:, + initial_capacity:, + performance_class:, + file_system_type:, + storage_class_suffix: nil, + description: nil, + tags: nil, + snapshots: nil, + autoscaling: nil + ) + super() + + @gvc = gvc + @name = name + @initial_capacity = initial_capacity + @performance_class = performance_class + @file_system_type = file_system_type + @storage_class_suffix = storage_class_suffix + @description = description + @tags = tags + @snapshots = snapshots + @autoscaling = autoscaling + + validate_attributes! + end + + def to_tf + block :resource, :cpln_volume_set, name do + base_arguments_tf + snapshots_tf + autoscaling_tf + end + end + + private + + def validate_attributes! + validate_initial_capacity! + validate_performance_class! + validate_file_system_type! + validate_autoscaling! if autoscaling + end + + def validate_initial_capacity! + return unless initial_capacity < MIN_CAPACITY + + raise ArgumentError, + "Initial capacity should be greater than or equal to #{MIN_CAPACITY}" + end + + def validate_performance_class! + return if PERFORMANCE_CLASSES.include?(performance_class.to_s) + + raise ArgumentError, + "Invalid performance class: #{performance_class}. Choose from #{PERFORMANCE_CLASSES.join(', ')}" + end + + def validate_file_system_type! + return if FILE_SYSTEM_TYPES.include?(file_system_type) + + raise ArgumentError, + "Invalid file system type: #{file_system_type}. Choose from #{FILE_SYSTEM_TYPES.join(', ')}" + end + + def validate_autoscaling! + validate_max_capacity! + validate_min_free_percentage! + validate_scaling_factor! + end + + def validate_max_capacity! + max_capacity = autoscaling.fetch(:max_capacity, nil) + return if max_capacity.nil? || max_capacity >= MIN_CAPACITY + + raise ArgumentError, "autoscaling.max_capacity should be >= #{MIN_CAPACITY}" + end + + def validate_min_free_percentage! + min_free_percentage = autoscaling.fetch(:min_free_percentage, nil) + return if min_free_percentage.nil? || min_free_percentage.between?(1, 100) + + raise ArgumentError, "autoscaling.min_free_percentage should be between 1 and 100" + end + + def validate_scaling_factor! + scaling_factor = autoscaling.fetch(:scaling_factor, nil) + return if scaling_factor.nil? || scaling_factor >= MIN_SCALING_FACTOR + + raise ArgumentError, "autoscaling.scaling_factor should be >= #{MIN_SCALING_FACTOR}" + end + + def base_arguments_tf + argument :gvc, gvc + + argument :name, name + argument :description, description, optional: true + argument :tags, tags, optional: true + + argument :initial_capacity, initial_capacity + argument :performance_class, performance_class + argument :storage_class_suffix, storage_class_suffix, optional: true + argument :file_system_type, file_system_type + end + + def snapshots_tf + return if snapshots.nil? + + block :snapshots do + %i[create_final_snapshot retention_duration schedule].each do |arg_name| + argument arg_name, snapshots.fetch(arg_name, nil), optional: true + end + end + end + + def autoscaling_tf + return if autoscaling.nil? + + block :autoscaling do + %i[max_capacity min_free_percentage scaling_factor].each do |arg_name| + argument arg_name, autoscaling.fetch(arg_name, nil), optional: true + end + end + end + end +end diff --git a/lib/patches/string.rb b/lib/patches/string.rb index bfc35d62..4f3d8a07 100644 --- a/lib/patches/string.rb +++ b/lib/patches/string.rb @@ -22,5 +22,19 @@ def unindent def underscore gsub("::", "/").gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2').gsub(/([a-z\d])([A-Z])/, '\1_\2').tr("-", "_").downcase end + + def pluralize + return self if empty? + + if end_with?("ies") + self + elsif end_with?("s", "x", "z", "ch", "sh") + end_with?("es") ? self : "#{self}es" + elsif end_with?("y") + "#{self[...-1]}ies" + else + end_with?("s") ? self : "#{self}s" + end + end end # rubocop:enable Style/OptionalBooleanParameter, Lint/UnderscorePrefixedVariableName diff --git a/spec/command/terraform/generate_spec.rb b/spec/command/terraform/generate_spec.rb index 917eab18..316e7bc6 100644 --- a/spec/command/terraform/generate_spec.rb +++ b/spec/command/terraform/generate_spec.rb @@ -99,7 +99,7 @@ def common_config_files end def app_config_files - %w[gvc.tf identities.tf secrets.tf policies.tf].map do |config_file_path| + %w[gvcs.tf identities.tf secrets.tf policies.tf volumesets.tf].map do |config_file_path| TERRAFORM_CONFIG_DIR_PATH.join(app, config_file_path) end end diff --git a/spec/core/terraform_config/generator_spec.rb b/spec/core/terraform_config/generator_spec.rb index 8034da97..8af724b1 100644 --- a/spec/core/terraform_config/generator_spec.rb +++ b/spec/core/terraform_config/generator_spec.rb @@ -10,9 +10,8 @@ context "when template's kind is unsupported" do let(:template) { { "kind" => "invalid" } } - it "does not generate terraform config or filename for it", :aggregate_failures do - expect { generator.tf_config }.to raise_error("Unsupported template kind - #{template['kind']}") - expect { generator.filename }.to raise_error("Unsupported template kind - #{template['kind']}") + it "raises an error when unsupported template kind is used", :aggregate_failures do + expect { generator }.to raise_error(ArgumentError, "Unsupported template kind: #{template['kind']}") end end @@ -72,7 +71,7 @@ expect(tf_config.load_balancer).to eq({ dedicated: true, trusted_proxies: 1 }) tf_filename = generator.filename - expect(tf_filename).to eq("gvc.tf") + expect(tf_filename).to eq("gvcs.tf") end end @@ -175,4 +174,57 @@ expect(tf_filename).to eq("policies.tf") end end + + context "when template's kind is volumeset" do + let(:template) do + { + "kind" => "volumeset", + "name" => "volume-set-name", + "description" => "volume set description", + "tags" => { "tag1" => "tag1_value", "tag2" => "tag2_value" }, + "spec" => { + "initialCapacity" => 20, + "performanceClass" => "general-purpose-ssd", + "fileSystemType" => "xfs", + "storageClassSuffix" => "suffix", + "snapshots" => { + "createFinalSnapshot" => true, + "retentionDuration" => "7d", + "schedule" => "0 1 * * *" + }, + "autoscaling" => { + "maxCapacity" => 100, + "minFreePercentage" => 20, + "scalingFactor" => 1.5 + } + } + } + end + + it "generates correct terraform config and filename for it", :aggregate_failures do + tf_config = generator.tf_config + expect(tf_config).to be_an_instance_of(TerraformConfig::VolumeSet) + + expect(tf_config.name).to eq("volume-set-name") + expect(tf_config.description).to eq("volume set description") + expect(tf_config.tags).to eq(tag1: "tag1_value", tag2: "tag2_value") + expect(tf_config.initial_capacity).to eq(20) + expect(tf_config.performance_class).to eq("general-purpose-ssd") + expect(tf_config.file_system_type).to eq("xfs") + expect(tf_config.storage_class_suffix).to eq("suffix") + expect(tf_config.snapshots).to eq( + create_final_snapshot: true, + retention_duration: "7d", + schedule: "0 1 * * *" + ) + expect(tf_config.autoscaling).to eq( + max_capacity: 100, + min_free_percentage: 20, + scaling_factor: 1.5 + ) + + tf_filename = generator.filename + expect(tf_filename).to eq("volumesets.tf") + end + end end diff --git a/spec/core/terraform_config/gvc_spec.rb b/spec/core/terraform_config/gvc_spec.rb index 2050914d..fd12b77e 100644 --- a/spec/core/terraform_config/gvc_spec.rb +++ b/spec/core/terraform_config/gvc_spec.rb @@ -18,7 +18,7 @@ tags: { "tag1" => "tag_value", "tag2" => true }, locations: %w[aws-us-east-1 aws-us-east-2], pull_secrets: ["cpln_secret.docker.name"], - load_balancer: { "dedicated" => true, "trusted_proxies" => 1 } + load_balancer: { "dedicated" => true, "trustedProxies" => 1 } } end diff --git a/spec/core/terraform_config/volume_set_spec.rb b/spec/core/terraform_config/volume_set_spec.rb new file mode 100644 index 00000000..c7268590 --- /dev/null +++ b/spec/core/terraform_config/volume_set_spec.rb @@ -0,0 +1,201 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe TerraformConfig::VolumeSet do + let(:config) { described_class.new(**options) } + + let(:options) do + { + gvc: "test-gvc", + name: "test-volume-set", + description: "Test volume set", + tags: { "env" => "test", "project" => "example" }, + initial_capacity: 20, + performance_class: "general-purpose-ssd", + file_system_type: "xfs" + } + end + + describe "#to_tf" do + subject(:generated) { config.to_tf } + + context "with basic configuration" do + it "generates correct config" do + expect(generated).to eq( + <<~EXPECTED + resource "cpln_volume_set" "#{options.fetch(:name)}" { + gvc = "#{options.fetch(:gvc)}" + name = "#{options.fetch(:name)}" + description = "#{options.fetch(:description)}" + tags = { + env = "test" + project = "example" + } + initial_capacity = #{options.fetch(:initial_capacity)} + performance_class = "#{options.fetch(:performance_class)}" + file_system_type = "#{options.fetch(:file_system_type)}" + } + EXPECTED + ) + end + end + + context "with storage_class_suffix" do + let(:config) { described_class.new(**options.merge(storage_class_suffix: "suffix")) } + + it "includes storage_class_suffix in the config" do + expect(generated).to include('storage_class_suffix = "suffix"') + end + end + + context "with snapshots" do + let(:config) do + described_class.new(**options.merge( + snapshots: { + create_final_snapshot: true, + retention_duration: "7d", + schedule: "0 1 * * *" + } + )) + end + + it "includes snapshots block in the config" do + expect(generated).to include( + <<~EXPECTED.strip.indent(2) + snapshots { + create_final_snapshot = true + retention_duration = "7d" + schedule = "0 1 * * *" + } + EXPECTED + ) + end + end + + context "with autoscaling" do + let(:config) do + described_class.new(**options.merge( + autoscaling: { + max_capacity: 100, + min_free_percentage: 20, + scaling_factor: 1.5 + } + )) + end + + it "includes autoscaling block in the config" do + expect(generated).to include( + <<~EXPECTED.strip.indent(2) + autoscaling { + max_capacity = 100 + min_free_percentage = 20 + scaling_factor = 1.5 + } + EXPECTED + ) + end + end + end + + describe "validations" do + context "with invalid initial_capacity" do + it "raises an error" do + expect { described_class.new(**options.merge(initial_capacity: 5)) }.to raise_error( + ArgumentError, "Initial capacity should be greater than or equal to 10" + ) + end + end + + context "with invalid performance_class" do + it "raises an error" do + expect { described_class.new(**options.merge(performance_class: "invalid")) }.to raise_error( + ArgumentError, "Invalid performance class: invalid. Choose from general-purpose-ssd, high-throughput-ssd" + ) + end + end + + context "with invalid file_system_type" do + it "raises an error" do + expect { described_class.new(**options.merge(file_system_type: "invalid")) }.to raise_error( + ArgumentError, "Invalid file system type: invalid. Choose from xfs, ext4" + ) + end + end + end + + describe "autoscaling validations" do + context "with invalid max_capacity" do + let(:invalid_autoscaling) do + options.merge(autoscaling: { max_capacity: 5 }) + end + + it "raises an error" do + expect { described_class.new(**invalid_autoscaling) }.to raise_error( + ArgumentError, "autoscaling.max_capacity should be >= 10" + ) + end + end + + context "with invalid min_free_percentage" do + let(:invalid_autoscaling) do + options.merge(autoscaling: { min_free_percentage: 0 }) + end + + it "raises an error for value below 1" do + expect { described_class.new(**invalid_autoscaling) }.to raise_error( + ArgumentError, "autoscaling.min_free_percentage should be between 1 and 100" + ) + end + + it "raises an error for value above 100" do + invalid_autoscaling[:autoscaling][:min_free_percentage] = 101 + expect { described_class.new(**invalid_autoscaling) }.to raise_error( + ArgumentError, "autoscaling.min_free_percentage should be between 1 and 100" + ) + end + end + + context "with invalid scaling_factor" do + let(:invalid_autoscaling) do + options.merge(autoscaling: { scaling_factor: 1.0 }) + end + + it "raises an error" do + expect { described_class.new(**invalid_autoscaling) }.to raise_error( + ArgumentError, "autoscaling.scaling_factor should be >= 1.1" + ) + end + end + + context "with valid autoscaling values" do + let(:valid_autoscaling) do + options.merge( + autoscaling: { + max_capacity: 100, + min_free_percentage: 20, + scaling_factor: 1.5 + } + ) + end + + it "does not raise an error" do + expect { described_class.new(**valid_autoscaling) }.not_to raise_error + end + end + + context "with partial autoscaling values" do + let(:partial_autoscaling) do + options.merge( + autoscaling: { + max_capacity: 100 + } + ) + end + + it "does not raise an error" do + expect { described_class.new(**partial_autoscaling) }.not_to raise_error + end + end + end +end diff --git a/spec/pathces/hash_spec.rb b/spec/patches/hash_spec.rb similarity index 100% rename from spec/pathces/hash_spec.rb rename to spec/patches/hash_spec.rb diff --git a/spec/patches/string_spec.rb b/spec/patches/string_spec.rb new file mode 100644 index 00000000..3b481cd1 --- /dev/null +++ b/spec/patches/string_spec.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe String do + describe "#pluralize" do + context "when word is empty" do + it "returns an empty string" do + expect("".pluralize).to eq("") + end + end + + context "when word already ends with 'ies'" do + it "returns the word unchanged" do + expect("cities".pluralize).to eq("cities") + expect("babies".pluralize).to eq("babies") + end + end + + context "when word ends with 's', 'x', 'z', 'ch', or 'sh'" do + it "adds 'es' to the end if it doesn't already end with 'es'" do + expect("bus".pluralize).to eq("buses") + expect("box".pluralize).to eq("boxes") + expect("buzz".pluralize).to eq("buzzes") + expect("church".pluralize).to eq("churches") + expect("dish".pluralize).to eq("dishes") + end + + it "returns the word unchanged if it already ends with 'es'" do + expect("buses".pluralize).to eq("buses") + expect("boxes".pluralize).to eq("boxes") + end + end + + context "when word ends with 'y'" do + it "changes 'y' to 'ies'" do + expect("city".pluralize).to eq("cities") + expect("baby".pluralize).to eq("babies") + end + end + + context "when word doesn't end with 'y', 's', 'x', 'z', 'ch', or 'sh'" do + it "adds 's' to the end if it doesn't already end with 's'" do + expect("cat".pluralize).to eq("cats") + expect("dog".pluralize).to eq("dogs") + expect("book".pluralize).to eq("books") + end + end + + context "when word is a single character" do + it "applies the rules correctly" do + expect("a".pluralize).to eq("as") + expect("s".pluralize).to eq("ses") + expect("x".pluralize).to eq("xes") + expect("y".pluralize).to eq("ies") + end + end + end +end From ed31386e3e4edb7e3f5abfd0f86a58d5e6ec2528 Mon Sep 17 00:00:00 2001 From: Zakir Dzhamaliddinov Date: Mon, 21 Oct 2024 13:51:22 +0300 Subject: [PATCH 13/16] Fix errors --- lib/command/terraform/generate.rb | 8 +++----- lib/core/terraform_config/generator.rb | 4 +++- spec/command/terraform/generate_spec.rb | 18 ++++++++++++++++++ 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/lib/command/terraform/generate.rb b/lib/command/terraform/generate.rb index f3e9dfcf..6f650fb5 100644 --- a/lib/command/terraform/generate.rb +++ b/lib/command/terraform/generate.rb @@ -34,7 +34,7 @@ def generate_common_configs def generate_app_configs Array(config.app || config.apps.keys).each do |app| - config.instance_variable_set(:@app, app) + config.instance_variable_set(:@app, app.to_s) generate_app_config end end @@ -43,12 +43,10 @@ def generate_app_config terraform_app_dir = recreate_terraform_app_dir templates.each do |template| - # TODO: Raise error i/o ignoring invalid template kind after all template kinds are supported - next unless TerraformConfig::Generator::SUPPORTED_TEMPLATE_KINDS.include?(template["kind"]) - generator = TerraformConfig::Generator.new(config: config, template: template) - File.write(terraform_app_dir.join(generator.filename), generator.tf_config.to_tf, mode: "a+") + rescue TerraformConfig::Generator::InvalidTemplateError => e + Shell.warn(e.message) end end diff --git a/lib/core/terraform_config/generator.rb b/lib/core/terraform_config/generator.rb index 7357ba74..25b3ebc0 100644 --- a/lib/core/terraform_config/generator.rb +++ b/lib/core/terraform_config/generator.rb @@ -4,6 +4,8 @@ module TerraformConfig class Generator SUPPORTED_TEMPLATE_KINDS = %w[gvc secret identity policy volumeset].freeze + InvalidTemplateError = Class.new(ArgumentError) + attr_reader :config, :template def initialize(config:, template:) @@ -27,7 +29,7 @@ def tf_config def validate_template_kind! return if SUPPORTED_TEMPLATE_KINDS.include?(kind) - raise ArgumentError, "Unsupported template kind: #{kind}" + raise InvalidTemplateError, "Unsupported template kind: #{kind}" end def config_class diff --git a/spec/command/terraform/generate_spec.rb b/spec/command/terraform/generate_spec.rb index bcd58509..b5116d23 100644 --- a/spec/command/terraform/generate_spec.rb +++ b/spec/command/terraform/generate_spec.rb @@ -86,6 +86,24 @@ end end + context "when InvalidTemplateError is raised" do + before do + allow_any_instance_of(TerraformConfig::Generator).to receive(:tf_config).and_raise( # rubocop:disable RSpec/AnyInstance + TerraformConfig::Generator::InvalidTemplateError, "Invalid template: error message" + ) + end + + it "generates common config files and warns about invalid template" do + config_file_paths.each { |config_file_path| expect(config_file_path).not_to exist } + + expect(result[:status]).to eq(0) + expect(result[:stderr]).to include("Invalid template: error message") + + expect(common_config_files).to all(exist) + app_config_files.each { |config_file_path| expect(config_file_path).not_to exist } + end + end + def config_file_paths common_config_files + app_config_files end From d41fb626da1d0d483d7a64e81ec4796c3c5a0c13 Mon Sep 17 00:00:00 2001 From: Zakir Dzhamaliddinov Date: Mon, 21 Oct 2024 16:35:02 +0300 Subject: [PATCH 14/16] coderabbitai review fixes --- lib/core/terraform_config/generator.rb | 23 +++--- lib/core/terraform_config/volume_set.rb | 24 ++++-- spec/core/terraform_config/volume_set_spec.rb | 78 ++++++++----------- 3 files changed, 58 insertions(+), 67 deletions(-) diff --git a/lib/core/terraform_config/generator.rb b/lib/core/terraform_config/generator.rb index 25b3ebc0..821ff8a2 100644 --- a/lib/core/terraform_config/generator.rb +++ b/lib/core/terraform_config/generator.rb @@ -70,18 +70,17 @@ def policy_config_params .merge(gvc: gvc, target_links: policy_target_links, bindings: policy_bindings) end - def volumeset_config_params # rubocop:disable Metrics/MethodLength - template - .slice(:name, :description, :tags) - .merge( - gvc: gvc, - initial_capacity: template.dig(:spec, :initial_capacity), - performance_class: template.dig(:spec, :performance_class), - file_system_type: template.dig(:spec, :file_system_type), - storage_class_suffix: template.dig(:spec, :storage_class_suffix), - snapshots: template.dig(:spec, :snapshots), - autoscaling: template.dig(:spec, :autoscaling) - ) + def volumeset_config_params + specs = %i[ + initial_capacity + performance_class + file_system_type + storage_class_suffix + snapshots + autoscaling + ].to_h { |key| [key, template.dig(:spec, key)] } + + template.slice(:name, :description, :tags).merge(gvc: gvc).merge(specs) end # GVC name matches application name diff --git a/lib/core/terraform_config/volume_set.rb b/lib/core/terraform_config/volume_set.rb index 01e28cef..51eb3054 100644 --- a/lib/core/terraform_config/volume_set.rb +++ b/lib/core/terraform_config/volume_set.rb @@ -56,10 +56,10 @@ def validate_attributes! end def validate_initial_capacity! - return unless initial_capacity < MIN_CAPACITY + raise ArgumentError, "Initial capacity must be numeric" unless initial_capacity.is_a?(Numeric) + return if initial_capacity >= MIN_CAPACITY - raise ArgumentError, - "Initial capacity should be greater than or equal to #{MIN_CAPACITY}" + raise ArgumentError, "Initial capacity should be >= #{MIN_CAPACITY}" end def validate_performance_class! @@ -72,8 +72,7 @@ def validate_performance_class! def validate_file_system_type! return if FILE_SYSTEM_TYPES.include?(file_system_type) - raise ArgumentError, - "Invalid file system type: #{file_system_type}. Choose from #{FILE_SYSTEM_TYPES.join(', ')}" + raise ArgumentError, "Invalid file system type: #{file_system_type}. Choose from #{FILE_SYSTEM_TYPES.join(', ')}" end def validate_autoscaling! @@ -84,21 +83,30 @@ def validate_autoscaling! def validate_max_capacity! max_capacity = autoscaling.fetch(:max_capacity, nil) - return if max_capacity.nil? || max_capacity >= MIN_CAPACITY + return if max_capacity.nil? + + raise ArgumentError, "autoscaling.max_capacity must be numeric" unless max_capacity.is_a?(Numeric) + return if max_capacity >= MIN_CAPACITY raise ArgumentError, "autoscaling.max_capacity should be >= #{MIN_CAPACITY}" end def validate_min_free_percentage! min_free_percentage = autoscaling.fetch(:min_free_percentage, nil) - return if min_free_percentage.nil? || min_free_percentage.between?(1, 100) + return if min_free_percentage.nil? + + raise ArgumentError, "autoscaling.min_free_percentage must be numeric" unless min_free_percentage.is_a?(Numeric) + return if min_free_percentage.between?(1, 100) raise ArgumentError, "autoscaling.min_free_percentage should be between 1 and 100" end def validate_scaling_factor! scaling_factor = autoscaling.fetch(:scaling_factor, nil) - return if scaling_factor.nil? || scaling_factor >= MIN_SCALING_FACTOR + return if scaling_factor.nil? + + raise ArgumentError, "autoscaling.scaling_factor must be numeric" unless scaling_factor.is_a?(Numeric) + return if scaling_factor >= MIN_SCALING_FACTOR raise ArgumentError, "autoscaling.scaling_factor should be >= #{MIN_SCALING_FACTOR}" end diff --git a/spec/core/terraform_config/volume_set_spec.rb b/spec/core/terraform_config/volume_set_spec.rb index c7268590..1a0c09f1 100644 --- a/spec/core/terraform_config/volume_set_spec.rb +++ b/spec/core/terraform_config/volume_set_spec.rb @@ -99,73 +99,57 @@ end describe "validations" do - context "with invalid initial_capacity" do - it "raises an error" do - expect { described_class.new(**options.merge(initial_capacity: 5)) }.to raise_error( - ArgumentError, "Initial capacity should be greater than or equal to 10" - ) + shared_examples "an invalid parameter" do |param, value, err_msg| + it "raises an error for invalid #{param}" do + expect { described_class.new(**options.merge(param => value)) }.to raise_error(ArgumentError, err_msg) end end - context "with invalid performance_class" do - it "raises an error" do - expect { described_class.new(**options.merge(performance_class: "invalid")) }.to raise_error( - ArgumentError, "Invalid performance class: invalid. Choose from general-purpose-ssd, high-throughput-ssd" - ) - end - end + include_examples "an invalid parameter", :initial_capacity, 5, "Initial capacity should be >= 10" + include_examples "an invalid parameter", :initial_capacity, "10", "Initial capacity must be numeric" + + include_examples "an invalid parameter", + :performance_class, "invalid", + "Invalid performance class: invalid. Choose from general-purpose-ssd, high-throughput-ssd" - context "with invalid file_system_type" do + include_examples "an invalid parameter", + :file_system_type, "invalid", + "Invalid file system type: invalid. Choose from xfs, ext4" + end + + describe "autoscaling validations" do + shared_examples "invalid autoscaling parameter" do |autoscaling_options, err_msg| it "raises an error" do - expect { described_class.new(**options.merge(file_system_type: "invalid")) }.to raise_error( - ArgumentError, "Invalid file system type: invalid. Choose from xfs, ext4" + expect { described_class.new(**options.merge(autoscaling: autoscaling_options)) }.to raise_error( + ArgumentError, err_msg ) end end - end - describe "autoscaling validations" do context "with invalid max_capacity" do - let(:invalid_autoscaling) do - options.merge(autoscaling: { max_capacity: 5 }) - end + include_examples "invalid autoscaling parameter", { max_capacity: 5 }, "autoscaling.max_capacity should be >= 10" - it "raises an error" do - expect { described_class.new(**invalid_autoscaling) }.to raise_error( - ArgumentError, "autoscaling.max_capacity should be >= 10" - ) - end + include_examples "invalid autoscaling parameter", { max_capacity: "100" }, + "autoscaling.max_capacity must be numeric" end context "with invalid min_free_percentage" do - let(:invalid_autoscaling) do - options.merge(autoscaling: { min_free_percentage: 0 }) - end + include_examples "invalid autoscaling parameter", { min_free_percentage: 0 }, + "autoscaling.min_free_percentage should be between 1 and 100" - it "raises an error for value below 1" do - expect { described_class.new(**invalid_autoscaling) }.to raise_error( - ArgumentError, "autoscaling.min_free_percentage should be between 1 and 100" - ) - end + include_examples "invalid autoscaling parameter", { min_free_percentage: 101 }, + "autoscaling.min_free_percentage should be between 1 and 100" - it "raises an error for value above 100" do - invalid_autoscaling[:autoscaling][:min_free_percentage] = 101 - expect { described_class.new(**invalid_autoscaling) }.to raise_error( - ArgumentError, "autoscaling.min_free_percentage should be between 1 and 100" - ) - end + include_examples "invalid autoscaling parameter", { min_free_percentage: "50" }, + "autoscaling.min_free_percentage must be numeric" end context "with invalid scaling_factor" do - let(:invalid_autoscaling) do - options.merge(autoscaling: { scaling_factor: 1.0 }) - end + include_examples "invalid autoscaling parameter", { scaling_factor: 1.0 }, + "autoscaling.scaling_factor should be >= 1.1" - it "raises an error" do - expect { described_class.new(**invalid_autoscaling) }.to raise_error( - ArgumentError, "autoscaling.scaling_factor should be >= 1.1" - ) - end + include_examples "invalid autoscaling parameter", { scaling_factor: "1.5" }, + "autoscaling.scaling_factor must be numeric" end context "with valid autoscaling values" do From 97cd911f73158a1e71b05d7fa903b21ac996084b Mon Sep 17 00:00:00 2001 From: Zakir Dzhamaliddinov Date: Mon, 21 Oct 2024 16:47:20 +0300 Subject: [PATCH 15/16] coderabbitai review fixes --- lib/core/terraform_config/volume_set.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/core/terraform_config/volume_set.rb b/lib/core/terraform_config/volume_set.rb index 51eb3054..f1357808 100644 --- a/lib/core/terraform_config/volume_set.rb +++ b/lib/core/terraform_config/volume_set.rb @@ -70,7 +70,7 @@ def validate_performance_class! end def validate_file_system_type! - return if FILE_SYSTEM_TYPES.include?(file_system_type) + return if FILE_SYSTEM_TYPES.include?(file_system_type.to_s) raise ArgumentError, "Invalid file system type: #{file_system_type}. Choose from #{FILE_SYSTEM_TYPES.join(', ')}" end From 926f933ea0e517d1d2b58202e91dfae1b629117a Mon Sep 17 00:00:00 2001 From: Zakir Dzhamaliddinov Date: Tue, 22 Oct 2024 17:12:08 +0300 Subject: [PATCH 16/16] Review fixes --- lib/core/terraform_config/generator.rb | 2 +- lib/patches/string.rb | 12 ++--- spec/patches/string_spec.rb | 44 +++------------- spec/pathces/hash_spec.rb | 73 -------------------------- 4 files changed, 10 insertions(+), 121 deletions(-) delete mode 100644 spec/pathces/hash_spec.rb diff --git a/lib/core/terraform_config/generator.rb b/lib/core/terraform_config/generator.rb index 821ff8a2..2e437a83 100644 --- a/lib/core/terraform_config/generator.rb +++ b/lib/core/terraform_config/generator.rb @@ -4,7 +4,7 @@ module TerraformConfig class Generator SUPPORTED_TEMPLATE_KINDS = %w[gvc secret identity policy volumeset].freeze - InvalidTemplateError = Class.new(ArgumentError) + class InvalidTemplateError < ArgumentError; end attr_reader :config, :template diff --git a/lib/patches/string.rb b/lib/patches/string.rb index 4f3d8a07..52a9b79a 100644 --- a/lib/patches/string.rb +++ b/lib/patches/string.rb @@ -23,18 +23,12 @@ def underscore gsub("::", "/").gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2').gsub(/([a-z\d])([A-Z])/, '\1_\2').tr("-", "_").downcase end + # Covers only simple cases and used for pluralizing controlplane template kinds (`gvc`, `secret`, `policy`, etc.) def pluralize return self if empty? + return "#{self[...-1]}ies" if end_with?("y") - if end_with?("ies") - self - elsif end_with?("s", "x", "z", "ch", "sh") - end_with?("es") ? self : "#{self}es" - elsif end_with?("y") - "#{self[...-1]}ies" - else - end_with?("s") ? self : "#{self}s" - end + "#{self}s" end end # rubocop:enable Style/OptionalBooleanParameter, Lint/UnderscorePrefixedVariableName diff --git a/spec/patches/string_spec.rb b/spec/patches/string_spec.rb index 3b481cd1..b1fa78de 100644 --- a/spec/patches/string_spec.rb +++ b/spec/patches/string_spec.rb @@ -10,49 +10,17 @@ end end - context "when word already ends with 'ies'" do - it "returns the word unchanged" do - expect("cities".pluralize).to eq("cities") - expect("babies".pluralize).to eq("babies") - end - end - - context "when word ends with 's', 'x', 'z', 'ch', or 'sh'" do - it "adds 'es' to the end if it doesn't already end with 'es'" do - expect("bus".pluralize).to eq("buses") - expect("box".pluralize).to eq("boxes") - expect("buzz".pluralize).to eq("buzzes") - expect("church".pluralize).to eq("churches") - expect("dish".pluralize).to eq("dishes") - end - - it "returns the word unchanged if it already ends with 'es'" do - expect("buses".pluralize).to eq("buses") - expect("boxes".pluralize).to eq("boxes") - end - end - context "when word ends with 'y'" do it "changes 'y' to 'ies'" do - expect("city".pluralize).to eq("cities") - expect("baby".pluralize).to eq("babies") - end - end - - context "when word doesn't end with 'y', 's', 'x', 'z', 'ch', or 'sh'" do - it "adds 's' to the end if it doesn't already end with 's'" do - expect("cat".pluralize).to eq("cats") - expect("dog".pluralize).to eq("dogs") - expect("book".pluralize).to eq("books") + expect("policy".pluralize).to eq("policies") + expect("identity".pluralize).to eq("identities") end end - context "when word is a single character" do - it "applies the rules correctly" do - expect("a".pluralize).to eq("as") - expect("s".pluralize).to eq("ses") - expect("x".pluralize).to eq("xes") - expect("y".pluralize).to eq("ies") + context "when word does not end with 'y'" do + it "adds 's' to the end of the word" do + expect("secret".pluralize).to eq("secrets") + expect("volumeset".pluralize).to eq("volumesets") end end end diff --git a/spec/pathces/hash_spec.rb b/spec/pathces/hash_spec.rb deleted file mode 100644 index afe7dd07..00000000 --- a/spec/pathces/hash_spec.rb +++ /dev/null @@ -1,73 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" - -describe Hash do - describe "#deep_underscore_keys" do - subject(:deep_underscored_keys_hash) { hash.deep_underscore_keys } - - context "with an empty hash" do - let(:hash) { {} } - - it "returns an empty hash" do - expect(deep_underscored_keys_hash).to eq({}) - end - end - - context "with a nested hash" do - let(:hash) { { "outerCamelCase" => { innerCamelCase: "value" } } } - - it "transforms keys at all levels" do - expect(deep_underscored_keys_hash).to eq("outer_camel_case" => { inner_camel_case: "value" }) - end - end - - context "with already underscored keys" do - let(:hash) { { "already_underscored" => "value" } } - - it "leaves underscored keys unchanged" do - expect(deep_underscored_keys_hash).to eq("already_underscored" => "value") - end - end - - context "with keys containing numbers or special characters" do - let(:hash) { { "camelCase123" => "value1", "special@CaseKey" => "value2" } } - - it "correctly transforms keys with numbers or special characters" do - expect(deep_underscored_keys_hash).to eq("camel_case123" => "value1", "special@case_key" => "value2") - end - end - - context "with string keys" do - let(:hash) { { "camelCaseKey" => "value1", "snake_case_key" => "value2", "XMLHttpRequest" => "value3" } } - - it "transforms camelCase keys to snake_case" do - expect(deep_underscored_keys_hash["camel_case_key"]).to eq("value1") - end - - it "leaves snake_case keys unchanged" do - expect(deep_underscored_keys_hash["snake_case_key"]).to eq("value2") - end - - it "correctly handles keys with multiple uppercase letters" do - expect(deep_underscored_keys_hash["xml_http_request"]).to eq("value3") - end - end - - context "with symbol keys" do - let(:hash) { { camelCaseKey: "value1", snake_case_key: "value2", XMLHttpRequest: "value3" } } - - it "transforms camelCase symbol keys to snake_case" do - expect(deep_underscored_keys_hash[:camel_case_key]).to eq("value1") - end - - it "leaves snake_case symbol keys unchanged" do - expect(deep_underscored_keys_hash[:snake_case_key]).to eq("value2") - end - - it "correctly handles symbol keys with multiple uppercase letters" do - expect(deep_underscored_keys_hash[:xml_http_request]).to eq("value3") - end - end - end -end