From d01b5f487461becdc4b10d06fcc718cec55abcd0 Mon Sep 17 00:00:00 2001 From: Zakir Dzhamaliddinov Date: Sun, 1 Dec 2024 12:10:06 +0300 Subject: [PATCH] Add terraform import command (#244) --- docs/commands.md | 8 + lib/command/base.rb | 2 +- lib/command/terraform/base.rb | 35 ++++ lib/command/terraform/generate.rb | 26 --- lib/command/terraform/import.rb | 79 +++++++++ lib/core/shell.rb | 13 +- lib/core/terraform_config/agent.rb | 8 + lib/core/terraform_config/audit_context.rb | 8 + lib/core/terraform_config/base.rb | 8 + lib/core/terraform_config/gvc.rb | 8 + lib/core/terraform_config/identity.rb | 8 + lib/core/terraform_config/policy.rb | 8 + lib/core/terraform_config/secret.rb | 8 + lib/core/terraform_config/volume_set.rb | 8 + lib/core/terraform_config/workload.rb | 10 +- lib/cpflow.rb | 1 + spec/command/terraform/import_spec.rb | 164 ++++++++++++++++++ spec/core/terraform_config/agent_spec.rb | 18 +- .../terraform_config/audit_context_spec.rb | 18 +- spec/core/terraform_config/gvc_spec.rb | 76 ++++---- spec/core/terraform_config/identity_spec.rb | 20 ++- .../terraform_config/local_variable_spec.rb | 28 +-- spec/core/terraform_config/policy_spec.rb | 5 + spec/core/terraform_config/provider_spec.rb | 12 +- .../required_provider_spec.rb | 14 +- spec/core/terraform_config/secret_spec.rb | 10 ++ spec/core/terraform_config/volume_set_spec.rb | 2 + spec/core/terraform_config/workload_spec.rb | 2 + spec/spec_helper.rb | 1 + .../importable_terraform_resource.rb | 29 ++++ spec/support/verified_double.rb | 17 ++ 31 files changed, 535 insertions(+), 119 deletions(-) create mode 100644 lib/command/terraform/base.rb create mode 100644 lib/command/terraform/import.rb create mode 100644 spec/command/terraform/import_spec.rb create mode 100644 spec/support/shared_examples/terraform_config/importable_terraform_resource.rb create mode 100644 spec/support/verified_double.rb diff --git a/docs/commands.md b/docs/commands.md index 14fa758a..667b63c7 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -452,6 +452,14 @@ cpflow setup-app -a $APP_NAME cpflow terraform generate ``` +### `terraform import` + +- Imports terraform resources from the generated configuration files + +```sh +cpflow terraform import +``` + ### `version` - Displays the current version of the CLI diff --git a/lib/command/base.rb b/lib/command/base.rb index a7c788d6..9088832f 100644 --- a/lib/command/base.rb +++ b/lib/command/base.rb @@ -49,7 +49,7 @@ def self.all_commands # rubocop:disable Metrics/MethodLength Dir["#{__dir__}/**/*.rb"].each_with_object({}) do |file, result| content = File.read(file) - classname = content.match(/^\s+class (\w+) < (?:.*Base)(?:$| .*$)/)&.captures&.first + classname = content.match(/^\s+class (?!Base\b)(\w+) < (?:.*(?!Command::)Base)(?:$| .*$)/)&.captures&.first next unless classname namespaces = content.scan(/^\s+module (\w+)/).flatten diff --git a/lib/command/terraform/base.rb b/lib/command/terraform/base.rb new file mode 100644 index 00000000..6da2fbad --- /dev/null +++ b/lib/command/terraform/base.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module Command + module Terraform + class Base < Command::Base + private + + def templates + parser = TemplateParser.new(self) + 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 + @terraform_dir ||= begin + full_path = config.options.fetch(:dir, Cpflow.root_path.join("terraform")) + Pathname.new(full_path).tap do |path| + FileUtils.mkdir_p(path) + rescue StandardError => e + Shell.abort("Invalid directory: #{e.message}") + end + end + end + end + end +end diff --git a/lib/command/terraform/generate.rb b/lib/command/terraform/generate.rb index 317b04e8..57144f06 100644 --- a/lib/command/terraform/generate.rb +++ b/lib/command/terraform/generate.rb @@ -94,32 +94,6 @@ def clean_terraform_app_dir(terraform_app_dir) FileUtils.rm_rf(terraform_app_dir.join(child)) end end - - def templates - parser = TemplateParser.new(self) - 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 - @terraform_dir ||= begin - full_path = config.options.fetch(:dir, Cpflow.root_path.join("terraform")) - Pathname.new(full_path).tap do |path| - FileUtils.mkdir_p(path) - rescue StandardError => e - Shell.abort("Invalid directory: #{e.message}") - end - end - end end end end diff --git a/lib/command/terraform/import.rb b/lib/command/terraform/import.rb new file mode 100644 index 00000000..5ccae82e --- /dev/null +++ b/lib/command/terraform/import.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +module Command + module Terraform + class Import < Base + SUBCOMMAND_NAME = "terraform" + NAME = "import" + OPTIONS = [ + app_option, + dir_option + ].freeze + DESCRIPTION = "Imports terraform resources" + LONG_DESCRIPTION = <<~DESC + - Imports terraform resources from the generated configuration files + DESC + WITH_INFO_HEADER = false + + def call + Array(config.app || config.apps.keys).each do |app| + config.instance_variable_set(:@app, app.to_s) + + Dir.chdir(terraform_app_dir) do + run_terraform_init + + resources.each do |resource| + run_terraform_import(resource[:address], resource[:id]) + end + end + end + end + + private + + def run_terraform_init + result = Shell.cmd("terraform", "init", capture_stderr: true) + + if result[:success] + Shell.info(result[:output]) + else + Shell.abort("Failed to initialize terraform - #{result[:output]}") + end + end + + def run_terraform_import(address, id) + result = Shell.cmd("terraform", "import", address, id, capture_stderr: true) + Shell.info(result[:output]) + end + + def resources + tf_configs.filter_map do |tf_config| + next unless tf_config.importable? + + { address: tf_config.reference, id: resource_id(tf_config) } + end + end + + def tf_configs + templates.flat_map do |template| + TerraformConfig::Generator.new(config: config, template: template).tf_configs.values + end + end + + def resource_id(tf_config) + case tf_config + when TerraformConfig::Gvc, TerraformConfig::Policy, + TerraformConfig::Secret, TerraformConfig::Agent, + TerraformConfig::AuditContext + tf_config.name + else + "#{config.app}:#{tf_config.name}" + end + end + + def terraform_app_dir + terraform_dir.join(config.app) + end + end + end +end diff --git a/lib/core/shell.rb b/lib/core/shell.rb index f743fba0..3e44f692 100644 --- a/lib/core/shell.rb +++ b/lib/core/shell.rb @@ -5,10 +5,6 @@ class << self attr_reader :tmp_stderr, :verbose end - def self.shell - @shell ||= Thor::Shell::Color.new - end - def self.use_tmp_stderr @tmp_stderr = Tempfile.create @@ -35,6 +31,10 @@ def self.confirm(message) shell.yes?("#{message} (y/N)") end + def self.info(message) + shell.say(message) + end + def self.warn(message) Kernel.warn(color("WARNING: #{message}", :yellow)) end @@ -97,4 +97,9 @@ def self.trap_interrupt exit(ExitCode::INTERRUPT) end end + + def self.shell + @shell ||= Thor::Shell::Color.new + end + private_class_method :shell end diff --git a/lib/core/terraform_config/agent.rb b/lib/core/terraform_config/agent.rb index 08508e1d..608dd698 100644 --- a/lib/core/terraform_config/agent.rb +++ b/lib/core/terraform_config/agent.rb @@ -12,6 +12,14 @@ def initialize(name:, description: nil, tags: nil) @tags = tags end + def importable? + true + end + + def reference + "cpln_agent.#{name}" + end + def to_tf block :resource, :cpln_agent, name do argument :name, name diff --git a/lib/core/terraform_config/audit_context.rb b/lib/core/terraform_config/audit_context.rb index d79eb79c..b3406f20 100644 --- a/lib/core/terraform_config/audit_context.rb +++ b/lib/core/terraform_config/audit_context.rb @@ -12,6 +12,14 @@ def initialize(name:, description: nil, tags: nil) @tags = tags end + def importable? + true + end + + def reference + "cpln_audit_context.#{name}" + end + def to_tf block :resource, :cpln_audit_context, name do argument :name, name diff --git a/lib/core/terraform_config/base.rb b/lib/core/terraform_config/base.rb index ffc96fac..c89f9215 100644 --- a/lib/core/terraform_config/base.rb +++ b/lib/core/terraform_config/base.rb @@ -6,6 +6,14 @@ module TerraformConfig class Base include Dsl + def importable? + false + end + + def reference + raise NotImplementedError if importable? + end + def to_tf raise NotImplementedError end diff --git a/lib/core/terraform_config/gvc.rb b/lib/core/terraform_config/gvc.rb index 606ee23d..0d33ab5f 100644 --- a/lib/core/terraform_config/gvc.rb +++ b/lib/core/terraform_config/gvc.rb @@ -26,6 +26,14 @@ def initialize( # rubocop:disable Metrics/ParameterLists @load_balancer = load_balancer&.deep_underscore_keys&.deep_symbolize_keys end + def importable? + true + end + + def reference + "cpln_gvc.#{name}" + end + def to_tf block :resource, :cpln_gvc, name do argument :name, name diff --git a/lib/core/terraform_config/identity.rb b/lib/core/terraform_config/identity.rb index a211ff46..36da00f3 100644 --- a/lib/core/terraform_config/identity.rb +++ b/lib/core/terraform_config/identity.rb @@ -13,6 +13,14 @@ def initialize(gvc:, name:, description: nil, tags: nil) @tags = tags end + def importable? + true + end + + def reference + "cpln_identity.#{name}" + end + def to_tf block :resource, :cpln_identity, name do argument :gvc, gvc diff --git a/lib/core/terraform_config/policy.rb b/lib/core/terraform_config/policy.rb index 65d0692a..e825fab6 100644 --- a/lib/core/terraform_config/policy.rb +++ b/lib/core/terraform_config/policy.rb @@ -41,6 +41,14 @@ def initialize( # rubocop:disable Metrics/ParameterLists, Metrics/MethodLength @bindings = bindings&.map { |data| data.deep_underscore_keys.deep_symbolize_keys } end + def importable? + true + end + + def reference + "cpln_policy.#{name}" + end + def to_tf block :resource, :cpln_policy, name do argument :name, name diff --git a/lib/core/terraform_config/secret.rb b/lib/core/terraform_config/secret.rb index efbb7515..844cccc3 100644 --- a/lib/core/terraform_config/secret.rb +++ b/lib/core/terraform_config/secret.rb @@ -26,6 +26,14 @@ def initialize(name:, type:, data:, description: nil, tags: nil) @data = prepare_data(type: type, data: data) end + def importable? + true + end + + def reference + "cpln_secret.#{name}" + end + def to_tf block :resource, :cpln_secret, name do argument :name, name diff --git a/lib/core/terraform_config/volume_set.rb b/lib/core/terraform_config/volume_set.rb index f1357808..c4d88e8a 100644 --- a/lib/core/terraform_config/volume_set.rb +++ b/lib/core/terraform_config/volume_set.rb @@ -38,6 +38,14 @@ def initialize( # rubocop:disable Metrics/ParameterLists, Metrics/MethodLength validate_attributes! end + def importable? + true + end + + def reference + "cpln_volume_set.#{name}" + end + def to_tf block :resource, :cpln_volume_set, name do base_arguments_tf diff --git a/lib/core/terraform_config/workload.rb b/lib/core/terraform_config/workload.rb index 8b997c19..ac0c4432 100644 --- a/lib/core/terraform_config/workload.rb +++ b/lib/core/terraform_config/workload.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module TerraformConfig - class Workload < Base + class Workload < Base # rubocop:disable Metrics/ClassLength RAW_ARGS = %i[ containers options local_options rollout_options security_options firewall_spec load_balancer job @@ -64,6 +64,14 @@ def initialize( # rubocop:disable Metrics/ParameterLists, Metrics/MethodLength @job = job end + def importable? + true + end + + def reference + "module.#{name}.cpln_workload.workload" + end + def to_tf block :module, name do argument :source, "../workload" diff --git a/lib/cpflow.rb b/lib/cpflow.rb index 4b352784..49047c25 100644 --- a/lib/cpflow.rb +++ b/lib/cpflow.rb @@ -17,6 +17,7 @@ # We need to require base before all commands, since the commands inherit from it require_relative "command/base" +require_relative "command/terraform/base" # We need to require base terraform config before all commands, since the terraform configs inherit from it require_relative "core/terraform_config/base" diff --git a/spec/command/terraform/import_spec.rb b/spec/command/terraform/import_spec.rb new file mode 100644 index 00000000..0e3516f1 --- /dev/null +++ b/spec/command/terraform/import_spec.rb @@ -0,0 +1,164 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe Command::Terraform::Import do + let(:import_command) { described_class.new(config) } + + let(:config) { instance_double(Config, app: "test-app", apps: { "test-app" => {} }) } + let(:terraform_dir) { Pathname.new("/fake/terraform/dir") } + + before do + allow(import_command).to receive(:terraform_dir).and_return(terraform_dir) + end + + describe "#call" do + subject(:call) { import_command.call } + + before do + allow(import_command).to receive(:resources).and_return( + [ + { address: "cpln_gvc.test-app", id: "test-app" }, + { address: "module.main.cpln_workload.workload", id: "test-app:main" } + ] + ) + allow(import_command).to receive(:run_terraform_init) + allow(import_command).to receive(:run_terraform_import) + end + + it "initializes terraform and imports resources" do + expect(Dir).to receive(:chdir).with(terraform_dir.join(config.app)).and_yield # rubocop:disable RSpec/MessageSpies + + call + + expect(import_command).to have_received(:run_terraform_init) + expect(import_command).to have_received(:run_terraform_import).with("cpln_gvc.test-app", "test-app") + expect(import_command).to have_received(:run_terraform_import).with( + "module.main.cpln_workload.workload", + "test-app:main" + ) + end + end + + describe "#run_terraform_init" do + subject(:terraform_init) { import_command.send(:run_terraform_init) } + + before do + allow(Shell).to receive(:info) + allow(Shell).to receive(:abort) + end + + context "when initialization succeeds" do + before do + stub_terraform_init_with(true, "Terraform initialized") + end + + it "logs success message" do + terraform_init + + expect(Shell).to have_received(:info).with("Terraform initialized") + end + end + + context "when initialization fails" do + before do + stub_terraform_init_with(false, "Initialization failed") + end + + it "aborts with error message" do + terraform_init + + expect(Shell).to have_received(:abort).with("Failed to initialize terraform - Initialization failed") + end + end + + def stub_terraform_init_with(success, output) + allow(Shell).to receive(:cmd).with("terraform", "init", capture_stderr: true).and_return( + success: success, output: output + ) + end + end + + describe "#resources" do + before do + allow(import_command).to receive(:tf_configs).and_return( + [ + verified_double(TerraformConfig::Gvc, importable?: true, reference: "cpln_gvc.app", name: "app"), + verified_double( + TerraformConfig::Workload, + importable?: true, + reference: "module.main.cpln_workload.workload", + name: "main" + ), + verified_double(TerraformConfig::LocalVariable, importable?: false) + ] + ) + end + + it "returns only importable resources with correct format" do + expect(import_command.send(:resources)).to contain_exactly( + { address: "cpln_gvc.app", id: "app" }, + { address: "module.main.cpln_workload.workload", id: "test-app:main" } + ) + end + end + + describe "#run_terraform_import" do + subject(:terraform_import) { import_command.send(:run_terraform_import, resource_address, resource_id) } + + let(:resource_address) { "resource_address" } + let(:resource_id) { "resource_id" } + + before do + allow(Shell).to receive(:cmd).and_call_original + allow(Shell).to receive(:info) + allow(Shell).to receive(:abort) + end + + context "when import succeeds" do + before do + stub_terraform_import_with(true, "Import successful") + end + + it "logs the success message" do + terraform_import + + expect(Shell).to have_received(:info).with("Import successful") + end + end + + context "when import fails" do + before do + stub_terraform_import_with(false, "Import failed") + end + + it "logs error" do + terraform_import + + expect(Shell).to have_received(:info).with("Import failed") + end + end + + context "with special characters in resource address and resource id" do + let(:resource_address) { "cpln_gvc.test-app;rm -rf /" } + let(:resource_id) { "test-app;rm -rf /" } + + it "is protected from shell injection" do + terraform_import + + expect(Shell).to have_received(:cmd).with( + "terraform", "import", resource_address, "test-app;rm -rf /", + capture_stderr: true + ) + + expect(Shell).to have_received(:info).with(/Invalid character/) + end + end + + def stub_terraform_import_with(success, output) + allow(Shell).to receive(:cmd) + .with("terraform", "import", resource_address, resource_id, capture_stderr: true) + .and_return(success: success, output: output) + end + end +end diff --git a/spec/core/terraform_config/agent_spec.rb b/spec/core/terraform_config/agent_spec.rb index efd9228b..227fbe4f 100644 --- a/spec/core/terraform_config/agent_spec.rb +++ b/spec/core/terraform_config/agent_spec.rb @@ -5,17 +5,17 @@ describe TerraformConfig::Agent do let(:config) { described_class.new(**options) } + let(:options) do + { + name: "agent-name", + description: "agent description", + tags: { "tag1" => "true", "tag2" => "value" } + } + end + describe "#to_tf" do subject(:generated) { config.to_tf } - let(:options) do - { - name: "agent-name", - description: "agent description", - tags: { "tag1" => "true", "tag2" => "value" } - } - end - it "generates correct config" do expect(generated).to eq( <<~EXPECTED @@ -31,4 +31,6 @@ ) end end + + it_behaves_like "importable terraform resource", reference: "cpln_agent.agent-name" end diff --git a/spec/core/terraform_config/audit_context_spec.rb b/spec/core/terraform_config/audit_context_spec.rb index 92e4b2be..47c4415f 100644 --- a/spec/core/terraform_config/audit_context_spec.rb +++ b/spec/core/terraform_config/audit_context_spec.rb @@ -5,17 +5,17 @@ describe TerraformConfig::AuditContext do let(:config) { described_class.new(**options) } + let(:options) do + { + name: "audit-context-name", + description: "audit context description", + tags: { "tag1" => "true", "tag2" => "value" } + } + end + describe "#to_tf" do subject(:generated) { config.to_tf } - let(:options) do - { - name: "audit-context-name", - description: "audit context description", - tags: { "tag1" => "true", "tag2" => "value" } - } - end - it "generates correct config" do expect(generated).to eq( <<~EXPECTED @@ -31,4 +31,6 @@ ) end end + + it_behaves_like "importable terraform resource", reference: "cpln_audit_context.audit-context-name" end diff --git a/spec/core/terraform_config/gvc_spec.rb b/spec/core/terraform_config/gvc_spec.rb index e9b8f935..998121ae 100644 --- a/spec/core/terraform_config/gvc_spec.rb +++ b/spec/core/terraform_config/gvc_spec.rb @@ -5,48 +5,48 @@ describe TerraformConfig::Gvc do let(:config) { described_class.new(**options) } + 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, "trustedProxies" => 1 } + } + end + 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, "trustedProxies" => 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 - } + 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 } - EXPECTED - ) - end + 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 + + it_behaves_like "importable terraform resource", reference: "cpln_gvc.gvc-name" end diff --git a/spec/core/terraform_config/identity_spec.rb b/spec/core/terraform_config/identity_spec.rb index ca39eeac..fcb27c67 100644 --- a/spec/core/terraform_config/identity_spec.rb +++ b/spec/core/terraform_config/identity_spec.rb @@ -5,18 +5,18 @@ describe TerraformConfig::Identity do let(:config) { described_class.new(**options) } + let(:options) do + { + gvc: "cpln_gvc.some-gvc.name", + name: "identity-name", + description: "identity description", + tags: { "tag1" => "true", "tag2" => "false" } + } + end + 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 @@ -33,4 +33,6 @@ ) end end + + it_behaves_like "importable terraform resource", reference: "cpln_identity.identity-name" end diff --git a/spec/core/terraform_config/local_variable_spec.rb b/spec/core/terraform_config/local_variable_spec.rb index 9c09f269..a84c3bd8 100644 --- a/spec/core/terraform_config/local_variable_spec.rb +++ b/spec/core/terraform_config/local_variable_spec.rb @@ -5,6 +5,19 @@ describe TerraformConfig::LocalVariable do let(:config) { described_class.new(**variables) } + let(:variables) do + { + hash_var: { + key1: "value1", + key2: "value2" + }, + int_var: 1, + string_var: "string", + input_var: "var.input_var", + local_var: "local.local_var" + } + end + describe "#initialize" do context "when variables are empty" do let(:variables) { {} } @@ -18,19 +31,6 @@ describe "#to_tf" do subject(:generated) { config.to_tf } - let(:variables) do - { - hash_var: { - key1: "value1", - key2: "value2" - }, - int_var: 1, - string_var: "string", - input_var: "var.input_var", - local_var: "local.local_var" - } - end - it "generates correct config" do expect(generated).to eq( <<~EXPECTED @@ -48,4 +48,6 @@ ) end end + + it_behaves_like "unimportable terraform resource" end diff --git a/spec/core/terraform_config/policy_spec.rb b/spec/core/terraform_config/policy_spec.rb index f0b742a1..69685e1f 100644 --- a/spec/core/terraform_config/policy_spec.rb +++ b/spec/core/terraform_config/policy_spec.rb @@ -5,6 +5,9 @@ describe TerraformConfig::Policy do let(:config) { described_class.new(**base_options.merge(extra_options)) } + let(:base_options) { { name: "policy-name" } } + let(:extra_options) { {} } + describe "#to_tf" do subject(:generated) { config.to_tf } @@ -177,4 +180,6 @@ end end end + + it_behaves_like "importable terraform resource", reference: "cpln_policy.policy-name" end diff --git a/spec/core/terraform_config/provider_spec.rb b/spec/core/terraform_config/provider_spec.rb index 990b396c..7d1821ba 100644 --- a/spec/core/terraform_config/provider_spec.rb +++ b/spec/core/terraform_config/provider_spec.rb @@ -5,12 +5,12 @@ describe TerraformConfig::Provider do let(:config) { described_class.new(name: name, **options) } - describe "#to_tf" do - subject(:generated) { config.to_tf } + context "when provider is cpln" do + let(:name) { "cpln" } + let(:options) { { org: "test-org" } } - context "when provider is cpln" do - let(:name) { "cpln" } - let(:options) { { org: "test-org" } } + describe "#to_tf" do + subject(:generated) { config.to_tf } it "generates correct config" do expect(generated).to eq( @@ -22,5 +22,7 @@ ) end end + + it_behaves_like "unimportable terraform resource" end end diff --git a/spec/core/terraform_config/required_provider_spec.rb b/spec/core/terraform_config/required_provider_spec.rb index 867af4bd..4f93c7d4 100644 --- a/spec/core/terraform_config/required_provider_spec.rb +++ b/spec/core/terraform_config/required_provider_spec.rb @@ -5,13 +5,13 @@ describe TerraformConfig::RequiredProvider do let(:config) { described_class.new(name: name, org: org, **options) } - describe "#to_tf" do - subject(:generated) { config.to_tf } + context "when provider is cpln" do + let(:name) { "cpln" } + let(:org) { "test-org" } + let(:options) { { source: "controlplane-com/cpln", version: "~> 1.0" } } - context "when provider is cpln" do - let(:name) { "cpln" } - let(:org) { "test-org" } - let(:options) { { source: "controlplane-com/cpln", version: "~> 1.0" } } + describe "#to_tf" do + subject(:generated) { config.to_tf } it "generates correct config" do expect(generated).to eq( @@ -28,5 +28,7 @@ ) end end + + it_behaves_like "unimportable terraform resource" end end diff --git a/spec/core/terraform_config/secret_spec.rb b/spec/core/terraform_config/secret_spec.rb index 673a1c0f..827da3f2 100644 --- a/spec/core/terraform_config/secret_spec.rb +++ b/spec/core/terraform_config/secret_spec.rb @@ -13,6 +13,14 @@ } end + let(:type) { "dictionary" } + let(:data) do + { + "key1" => "value1", + "key2" => "value2" + } + end + describe "#to_tf" do subject(:generated) { config.to_tf } @@ -410,4 +418,6 @@ end end end + + it_behaves_like "importable terraform resource", reference: "cpln_secret.some-secret" end diff --git a/spec/core/terraform_config/volume_set_spec.rb b/spec/core/terraform_config/volume_set_spec.rb index 1a0c09f1..613d7d26 100644 --- a/spec/core/terraform_config/volume_set_spec.rb +++ b/spec/core/terraform_config/volume_set_spec.rb @@ -182,4 +182,6 @@ end end end + + it_behaves_like "importable terraform resource", reference: "cpln_volume_set.test-volume-set" end diff --git a/spec/core/terraform_config/workload_spec.rb b/spec/core/terraform_config/workload_spec.rb index a4ed93b5..86d26ecc 100644 --- a/spec/core/terraform_config/workload_spec.rb +++ b/spec/core/terraform_config/workload_spec.rb @@ -445,6 +445,8 @@ module "main" { end end + it_behaves_like "importable terraform resource", reference: "module.main.cpln_workload.workload" + def postgres_container # rubocop:disable Metrics/MethodLength { name: "postgres", diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 1a319a27..7de71b5e 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -130,6 +130,7 @@ config.include CommandHelpers config.include DateTimeHelpers config.include StubENV + config.include VerifiedDouble config.verbose_retry = true config.display_try_failure_messages = true diff --git a/spec/support/shared_examples/terraform_config/importable_terraform_resource.rb b/spec/support/shared_examples/terraform_config/importable_terraform_resource.rb new file mode 100644 index 00000000..6ee6ae22 --- /dev/null +++ b/spec/support/shared_examples/terraform_config/importable_terraform_resource.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +RSpec.shared_examples_for "unimportable terraform resource" do + describe "#importable?" do + subject { config.importable? } + + it { is_expected.to be(false) } + end + + describe "#reference" do + subject { config.reference } + + it { is_expected.to be_nil } + end +end + +RSpec.shared_examples_for "importable terraform resource" do |reference:| + describe "#importable?" do + subject { config.importable? } + + it { is_expected.to be(true) } + end + + describe "#reference" do + subject { config.reference } + + it { is_expected.to eq(reference) } + end +end diff --git a/spec/support/verified_double.rb b/spec/support/verified_double.rb new file mode 100644 index 00000000..b5af3edf --- /dev/null +++ b/spec/support/verified_double.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module VerifiedDouble + CLASS_EQUIVALENCE_FUNCTIONS = %i[is_a? kind_of? instance_of?].freeze + + def verified_double(klass, *args) + instance_double(klass, *args).tap do |dbl| + CLASS_EQUIVALENCE_FUNCTIONS.each do |fn| + allow(dbl).to receive(fn) do |*fn_args| + klass.allocate.send(fn, *fn_args) + end + end + allow(klass).to receive(:===).and_call_original + allow(klass).to receive(:===).with(dbl).and_return true + end + end +end