diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-07-11 15:07:02 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-07-11 15:07:02 +0300 |
commit | bcd11d993d80d46053a97ee3b0344ed4d2b4571b (patch) | |
tree | e3b4047cafd580d3a3d7d8cde094c183ee9aabfc /app/assets/javascripts/environments | |
parent | 871b886a1794e5baefd6b2f96caf2ac4ce5da6ca (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts/environments')
14 files changed, 260 insertions, 19 deletions
diff --git a/app/assets/javascripts/environments/components/edit_environment.vue b/app/assets/javascripts/environments/components/edit_environment.vue index 9e3f8d996e0..a2405d23924 100644 --- a/app/assets/javascripts/environments/components/edit_environment.vue +++ b/app/assets/javascripts/environments/components/edit_environment.vue @@ -2,7 +2,9 @@ import { GlLoadingIcon } from '@gitlab/ui'; import { createAlert } from '~/alert'; import { visitUrl } from '~/lib/utils/url_utility'; +import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import getEnvironment from '../graphql/queries/environment.query.graphql'; +import getEnvironmentWithNamespace from '../graphql/queries/environment_with_namespace.graphql'; import updateEnvironment from '../graphql/mutations/update_environment.mutation.graphql'; import EnvironmentForm from './environment_form.vue'; @@ -11,10 +13,15 @@ export default { GlLoadingIcon, EnvironmentForm, }, + mixins: [glFeatureFlagsMixin()], inject: ['projectEnvironmentsPath', 'projectPath', 'environmentName'], apollo: { environment: { - query: getEnvironment, + query() { + return this.glFeatures?.kubernetesNamespaceForEnvironment + ? getEnvironmentWithNamespace + : getEnvironment; + }, variables() { return { environmentName: this.environmentName, @@ -52,6 +59,7 @@ export default { id: this.formEnvironment.id, externalUrl: this.formEnvironment.externalUrl, clusterAgentId: this.formEnvironment.clusterAgentId, + kubernetesNamespace: this.formEnvironment.kubernetesNamespace, }, }, }); diff --git a/app/assets/javascripts/environments/components/environment_form.vue b/app/assets/javascripts/environments/components/environment_form.vue index 727d86fbb55..58e57f365eb 100644 --- a/app/assets/javascripts/environments/components/environment_form.vue +++ b/app/assets/javascripts/environments/components/environment_form.vue @@ -7,6 +7,7 @@ import { GlCollapsibleListbox, GlLink, GlSprintf, + GlAlert, } from '@gitlab/ui'; import { helpPagePath } from '~/helpers/help_page_helper'; import { isAbsolute } from '~/lib/utils/url_utility'; @@ -15,6 +16,10 @@ import { ENVIRONMENT_NEW_HELP_TEXT, ENVIRONMENT_EDIT_HELP_TEXT, } from 'ee_else_ce/environments/constants'; +import csrf from '~/lib/utils/csrf'; +import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; +import getNamespacesQuery from '../graphql/queries/k8s_namespaces.query.graphql'; import getUserAuthorizedAgents from '../graphql/queries/user_authorized_agents.query.graphql'; export default { @@ -26,10 +31,13 @@ export default { GlCollapsibleListbox, GlLink, GlSprintf, + GlAlert, }, + mixins: [glFeatureFlagsMixin()], inject: { protectedEnvironmentSettingsPath: { default: '' }, projectPath: { default: '' }, + kasTunnelUrl: { default: '' }, }, props: { environment: { @@ -62,6 +70,8 @@ export default { urlFeedback: __('The URL should start with http:// or https://'), agentLabel: s__('Environments|GitLab agent'), agentHelpText: s__('Environments|Select agent'), + namespaceLabel: s__('Environments|Kubernetes namespace (optional)'), + namespaceHelpText: s__('Environments|Select namespace'), save: __('Save'), cancel: __('Cancel'), reset: __('Reset'), @@ -79,10 +89,41 @@ export default { userAccessAuthorizedAgents: [], loadingAgentsList: false, selectedAgentId: this.environment.clusterAgentId, - searchTerm: '', + agentSearchTerm: '', + selectedNamespace: this.environment.kubernetesNamespace, + k8sNamespaces: [], + namespaceSearchTerm: '', + kubernetesError: '', }; }, + apollo: { + k8sNamespaces: { + query: getNamespacesQuery, + skip() { + return !this.showNamespaceSelector; + }, + variables() { + return { + configuration: this.k8sAccessConfiguration, + }; + }, + update(data) { + return data?.k8sNamespaces || []; + }, + error(error) { + this.kubernetesError = error.message; + }, + result(result) { + if (!result?.error && !result.errors?.length) { + this.kubernetesError = null; + } + }, + }, + }, computed: { + loadingNamespacesList() { + return this.$apollo.queries.k8sNamespaces.loading; + }, isNameDisabled() { return Boolean(this.environment.id); }, @@ -103,7 +144,7 @@ export default { }; }); }, - dropdownToggleText() { + agentDropdownToggleText() { if (!this.selectedAgentId) { return this.$options.i18n.agentHelpText; } @@ -113,11 +154,56 @@ export default { return selectedAgentById?.text || this.environment.clusterAgent?.name; }, filteredAgentsList() { - const lowerCasedSearchTerm = this.searchTerm.toLowerCase(); + const lowerCasedSearchTerm = this.agentSearchTerm.toLowerCase(); return this.agentsList.filter((item) => item.text.toLowerCase().includes(lowerCasedSearchTerm), ); }, + namespacesList() { + return this.k8sNamespaces.map((item) => { + return { + value: item.metadata.name, + text: item.metadata.name, + }; + }); + }, + filteredNamespacesList() { + const lowerCasedSearchTerm = this.namespaceSearchTerm.toLowerCase(); + return this.namespacesList.filter((item) => + item.text.toLowerCase().includes(lowerCasedSearchTerm), + ); + }, + isKasUserAccessAvailable() { + return this.glFeatures?.kasUserAccessProject; + }, + isKasKubernetesNamespaceAvailable() { + return this.glFeatures?.kubernetesNamespaceForEnvironment; + }, + showNamespaceSelector() { + return Boolean( + this.isKasUserAccessAvailable && + this.isKasKubernetesNamespaceAvailable && + this.selectedAgentId, + ); + }, + namespaceDropdownToggleText() { + return this.selectedNamespace || this.$options.i18n.namespaceHelpText; + }, + k8sAccessConfiguration() { + if (!this.showNamespaceSelector) { + return null; + } + return { + basePath: this.kasTunnelUrl, + baseOptions: { + headers: { + 'GitLab-Agent-Id': getIdFromGraphQLId(this.selectedAgentId), + ...csrf.headers, + }, + withCredentials: true, + }, + }; + }, }, watch: { environment(change) { @@ -146,7 +232,14 @@ export default { }); }, onAgentSearch(search) { - this.searchTerm = search; + this.agentSearchTerm = search; + }, + onAgentChange($event) { + this.selectedNamespace = null; + this.onChange({ ...this.environment, clusterAgentId: $event, kubernetesNamespace: null }); + }, + onNamespaceSearch(search) { + this.namespaceSearchTerm = search; }, }, }; @@ -225,20 +318,48 @@ export default { id="environment_agent" v-model="selectedAgentId" class="gl-w-full" + data-testid="agent-selector" block :items="filteredAgentsList" :loading="loadingAgentsList" - :toggle-text="dropdownToggleText" + :toggle-text="agentDropdownToggleText" :header-text="$options.i18n.agentHelpText" :reset-button-label="$options.i18n.reset" :searchable="true" @shown="getAgentsList" @search="onAgentSearch" - @select="onChange({ ...environment, clusterAgentId: $event })" + @select="onAgentChange" @reset="onChange({ ...environment, clusterAgentId: null })" /> </gl-form-group> + <gl-form-group + v-if="showNamespaceSelector" + :label="$options.i18n.namespaceLabel" + label-for="environment_namespace" + > + <gl-alert v-if="kubernetesError" variant="warning" :dismissible="false" class="gl-mb-5"> + {{ kubernetesError }} + </gl-alert> + <gl-collapsible-listbox + v-else + id="environment_namespace" + v-model="selectedNamespace" + class="gl-w-full" + data-testid="namespace-selector" + block + :items="filteredNamespacesList" + :loading="loadingNamespacesList" + :toggle-text="namespaceDropdownToggleText" + :header-text="$options.i18n.namespaceHelpText" + :reset-button-label="$options.i18n.reset" + :searchable="true" + @search="onNamespaceSearch" + @select="onChange({ ...environment, kubernetesNamespace: $event })" + @reset="onChange({ ...environment, kubernetesNamespace: null })" + /> + </gl-form-group> + <div class="gl-mr-6"> <gl-button :loading="loading" diff --git a/app/assets/javascripts/environments/components/new_environment.vue b/app/assets/javascripts/environments/components/new_environment.vue index 464b530b503..c6bc94b0b80 100644 --- a/app/assets/javascripts/environments/components/new_environment.vue +++ b/app/assets/javascripts/environments/components/new_environment.vue @@ -34,6 +34,7 @@ export default { externalUrl: this.environment.externalUrl, projectPath: this.projectPath, clusterAgentId: this.environment.clusterAgentId, + kubernetesNamespace: this.environment.kubernetesNamespace, }, }, }); diff --git a/app/assets/javascripts/environments/components/new_environment_item.vue b/app/assets/javascripts/environments/components/new_environment_item.vue index 1f3d429cc3e..aa2960c810a 100644 --- a/app/assets/javascripts/environments/components/new_environment_item.vue +++ b/app/assets/javascripts/environments/components/new_environment_item.vue @@ -14,6 +14,7 @@ import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import isLastDeployment from '../graphql/queries/is_last_deployment.query.graphql'; import getEnvironmentClusterAgent from '../graphql/queries/environment_cluster_agent.query.graphql'; +import getEnvironmentClusterAgentWithNamespace from '../graphql/queries/environment_cluster_agent_with_namespace.query.graphql'; import ExternalUrl from './environment_external_url.vue'; import Actions from './environment_actions.vue'; import StopComponent from './environment_stop.vue'; @@ -82,7 +83,7 @@ export default { tierTooltip: s__('Environment|Deployment tier'), }, data() { - return { visible: false, clusterAgent: null }; + return { visible: false, clusterAgent: null, kubernetesNamespace: '' }; }, computed: { icon() { @@ -167,6 +168,9 @@ export default { isKubernetesOverviewAvailable() { return this.glFeatures?.kasUserAccessProject; }, + isKubernetesNamespaceAvailable() { + return this.glFeatures?.kubernetesNamespaceForEnvironment; + }, showKubernetesOverview() { return Boolean(this.isKubernetesOverviewAvailable && this.clusterAgent); }, @@ -186,9 +190,14 @@ export default { variables() { return { environmentName: this.environment.name, projectFullPath: this.projectPath }; }, - query: getEnvironmentClusterAgent, + query() { + return this.isKubernetesNamespaceAvailable + ? getEnvironmentClusterAgentWithNamespace + : getEnvironmentClusterAgent; + }, update(data) { this.clusterAgent = data?.project?.environment?.clusterAgent; + this.kubernetesNamespace = data?.project?.environment?.kubernetesNamespace || ''; }, }); }, @@ -369,10 +378,7 @@ export default { </gl-sprintf> </div> <div v-if="showKubernetesOverview" :class="$options.kubernetesOverviewClasses"> - <kubernetes-overview - :cluster-agent="clusterAgent" - :namespace="environment.kubernetesNamespace" - /> + <kubernetes-overview :cluster-agent="clusterAgent" :namespace="kubernetesNamespace" /> </div> <div v-if="rolloutStatus" :class="$options.deployBoardClasses"> <deploy-board-wrapper diff --git a/app/assets/javascripts/environments/constants.js b/app/assets/javascripts/environments/constants.js index 2b178964c37..dc9481a5429 100644 --- a/app/assets/javascripts/environments/constants.js +++ b/app/assets/javascripts/environments/constants.js @@ -108,3 +108,19 @@ export const PHASE_RUNNING = 'Running'; export const PHASE_PENDING = 'Pending'; export const PHASE_SUCCEEDED = 'Succeeded'; export const PHASE_FAILED = 'Failed'; + +const ERROR_UNAUTHORIZED = 'unauthorized'; +const ERROR_FORBIDDEN = 'forbidden'; +const ERROR_NOT_FOUND = 'not found'; +const ERROR_OTHER = 'other'; + +export const CLUSTER_AGENT_ERROR_MESSAGES = { + [ERROR_UNAUTHORIZED]: s__( + 'Environment|Unauthorized to access the cluster agent from this environment. Check your authentication and try again.', + ), + [ERROR_FORBIDDEN]: s__( + 'Environment|Forbidden to access the cluster agent from this environment.', + ), + [ERROR_NOT_FOUND]: s__('Environment|Cluster agent not found.'), + [ERROR_OTHER]: s__('Environment|There was an error connecting to the cluster agent.'), +}; diff --git a/app/assets/javascripts/environments/edit.js b/app/assets/javascripts/environments/edit.js index f936085af15..3f22b83e618 100644 --- a/app/assets/javascripts/environments/edit.js +++ b/app/assets/javascripts/environments/edit.js @@ -1,5 +1,6 @@ import Vue from 'vue'; import VueApollo from 'vue-apollo'; +import { removeLastSlashInUrlPath } from '~/lib/utils/url_utility'; import EditEnvironment from './components/edit_environment.vue'; import { apolloProvider } from './graphql/client'; @@ -15,6 +16,7 @@ export default (el) => { protectedEnvironmentSettingsPath, projectPath, environmentName, + kasTunnelUrl, } = el.dataset; return new Vue({ @@ -25,6 +27,7 @@ export default (el) => { protectedEnvironmentSettingsPath, projectPath, environmentName, + kasTunnelUrl: removeLastSlashInUrlPath(kasTunnelUrl), }, render(h) { return h(EditEnvironment); diff --git a/app/assets/javascripts/environments/graphql/client.js b/app/assets/javascripts/environments/graphql/client.js index 6d06cff06b9..553b06e632f 100644 --- a/app/assets/javascripts/environments/graphql/client.js +++ b/app/assets/javascripts/environments/graphql/client.js @@ -8,6 +8,7 @@ import environmentToStopQuery from './queries/environment_to_stop.query.graphql' import k8sPodsQuery from './queries/k8s_pods.query.graphql'; import k8sServicesQuery from './queries/k8s_services.query.graphql'; import k8sWorkloadsQuery from './queries/k8s_workloads.query.graphql'; +import k8sNamespacesQuery from './queries/k8s_namespaces.query.graphql'; import { resolvers } from './resolvers'; import typeDefs from './typedefs.graphql'; @@ -161,6 +162,14 @@ export const apolloProvider = (endpoint) => { }, }, }); + cache.writeQuery({ + query: k8sNamespacesQuery, + data: { + metadata: { + name: null, + }, + }, + }); return new VueApollo({ defaultClient, }); diff --git a/app/assets/javascripts/environments/graphql/queries/environment_cluster_agent_with_namespace.query.graphql b/app/assets/javascripts/environments/graphql/queries/environment_cluster_agent_with_namespace.query.graphql new file mode 100644 index 00000000000..5e72c2dac20 --- /dev/null +++ b/app/assets/javascripts/environments/graphql/queries/environment_cluster_agent_with_namespace.query.graphql @@ -0,0 +1,20 @@ +query getEnvironmentClusterAgentWithNamespace($projectFullPath: ID!, $environmentName: String) { + project(fullPath: $projectFullPath) { + id + environment(name: $environmentName) { + id + kubernetesNamespace + clusterAgent { + id + name + webPath + tokens { + nodes { + id + lastUsedAt + } + } + } + } + } +} diff --git a/app/assets/javascripts/environments/graphql/queries/environment_with_namespace.graphql b/app/assets/javascripts/environments/graphql/queries/environment_with_namespace.graphql new file mode 100644 index 00000000000..42796f982b6 --- /dev/null +++ b/app/assets/javascripts/environments/graphql/queries/environment_with_namespace.graphql @@ -0,0 +1,15 @@ +query getEnvironmentWithNamespace($projectFullPath: ID!, $environmentName: String) { + project(fullPath: $projectFullPath) { + id + environment(name: $environmentName) { + id + name + externalUrl + kubernetesNamespace + clusterAgent { + id + name + } + } + } +} diff --git a/app/assets/javascripts/environments/graphql/queries/k8s_namespaces.query.graphql b/app/assets/javascripts/environments/graphql/queries/k8s_namespaces.query.graphql new file mode 100644 index 00000000000..c05d09b6ca2 --- /dev/null +++ b/app/assets/javascripts/environments/graphql/queries/k8s_namespaces.query.graphql @@ -0,0 +1,7 @@ +query getK8sNamespaces($configuration: LocalConfiguration) { + k8sNamespaces(configuration: $configuration) @client { + metadata { + name + } + } +} diff --git a/app/assets/javascripts/environments/graphql/resolvers.js b/app/assets/javascripts/environments/graphql/resolvers.js index 044e7927606..8cfe44c5a05 100644 --- a/app/assets/javascripts/environments/graphql/resolvers.js +++ b/app/assets/javascripts/environments/graphql/resolvers.js @@ -6,6 +6,7 @@ import { parseIntPagination, normalizeHeaders, } from '~/lib/utils/common_utils'; +import { humanizeClusterErrors } from '../helpers/k8s_integration_helper'; import pollIntervalQuery from './queries/poll_interval.query.graphql'; import environmentToRollbackQuery from './queries/environment_to_rollback.query.graphql'; @@ -72,6 +73,11 @@ const mapWorkloadItems = (items, kind) => { }); }; +const handleClusterError = (err) => { + const error = err?.response?.data?.message ? new Error(err.response.data.message) : err; + throw error; +}; + export const resolvers = (endpoint) => ({ Query: { environmentApp(_context, { page, scope, search }, { cache }) { @@ -124,8 +130,7 @@ export const resolvers = (endpoint) => ({ return podsApi .then((res) => res?.data?.items || []) .catch((err) => { - const error = err?.response?.data?.message ? new Error(err.response.data.message) : err; - throw error; + handleClusterError(err); }); }, k8sServices(_, { configuration }) { @@ -148,8 +153,7 @@ export const resolvers = (endpoint) => ({ }); }) .catch((err) => { - const error = err?.response?.data?.message ? new Error(err.response.data.message) : err; - throw error; + handleClusterError(err); }); }, k8sWorkloads(_, { configuration, namespace }) { @@ -206,6 +210,19 @@ export const resolvers = (endpoint) => ({ return summaryList; }); }, + k8sNamespaces(_, { configuration }) { + const coreV1Api = new CoreV1Api(new Configuration(configuration)); + const namespacesApi = coreV1Api.listCoreV1Namespace(); + + return namespacesApi + .then((res) => { + return res?.data?.items || []; + }) + .catch((err) => { + const error = err?.response?.data?.reason || err; + throw new Error(humanizeClusterErrors(error)); + }); + }, }, Mutation: { stopEnvironmentREST(_, { environment }, { client }) { diff --git a/app/assets/javascripts/environments/graphql/typedefs.graphql b/app/assets/javascripts/environments/graphql/typedefs.graphql index 7e46385946f..e2c22dda554 100644 --- a/app/assets/javascripts/environments/graphql/typedefs.graphql +++ b/app/assets/javascripts/environments/graphql/typedefs.graphql @@ -160,6 +160,12 @@ type LocalK8sWorkloads { JobList: [localK8sJob] CronJobList: [localK8sCronJob] } +type k8sNamespaceMetadata { + name: String +} +type LocalK8sNamespaces { + metadata: k8sNamespaceMetadata +} extend type Query { environmentApp(page: Int, scope: String): LocalEnvironmentApp diff --git a/app/assets/javascripts/environments/helpers/k8s_integration_helper.js b/app/assets/javascripts/environments/helpers/k8s_integration_helper.js index 45c65c93a91..e49f1451759 100644 --- a/app/assets/javascripts/environments/helpers/k8s_integration_helper.js +++ b/app/assets/javascripts/environments/helpers/k8s_integration_helper.js @@ -1,4 +1,5 @@ import { differenceInSeconds } from '~/lib/utils/datetime_utility'; +import { CLUSTER_AGENT_ERROR_MESSAGES } from '../constants'; export function generateServicePortsString(ports) { if (!ports?.length) return ''; @@ -139,3 +140,9 @@ export function getCronJobsStatuses(items) { ...(ready.length && { ready }), }; } + +export function humanizeClusterErrors(reason) { + const errorReason = reason.toLowerCase(); + const errorMessage = CLUSTER_AGENT_ERROR_MESSAGES[errorReason]; + return errorMessage || CLUSTER_AGENT_ERROR_MESSAGES.other; +} diff --git a/app/assets/javascripts/environments/new.js b/app/assets/javascripts/environments/new.js index 5dd112ac5e6..652085b1f28 100644 --- a/app/assets/javascripts/environments/new.js +++ b/app/assets/javascripts/environments/new.js @@ -1,5 +1,6 @@ import Vue from 'vue'; import VueApollo from 'vue-apollo'; +import { removeLastSlashInUrlPath } from '~/lib/utils/url_utility'; import NewEnvironment from './components/new_environment.vue'; import { apolloProvider } from './graphql/client'; @@ -10,12 +11,16 @@ export default (el) => { return null; } - const { projectEnvironmentsPath, projectPath } = el.dataset; + const { projectEnvironmentsPath, projectPath, kasTunnelUrl } = el.dataset; return new Vue({ el, apolloProvider: apolloProvider(), - provide: { projectEnvironmentsPath, projectPath }, + provide: { + projectEnvironmentsPath, + projectPath, + kasTunnelUrl: removeLastSlashInUrlPath(kasTunnelUrl), + }, render(h) { return h(NewEnvironment); }, |