diff --git a/devops-client/lib/devops-client/handler/stack.rb b/devops-client/lib/devops-client/handler/stack.rb index c04d2ad..d681a45 100644 --- a/devops-client/lib/devops-client/handler/stack.rb +++ b/devops-client/lib/devops-client/handler/stack.rb @@ -50,8 +50,19 @@ class Stack < Handler env = fetcher.fetch_project(q[:project])['deploy_envs'].detect {|env| env['identifier'] == q[:deploy_env]} q[:provider] = env['provider'] - filepath = options[:parameters_file] || enter_parameter(I18n.t('handler.stack.create.parameters_file')) - q[:parameters] = JSON.parse(File.read(filepath)) + params_filepath = options[:parameters_file] || enter_parameter_or_empty(I18n.t('handler.stack.create.parameters_file')) + if params_filepath.empty? + q[:parameters] = {} + else + q[:parameters] = JSON.parse(File.read(params_filepath)) + end + + tags_filepath = options[:tags_file] || enter_parameter_or_empty(I18n.t('handler.stack.create.tags_file')) + if tags_filepath.empty? + q[:tags] = {} + else + q[:tags] = JSON.parse(File.read(tags_filepath)) + end json = JSON.pretty_generate(q) if question(I18n.t("handler.stack.question.create")) {puts json} diff --git a/devops-client/lib/devops-client/options/stack_options.rb b/devops-client/lib/devops-client/options/stack_options.rb index 1c2daa5..435f855 100644 --- a/devops-client/lib/devops-client/options/stack_options.rb +++ b/devops-client/lib/devops-client/options/stack_options.rb @@ -28,6 +28,7 @@ class StackOptions < CommonOptions parser.recognize_option_value(:deploy_env) parser.recognize_option_value(:stack_template) parser.recognize_option_value(:parameters_file) + parser.recognize_option_value(:tags_file) parser.recognize_option_value(:run_list) parser.recognize_option_value(:without_bootstrap, type: :switch, switch_value: true) end diff --git a/devops-client/locales/en.yml b/devops-client/locales/en.yml index 383076e..a4a968b 100644 --- a/devops-client/locales/en.yml +++ b/devops-client/locales/en.yml @@ -115,8 +115,9 @@ en: create: id: "Id: " deploy_env: "Deploy env: " - parameters_file: "Path to file with JSON parameters: " + parameters_file: "Path to file with parameters in JSON (or Enter): " run_list: "Run list: " + tags_file: "Path to file with tags in JSON (or Enter): " question: create: "Are you sure to create stack?" delete: "Are you sure to delete stack '%{name}'?" @@ -374,6 +375,7 @@ en: project: Stack project stack_template: Stack template parameters_file: File with parameters JSON + tags_file: File with tags JSON run_list: Stack run list without_bootstrap: Skip bootsraping phase on created instances stack_template: diff --git a/devops-service/app/api2/routes/stack.rb b/devops-service/app/api2/routes/stack.rb index 8d484c8..6db26fc 100644 --- a/devops-service/app/api2/routes/stack.rb +++ b/devops-service/app/api2/routes/stack.rb @@ -21,6 +21,29 @@ module Devops json Devops::API2_0::Handler::Stack.new(request).stack_servers(name).map(&:to_hash) end + # Create stack + # + # * *Request* + # - method : PATCH + # - headers : + # - Accept: application/json + # - Content-Type: application/json + # - body : + # { + # "without_bootstrap": null, + # "project": "project_name", + # "deploy_env": "test", + # "provider": "ec2", + # "tags": { + # "tagName": "tagValue" + # }, + # "parameters": { + # "KeyName": "Value" + # } + # } + # + # * *Returns* : + # [report_id] app.post_with_headers "/stack", :headers => [:accept, :content_type] do check_privileges("stack", "w") json Devops::API2_0::Handler::Stack.new(request).create_stack diff --git a/devops-service/db/mongo/models/stack/stack_base.rb b/devops-service/db/mongo/models/stack/stack_base.rb index b9b2537..a6a882e 100644 --- a/devops-service/db/mongo/models/stack/stack_base.rb +++ b/devops-service/db/mongo/models/stack/stack_base.rb @@ -41,6 +41,9 @@ module Devops ::Validators::FieldValidator::FieldType::Array, ::Validators::FieldValidator::RunList] + set_field_validators :tags, [::Validators::FieldValidator::Nil, + ::Validators::FieldValidator::FieldType::Hash] + def initialize attrs={} self.set_provider(attrs) self.id = attrs['id'] @@ -51,6 +54,7 @@ module Devops self.parameters = attrs['parameters'] self.owner = attrs['owner'] self.run_list = attrs['run_list'] || [] + self.tags = attrs['tags'] || {} self.stack_status = attrs['stack_status'] self end @@ -65,7 +69,8 @@ module Devops parameters: parameters, stack_status: stack_status, owner: owner, - run_list: run_list + run_list: run_list, + tags: tags }.merge(provider_hash) end diff --git a/devops-service/lib/executors/server_executor.rb b/devops-service/lib/executors/server_executor.rb index 08e4dc3..a776018 100644 --- a/devops-service/lib/executors/server_executor.rb +++ b/devops-service/lib/executors/server_executor.rb @@ -262,8 +262,8 @@ module Devops return error_code(:server_not_in_chef_nodes) end else - # @out << roll_back - # mongo.server_delete @server.id + roll_back + mongo.server_delete @server.id msg = "Failed while bootstraping server with id '#{@server.id}'\n" msg << "Bootstraping operation result was #{bootstrap_status}" DevopsLogger.logger.error msg @@ -510,7 +510,7 @@ module Devops def schedule_expiration if @deploy_env.expires - @out << "Planning expiration in #{@deploy_env.expires}" + puts_and_flush "Planning expiration in #{@deploy_env.expires}" ExpirationScheduler.new(@deploy_env.expires, @server).schedule_expiration! end end diff --git a/devops-service/providers/ec2.rb b/devops-service/providers/ec2.rb index f882c68..d07b699 100644 --- a/devops-service/providers/ec2.rb +++ b/devops-service/providers/ec2.rb @@ -220,6 +220,7 @@ module Provider out << "Stack name: #{stack.name}\n" out << "Stack template: #{stack.stack_template}\n" out << "Stack parameters: #{stack.parameters}\n" + out << "Stack tags: #{JSON.pretty_generate(stack_tags(stack))}\n" out.flush response = cloud_formation.create_stack(stack.name, { @@ -254,7 +255,7 @@ module Provider "cid:project" => stack.project, "cid:deployEnv" => stack.deploy_env, "cid:user" => stack.owner - } + }.merge(stack.tags) end def update_stack(stack, params) diff --git a/devops-service/spec/executors/server_executor_spec.rb b/devops-service/spec/executors/server_executor_spec.rb index ce8135f..bc272ee 100644 --- a/devops-service/spec/executors/server_executor_spec.rb +++ b/devops-service/spec/executors/server_executor_spec.rb @@ -366,10 +366,19 @@ RSpec.describe Devops::Executor::ServerExecutor, type: :executor, stubbed_connec end context "when bootstrap wasn't successful" do - it 'returns :server_bootstrap_fail error code' do + before do allow(executor).to receive(:bootstrap) { 1 } + allow(executor).to receive(:roll_back) + end + + it 'returns :server_bootstrap_fail error code' do expect(two_phase_bootstrap).to eq 2 end + + it 'rollbacks server' do + allow(executor).to receive(:roll_back) + two_phase_bootstrap + end end context 'when an error occured during bootstrap' do diff --git a/devops-service/spec/models/stack/stack_ec2_spec.rb b/devops-service/spec/models/stack/stack_ec2_spec.rb index 8e68708..ddfb952 100644 --- a/devops-service/spec/models/stack/stack_ec2_spec.rb +++ b/devops-service/spec/models/stack/stack_ec2_spec.rb @@ -19,6 +19,7 @@ RSpec.describe Devops::Model::StackEc2, type: :model do include_examples 'field type validation', :name, :maybe_nil, :non_empty_string, :field_validator include_examples 'field type validation', :owner, :not_nil, :non_empty_string, :field_validator include_examples 'field type validation', :run_list, :maybe_nil, :maybe_empty_array, :run_list, :field_validator + include_examples 'field type validation', :tags, :maybe_nil, :field_validator it 'validates provider' do stack.provider = nil @@ -27,11 +28,13 @@ RSpec.describe Devops::Model::StackEc2, type: :model do stack.provider = '' expect(stack).not_to be_valid end + + pending 'stack tags should be a hash' end describe '#to_hash_without_id' do it 'returns hash with several fields' do - expect(stack.to_hash_without_id.keys).to include('provider', :project, :deploy_env, :stack_template, :name, :owner, :run_list) + expect(stack.to_hash_without_id.keys).to include('provider', :project, :deploy_env, :stack_template, :name, :owner, :run_list, :tags) end end @@ -43,6 +46,10 @@ RSpec.describe Devops::Model::StackEc2, type: :model do it 'sets run_list to empty array if it\'s blank' do expect(described_class.new.run_list).to eq [] end + + it 'sets tags to empty hash if it\'s blank' do + expect(described_class.new.tags).to eq({}) + end end diff --git a/devops-service/spec/workers/chef_node_name_builder_spec.rb b/devops-service/spec/workers/chef_node_name_builder_spec.rb index 246dcab..ef14d0c 100644 --- a/devops-service/spec/workers/chef_node_name_builder_spec.rb +++ b/devops-service/spec/workers/chef_node_name_builder_spec.rb @@ -1,45 +1,69 @@ require 'workers/stack_bootstrap/chef_node_name_builder' RSpec.describe ChefNodeNameBuilder do + # real response let(:server_info) do { - 'id' => 'server1', - 'name' => 'server_name', - 'key_name' => 'key', - 'private_ip' => '127.0.0.1', - 'public_ip' => '127.0.0.2', - 'tags' => { - 'cid:priority' => '3' + "name"=>"stack-achuchkalov-aws-test-1455976199-master01", + "id"=>"i-fac32c7e", + "key_name"=>"achuchkalov", + "private_ip"=>"172.31.11.30", + "public_ip"=>"52.90.250.51", + "tags" => { + "Name"=>"master01", + "aws:cloudformation:logical-id"=>"EC2Instance1", + "aws:cloudformation:stack-name"=>"stack-achuchkalov-aws-test-1455976199", + "StackTemplate"=>"1inst", + "aws:cloudformation:stack-id" => "arn:aws:cloudformation:us-east-1:736558555923:stack/stack-achuchkalov-aws-test-1455976199/d5f3ca60-d7d8-11e5-9ba1-50d5cd24fac6", + "cid:deployEnv"=>"test", + "cid:project"=>"aws", + "cid:user"=>"root", + "cid:priority"=>0 } } end let(:project) { build(:project, id: 'proj', with_deploy_env_identifiers: %w(dev)) } let(:env) { project.deploy_env('dev') } - let(:name_builder) { described_class.new(server_info, project, env) } - let(:build_name) { name_builder.build_node_name } + let(:build_node_name) { + described_class.new(server_info, project, env).build_node_name + } def set_mask(mask) server_info['tags']['cid:node-name-mask'] = mask end describe '#build_node_name' do - it 'uses default mask ("$project-$nodename-$env")' do - expect(build_name).to eq 'proj-server1-dev' + it 'uses default mask ("$project-$cfname-$env")' do + expect(build_node_name).to eq 'proj-master01-dev' end - it 'substitutes project, env and nodename' do - set_mask('$project/$env/$nodename') - expect(build_name).to eq 'proj/dev/server1' + it 'substitutes $project, $env, $instanceid and $cfname' do + set_mask('$project/$env/$instanceid/$cfname') + expect(build_node_name).to eq 'proj/dev/i-fac32c7e/master01' end it 'substitutes $time' do - set_mask('$nodename-$time') - expect(build_name).to match /server1-\d+/ + set_mask('$project-$time') + expect(build_node_name).to match /proj-\d+/ + end + + it 'substitutes :project, :env, :instanceid and :cfname' do + set_mask(':project/:env/:instanceid/:cfname') + expect(build_node_name).to eq 'proj/dev/i-fac32c7e/master01' + end + + it 'substitutes :time' do + set_mask(':project-:time') + expect(build_node_name).to match /proj-\d+/ + end + + it 'works with both colon and dollar variables' do + set_mask('$project/$env/:instanceid/:cfname') + expect(build_node_name).to eq 'proj/dev/i-fac32c7e/master01' end it 'substitutes underscores to dashes' do - server_info['id'] = 'server_1' - expect(build_name).to match 'proj-server-1-dev' + server_info['tags']['Name'] = 'server_1' + expect(build_node_name).to match 'proj-server-1-dev' end end - end \ No newline at end of file diff --git a/devops-service/spec/workers/stack_servers_persister_spec.rb b/devops-service/spec/workers/stack_servers_persister_spec.rb index c24ba8b..00a844e 100644 --- a/devops-service/spec/workers/stack_servers_persister_spec.rb +++ b/devops-service/spec/workers/stack_servers_persister_spec.rb @@ -15,7 +15,8 @@ RSpec.describe StackServersPersister, stubbed_connector: true do 'private_ip' => '127.0.0.1', 'public_ip' => '127.0.0.2', 'tags' => { - 'cid:priority' => '3' + 'cid:priority' => '3', + 'Name' => 'server1' } } end @@ -97,7 +98,7 @@ RSpec.describe StackServersPersister, stubbed_connector: true do persister.persist end - it 'build chef_node_name with default mask "$project-$nodename-$env"' do + it 'build chef_node_name with default mask "$project-$cfname-$env"' do expect(stubbed_connector).to receive(:server_insert) do |server| expect(server.chef_node_name).to eq 'name-server1-foo' end @@ -105,7 +106,7 @@ RSpec.describe StackServersPersister, stubbed_connector: true do end it "builds chef_node_name with custom mask if info['tags']['cid:node-name-mask'] exists" do - server_info_hash['tags']['cid:node-name-mask'] = '$project-$nodename-123' + server_info_hash['tags']['cid:node-name-mask'] = '$project-$cfname-123' expect(stubbed_connector).to receive(:server_insert) do |server| expect(server.chef_node_name).to eq 'name-server1-123' end diff --git a/devops-service/workers/stack_bootstrap/chef_node_name_builder.rb b/devops-service/workers/stack_bootstrap/chef_node_name_builder.rb index 660b23a..6315696 100644 --- a/devops-service/workers/stack_bootstrap/chef_node_name_builder.rb +++ b/devops-service/workers/stack_bootstrap/chef_node_name_builder.rb @@ -1,15 +1,34 @@ class ChefNodeNameBuilder + DEFAULT_MASK = '$project-$cfname-$env' + def initialize(server_info, project, env) @server_info, @project, @env = server_info, project, env - @mask = server_info['tags']['cid:node-name-mask'] || '$project-$nodename-$env' + @mask = server_info['tags']['cid:node-name-mask'] || DEFAULT_MASK end def build_node_name - @mask.gsub!('$project', @project.id) - @mask.gsub!('$env', @env.identifier) - @mask.gsub!('$nodename', @server_info['id']) - @mask.gsub!('$time', Time.now.to_i.to_s) - @mask.gsub!('_', '-') - @mask + result = @mask.dup + replace_dollar_variables!(result) + replace_colon_variables!(result) + result.gsub!('_', '-') + result + end + + private + + def replace_dollar_variables!(result) + result.gsub!('$project', @project.id) + result.gsub!('$env', @env.identifier) + result.gsub!('$instanceid', @server_info['id']) + result.gsub!('$cfname', @server_info['tags']['Name'] || '') + result.gsub!('$time', Time.now.to_i.to_s) + end + + def replace_colon_variables!(result) + result.gsub!(':project', @project.id) + result.gsub!(':env', @env.identifier) + result.gsub!(':instanceid', @server_info['id']) + result.gsub!(':cfname', @server_info['tags']['Name'] || '') + result.gsub!(':time', Time.now.to_i.to_s) end end \ No newline at end of file