From 46ac35f4e515544d311e2bdeaf592b86443e02dd Mon Sep 17 00:00:00 2001 From: Alishan Ladhani Date: Thu, 5 Mar 2020 15:05:59 -0500 Subject: [PATCH 1/9] Add lockbox gem To be used for encrypting terraform state files. See https://gitlab.com/gitlab-org/gitlab/issues/207401. --- Gemfile | 3 +++ Gemfile.lock | 2 ++ 2 files changed, 5 insertions(+) diff --git a/Gemfile b/Gemfile index 414a8a6774430d..53754392fcae9c 100644 --- a/Gemfile +++ b/Gemfile @@ -493,3 +493,6 @@ gem 'erubi', '~> 1.9.0' # Monkey-patched in `config/initializers/mail_encoding_patch.rb` # See https://gitlab.com/gitlab-org/gitlab/issues/197386 gem 'mail', '= 2.7.1' + +# File encryption +gem 'lockbox', '~> 0.3.3' diff --git a/Gemfile.lock b/Gemfile.lock index bb64fb09649fd4..9e5c29dc820825 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -605,6 +605,7 @@ GEM rb-inotify (~> 0.9, >= 0.9.7) ruby_dep (~> 1.2) locale (2.1.2) + lockbox (0.3.3) lograge (0.10.0) actionpack (>= 4) activesupport (>= 4) @@ -1280,6 +1281,7 @@ DEPENDENCIES license_finder (~> 5.4) licensee (~> 8.9) liquid (~> 4.0) + lockbox (~> 0.3.3) lograge (~> 0.5) loofah (~> 2.2) lru_redux -- GitLab From 0bf3c56d52792fda68980007e2a5af061b3263aa Mon Sep 17 00:00:00 2001 From: Alishan Ladhani Date: Thu, 5 Mar 2020 15:11:28 -0500 Subject: [PATCH 2/9] Create terraform state model This model will be used as part of GitLab's terraform state backend --- app/models/terraform_state.rb | 7 +++++ ...age-to-support-terraform-state-backend.yml | 5 ++++ .../20200305200641_create_terraform_states.rb | 14 +++++++++ db/structure.sql | 29 +++++++++++++++++++ spec/models/terraform_state_spec.rb | 9 ++++++ 5 files changed, 64 insertions(+) create mode 100644 app/models/terraform_state.rb create mode 100644 changelogs/unreleased/207401-encrypt-decrypt-object-storage-to-support-terraform-state-backend.yml create mode 100644 db/migrate/20200305200641_create_terraform_states.rb create mode 100644 spec/models/terraform_state_spec.rb diff --git a/app/models/terraform_state.rb b/app/models/terraform_state.rb new file mode 100644 index 00000000000000..a1966a97f61da6 --- /dev/null +++ b/app/models/terraform_state.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class TerraformState < ApplicationRecord + belongs_to :project + + validates :project_id, presence: true +end diff --git a/changelogs/unreleased/207401-encrypt-decrypt-object-storage-to-support-terraform-state-backend.yml b/changelogs/unreleased/207401-encrypt-decrypt-object-storage-to-support-terraform-state-backend.yml new file mode 100644 index 00000000000000..31d4c0d1587597 --- /dev/null +++ b/changelogs/unreleased/207401-encrypt-decrypt-object-storage-to-support-terraform-state-backend.yml @@ -0,0 +1,5 @@ +--- +title: Create model to store Terraform state files +merge_request: 26619 +author: +type: added diff --git a/db/migrate/20200305200641_create_terraform_states.rb b/db/migrate/20200305200641_create_terraform_states.rb new file mode 100644 index 00000000000000..3e137369e33127 --- /dev/null +++ b/db/migrate/20200305200641_create_terraform_states.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +class CreateTerraformStates < ActiveRecord::Migration[6.0] + DOWNTIME = false + + def change + create_table :terraform_states do |t| + t.references :project, index: true, foreign_key: { on_delete: :cascade }, null: false + t.timestamps_with_timezone null: false + t.integer :file_store, limit: 2 + t.string :file, limit: 255 + end + end +end diff --git a/db/structure.sql b/db/structure.sql index 6301ec6df80f4b..0bde7c7469e3a1 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -5975,6 +5975,24 @@ CREATE SEQUENCE public.term_agreements_id_seq ALTER SEQUENCE public.term_agreements_id_seq OWNED BY public.term_agreements.id; +CREATE TABLE public.terraform_states ( + id bigint NOT NULL, + project_id bigint NOT NULL, + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone NOT NULL, + file_store smallint, + file character varying(255) +); + +CREATE SEQUENCE public.terraform_states_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +ALTER SEQUENCE public.terraform_states_id_seq OWNED BY public.terraform_states.id; + CREATE TABLE public.timelogs ( id integer NOT NULL, time_spent integer NOT NULL, @@ -7327,6 +7345,8 @@ ALTER TABLE ONLY public.tags ALTER COLUMN id SET DEFAULT nextval('public.tags_id ALTER TABLE ONLY public.term_agreements ALTER COLUMN id SET DEFAULT nextval('public.term_agreements_id_seq'::regclass); +ALTER TABLE ONLY public.terraform_states ALTER COLUMN id SET DEFAULT nextval('public.terraform_states_id_seq'::regclass); + ALTER TABLE ONLY public.timelogs ALTER COLUMN id SET DEFAULT nextval('public.timelogs_id_seq'::regclass); ALTER TABLE ONLY public.todos ALTER COLUMN id SET DEFAULT nextval('public.todos_id_seq'::regclass); @@ -8228,6 +8248,9 @@ ALTER TABLE ONLY public.tags ALTER TABLE ONLY public.term_agreements ADD CONSTRAINT term_agreements_pkey PRIMARY KEY (id); +ALTER TABLE ONLY public.terraform_states + ADD CONSTRAINT terraform_states_pkey PRIMARY KEY (id); + ALTER TABLE ONLY public.timelogs ADD CONSTRAINT timelogs_pkey PRIMARY KEY (id); @@ -9966,6 +9989,8 @@ CREATE INDEX index_term_agreements_on_term_id ON public.term_agreements USING bt CREATE INDEX index_term_agreements_on_user_id ON public.term_agreements USING btree (user_id); +CREATE INDEX index_terraform_states_on_project_id ON public.terraform_states USING btree (project_id); + CREATE INDEX index_timelogs_on_issue_id ON public.timelogs USING btree (issue_id); CREATE INDEX index_timelogs_on_merge_request_id ON public.timelogs USING btree (merge_request_id); @@ -11271,6 +11296,9 @@ ALTER TABLE ONLY public.pages_domain_acme_orders ALTER TABLE ONLY public.ci_subscriptions_projects ADD CONSTRAINT fk_rails_7871f9a97b FOREIGN KEY (upstream_project_id) REFERENCES public.projects(id) ON DELETE CASCADE; +ALTER TABLE ONLY public.terraform_states + ADD CONSTRAINT fk_rails_78f54ca485 FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE; + ALTER TABLE ONLY public.software_license_policies ADD CONSTRAINT fk_rails_7a7a2a92de FOREIGN KEY (software_license_id) REFERENCES public.software_licenses(id) ON DELETE CASCADE; @@ -12742,6 +12770,7 @@ COPY "schema_migrations" (version) FROM STDIN; 20200304211738 20200305121159 20200305151736 +20200305200641 20200306095654 20200306160521 20200306170211 diff --git a/spec/models/terraform_state_spec.rb b/spec/models/terraform_state_spec.rb new file mode 100644 index 00000000000000..b7a482cc1219b0 --- /dev/null +++ b/spec/models/terraform_state_spec.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe TerraformState do + it { is_expected.to belong_to(:project) } + + it { is_expected.to validate_presence_of(:project_id) } +end -- GitLab From 750b5b033d9cf12c0872e963aed814ac5ffe8163 Mon Sep 17 00:00:00 2001 From: Alishan Ladhani Date: Fri, 6 Mar 2020 14:55:56 -0500 Subject: [PATCH 3/9] Create terraform state uploader Uses lockbox gem to encrypt and decrypt files Encryption is necessary because terraform state files contain secrets --- app/models/terraform_state.rb | 2 + app/uploaders/terraform_state_uploader.rb | 24 ++++++++++ spec/factories/terraform_state.rb | 11 +++++ spec/fixtures/terraform.tfstate | 8 ++++ .../terraform_state_uploader_spec.rb | 46 +++++++++++++++++++ 5 files changed, 91 insertions(+) create mode 100644 app/uploaders/terraform_state_uploader.rb create mode 100644 spec/factories/terraform_state.rb create mode 100644 spec/fixtures/terraform.tfstate create mode 100644 spec/uploaders/terraform_state_uploader_spec.rb diff --git a/app/models/terraform_state.rb b/app/models/terraform_state.rb index a1966a97f61da6..34ae5d79ae95f7 100644 --- a/app/models/terraform_state.rb +++ b/app/models/terraform_state.rb @@ -4,4 +4,6 @@ class TerraformState < ApplicationRecord belongs_to :project validates :project_id, presence: true + + mount_uploader :file, TerraformStateUploader end diff --git a/app/uploaders/terraform_state_uploader.rb b/app/uploaders/terraform_state_uploader.rb new file mode 100644 index 00000000000000..673cf892acc055 --- /dev/null +++ b/app/uploaders/terraform_state_uploader.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +class TerraformStateUploader < GitlabUploader + include ObjectStorage::Concern + + storage_options Gitlab.config.uploads + + delegate :project_id, to: :model + + # Use Lockbox to encrypt/decrypt the stored file (registers CarrierWave callbacks) + encrypt(key: :key) + + def filename + "#{model.id}.tfstate" + end + + def store_dir + project_id.to_s + end + + def key + OpenSSL::HMAC.digest('SHA256', Gitlab::Application.secrets.db_key_base, project_id.to_s) + end +end diff --git a/spec/factories/terraform_state.rb b/spec/factories/terraform_state.rb new file mode 100644 index 00000000000000..8df1b070581e62 --- /dev/null +++ b/spec/factories/terraform_state.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :terraform_state do + project { create(:project) } + + trait :with_file do + file { fixture_file_upload('spec/fixtures/terraform.tfstate') } + end + end +end diff --git a/spec/fixtures/terraform.tfstate b/spec/fixtures/terraform.tfstate new file mode 100644 index 00000000000000..3384d9eb005fc3 --- /dev/null +++ b/spec/fixtures/terraform.tfstate @@ -0,0 +1,8 @@ +{ + "version": 4, + "terraform_version": "0.12.21", + "serial": 1, + "lineage": "25e05991-243d-28d6-ebe3-ee0baae462cf", + "outputs": {}, + "resources": [] +} \ No newline at end of file diff --git a/spec/uploaders/terraform_state_uploader_spec.rb b/spec/uploaders/terraform_state_uploader_spec.rb new file mode 100644 index 00000000000000..c1ae98e7175e80 --- /dev/null +++ b/spec/uploaders/terraform_state_uploader_spec.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe TerraformStateUploader do + subject { terraform_state.file } + + let(:terraform_state) { create(:terraform_state, file: fixture_file_upload('spec/fixtures/terraform.tfstate')) } + + before do + stub_uploads_object_storage + end + + describe '#filename' do + it 'contains the ID of the terraform state record' do + expect(subject.filename).to include(terraform_state.id.to_s) + end + end + + describe '#store_dir' do + it 'contains the ID of the project' do + expect(subject.store_dir).to include(terraform_state.project_id.to_s) + end + end + + describe '#key' do + it 'creates a digest with a secret key and the project id' do + expect(OpenSSL::HMAC) + .to receive(:digest) + .with('SHA256', Gitlab::Application.secrets.db_key_base, terraform_state.project_id.to_s) + .and_return('digest') + + expect(subject.key).to eq('digest') + end + end + + describe 'encryption' do + it 'encrypts the stored file' do + expect(subject.file.read).not_to eq(fixture_file('terraform.tfstate')) + end + + it 'decrypts the file when reading' do + expect(subject.read).to eq(fixture_file('terraform.tfstate')) + end + end +end -- GitLab From b21e823c33d8ee3d3502d670d1abeb93c5a78d59 Mon Sep 17 00:00:00 2001 From: Alishan Ladhani Date: Mon, 9 Mar 2020 18:38:15 -0400 Subject: [PATCH 4/9] Add setting for terraform state storage --- config/gitlab.yml.example | 31 +++++++++++++++++++++++++++++++ config/initializers/1_settings.rb | 8 ++++++++ 2 files changed, 39 insertions(+) diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index eb41e4ac423348..b66389b1a6f9de 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -320,6 +320,24 @@ production: &base # aws_signature_version: 4 # For creation of signed URLs. Set to 2 if provider does not support v4. # path_style: true # Use 'host/bucket_name/object' instead of 'bucket_name.host/object' + ## Terraform state + terraform_state: + enabled: true + # The location where Terraform state files are stored (default: shared/terraform_state). + # storage_path: shared/terraform_state + object_store: + enabled: false + remote_directory: terraform_state # The bucket name + connection: + provider: AWS + aws_access_key_id: AWS_ACCESS_KEY_ID + aws_secret_access_key: AWS_SECRET_ACCESS_KEY + region: us-east-1 + # host: 'localhost' # default: s3.amazonaws.com + # endpoint: 'http://127.0.0.1:9000' # default: nil + # aws_signature_version: 4 # For creation of signed URLs. Set to 2 if provider does not support v4. + # path_style: true # Use 'host/bucket_name/object' instead of 'bucket_name.host/object' + ## GitLab Pages pages: enabled: false @@ -1193,6 +1211,19 @@ test: aws_access_key_id: AWS_ACCESS_KEY_ID aws_secret_access_key: AWS_SECRET_ACCESS_KEY region: us-east-1 + + terraform_state: + enabled: true + storage_path: tmp/tests/terraform_state + object_store: + enabled: false + remote_directory: terraform_state + connection: + provider: AWS # Only AWS supported at the moment + aws_access_key_id: AWS_ACCESS_KEY_ID + aws_secret_access_key: AWS_SECRET_ACCESS_KEY + region: us-east-1 + gitlab: host: localhost port: 80 diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 79bfcfd79e1378..f31762d9ac6ea4 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -369,6 +369,14 @@ Settings.dependency_proxy['enabled'] = false unless Gitlab::Runtime.puma? end +# +# Terraform state +# +Settings['terraform_state'] ||= Settingslogic.new({}) +Settings.terraform_state['enabled'] = true if Settings.terraform_state['enabled'].nil? +Settings.terraform_state['storage_path'] = Settings.absolute(Settings.terraform_state['storage_path'] || File.join(Settings.shared['path'], "terraform_state")) +Settings.terraform_state['object_store'] = ObjectStoreSettings.parse(Settings.terraform_state['object_store']) + # # Mattermost # -- GitLab From a33f0126c07a9b63be26bf997e1b53bf8bc134a5 Mon Sep 17 00:00:00 2001 From: Alishan Ladhani Date: Mon, 9 Mar 2020 18:47:59 -0400 Subject: [PATCH 5/9] Use terraform_state storage config --- app/uploaders/terraform_state_uploader.rb | 2 +- spec/support/helpers/stub_object_storage.rb | 7 +++++++ spec/uploaders/terraform_state_uploader_spec.rb | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/app/uploaders/terraform_state_uploader.rb b/app/uploaders/terraform_state_uploader.rb index 673cf892acc055..7b2225808b2743 100644 --- a/app/uploaders/terraform_state_uploader.rb +++ b/app/uploaders/terraform_state_uploader.rb @@ -3,7 +3,7 @@ class TerraformStateUploader < GitlabUploader include ObjectStorage::Concern - storage_options Gitlab.config.uploads + storage_options Gitlab.config.terraform_state delegate :project_id, to: :model diff --git a/spec/support/helpers/stub_object_storage.rb b/spec/support/helpers/stub_object_storage.rb index 392300a443637e..d4ac286e959661 100644 --- a/spec/support/helpers/stub_object_storage.rb +++ b/spec/support/helpers/stub_object_storage.rb @@ -70,6 +70,13 @@ def stub_uploads_object_storage(uploader = described_class, **params) **params) end + def stub_terraform_state_object_storage(uploader = described_class, **params) + stub_object_storage_uploader(config: Gitlab.config.terraform_state.object_store, + uploader: uploader, + remote_directory: 'terraform_state', + **params) + end + def stub_object_storage_multipart_init(endpoint, upload_id = "upload_id") stub_request(:post, %r{\A#{endpoint}tmp/uploads/[a-z0-9-]*\?uploads\z}) .to_return status: 200, body: <<-EOS.strip_heredoc diff --git a/spec/uploaders/terraform_state_uploader_spec.rb b/spec/uploaders/terraform_state_uploader_spec.rb index c1ae98e7175e80..428febb0ba8ff2 100644 --- a/spec/uploaders/terraform_state_uploader_spec.rb +++ b/spec/uploaders/terraform_state_uploader_spec.rb @@ -8,7 +8,7 @@ let(:terraform_state) { create(:terraform_state, file: fixture_file_upload('spec/fixtures/terraform.tfstate')) } before do - stub_uploads_object_storage + stub_terraform_state_object_storage end describe '#filename' do -- GitLab From 487c3ae92c2f266a65d6a9d6782493ba3f24a3db Mon Sep 17 00:00:00 2001 From: Alishan Ladhani Date: Thu, 12 Mar 2020 17:35:18 -0400 Subject: [PATCH 6/9] Hardcode object storage settings - direct_upload is false because files need to be encrypted before uploading - background_upload is false because files need to be available immediately - proxy_download is true because downloads need to be decrypted before being sent to the user --- app/uploaders/terraform_state_uploader.rb | 14 ++++++++++++++ .../uploaders/terraform_state_uploader_spec.rb | 18 ++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/app/uploaders/terraform_state_uploader.rb b/app/uploaders/terraform_state_uploader.rb index 7b2225808b2743..4ecc6ca4f931fd 100644 --- a/app/uploaders/terraform_state_uploader.rb +++ b/app/uploaders/terraform_state_uploader.rb @@ -21,4 +21,18 @@ def store_dir def key OpenSSL::HMAC.digest('SHA256', Gitlab::Application.secrets.db_key_base, project_id.to_s) end + + class << self + def direct_upload_enabled? + false + end + + def background_upload_enabled? + false + end + + def proxy_download_enabled? + true + end + end end diff --git a/spec/uploaders/terraform_state_uploader_spec.rb b/spec/uploaders/terraform_state_uploader_spec.rb index 428febb0ba8ff2..4db221b9a18c62 100644 --- a/spec/uploaders/terraform_state_uploader_spec.rb +++ b/spec/uploaders/terraform_state_uploader_spec.rb @@ -43,4 +43,22 @@ expect(subject.read).to eq(fixture_file('terraform.tfstate')) end end + + describe '.direct_upload_enabled?' do + it 'returns false' do + expect(described_class.direct_upload_enabled?).to eq(false) + end + end + + describe '.background_upload_enabled?' do + it 'returns false' do + expect(described_class.background_upload_enabled?).to eq(false) + end + end + + describe '.proxy_download_enabled?' do + it 'returns true' do + expect(described_class.proxy_download_enabled?).to eq(true) + end + end end -- GitLab From 24002101c6895e2d285ed9638ab5432e1cd0bd5e Mon Sep 17 00:00:00 2001 From: Alishan Ladhani Date: Thu, 12 Mar 2020 19:29:47 -0400 Subject: [PATCH 7/9] Make TerraformState work with object storage - Default `file_store` to the terraform_state object storage setting - Update `file_store` whenever the file changes --- app/models/terraform_state.rb | 12 ++++++ app/uploaders/terraform_state_uploader.rb | 4 ++ spec/models/terraform_state_spec.rb | 43 +++++++++++++++++++ .../terraform_state_uploader_spec.rb | 18 ++++++++ 4 files changed, 77 insertions(+) diff --git a/app/models/terraform_state.rb b/app/models/terraform_state.rb index 34ae5d79ae95f7..eaf4c4cad988ea 100644 --- a/app/models/terraform_state.rb +++ b/app/models/terraform_state.rb @@ -5,5 +5,17 @@ class TerraformState < ApplicationRecord validates :project_id, presence: true + after_save :update_file_store, if: :saved_change_to_file? + mount_uploader :file, TerraformStateUploader + + def update_file_store + # The file.object_store is set during `uploader.store!` + # which happens after object is inserted/updated + self.update_column(:file_store, file.object_store) + end + + def file_store + super || TerraformStateUploader.default_store + end end diff --git a/app/uploaders/terraform_state_uploader.rb b/app/uploaders/terraform_state_uploader.rb index 4ecc6ca4f931fd..d597307cfb082a 100644 --- a/app/uploaders/terraform_state_uploader.rb +++ b/app/uploaders/terraform_state_uploader.rb @@ -34,5 +34,9 @@ def background_upload_enabled? def proxy_download_enabled? true end + + def default_store + object_store_enabled? ? ObjectStorage::Store::REMOTE : ObjectStorage::Store::LOCAL + end end end diff --git a/spec/models/terraform_state_spec.rb b/spec/models/terraform_state_spec.rb index b7a482cc1219b0..64e3efd1637d6f 100644 --- a/spec/models/terraform_state_spec.rb +++ b/spec/models/terraform_state_spec.rb @@ -3,7 +3,50 @@ require 'spec_helper' describe TerraformState do + subject { create(:terraform_state, :with_file) } + it { is_expected.to belong_to(:project) } it { is_expected.to validate_presence_of(:project_id) } + + before do + stub_terraform_state_object_storage(TerraformStateUploader) + end + + describe '#file_store' do + context 'when no value is set' do + it 'returns the default store of the uploader' do + [ObjectStorage::Store::LOCAL, ObjectStorage::Store::REMOTE].each do |store| + expect(TerraformStateUploader).to receive(:default_store).and_return(store) + expect(described_class.new.file_store).to eq(store) + end + end + end + + context 'when a value is set' do + it 'returns the value' do + [ObjectStorage::Store::LOCAL, ObjectStorage::Store::REMOTE].each do |store| + expect(build(:terraform_state, file_store: store).file_store).to eq(store) + end + end + end + end + + describe '#update_file_store' do + context 'when file is stored in object storage' do + it 'sets file_store to remote' do + expect(subject.file_store).to eq(ObjectStorage::Store::REMOTE) + end + end + + context 'when file is stored locally' do + before do + stub_terraform_state_object_storage(Terraform::StateUploader, enabled: false) + end + + it 'sets file_store to local' do + expect(subject.file_store).to eq(ObjectStorage::Store::LOCAL) + end + end + end end diff --git a/spec/uploaders/terraform_state_uploader_spec.rb b/spec/uploaders/terraform_state_uploader_spec.rb index 4db221b9a18c62..e1c34038dd278b 100644 --- a/spec/uploaders/terraform_state_uploader_spec.rb +++ b/spec/uploaders/terraform_state_uploader_spec.rb @@ -61,4 +61,22 @@ expect(described_class.proxy_download_enabled?).to eq(true) end end + + describe '.default_store' do + context 'when object storage is enabled' do + it 'returns REMOTE' do + expect(described_class.default_store).to eq(ObjectStorage::Store::REMOTE) + end + end + + context 'when object storage is disabled' do + before do + stub_terraform_state_object_storage(enabled: false) + end + + it 'returns LOCAL' do + expect(described_class.default_store).to eq(ObjectStorage::Store::LOCAL) + end + end + end end -- GitLab From 5b797471923f391668fcd3841abb418c5c334037 Mon Sep 17 00:00:00 2001 From: Alishan Ladhani Date: Mon, 16 Mar 2020 21:07:33 -0400 Subject: [PATCH 8/9] Move files into Terraform module - Model - Uploader - Tests --- app/models/terraform.rb | 7 +++ app/models/terraform/state.rb | 23 ++++++++++ app/models/terraform_state.rb | 21 --------- app/uploaders/terraform/state_uploader.rb | 44 +++++++++++++++++++ app/uploaders/terraform_state_uploader.rb | 42 ------------------ spec/factories/terraform/state.rb | 11 +++++ spec/factories/terraform_state.rb | 11 ----- .../{ => terraform}/terraform.tfstate | 0 .../state_spec.rb} | 6 +-- .../state_uploader_spec.rb} | 8 ++-- 10 files changed, 92 insertions(+), 81 deletions(-) create mode 100644 app/models/terraform.rb create mode 100644 app/models/terraform/state.rb delete mode 100644 app/models/terraform_state.rb create mode 100644 app/uploaders/terraform/state_uploader.rb delete mode 100644 app/uploaders/terraform_state_uploader.rb create mode 100644 spec/factories/terraform/state.rb delete mode 100644 spec/factories/terraform_state.rb rename spec/fixtures/{ => terraform}/terraform.tfstate (100%) rename spec/models/{terraform_state_spec.rb => terraform/state_spec.rb} (87%) rename spec/uploaders/{terraform_state_uploader_spec.rb => terraform/state_uploader_spec.rb} (90%) diff --git a/app/models/terraform.rb b/app/models/terraform.rb new file mode 100644 index 00000000000000..3d974ffe0510f1 --- /dev/null +++ b/app/models/terraform.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module Terraform + def self.table_name_prefix + 'terraform_' + end +end diff --git a/app/models/terraform/state.rb b/app/models/terraform/state.rb new file mode 100644 index 00000000000000..8ca4ee9239a81e --- /dev/null +++ b/app/models/terraform/state.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Terraform + class State < ApplicationRecord + belongs_to :project + + validates :project_id, presence: true + + after_save :update_file_store, if: :saved_change_to_file? + + mount_uploader :file, StateUploader + + def update_file_store + # The file.object_store is set during `uploader.store!` + # which happens after object is inserted/updated + self.update_column(:file_store, file.object_store) + end + + def file_store + super || StateUploader.default_store + end + end +end diff --git a/app/models/terraform_state.rb b/app/models/terraform_state.rb deleted file mode 100644 index eaf4c4cad988ea..00000000000000 --- a/app/models/terraform_state.rb +++ /dev/null @@ -1,21 +0,0 @@ -# frozen_string_literal: true - -class TerraformState < ApplicationRecord - belongs_to :project - - validates :project_id, presence: true - - after_save :update_file_store, if: :saved_change_to_file? - - mount_uploader :file, TerraformStateUploader - - def update_file_store - # The file.object_store is set during `uploader.store!` - # which happens after object is inserted/updated - self.update_column(:file_store, file.object_store) - end - - def file_store - super || TerraformStateUploader.default_store - end -end diff --git a/app/uploaders/terraform/state_uploader.rb b/app/uploaders/terraform/state_uploader.rb new file mode 100644 index 00000000000000..9c5ae8a8bdc52e --- /dev/null +++ b/app/uploaders/terraform/state_uploader.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +module Terraform + class StateUploader < GitlabUploader + include ObjectStorage::Concern + + storage_options Gitlab.config.terraform_state + + delegate :project_id, to: :model + + # Use Lockbox to encrypt/decrypt the stored file (registers CarrierWave callbacks) + encrypt(key: :key) + + def filename + "#{model.id}.tfstate" + end + + def store_dir + project_id.to_s + end + + def key + OpenSSL::HMAC.digest('SHA256', Gitlab::Application.secrets.db_key_base, project_id.to_s) + end + + class << self + def direct_upload_enabled? + false + end + + def background_upload_enabled? + false + end + + def proxy_download_enabled? + true + end + + def default_store + object_store_enabled? ? ObjectStorage::Store::REMOTE : ObjectStorage::Store::LOCAL + end + end + end +end diff --git a/app/uploaders/terraform_state_uploader.rb b/app/uploaders/terraform_state_uploader.rb deleted file mode 100644 index d597307cfb082a..00000000000000 --- a/app/uploaders/terraform_state_uploader.rb +++ /dev/null @@ -1,42 +0,0 @@ -# frozen_string_literal: true - -class TerraformStateUploader < GitlabUploader - include ObjectStorage::Concern - - storage_options Gitlab.config.terraform_state - - delegate :project_id, to: :model - - # Use Lockbox to encrypt/decrypt the stored file (registers CarrierWave callbacks) - encrypt(key: :key) - - def filename - "#{model.id}.tfstate" - end - - def store_dir - project_id.to_s - end - - def key - OpenSSL::HMAC.digest('SHA256', Gitlab::Application.secrets.db_key_base, project_id.to_s) - end - - class << self - def direct_upload_enabled? - false - end - - def background_upload_enabled? - false - end - - def proxy_download_enabled? - true - end - - def default_store - object_store_enabled? ? ObjectStorage::Store::REMOTE : ObjectStorage::Store::LOCAL - end - end -end diff --git a/spec/factories/terraform/state.rb b/spec/factories/terraform/state.rb new file mode 100644 index 00000000000000..4b83128ff6edb6 --- /dev/null +++ b/spec/factories/terraform/state.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :terraform_state, class: 'Terraform::State' do + project { create(:project) } + + trait :with_file do + file { fixture_file_upload('spec/fixtures/terraform/terraform.tfstate') } + end + end +end diff --git a/spec/factories/terraform_state.rb b/spec/factories/terraform_state.rb deleted file mode 100644 index 8df1b070581e62..00000000000000 --- a/spec/factories/terraform_state.rb +++ /dev/null @@ -1,11 +0,0 @@ -# frozen_string_literal: true - -FactoryBot.define do - factory :terraform_state do - project { create(:project) } - - trait :with_file do - file { fixture_file_upload('spec/fixtures/terraform.tfstate') } - end - end -end diff --git a/spec/fixtures/terraform.tfstate b/spec/fixtures/terraform/terraform.tfstate similarity index 100% rename from spec/fixtures/terraform.tfstate rename to spec/fixtures/terraform/terraform.tfstate diff --git a/spec/models/terraform_state_spec.rb b/spec/models/terraform/state_spec.rb similarity index 87% rename from spec/models/terraform_state_spec.rb rename to spec/models/terraform/state_spec.rb index 64e3efd1637d6f..1d677e7ece517f 100644 --- a/spec/models/terraform_state_spec.rb +++ b/spec/models/terraform/state_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe TerraformState do +describe Terraform::State do subject { create(:terraform_state, :with_file) } it { is_expected.to belong_to(:project) } @@ -10,14 +10,14 @@ it { is_expected.to validate_presence_of(:project_id) } before do - stub_terraform_state_object_storage(TerraformStateUploader) + stub_terraform_state_object_storage(Terraform::StateUploader) end describe '#file_store' do context 'when no value is set' do it 'returns the default store of the uploader' do [ObjectStorage::Store::LOCAL, ObjectStorage::Store::REMOTE].each do |store| - expect(TerraformStateUploader).to receive(:default_store).and_return(store) + expect(Terraform::StateUploader).to receive(:default_store).and_return(store) expect(described_class.new.file_store).to eq(store) end end diff --git a/spec/uploaders/terraform_state_uploader_spec.rb b/spec/uploaders/terraform/state_uploader_spec.rb similarity index 90% rename from spec/uploaders/terraform_state_uploader_spec.rb rename to spec/uploaders/terraform/state_uploader_spec.rb index e1c34038dd278b..4577a2c4738215 100644 --- a/spec/uploaders/terraform_state_uploader_spec.rb +++ b/spec/uploaders/terraform/state_uploader_spec.rb @@ -2,10 +2,10 @@ require 'spec_helper' -describe TerraformStateUploader do +describe Terraform::StateUploader do subject { terraform_state.file } - let(:terraform_state) { create(:terraform_state, file: fixture_file_upload('spec/fixtures/terraform.tfstate')) } + let(:terraform_state) { create(:terraform_state, file: fixture_file_upload('spec/fixtures/terraform/terraform.tfstate')) } before do stub_terraform_state_object_storage @@ -36,11 +36,11 @@ describe 'encryption' do it 'encrypts the stored file' do - expect(subject.file.read).not_to eq(fixture_file('terraform.tfstate')) + expect(subject.file.read).not_to eq(fixture_file('terraform/terraform.tfstate')) end it 'decrypts the file when reading' do - expect(subject.read).to eq(fixture_file('terraform.tfstate')) + expect(subject.read).to eq(fixture_file('terraform/terraform.tfstate')) end end -- GitLab From e17d5bf16e183b30c8b4e57bcac65468c2f62b67 Mon Sep 17 00:00:00 2001 From: Alishan Ladhani Date: Thu, 19 Mar 2020 16:01:07 -0400 Subject: [PATCH 9/9] Document terraform state storage settings --- doc/administration/terraform_state.md | 135 ++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 doc/administration/terraform_state.md diff --git a/doc/administration/terraform_state.md b/doc/administration/terraform_state.md new file mode 100644 index 00000000000000..c684178f13ea14 --- /dev/null +++ b/doc/administration/terraform_state.md @@ -0,0 +1,135 @@ +# Terraform state administration (alpha) + +> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/2673) in GitLab 12.10. + +GitLab can be used as a backend for [Terraform](../user/infrastructure/index.md) state +files. The files are encrypted before being stored. This feature is enabled by default. + +The storage location of these files defaults to: + +- `/var/opt/gitlab/gitlab-rails/shared/terraform_state` for Omnibus GitLab installations. +- `/home/git/gitlab/shared/terraform_state` for source installations. + +These locations can be configured using the options described below. + +## Using local storage + +NOTE: **Note:** +This is the default configuration + +To change the location where Terraform state files are stored locally, follow the steps +below. + +**In Omnibus installations:** + +1. To change the storage path for example to `/mnt/storage/terraform_state`, edit + `/etc/gitlab/gitlab.rb` and add the following line: + + ```ruby + gitlab_rails['terraform_state_enabled'] = true + gitlab_rails['terraform_state_storage_path'] = "/mnt/storage/terraform_state" + ``` + +1. Save the file and [reconfigure GitLab][] for the changes to take effect. + +**In installations from source:** + +1. To change the storage path for example to `/mnt/storage/terraform_state`, edit + `/home/git/gitlab/config/gitlab.yml` and add or amend the following lines: + + ```yaml + terraform_state: + enabled: true + storage_path: /mnt/storage/terraform_state + ``` + +1. Save the file and [restart GitLab][] for the changes to take effect. + +## Using object storage **(CORE ONLY)** + +Instead of storing Terraform state files on disk, we recommend the use of an object +store that is S3-compatible instead. This configuration relies on valid credentials to +be configured already. + +### Object storage settings + +The following settings are: + +- Nested under `terraform_state:` and then `object_store:` on source installations. +- Prefixed by `terraform_state_object_store_` on Omnibus GitLab installations. + +| Setting | Description | Default | +|---------|-------------|---------| +| `enabled` | Enable/disable object storage | `true` | +| `remote_directory` | The bucket name where Terraform state files will be stored | | +| `connection` | Various connection options described below | | + +### S3-compatible connection settings + +The connection settings match those provided by [Fog](https://github.com/fog), and are as follows: + +| Setting | Description | Default | +|---------|-------------|---------| +| `provider` | Always `AWS` for compatible hosts | `AWS` | +| `aws_access_key_id` | Credentials for AWS or compatible provider | | +| `aws_secret_access_key` | Credentials for AWS or compatible provider | | +| `aws_signature_version` | AWS signature version to use. 2 or 4 are valid options. Digital Ocean Spaces and other providers may need 2. | 4 | +| `enable_signature_v4_streaming` | Set to true to enable HTTP chunked transfers with [AWS v4 signatures](https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-streaming.html). Oracle Cloud S3 needs this to be false | `true` | +| `region` | AWS region | us-east-1 | +| `host` | S3-compatible host when not using AWS. For example, `localhost` or `storage.example.com` | `s3.amazonaws.com` | +| `endpoint` | Can be used when configuring an S3-compatible service such as [MinIO](https://min.io), by entering a URL such as `http://127.0.0.1:9000` | (optional) | +| `path_style` | Set to true to use `host/bucket_name/object` style paths instead of `bucket_name.host/object`. Leave as false for AWS S3 | `false` | +| `use_iam_profile` | For AWS S3, set to true to use an IAM profile instead of access keys | `false` | + +**In Omnibus installations:** + +1. Edit `/etc/gitlab/gitlab.rb` and add the following lines; replacing with + the values you want: + + ```ruby + gitlab_rails['terraform_state_enabled'] = true + gitlab_rails['terraform_state_object_store_enabled'] = true + gitlab_rails['terraform_state_object_store_remote_directory'] = "terraform_state" + gitlab_rails['terraform_state_object_store_connection'] = { + 'provider' => 'AWS', + 'region' => 'eu-central-1', + 'aws_access_key_id' => 'AWS_ACCESS_KEY_ID', + 'aws_secret_access_key' => 'AWS_SECRET_ACCESS_KEY' + } + ``` + + NOTE: **Note:** + If you are using AWS IAM profiles, be sure to omit the AWS access key and secret access key/value pairs. + + ```ruby + gitlab_rails['terraform_state_object_store_connection'] = { + 'provider' => 'AWS', + 'region' => 'eu-central-1', + 'use_iam_profile' => true + } + ``` + +1. Save the file and [reconfigure GitLab][] for the changes to take effect. + +**In installations from source:** + +1. Edit `/home/git/gitlab/config/gitlab.yml` and add or amend the following + lines: + + ```yaml + terraform_state: + enabled: true + object_store: + enabled: true + remote_directory: "terraform_state" # The bucket name + connection: + provider: AWS # Only AWS supported at the moment + aws_access_key_id: AWS_ACESS_KEY_ID + aws_secret_access_key: AWS_SECRET_ACCESS_KEY + region: eu-central-1 + ``` + +1. Save the file and [restart GitLab][] for the changes to take effect. + +[reconfigure gitlab]: restart_gitlab.md#omnibus-gitlab-reconfigure "How to reconfigure Omnibus GitLab" +[restart gitlab]: restart_gitlab.md#installations-from-source "How to restart GitLab" -- GitLab