Commit 7b276cb7 authored by Phil Hughes's avatar Phil Hughes

Merge branch...

Merge branch '323245-provide-terraform-backend-environment-variables-in-operations-terraform-ui' into 'master'

Provide Terraform backend environment variables in Operations-->Terraform UI

See merge request gitlab-org/gitlab!67417
parents 93d7f2ac 596e56ce
<script>
import { GlModal, GlSprintf, GlLink } from '@gitlab/ui';
import { __, s__ } from '~/locale';
import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
export default {
i18n: {
title: s__('Terraform|Terraform init command'),
explanatoryText: s__(
`Terraform|To get access to this terraform state from your local computer, run the following command at the command line. The first line requires a personal access token with API read and write access. %{linkStart}How do I create a personal access token?%{linkEnd}.`,
),
closeText: __('Close'),
copyToClipboardText: __('Copy'),
},
components: {
GlModal,
GlSprintf,
GlLink,
ModalCopyButton,
},
inject: ['accessTokensPath', 'terraformApiUrl', 'username'],
props: {
modalId: {
type: String,
required: true,
},
stateName: {
type: String,
required: true,
},
},
computed: {
closeModalProps() {
return {
text: this.$options.i18n.closeText,
attributes: [],
};
},
},
methods: {
getModalInfoCopyStr() {
return `export GITLAB_ACCESS_TOKEN=<YOUR-ACCESS-TOKEN>
terraform init \\
-backend-config="address=${this.terraformApiUrl}/${this.stateName}" \\
-backend-config="lock_address=${this.terraformApiUrl}/${this.stateName}/lock" \\
-backend-config="unlock_address=${this.terraformApiUrl}/${this.stateName}/lock" \\
-backend-config="username=${this.username}" \\
-backend-config="password=$GITLAB_ACCESS_TOKEN" \\
-backend-config="lock_method=POST" \\
-backend-config="unlock_method=DELETE" \\
-backend-config="retry_wait_min=5"
`;
},
},
};
</script>
<template>
<gl-modal
ref="initCommandModal"
:modal-id="modalId"
:title="$options.i18n.title"
:action-cancel="closeModalProps"
>
<p data-testid="init-command-explanatory-text">
<gl-sprintf :message="$options.i18n.explanatoryText">
<template #link="{ content }">
<gl-link :href="accessTokensPath" target="_blank">{{ content }}</gl-link>
</template>
</gl-sprintf>
</p>
<div class="gl-display-flex">
<pre class="gl-bg-gray gl-white-space-pre-wrap" data-testid="terraform-init-command">{{
getModalInfoCopyStr()
}}</pre>
<modal-copy-button
:title="$options.i18n.copyToClipboardText"
:text="getModalInfoCopyStr()"
:modal-id="$options.modalId"
data-testid="init-command-copy-clipboard"
css-classes="gl-align-self-start gl-ml-2"
/>
</div>
</gl-modal>
</template>
......@@ -8,12 +8,14 @@ import {
GlIcon,
GlModal,
GlSprintf,
GlModalDirective,
} from '@gitlab/ui';
import { s__, sprintf } from '~/locale';
import addDataToState from '../graphql/mutations/add_data_to_state.mutation.graphql';
import lockState from '../graphql/mutations/lock_state.mutation.graphql';
import removeState from '../graphql/mutations/remove_state.mutation.graphql';
import unlockState from '../graphql/mutations/unlock_state.mutation.graphql';
import InitCommandModal from './init_command_modal.vue';
export default {
components: {
......@@ -25,6 +27,10 @@ export default {
GlIcon,
GlModal,
GlSprintf,
InitCommandModal,
},
directives: {
GlModalDirective,
},
props: {
state: {
......@@ -36,6 +42,7 @@ export default {
return {
showRemoveModal: false,
removeConfirmText: '',
showCommandModal: false,
};
},
i18n: {
......@@ -54,6 +61,7 @@ export default {
remove: s__('Terraform|Remove state file and versions'),
removeSuccessful: s__('Terraform|%{name} successfully removed'),
unlock: s__('Terraform|Unlock'),
copyCommand: s__('Terraform|Copy Terraform init command'),
},
computed: {
cancelModalProps() {
......@@ -74,6 +82,9 @@ export default {
attributes: [{ disabled: this.disableModalSubmit }, { variant: 'danger' }],
};
},
commandModalId() {
return `init-command-modal-${this.state.name}`;
},
},
methods: {
hideModal() {
......@@ -164,6 +175,9 @@ export default {
});
});
},
copyInitCommand() {
this.showCommandModal = true;
},
},
};
</script>
......@@ -181,6 +195,14 @@ export default {
<gl-icon class="gl-mr-0" name="ellipsis_v" />
</template>
<gl-dropdown-item
v-gl-modal-directive="commandModalId"
data-testid="terraform-state-copy-init-command"
@click="copyInitCommand"
>
{{ $options.i18n.copyCommand }}
</gl-dropdown-item>
<gl-dropdown-item
v-if="state.latestVersion"
data-testid="terraform-state-download"
......@@ -248,5 +270,11 @@ export default {
/>
</gl-form-group>
</gl-modal>
<init-command-modal
v-if="showCommandModal"
:modal-id="commandModalId"
:state-name="state.name"
/>
</div>
</template>
......@@ -24,11 +24,16 @@ export default () => {
},
});
const { emptyStateImage, projectPath } = el.dataset;
const { emptyStateImage, projectPath, accessTokensPath, terraformApiUrl, username } = el.dataset;
return new Vue({
el,
apolloProvider: new VueApollo({ defaultClient }),
provide: {
accessTokensPath,
terraformApiUrl,
username,
},
render(createElement) {
return createElement(TerraformList, {
props: {
......
......@@ -5,7 +5,10 @@ module Projects::TerraformHelper
{
empty_state_image: image_path('illustrations/empty-state/empty-serverless-lg.svg'),
project_path: project.full_path,
terraform_admin: current_user&.can?(:admin_terraform_state, project)
terraform_admin: current_user&.can?(:admin_terraform_state, project),
access_tokens_path: profile_personal_access_tokens_path,
username: current_user&.username,
terraform_api_url: "#{Settings.gitlab.url}/api/v4/projects/#{project.id}/terraform/state"
}
end
end
......@@ -83,6 +83,14 @@ local machine, this is a simple way to get started:
-backend-config="retry_wait_min=5"
```
If you already have a GitLab-managed Terraform state, you can use the `terraform init` command
with the prepopulated parameters values:
1. On the top bar, select **Menu > Projects** and find your project.
1. On the left sidebar, select **Infrastructure > Terraform**.
1. Next to the environment you want to use, select the [Actions menu](#managing-state-files)
**{ellipsis_v}** and select **Copy Terraform init command**.
You can now run `terraform plan` and `terraform apply` as you normally would.
### Get started using GitLab CI
......@@ -222,7 +230,7 @@ An example setup is shown below:
```plaintext
example_remote_state_address=https://gitlab.com/api/v4/projects/<TARGET-PROJECT-ID>/terraform/state/<TARGET-STATE-NAME>
example_username=<GitLab username>
example_access_token=<GitLab Personal Acceess Token>
example_access_token=<GitLab Personal Access Token>
```
1. Define the data source by adding the following code block in a `.tf` file (such as `data.tf`):
......@@ -362,10 +370,8 @@ contains these fields:
state file is locked.
- **Pipeline**: A link to the most recent pipeline and its status.
- **Details**: Information about when the state file was created or changed.
- **Actions**: Actions you can take on the state file, including downloading,
locking, unlocking, or [removing](#remove-a-state-file) the state file and versions:
![Terraform state list](img/terraform_list_view_actions_v13_8.png)
- **Actions**: Actions you can take on the state file, including copying the `terraform init` command,
downloading, locking, unlocking, or [removing](#remove-a-state-file) the state file and versions.
NOTE:
Additional improvements to the
......
......@@ -32641,6 +32641,9 @@ msgstr ""
msgid "Terraform|Cancel"
msgstr ""
msgid "Terraform|Copy Terraform init command"
msgstr ""
msgid "Terraform|Details"
msgstr ""
......@@ -32692,12 +32695,18 @@ msgstr ""
msgid "Terraform|States"
msgstr ""
msgid "Terraform|Terraform init command"
msgstr ""
msgid "Terraform|The report %{name} failed to generate."
msgstr ""
msgid "Terraform|The report %{name} was generated in your pipelines."
msgstr ""
msgid "Terraform|To get access to this terraform state from your local computer, run the following command at the command line. The first line requires a personal access token with API read and write access. %{linkStart}How do I create a personal access token?%{linkEnd}."
msgstr ""
msgid "Terraform|To remove the State file and its versions, type %{name} to confirm:"
msgstr ""
......
......@@ -38,7 +38,7 @@ RSpec.describe 'Terraform', :js do
it 'displays a table with terraform states' do
expect(page).to have_selector(
'[data-testid="terraform-states-table-name"]',
"[data-testid='terraform-states-table-name']",
count: project.terraform_states.size
)
end
......@@ -64,7 +64,7 @@ RSpec.describe 'Terraform', :js do
expect(page).to have_content(additional_state.name)
find("[data-testid='terraform-state-actions-#{additional_state.name}']").click
find('[data-testid="terraform-state-remove"]').click
find("[data-testid='terraform-state-remove']").click
fill_in "terraform-state-remove-input-#{additional_state.name}", with: additional_state.name
click_button 'Remove'
......@@ -72,6 +72,21 @@ RSpec.describe 'Terraform', :js do
expect { additional_state.reload }.to raise_error ActiveRecord::RecordNotFound
end
end
context 'when clicking on copy Terraform init command' do
it 'shows the modal with the init command' do
visit project_terraform_index_path(project)
expect(page).to have_content(terraform_state.name)
page.within("[data-testid='terraform-state-actions-#{terraform_state.name}']") do
click_button class: 'gl-dropdown-toggle'
click_button 'Copy Terraform init command'
end
expect(page).to have_content("To get access to this terraform state from your local computer, run the following command at the command line.")
end
end
end
end
......@@ -87,11 +102,11 @@ RSpec.describe 'Terraform', :js do
context 'when user visits the index page' do
it 'displays a table without an action dropdown', :aggregate_failures do
expect(page).to have_selector(
'[data-testid="terraform-states-table-name"]',
"[data-testid='terraform-states-table-name']",
count: project.terraform_states.size
)
expect(page).not_to have_selector('[data-testid*="terraform-state-actions"]')
expect(page).not_to have_selector("[data-testid*='terraform-state-actions']")
end
end
end
......
import { GlLink, GlSprintf } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import InitCommandModal from '~/terraform/components/init_command_modal.vue';
import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
const accessTokensPath = '/path/to/access-tokens-page';
const terraformApiUrl = 'https://gitlab.com/api/v4/projects/1';
const username = 'username';
const modalId = 'fake-modal-id';
const stateName = 'production';
const modalInfoCopyStr = `export GITLAB_ACCESS_TOKEN=<YOUR-ACCESS-TOKEN>
terraform init \\
-backend-config="address=${terraformApiUrl}/${stateName}" \\
-backend-config="lock_address=${terraformApiUrl}/${stateName}/lock" \\
-backend-config="unlock_address=${terraformApiUrl}/${stateName}/lock" \\
-backend-config="username=${username}" \\
-backend-config="password=$GITLAB_ACCESS_TOKEN" \\
-backend-config="lock_method=POST" \\
-backend-config="unlock_method=DELETE" \\
-backend-config="retry_wait_min=5"
`;
describe('InitCommandModal', () => {
let wrapper;
const propsData = {
modalId,
stateName,
};
const provideData = {
accessTokensPath,
terraformApiUrl,
username,
};
const findExplanatoryText = () => wrapper.findByTestId('init-command-explanatory-text');
const findLink = () => wrapper.findComponent(GlLink);
const findInitCommand = () => wrapper.findByTestId('terraform-init-command');
const findCopyButton = () => wrapper.findComponent(ModalCopyButton);
beforeEach(() => {
wrapper = shallowMountExtended(InitCommandModal, {
propsData,
provide: provideData,
stubs: {
GlSprintf,
},
});
});
afterEach(() => {
wrapper.destroy();
});
describe('on rendering', () => {
it('renders the explanatory text', () => {
expect(findExplanatoryText().text()).toContain('personal access token');
});
it('renders the personal access token link', () => {
expect(findLink().attributes('href')).toBe(accessTokensPath);
});
it('renders the init command with the username and state name prepopulated', () => {
expect(findInitCommand().text()).toContain(username);
expect(findInitCommand().text()).toContain(stateName);
});
it('renders the copyToClipboard button', () => {
expect(findCopyButton().exists()).toBe(true);
});
});
describe('when copy button is clicked', () => {
it('copies init command to clipboard', () => {
expect(findCopyButton().props('text')).toBe(modalInfoCopyStr);
});
});
});
......@@ -3,6 +3,7 @@ import { createLocalVue, shallowMount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import InitCommandModal from '~/terraform/components/init_command_modal.vue';
import StateActions from '~/terraform/components/states_table_actions.vue';
import lockStateMutation from '~/terraform/graphql/mutations/lock_state.mutation.graphql';
import removeStateMutation from '~/terraform/graphql/mutations/remove_state.mutation.graphql';
......@@ -73,12 +74,14 @@ describe('StatesTableActions', () => {
return wrapper.vm.$nextTick();
};
const findActionsDropdown = () => wrapper.find(GlDropdown);
const findActionsDropdown = () => wrapper.findComponent(GlDropdown);
const findCopyBtn = () => wrapper.find('[data-testid="terraform-state-copy-init-command"]');
const findCopyModal = () => wrapper.findComponent(InitCommandModal);
const findLockBtn = () => wrapper.find('[data-testid="terraform-state-lock"]');
const findUnlockBtn = () => wrapper.find('[data-testid="terraform-state-unlock"]');
const findDownloadBtn = () => wrapper.find('[data-testid="terraform-state-download"]');
const findRemoveBtn = () => wrapper.find('[data-testid="terraform-state-remove"]');
const findRemoveModal = () => wrapper.find(GlModal);
const findRemoveModal = () => wrapper.findComponent(GlModal);
beforeEach(() => {
return createComponent();
......@@ -125,6 +128,25 @@ describe('StatesTableActions', () => {
});
});
describe('copy command button', () => {
it('displays a copy init command button', () => {
expect(findCopyBtn().text()).toBe('Copy Terraform init command');
});
describe('when clicking the copy init command button', () => {
beforeEach(() => {
findCopyBtn().vm.$emit('click');
return waitForPromises();
});
it('opens the modal', async () => {
expect(findCopyModal().exists()).toBe(true);
expect(findCopyModal().isVisible()).toBe(true);
});
});
});
describe('download button', () => {
it('displays a download button', () => {
expect(findDownloadBtn().text()).toBe('Download JSON');
......
......@@ -22,6 +22,18 @@ RSpec.describe Projects::TerraformHelper do
expect(subject[:project_path]).to eq(project.full_path)
end
it 'includes access token path' do
expect(subject[:access_tokens_path]).to eq(profile_personal_access_tokens_path)
end
it 'includes username' do
expect(subject[:username]).to eq(current_user.username)
end
it 'includes terraform state api url' do
expect(subject[:terraform_api_url]).to eq("#{Settings.gitlab.url}/api/v4/projects/#{project.id}/terraform/state")
end
it 'indicates the user is a terraform admin' do
expect(subject[:terraform_admin]).to eq(true)
end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment