Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-08-16 15:09:08 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-08-16 15:09:08 +0300
commit036cfe846472ee1cca9f7b8c43af28cd344ad66a (patch)
tree6712787dc2499e8ef8b9d887b8deed0c8a01dac6 /app/assets/javascripts/environments
parent06672560caf7701c357eb468ca17cce817b57239 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts/environments')
-rw-r--r--app/assets/javascripts/environments/components/edit_environment.vue8
-rw-r--r--app/assets/javascripts/environments/components/environment_flux_resource_selector.vue210
-rw-r--r--app/assets/javascripts/environments/components/environment_form.vue32
-rw-r--r--app/assets/javascripts/environments/components/kubernetes_overview.vue6
-rw-r--r--app/assets/javascripts/environments/components/kubernetes_status_bar.vue23
-rw-r--r--app/assets/javascripts/environments/components/new_environment.vue1
-rw-r--r--app/assets/javascripts/environments/components/new_environment_item.vue16
-rw-r--r--app/assets/javascripts/environments/constants.js13
-rw-r--r--app/assets/javascripts/environments/graphql/queries/environment_cluster_agent_with_flux_resource.query.graphql21
-rw-r--r--app/assets/javascripts/environments/graphql/queries/environment_with_flux_resource.query.graphql16
-rw-r--r--app/assets/javascripts/environments/graphql/queries/flux_helm_release_status.query.graphql2
-rw-r--r--app/assets/javascripts/environments/graphql/queries/flux_helm_releases.query.graphql9
-rw-r--r--app/assets/javascripts/environments/graphql/queries/flux_kustomization_status.query.graphql2
-rw-r--r--app/assets/javascripts/environments/graphql/queries/flux_kustomizations.query.graphql9
-rw-r--r--app/assets/javascripts/environments/graphql/resolvers.js369
-rw-r--r--app/assets/javascripts/environments/graphql/resolvers/base.js165
-rw-r--r--app/assets/javascripts/environments/graphql/resolvers/flux.js115
-rw-r--r--app/assets/javascripts/environments/graphql/resolvers/kubernetes.js155
-rw-r--r--app/assets/javascripts/environments/helpers/k8s_integration_helper.js2
19 files changed, 802 insertions, 372 deletions
diff --git a/app/assets/javascripts/environments/components/edit_environment.vue b/app/assets/javascripts/environments/components/edit_environment.vue
index a8e8e9a59e3..f90a1dcd193 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 getEnvironmentWithFluxResource from '../graphql/queries/environment_with_flux_resource.query.graphql';
import updateEnvironment from '../graphql/mutations/update_environment.mutation.graphql';
import EnvironmentForm from './environment_form.vue';
@@ -11,11 +13,14 @@ export default {
GlLoadingIcon,
EnvironmentForm,
},
+ mixins: [glFeatureFlagsMixin()],
inject: ['projectEnvironmentsPath', 'projectPath', 'environmentName'],
apollo: {
environment: {
query() {
- return getEnvironment;
+ return this.glFeatures?.fluxResourceForEnvironment
+ ? getEnvironmentWithFluxResource
+ : getEnvironment;
},
variables() {
return {
@@ -55,6 +60,7 @@ export default {
externalUrl: this.formEnvironment.externalUrl,
clusterAgentId: this.formEnvironment.clusterAgentId,
kubernetesNamespace: this.formEnvironment.kubernetesNamespace,
+ fluxResourcePath: this.formEnvironment.fluxResourcePath,
},
},
});
diff --git a/app/assets/javascripts/environments/components/environment_flux_resource_selector.vue b/app/assets/javascripts/environments/components/environment_flux_resource_selector.vue
new file mode 100644
index 00000000000..cad6752da94
--- /dev/null
+++ b/app/assets/javascripts/environments/components/environment_flux_resource_selector.vue
@@ -0,0 +1,210 @@
+<script>
+import { GlFormGroup, GlCollapsibleListbox, GlAlert } from '@gitlab/ui';
+import { __, s__ } from '~/locale';
+import fluxKustomizationsQuery from '../graphql/queries/flux_kustomizations.query.graphql';
+import fluxHelmReleasesQuery from '../graphql/queries/flux_helm_releases.query.graphql';
+import {
+ HELM_RELEASES_RESOURCE_TYPE,
+ KUSTOMIZATIONS_RESOURCE_TYPE,
+ KUSTOMIZATION,
+ HELM_RELEASE,
+} from '../constants';
+
+export default {
+ components: {
+ GlFormGroup,
+ GlCollapsibleListbox,
+ GlAlert,
+ },
+ props: {
+ configuration: {
+ required: true,
+ type: Object,
+ },
+ namespace: {
+ required: true,
+ type: String,
+ },
+ fluxResourcePath: {
+ required: false,
+ type: String,
+ default: '',
+ },
+ },
+ i18n: {
+ fluxResourceLabel: s__('Environments|Select Flux resource (optional)'),
+ kustomizationsGroupLabel: s__('Environments|Kustomizations'),
+ helmReleasesGroupLabel: s__('Environments|HelmReleases'),
+ fluxResourcesHelpText: s__('Environments|Select Flux resource'),
+ errorTitle: s__(
+ 'Environments|Unable to access the following resources from this environment. Check your authorization on the following and try again:',
+ ),
+ reset: __('Reset'),
+ },
+ data() {
+ return {
+ fluxResourceSearchTerm: '',
+ kustomizationsError: '',
+ helmReleasesError: '',
+ };
+ },
+ apollo: {
+ fluxKustomizations: {
+ query: fluxKustomizationsQuery,
+ variables() {
+ return {
+ configuration: this.configuration,
+ namespace: this.namespace,
+ };
+ },
+ skip() {
+ return !this.namespace;
+ },
+ update(data) {
+ return data?.fluxKustomizations || [];
+ },
+ error() {
+ this.kustomizationsError = KUSTOMIZATION;
+ },
+ result(result) {
+ if (!result?.error && !result.errors?.length) {
+ this.kustomizationsError = '';
+ }
+ },
+ },
+ fluxHelmReleases: {
+ query: fluxHelmReleasesQuery,
+ variables() {
+ return {
+ configuration: this.configuration,
+ namespace: this.namespace,
+ };
+ },
+ skip() {
+ return !this.namespace;
+ },
+ update(data) {
+ return data?.fluxHelmReleases || [];
+ },
+ error() {
+ this.helmReleasesError = HELM_RELEASE;
+ },
+ result(result) {
+ if (!result?.error && !result.errors?.length) {
+ this.helmReleasesError = '';
+ }
+ },
+ },
+ },
+ computed: {
+ variables() {
+ return {
+ configuration: this.configuration,
+ namespace: this.namespace,
+ };
+ },
+ loadingFluxResourcesList() {
+ return this.$apollo.loading;
+ },
+ kubernetesErrors() {
+ const errors = [];
+ if (this.kustomizationsError) {
+ errors.push(this.kustomizationsError);
+ }
+ if (this.helmReleasesError) {
+ errors.push(this.helmReleasesError);
+ }
+ return errors;
+ },
+ fluxResourcesDropdownToggleText() {
+ const selectedResourceParts = this.fluxResourcePath ? this.fluxResourcePath.split('/') : [];
+ return selectedResourceParts.length
+ ? selectedResourceParts.at(-1)
+ : this.$options.i18n.fluxResourcesHelpText;
+ },
+ fluxKustomizationsList() {
+ return (
+ this.fluxKustomizations?.map((item) => {
+ return {
+ value: `${item.apiVersion}/namespaces/${item.metadata.namespace}/${KUSTOMIZATIONS_RESOURCE_TYPE}/${item.metadata.name}`,
+ text: item.metadata.name,
+ };
+ }) || []
+ );
+ },
+ fluxHelmReleasesList() {
+ return (
+ this.fluxHelmReleases?.map((item) => {
+ return {
+ value: `${item.apiVersion}/namespaces/${item.metadata.namespace}/${HELM_RELEASES_RESOURCE_TYPE}/${item.metadata.name}`,
+ text: item.metadata.name,
+ };
+ }) || []
+ );
+ },
+ filteredKustomizationsList() {
+ const lowerCasedSearchTerm = this.fluxResourceSearchTerm.toLowerCase();
+ return this.fluxKustomizationsList.filter((item) =>
+ item.text.toLowerCase().includes(lowerCasedSearchTerm),
+ );
+ },
+ filteredHelmResourcesList() {
+ const lowerCasedSearchTerm = this.fluxResourceSearchTerm.toLowerCase();
+ return this.fluxHelmReleasesList.filter((item) =>
+ item.text.toLowerCase().includes(lowerCasedSearchTerm),
+ );
+ },
+ fluxResourcesList() {
+ const list = [];
+ if (this.filteredKustomizationsList?.length) {
+ list.push({
+ text: this.$options.i18n.kustomizationsGroupLabel,
+ options: this.filteredKustomizationsList,
+ });
+ }
+
+ if (this.filteredHelmResourcesList?.length) {
+ list.push({
+ text: this.$options.i18n.helmReleasesGroupLabel,
+ options: this.filteredHelmResourcesList,
+ });
+ }
+ return list;
+ },
+ },
+ methods: {
+ onChange(event) {
+ this.$emit('change', event);
+ },
+ onSearch(search) {
+ this.fluxResourceSearchTerm = search;
+ },
+ },
+};
+</script>
+<template>
+ <gl-form-group :label="$options.i18n.fluxResourceLabel" label-for="environment_flux_resource">
+ <gl-alert v-if="kubernetesErrors.length" variant="warning" :dismissible="false" class="gl-mb-5">
+ {{ $options.i18n.errorTitle }}
+ <ul class="gl-mb-0 gl-pl-6">
+ <li v-for="(error, index) of kubernetesErrors" :key="index">{{ error }}</li>
+ </ul>
+ </gl-alert>
+
+ <gl-collapsible-listbox
+ id="environment_flux_resource_path"
+ class="gl-w-full"
+ block
+ :selected="fluxResourcePath"
+ :items="fluxResourcesList"
+ :loading="loadingFluxResourcesList"
+ :toggle-text="fluxResourcesDropdownToggleText"
+ :header-text="$options.i18n.fluxResourcesHelpText"
+ :reset-button-label="$options.i18n.reset"
+ searchable
+ @search="onSearch"
+ @select="onChange"
+ @reset="onChange(null)"
+ />
+ </gl-form-group>
+</template>
diff --git a/app/assets/javascripts/environments/components/environment_form.vue b/app/assets/javascripts/environments/components/environment_form.vue
index 745a8a1d3ed..d89dcf56b7c 100644
--- a/app/assets/javascripts/environments/components/environment_form.vue
+++ b/app/assets/javascripts/environments/components/environment_form.vue
@@ -17,9 +17,11 @@ import {
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';
+import EnvironmentFluxResourceSelector from './environment_flux_resource_selector.vue';
export default {
components: {
@@ -31,7 +33,9 @@ export default {
GlLink,
GlSprintf,
GlAlert,
+ EnvironmentFluxResourceSelector,
},
+ mixins: [glFeatureFlagsMixin()],
inject: {
protectedEnvironmentSettingsPath: { default: '' },
projectPath: { default: '' },
@@ -177,6 +181,14 @@ export default {
namespaceDropdownToggleText() {
return this.selectedNamespace || this.$options.i18n.namespaceHelpText;
},
+ isKasFluxResourceAvailable() {
+ return this.glFeatures?.fluxResourceForEnvironment;
+ },
+ showFluxResourceSelector() {
+ return Boolean(
+ this.isKasFluxResourceAvailable && this.selectedNamespace && this.selectedAgentId,
+ );
+ },
k8sAccessConfiguration() {
if (!this.showNamespaceSelector) {
return null;
@@ -196,6 +208,7 @@ export default {
watch: {
environment(change) {
this.selectedAgentId = change.clusterAgentId;
+ this.selectedNamespace = change.kubernetesNamespace;
},
},
methods: {
@@ -224,7 +237,12 @@ export default {
},
onAgentChange($event) {
this.selectedNamespace = null;
- this.onChange({ ...this.environment, clusterAgentId: $event, kubernetesNamespace: null });
+ this.onChange({
+ ...this.environment,
+ clusterAgentId: $event,
+ kubernetesNamespace: null,
+ fluxResourcePath: null,
+ });
},
onNamespaceSearch(search) {
this.namespaceSearchTerm = search;
@@ -343,11 +361,21 @@ export default {
:reset-button-label="$options.i18n.reset"
:searchable="true"
@search="onNamespaceSearch"
- @select="onChange({ ...environment, kubernetesNamespace: $event })"
+ @select="
+ onChange({ ...environment, kubernetesNamespace: $event, fluxResourcePath: null })
+ "
@reset="onChange({ ...environment, kubernetesNamespace: null })"
/>
</gl-form-group>
+ <environment-flux-resource-selector
+ v-if="showFluxResourceSelector"
+ :namespace="selectedNamespace"
+ :configuration="k8sAccessConfiguration"
+ :flux-resource-path="environment.fluxResourcePath"
+ @change="onChange({ ...environment, fluxResourcePath: $event })"
+ />
+
<div class="gl-mr-6">
<gl-button
:loading="loading"
diff --git a/app/assets/javascripts/environments/components/kubernetes_overview.vue b/app/assets/javascripts/environments/components/kubernetes_overview.vue
index 8c30520ebec..0e52a80c2c5 100644
--- a/app/assets/javascripts/environments/components/kubernetes_overview.vue
+++ b/app/assets/javascripts/environments/components/kubernetes_overview.vue
@@ -33,6 +33,11 @@ export default {
type: String,
default: '',
},
+ fluxResourcePath: {
+ required: false,
+ type: String,
+ default: '',
+ },
},
data() {
return {
@@ -105,6 +110,7 @@ export default {
:configuration="k8sAccessConfiguration"
:namespace="namespace"
:environment-name="environmentName"
+ :flux-resource-path="fluxResourcePath"
class="gl-mb-3" />
<kubernetes-agent-info :cluster-agent="clusterAgent" class="gl-mb-5" />
diff --git a/app/assets/javascripts/environments/components/kubernetes_status_bar.vue b/app/assets/javascripts/environments/components/kubernetes_status_bar.vue
index b8859e65e38..c61ca4d749e 100644
--- a/app/assets/javascripts/environments/components/kubernetes_status_bar.vue
+++ b/app/assets/javascripts/environments/components/kubernetes_status_bar.vue
@@ -1,7 +1,14 @@
<script>
import { GlLoadingIcon, GlBadge } from '@gitlab/ui';
import { s__ } from '~/locale';
-import { HEALTH_BADGES, SYNC_STATUS_BADGES, STATUS_TRUE, STATUS_FALSE } from '../constants';
+import {
+ HEALTH_BADGES,
+ SYNC_STATUS_BADGES,
+ STATUS_TRUE,
+ STATUS_FALSE,
+ HELM_RELEASES_RESOURCE_TYPE,
+ KUSTOMIZATIONS_RESOURCE_TYPE,
+} from '../constants';
import fluxKustomizationStatusQuery from '../graphql/queries/flux_kustomization_status.query.graphql';
import fluxHelmReleaseStatusQuery from '../graphql/queries/flux_helm_release_status.query.graphql';
@@ -32,6 +39,11 @@ export default {
type: String,
default: '',
},
+ fluxResourcePath: {
+ required: false,
+ type: String,
+ default: '',
+ },
},
apollo: {
fluxKustomizationStatus: {
@@ -41,10 +53,13 @@ export default {
configuration: this.configuration,
namespace: this.namespace,
environmentName: this.environmentName.toLowerCase(),
+ fluxResourcePath: this.fluxResourcePath,
};
},
skip() {
- return !this.namespace;
+ return Boolean(
+ !this.namespace || this.fluxResourcePath?.includes(HELM_RELEASES_RESOURCE_TYPE),
+ );
},
},
fluxHelmReleaseStatus: {
@@ -54,13 +69,15 @@ export default {
configuration: this.configuration,
namespace: this.namespace,
environmentName: this.environmentName.toLowerCase(),
+ fluxResourcePath: this.fluxResourcePath,
};
},
skip() {
return Boolean(
!this.namespace ||
this.$apollo.queries.fluxKustomizationStatus.loading ||
- this.hasKustomizations,
+ this.hasKustomizations ||
+ this.fluxResourcePath?.includes(KUSTOMIZATIONS_RESOURCE_TYPE),
);
},
},
diff --git a/app/assets/javascripts/environments/components/new_environment.vue b/app/assets/javascripts/environments/components/new_environment.vue
index c6bc94b0b80..6a4ed34989f 100644
--- a/app/assets/javascripts/environments/components/new_environment.vue
+++ b/app/assets/javascripts/environments/components/new_environment.vue
@@ -35,6 +35,7 @@ export default {
projectPath: this.projectPath,
clusterAgentId: this.environment.clusterAgentId,
kubernetesNamespace: this.environment.kubernetesNamespace,
+ fluxResourcePath: this.environment.fluxResourcePath,
},
},
});
diff --git a/app/assets/javascripts/environments/components/new_environment_item.vue b/app/assets/javascripts/environments/components/new_environment_item.vue
index 48a3281c16f..2148343f690 100644
--- a/app/assets/javascripts/environments/components/new_environment_item.vue
+++ b/app/assets/javascripts/environments/components/new_environment_item.vue
@@ -11,8 +11,10 @@ import {
import { __, s__ } from '~/locale';
import { truncate } from '~/lib/utils/text_utility';
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 getEnvironmentClusterAgentWithFluxResource from '../graphql/queries/environment_cluster_agent_with_flux_resource.query.graphql';
import ExternalUrl from './environment_external_url.vue';
import Actions from './environment_actions.vue';
import StopComponent from './environment_stop.vue';
@@ -50,6 +52,7 @@ export default {
directives: {
GlTooltip,
},
+ mixins: [glFeatureFlagsMixin()],
inject: ['helpPagePath', 'projectPath'],
props: {
environment: {
@@ -80,7 +83,7 @@ export default {
tierTooltip: s__('Environment|Deployment tier'),
},
data() {
- return { visible: false, clusterAgent: null, kubernetesNamespace: '' };
+ return { visible: false, clusterAgent: null, kubernetesNamespace: '', fluxResourcePath: '' };
},
computed: {
icon() {
@@ -162,6 +165,9 @@ export default {
rolloutStatus() {
return this.environment?.rolloutStatus;
},
+ isFluxResourceAvailable() {
+ return this.glFeatures?.fluxResourceForEnvironment;
+ },
},
methods: {
toggleEnvironmentCollapse() {
@@ -179,11 +185,14 @@ export default {
return { environmentName: this.environment.name, projectFullPath: this.projectPath };
},
query() {
- return getEnvironmentClusterAgent;
+ return this.isFluxResourceAvailable
+ ? getEnvironmentClusterAgentWithFluxResource
+ : getEnvironmentClusterAgent;
},
update(data) {
this.clusterAgent = data?.project?.environment?.clusterAgent;
- this.kubernetesNamespace = data?.project?.environment?.kubernetesNamespace || '';
+ this.kubernetesNamespace = data?.project?.environment?.kubernetesNamespace;
+ this.fluxResourcePath = data?.project?.environment?.fluxResourcePath || '';
},
});
},
@@ -367,6 +376,7 @@ export default {
<kubernetes-overview
:cluster-agent="clusterAgent"
:namespace="kubernetesNamespace"
+ :flux-resource-path="fluxResourcePath"
:environment-name="environment.name"
/>
</div>
diff --git a/app/assets/javascripts/environments/constants.js b/app/assets/javascripts/environments/constants.js
index 921be902bdc..6afb143d2c1 100644
--- a/app/assets/javascripts/environments/constants.js
+++ b/app/assets/javascripts/environments/constants.js
@@ -160,3 +160,16 @@ export const CLUSTER_AGENT_ERROR_MESSAGES = {
[ERROR_NOT_FOUND]: s__('Environment|Cluster agent not found.'),
[ERROR_OTHER]: s__('Environment|There was an error connecting to the cluster agent.'),
};
+
+export const CLUSTER_FLUX_RECOURSES_ERROR_MESSAGES = {
+ [ERROR_UNAUTHORIZED]: s__(
+ 'Environment|Unauthorized to access %{resourceType} from this environment.',
+ ),
+ [ERROR_OTHER]: s__('Environment|There was an error fetching %{resourceType}.'),
+};
+
+export const HELM_RELEASES_RESOURCE_TYPE = 'helmreleases';
+export const KUSTOMIZATIONS_RESOURCE_TYPE = 'kustomizations';
+
+export const KUSTOMIZATION = 'Kustomization';
+export const HELM_RELEASE = 'HelmRelease';
diff --git a/app/assets/javascripts/environments/graphql/queries/environment_cluster_agent_with_flux_resource.query.graphql b/app/assets/javascripts/environments/graphql/queries/environment_cluster_agent_with_flux_resource.query.graphql
new file mode 100644
index 00000000000..80363a06d42
--- /dev/null
+++ b/app/assets/javascripts/environments/graphql/queries/environment_cluster_agent_with_flux_resource.query.graphql
@@ -0,0 +1,21 @@
+query getEnvironmentClusterAgentWithFluxResource($projectFullPath: ID!, $environmentName: String) {
+ project(fullPath: $projectFullPath) {
+ id
+ environment(name: $environmentName) {
+ id
+ kubernetesNamespace
+ fluxResourcePath
+ clusterAgent {
+ id
+ name
+ webPath
+ tokens {
+ nodes {
+ id
+ lastUsedAt
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/environments/graphql/queries/environment_with_flux_resource.query.graphql b/app/assets/javascripts/environments/graphql/queries/environment_with_flux_resource.query.graphql
new file mode 100644
index 00000000000..166cd64189f
--- /dev/null
+++ b/app/assets/javascripts/environments/graphql/queries/environment_with_flux_resource.query.graphql
@@ -0,0 +1,16 @@
+query getEnvironmentWithFluxResource($projectFullPath: ID!, $environmentName: String) {
+ project(fullPath: $projectFullPath) {
+ id
+ environment(name: $environmentName) {
+ id
+ name
+ externalUrl
+ kubernetesNamespace
+ fluxResourcePath
+ clusterAgent {
+ id
+ name
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/environments/graphql/queries/flux_helm_release_status.query.graphql b/app/assets/javascripts/environments/graphql/queries/flux_helm_release_status.query.graphql
index 0857f6f57df..fdccb087d28 100644
--- a/app/assets/javascripts/environments/graphql/queries/flux_helm_release_status.query.graphql
+++ b/app/assets/javascripts/environments/graphql/queries/flux_helm_release_status.query.graphql
@@ -2,11 +2,13 @@ query getFluxHelmReleaseStatusQuery(
$configuration: LocalConfiguration
$namespace: String
$environmentName: String
+ $fluxResourcePath: String
) {
fluxHelmReleaseStatus(
configuration: $configuration
namespace: $namespace
environmentName: $environmentName
+ fluxResourcePath: $fluxResourcePath
) @client {
status
type
diff --git a/app/assets/javascripts/environments/graphql/queries/flux_helm_releases.query.graphql b/app/assets/javascripts/environments/graphql/queries/flux_helm_releases.query.graphql
new file mode 100644
index 00000000000..fb37aba5adb
--- /dev/null
+++ b/app/assets/javascripts/environments/graphql/queries/flux_helm_releases.query.graphql
@@ -0,0 +1,9 @@
+query getFluxHelmReleasesQuery($configuration: LocalConfiguration, $namespace: String) {
+ fluxHelmReleases(configuration: $configuration, namespace: $namespace) @client {
+ apiVersion
+ metadata {
+ name
+ namespace
+ }
+ }
+}
diff --git a/app/assets/javascripts/environments/graphql/queries/flux_kustomization_status.query.graphql b/app/assets/javascripts/environments/graphql/queries/flux_kustomization_status.query.graphql
index 4eaea014718..bd00b8e9695 100644
--- a/app/assets/javascripts/environments/graphql/queries/flux_kustomization_status.query.graphql
+++ b/app/assets/javascripts/environments/graphql/queries/flux_kustomization_status.query.graphql
@@ -2,11 +2,13 @@ query getFluxHelmKustomizationStatusQuery(
$configuration: LocalConfiguration
$namespace: String
$environmentName: String
+ $fluxResourcePath: String
) {
fluxKustomizationStatus(
configuration: $configuration
namespace: $namespace
environmentName: $environmentName
+ fluxResourcePath: $fluxResourcePath
) @client {
status
type
diff --git a/app/assets/javascripts/environments/graphql/queries/flux_kustomizations.query.graphql b/app/assets/javascripts/environments/graphql/queries/flux_kustomizations.query.graphql
new file mode 100644
index 00000000000..ea7966560c3
--- /dev/null
+++ b/app/assets/javascripts/environments/graphql/queries/flux_kustomizations.query.graphql
@@ -0,0 +1,9 @@
+query getFluxKustomizationsQuery($configuration: LocalConfiguration, $namespace: String) {
+ fluxKustomizations(configuration: $configuration, namespace: $namespace) @client {
+ apiVersion
+ metadata {
+ name
+ namespace
+ }
+ }
+}
diff --git a/app/assets/javascripts/environments/graphql/resolvers.js b/app/assets/javascripts/environments/graphql/resolvers.js
index fcc17cea4e1..017e3ccb45b 100644
--- a/app/assets/javascripts/environments/graphql/resolvers.js
+++ b/app/assets/javascripts/environments/graphql/resolvers.js
@@ -1,369 +1,14 @@
-import { CoreV1Api, Configuration, AppsV1Api, BatchV1Api } from '@gitlab/cluster-client';
-import axios from '~/lib/utils/axios_utils';
-import { s__ } from '~/locale';
-import {
- convertObjectPropsToCamelCase,
- 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';
-import environmentToStopQuery from './queries/environment_to_stop.query.graphql';
-import environmentToDeleteQuery from './queries/environment_to_delete.query.graphql';
-import environmentToChangeCanaryQuery from './queries/environment_to_change_canary.query.graphql';
-import isEnvironmentStoppingQuery from './queries/is_environment_stopping.query.graphql';
-import pageInfoQuery from './queries/page_info.query.graphql';
-
-const helmReleasesResourceType = 'helmreleases';
-const kustomizationsResourceType = 'kustomizations';
-const helmReleasesApiVersion = 'helm.toolkit.fluxcd.io/v2beta1';
-const kustomizationsApiVersion = 'kustomize.toolkit.fluxcd.io/v1beta1';
-
-const buildErrors = (errors = []) => ({
- errors,
- __typename: 'LocalEnvironmentErrors',
-});
-
-const mapNestedEnvironment = (env) => ({
- ...convertObjectPropsToCamelCase(env, { deep: true }),
- __typename: 'NestedLocalEnvironment',
-});
-const mapEnvironment = (env) => ({
- ...convertObjectPropsToCamelCase(env, { deep: true }),
- __typename: 'LocalEnvironment',
-});
-
-const mapWorkloadItems = (items, kind) => {
- return items.map((item) => {
- const updatedItem = {
- status: {},
- spec: {},
- };
-
- switch (kind) {
- case 'DeploymentList':
- updatedItem.status.conditions = item.status.conditions || [];
- break;
- case 'DaemonSetList':
- updatedItem.status = {
- numberMisscheduled: item.status.numberMisscheduled || 0,
- numberReady: item.status.numberReady || 0,
- desiredNumberScheduled: item.status.desiredNumberScheduled || 0,
- };
- break;
- case 'StatefulSetList':
- case 'ReplicaSetList':
- updatedItem.status.readyReplicas = item.status.readyReplicas || 0;
- updatedItem.spec.replicas = item.spec.replicas || 0;
- break;
- case 'JobList':
- updatedItem.status.failed = item.status.failed || 0;
- updatedItem.status.succeeded = item.status.succeeded || 0;
- updatedItem.spec.completions = item.spec.completions || 0;
- break;
- case 'CronJobList':
- updatedItem.status.active = item.status.active || 0;
- updatedItem.status.lastScheduleTime = item.status.lastScheduleTime || '';
- updatedItem.spec.suspend = item.spec.suspend || 0;
- break;
- default:
- updatedItem.status = item?.status;
- updatedItem.spec = item?.spec;
- break;
- }
-
- return updatedItem;
- });
-};
-
-const handleClusterError = (err) => {
- const error = err?.response?.data?.message ? new Error(err.response.data.message) : err;
- throw error;
-};
-
-const buildFluxResourceUrl = ({
- basePath,
- namespace,
- apiVersion,
- resourceType,
- environmentName,
-}) => {
- return `${basePath}/apis/${apiVersion}/namespaces/${namespace}/${resourceType}/${environmentName}`;
-};
-
-const getFluxResourceStatus = (configuration, url) => {
- const { headers } = configuration.baseOptions;
- const withCredentials = true;
-
- return axios
- .get(url, { withCredentials, headers })
- .then((res) => {
- return res?.data?.status?.conditions || [];
- })
- .catch((err) => {
- handleClusterError(err);
- });
-};
+import { baseQueries, baseMutations } from './resolvers/base';
+import kubernetesQueries from './resolvers/kubernetes';
+import fluxQueries from './resolvers/flux';
export const resolvers = (endpoint) => ({
Query: {
- environmentApp(_context, { page, scope, search }, { cache }) {
- return axios.get(endpoint, { params: { nested: true, page, scope, search } }).then((res) => {
- const headers = normalizeHeaders(res.headers);
- const interval = headers['POLL-INTERVAL'];
- const pageInfo = { ...parseIntPagination(headers), __typename: 'LocalPageInfo' };
-
- if (interval) {
- cache.writeQuery({ query: pollIntervalQuery, data: { interval: parseFloat(interval) } });
- } else {
- cache.writeQuery({ query: pollIntervalQuery, data: { interval: undefined } });
- }
-
- cache.writeQuery({
- query: pageInfoQuery,
- data: { pageInfo },
- });
-
- return {
- availableCount: res.data.available_count,
- environments: res.data.environments.map(mapNestedEnvironment),
- reviewApp: {
- ...convertObjectPropsToCamelCase(res.data.review_app),
- __typename: 'ReviewApp',
- },
- canStopStaleEnvironments: res.data.can_stop_stale_environments,
- stoppedCount: res.data.stopped_count,
- __typename: 'LocalEnvironmentApp',
- };
- });
- },
- folder(_, { environment: { folderPath }, scope, search }) {
- return axios.get(folderPath, { params: { scope, search, per_page: 3 } }).then((res) => ({
- availableCount: res.data.available_count,
- environments: res.data.environments.map(mapEnvironment),
- stoppedCount: res.data.stopped_count,
- __typename: 'LocalEnvironmentFolder',
- }));
- },
- isLastDeployment(_, { environment }) {
- return environment?.lastDeployment?.isLast;
- },
- k8sPods(_, { configuration, namespace }) {
- const coreV1Api = new CoreV1Api(new Configuration(configuration));
- const podsApi = namespace
- ? coreV1Api.listCoreV1NamespacedPod(namespace)
- : coreV1Api.listCoreV1PodForAllNamespaces();
-
- return podsApi
- .then((res) => res?.data?.items || [])
- .catch((err) => {
- handleClusterError(err);
- });
- },
- k8sServices(_, { configuration }) {
- const coreV1Api = new CoreV1Api(new Configuration(configuration));
- return coreV1Api
- .listCoreV1ServiceForAllNamespaces()
- .then((res) => {
- const items = res?.data?.items || [];
- return items.map((item) => {
- const { type, clusterIP, externalIP, ports } = item.spec;
- return {
- metadata: item.metadata,
- spec: {
- type,
- clusterIP: clusterIP || '-',
- externalIP: externalIP || '-',
- ports,
- },
- };
- });
- })
- .catch((err) => {
- handleClusterError(err);
- });
- },
- k8sWorkloads(_, { configuration, namespace }) {
- const appsV1api = new AppsV1Api(configuration);
- const batchV1api = new BatchV1Api(configuration);
-
- let promises;
-
- if (namespace) {
- promises = [
- appsV1api.listAppsV1NamespacedDeployment(namespace),
- appsV1api.listAppsV1NamespacedDaemonSet(namespace),
- appsV1api.listAppsV1NamespacedStatefulSet(namespace),
- appsV1api.listAppsV1NamespacedReplicaSet(namespace),
- batchV1api.listBatchV1NamespacedJob(namespace),
- batchV1api.listBatchV1NamespacedCronJob(namespace),
- ];
- } else {
- promises = [
- appsV1api.listAppsV1DeploymentForAllNamespaces(),
- appsV1api.listAppsV1DaemonSetForAllNamespaces(),
- appsV1api.listAppsV1StatefulSetForAllNamespaces(),
- appsV1api.listAppsV1ReplicaSetForAllNamespaces(),
- batchV1api.listBatchV1JobForAllNamespaces(),
- batchV1api.listBatchV1CronJobForAllNamespaces(),
- ];
- }
-
- const summaryList = {
- DeploymentList: [],
- DaemonSetList: [],
- StatefulSetList: [],
- ReplicaSetList: [],
- JobList: [],
- CronJobList: [],
- };
-
- return Promise.allSettled(promises).then((results) => {
- if (results.every((res) => res.status === 'rejected')) {
- const error = results[0].reason;
- const errorMessage = error?.response?.data?.message ?? error;
- throw new Error(errorMessage);
- }
- for (const promiseResult of results) {
- if (promiseResult.status === 'fulfilled' && promiseResult?.value?.data) {
- const { kind, items } = promiseResult.value.data;
-
- if (items?.length > 0) {
- summaryList[kind] = mapWorkloadItems(items, kind);
- }
- }
- }
-
- 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));
- });
- },
- fluxKustomizationStatus(_, { configuration, namespace, environmentName }) {
- const url = buildFluxResourceUrl({
- basePath: configuration.basePath,
- resourceType: kustomizationsResourceType,
- apiVersion: kustomizationsApiVersion,
- namespace,
- environmentName,
- });
- return getFluxResourceStatus(configuration, url);
- },
- fluxHelmReleaseStatus(_, { configuration, namespace, environmentName }) {
- const url = buildFluxResourceUrl({
- basePath: configuration.basePath,
- resourceType: helmReleasesResourceType,
- apiVersion: helmReleasesApiVersion,
- namespace,
- environmentName,
- });
- return getFluxResourceStatus(configuration, url);
- },
+ ...baseQueries(endpoint),
+ ...kubernetesQueries,
+ ...fluxQueries,
},
Mutation: {
- stopEnvironmentREST(_, { environment }, { client }) {
- client.writeQuery({
- query: isEnvironmentStoppingQuery,
- variables: { environment },
- data: { isEnvironmentStopping: true },
- });
- return axios
- .post(environment.stopPath)
- .then(() => buildErrors())
- .catch(() => {
- client.writeQuery({
- query: isEnvironmentStoppingQuery,
- variables: { environment },
- data: { isEnvironmentStopping: false },
- });
- return buildErrors([
- s__('Environments|An error occurred while stopping the environment, please try again'),
- ]);
- });
- },
- deleteEnvironment(_, { environment: { deletePath } }) {
- return axios
- .delete(deletePath)
- .then(() => buildErrors())
- .catch(() =>
- buildErrors([
- s__(
- 'Environments|An error occurred while deleting the environment. Check if the environment stopped; if not, stop it and try again.',
- ),
- ]),
- );
- },
- rollbackEnvironment(_, { environment, isLastDeployment }) {
- return axios
- .post(environment?.retryUrl)
- .then(() => buildErrors())
- .catch(() => {
- buildErrors([
- isLastDeployment
- ? s__(
- 'Environments|An error occurred while re-deploying the environment, please try again',
- )
- : s__(
- 'Environments|An error occurred while rolling back the environment, please try again',
- ),
- ]);
- });
- },
- setEnvironmentToStop(_, { environment }, { client }) {
- client.writeQuery({
- query: environmentToStopQuery,
- data: { environmentToStop: environment },
- });
- },
- action(_, { action: { playPath } }) {
- return axios
- .post(playPath)
- .then(() => buildErrors())
- .catch(() =>
- buildErrors([s__('Environments|An error occurred while making the request.')]),
- );
- },
- setEnvironmentToDelete(_, { environment }, { client }) {
- client.writeQuery({
- query: environmentToDeleteQuery,
- data: { environmentToDelete: environment },
- });
- },
- setEnvironmentToRollback(_, { environment }, { client }) {
- client.writeQuery({
- query: environmentToRollbackQuery,
- data: { environmentToRollback: environment },
- });
- },
- setEnvironmentToChangeCanary(_, { environment, weight }, { client }) {
- client.writeQuery({
- query: environmentToChangeCanaryQuery,
- data: { environmentToChangeCanary: environment, weight },
- });
- },
- cancelAutoStop(_, { autoStopUrl }) {
- return axios
- .post(autoStopUrl)
- .then(() => buildErrors())
- .catch((err) =>
- buildErrors([
- err?.response?.data?.message ||
- s__('Environments|An error occurred while canceling the auto stop, please try again'),
- ]),
- );
- },
+ ...baseMutations,
},
});
diff --git a/app/assets/javascripts/environments/graphql/resolvers/base.js b/app/assets/javascripts/environments/graphql/resolvers/base.js
new file mode 100644
index 00000000000..9752a3a6634
--- /dev/null
+++ b/app/assets/javascripts/environments/graphql/resolvers/base.js
@@ -0,0 +1,165 @@
+import axios from '~/lib/utils/axios_utils';
+import { s__ } from '~/locale';
+import {
+ convertObjectPropsToCamelCase,
+ parseIntPagination,
+ normalizeHeaders,
+} from '~/lib/utils/common_utils';
+
+import pollIntervalQuery from '../queries/poll_interval.query.graphql';
+import environmentToRollbackQuery from '../queries/environment_to_rollback.query.graphql';
+import environmentToStopQuery from '../queries/environment_to_stop.query.graphql';
+import environmentToDeleteQuery from '../queries/environment_to_delete.query.graphql';
+import environmentToChangeCanaryQuery from '../queries/environment_to_change_canary.query.graphql';
+import isEnvironmentStoppingQuery from '../queries/is_environment_stopping.query.graphql';
+import pageInfoQuery from '../queries/page_info.query.graphql';
+
+const buildErrors = (errors = []) => ({
+ errors,
+ __typename: 'LocalEnvironmentErrors',
+});
+
+const mapNestedEnvironment = (env) => ({
+ ...convertObjectPropsToCamelCase(env, { deep: true }),
+ __typename: 'NestedLocalEnvironment',
+});
+const mapEnvironment = (env) => ({
+ ...convertObjectPropsToCamelCase(env, { deep: true }),
+ __typename: 'LocalEnvironment',
+});
+
+export const baseQueries = (endpoint) => ({
+ environmentApp(_context, { page, scope, search }, { cache }) {
+ return axios.get(endpoint, { params: { nested: true, page, scope, search } }).then((res) => {
+ const headers = normalizeHeaders(res.headers);
+ const interval = headers['POLL-INTERVAL'];
+ const pageInfo = { ...parseIntPagination(headers), __typename: 'LocalPageInfo' };
+
+ if (interval) {
+ cache.writeQuery({ query: pollIntervalQuery, data: { interval: parseFloat(interval) } });
+ } else {
+ cache.writeQuery({ query: pollIntervalQuery, data: { interval: undefined } });
+ }
+
+ cache.writeQuery({
+ query: pageInfoQuery,
+ data: { pageInfo },
+ });
+
+ return {
+ availableCount: res.data.available_count,
+ environments: res.data.environments.map(mapNestedEnvironment),
+ reviewApp: {
+ ...convertObjectPropsToCamelCase(res.data.review_app),
+ __typename: 'ReviewApp',
+ },
+ canStopStaleEnvironments: res.data.can_stop_stale_environments,
+ stoppedCount: res.data.stopped_count,
+ __typename: 'LocalEnvironmentApp',
+ };
+ });
+ },
+ folder(_, { environment: { folderPath }, scope, search }) {
+ return axios.get(folderPath, { params: { scope, search, per_page: 3 } }).then((res) => ({
+ availableCount: res.data.available_count,
+ environments: res.data.environments.map(mapEnvironment),
+ stoppedCount: res.data.stopped_count,
+ __typename: 'LocalEnvironmentFolder',
+ }));
+ },
+ isLastDeployment(_, { environment }) {
+ return environment?.lastDeployment?.isLast;
+ },
+});
+
+export const baseMutations = {
+ stopEnvironmentREST(_, { environment }, { client }) {
+ client.writeQuery({
+ query: isEnvironmentStoppingQuery,
+ variables: { environment },
+ data: { isEnvironmentStopping: true },
+ });
+ return axios
+ .post(environment.stopPath)
+ .then(() => buildErrors())
+ .catch(() => {
+ client.writeQuery({
+ query: isEnvironmentStoppingQuery,
+ variables: { environment },
+ data: { isEnvironmentStopping: false },
+ });
+ return buildErrors([
+ s__('Environments|An error occurred while stopping the environment, please try again'),
+ ]);
+ });
+ },
+ deleteEnvironment(_, { environment: { deletePath } }) {
+ return axios
+ .delete(deletePath)
+ .then(() => buildErrors())
+ .catch(() =>
+ buildErrors([
+ s__(
+ 'Environments|An error occurred while deleting the environment. Check if the environment stopped; if not, stop it and try again.',
+ ),
+ ]),
+ );
+ },
+ rollbackEnvironment(_, { environment, isLastDeployment }) {
+ return axios
+ .post(environment?.retryUrl)
+ .then(() => buildErrors())
+ .catch(() => {
+ buildErrors([
+ isLastDeployment
+ ? s__(
+ 'Environments|An error occurred while re-deploying the environment, please try again',
+ )
+ : s__(
+ 'Environments|An error occurred while rolling back the environment, please try again',
+ ),
+ ]);
+ });
+ },
+ setEnvironmentToStop(_, { environment }, { client }) {
+ client.writeQuery({
+ query: environmentToStopQuery,
+ data: { environmentToStop: environment },
+ });
+ },
+ action(_, { action: { playPath } }) {
+ return axios
+ .post(playPath)
+ .then(() => buildErrors())
+ .catch(() => buildErrors([s__('Environments|An error occurred while making the request.')]));
+ },
+ setEnvironmentToDelete(_, { environment }, { client }) {
+ client.writeQuery({
+ query: environmentToDeleteQuery,
+ data: { environmentToDelete: environment },
+ });
+ },
+ setEnvironmentToRollback(_, { environment }, { client }) {
+ client.writeQuery({
+ query: environmentToRollbackQuery,
+ data: { environmentToRollback: environment },
+ });
+ },
+ setEnvironmentToChangeCanary(_, { environment, weight }, { client }) {
+ client.writeQuery({
+ query: environmentToChangeCanaryQuery,
+ data: { environmentToChangeCanary: environment, weight },
+ });
+ },
+ cancelAutoStop(_, { autoStopUrl }) {
+ return axios
+ .post(autoStopUrl)
+ .then(() => buildErrors())
+ .catch((err) =>
+ buildErrors([
+ err?.response?.data?.message ||
+ s__('Environments|An error occurred while canceling the auto stop, please try again'),
+ ]),
+ );
+ },
+};
diff --git a/app/assets/javascripts/environments/graphql/resolvers/flux.js b/app/assets/javascripts/environments/graphql/resolvers/flux.js
new file mode 100644
index 00000000000..f9ca35a3165
--- /dev/null
+++ b/app/assets/javascripts/environments/graphql/resolvers/flux.js
@@ -0,0 +1,115 @@
+import axios from '~/lib/utils/axios_utils';
+import {
+ HELM_RELEASES_RESOURCE_TYPE,
+ KUSTOMIZATIONS_RESOURCE_TYPE,
+} from '~/environments/constants';
+
+const helmReleasesApiVersion = 'helm.toolkit.fluxcd.io/v2beta1';
+const kustomizationsApiVersion = 'kustomize.toolkit.fluxcd.io/v1beta1';
+
+const handleClusterError = (err) => {
+ const error = err?.response?.data?.message ? new Error(err.response.data.message) : err;
+ throw error;
+};
+
+const buildFluxResourceUrl = ({
+ basePath,
+ namespace,
+ apiVersion,
+ resourceType,
+ environmentName = '',
+}) => {
+ return `${basePath}/apis/${apiVersion}/namespaces/${namespace}/${resourceType}/${environmentName}`;
+};
+
+const getFluxResourceStatus = (configuration, url) => {
+ const { headers } = configuration.baseOptions;
+ const withCredentials = true;
+
+ return axios
+ .get(url, { withCredentials, headers })
+ .then((res) => {
+ return res?.data?.status?.conditions || [];
+ })
+ .catch((err) => {
+ handleClusterError(err);
+ });
+};
+
+const getFluxResources = (configuration, url) => {
+ const { headers } = configuration.baseOptions;
+ const withCredentials = true;
+
+ return axios
+ .get(url, { withCredentials, headers })
+ .then((res) => {
+ const items = res?.data?.items || [];
+ const result = items.map((item) => {
+ return {
+ apiVersion: item.apiVersion,
+ metadata: {
+ name: item.metadata?.name,
+ namespace: item.metadata?.namespace,
+ },
+ };
+ });
+ return result || [];
+ })
+ .catch((err) => {
+ const error = err?.response?.data?.reason || err;
+ throw new Error(error);
+ });
+};
+
+export default {
+ fluxKustomizationStatus(_, { configuration, namespace, environmentName, fluxResourcePath = '' }) {
+ let url;
+
+ if (fluxResourcePath) {
+ url = `${configuration.basePath}/apis/${fluxResourcePath}`;
+ } else {
+ url = buildFluxResourceUrl({
+ basePath: configuration.basePath,
+ resourceType: KUSTOMIZATIONS_RESOURCE_TYPE,
+ apiVersion: kustomizationsApiVersion,
+ namespace,
+ environmentName,
+ });
+ }
+ return getFluxResourceStatus(configuration, url);
+ },
+ fluxHelmReleaseStatus(_, { configuration, namespace, environmentName, fluxResourcePath }) {
+ let url;
+
+ if (fluxResourcePath) {
+ url = `${configuration.basePath}/apis/${fluxResourcePath}`;
+ } else {
+ url = buildFluxResourceUrl({
+ basePath: configuration.basePath,
+ resourceType: HELM_RELEASES_RESOURCE_TYPE,
+ apiVersion: helmReleasesApiVersion,
+ namespace,
+ environmentName,
+ });
+ }
+ return getFluxResourceStatus(configuration, url);
+ },
+ fluxKustomizations(_, { configuration, namespace }) {
+ const url = buildFluxResourceUrl({
+ basePath: configuration.basePath,
+ resourceType: KUSTOMIZATIONS_RESOURCE_TYPE,
+ apiVersion: kustomizationsApiVersion,
+ namespace,
+ });
+ return getFluxResources(configuration, url);
+ },
+ fluxHelmReleases(_, { configuration, namespace }) {
+ const url = buildFluxResourceUrl({
+ basePath: configuration.basePath,
+ resourceType: HELM_RELEASES_RESOURCE_TYPE,
+ apiVersion: helmReleasesApiVersion,
+ namespace,
+ });
+ return getFluxResources(configuration, url);
+ },
+};
diff --git a/app/assets/javascripts/environments/graphql/resolvers/kubernetes.js b/app/assets/javascripts/environments/graphql/resolvers/kubernetes.js
new file mode 100644
index 00000000000..9ab65d0bb7f
--- /dev/null
+++ b/app/assets/javascripts/environments/graphql/resolvers/kubernetes.js
@@ -0,0 +1,155 @@
+import { CoreV1Api, Configuration, AppsV1Api, BatchV1Api } from '@gitlab/cluster-client';
+import { humanizeClusterErrors } from '../../helpers/k8s_integration_helper';
+
+const mapWorkloadItems = (items, kind) => {
+ return items.map((item) => {
+ const updatedItem = {
+ status: {},
+ spec: {},
+ };
+
+ switch (kind) {
+ case 'DeploymentList':
+ updatedItem.status.conditions = item.status.conditions || [];
+ break;
+ case 'DaemonSetList':
+ updatedItem.status = {
+ numberMisscheduled: item.status.numberMisscheduled || 0,
+ numberReady: item.status.numberReady || 0,
+ desiredNumberScheduled: item.status.desiredNumberScheduled || 0,
+ };
+ break;
+ case 'StatefulSetList':
+ case 'ReplicaSetList':
+ updatedItem.status.readyReplicas = item.status.readyReplicas || 0;
+ updatedItem.spec.replicas = item.spec.replicas || 0;
+ break;
+ case 'JobList':
+ updatedItem.status.failed = item.status.failed || 0;
+ updatedItem.status.succeeded = item.status.succeeded || 0;
+ updatedItem.spec.completions = item.spec.completions || 0;
+ break;
+ case 'CronJobList':
+ updatedItem.status.active = item.status.active || 0;
+ updatedItem.status.lastScheduleTime = item.status.lastScheduleTime || '';
+ updatedItem.spec.suspend = item.spec.suspend || 0;
+ break;
+ default:
+ updatedItem.status = item?.status;
+ updatedItem.spec = item?.spec;
+ break;
+ }
+
+ return updatedItem;
+ });
+};
+
+const handleClusterError = (err) => {
+ const error = err?.response?.data?.message ? new Error(err.response.data.message) : err;
+ throw error;
+};
+
+export default {
+ k8sPods(_, { configuration, namespace }) {
+ const coreV1Api = new CoreV1Api(new Configuration(configuration));
+ const podsApi = namespace
+ ? coreV1Api.listCoreV1NamespacedPod(namespace)
+ : coreV1Api.listCoreV1PodForAllNamespaces();
+
+ return podsApi
+ .then((res) => res?.data?.items || [])
+ .catch((err) => {
+ handleClusterError(err);
+ });
+ },
+ k8sServices(_, { configuration }) {
+ const coreV1Api = new CoreV1Api(new Configuration(configuration));
+ return coreV1Api
+ .listCoreV1ServiceForAllNamespaces()
+ .then((res) => {
+ const items = res?.data?.items || [];
+ return items.map((item) => {
+ const { type, clusterIP, externalIP, ports } = item.spec;
+ return {
+ metadata: item.metadata,
+ spec: {
+ type,
+ clusterIP: clusterIP || '-',
+ externalIP: externalIP || '-',
+ ports,
+ },
+ };
+ });
+ })
+ .catch((err) => {
+ handleClusterError(err);
+ });
+ },
+ k8sWorkloads(_, { configuration, namespace }) {
+ const appsV1api = new AppsV1Api(configuration);
+ const batchV1api = new BatchV1Api(configuration);
+
+ let promises;
+
+ if (namespace) {
+ promises = [
+ appsV1api.listAppsV1NamespacedDeployment(namespace),
+ appsV1api.listAppsV1NamespacedDaemonSet(namespace),
+ appsV1api.listAppsV1NamespacedStatefulSet(namespace),
+ appsV1api.listAppsV1NamespacedReplicaSet(namespace),
+ batchV1api.listBatchV1NamespacedJob(namespace),
+ batchV1api.listBatchV1NamespacedCronJob(namespace),
+ ];
+ } else {
+ promises = [
+ appsV1api.listAppsV1DeploymentForAllNamespaces(),
+ appsV1api.listAppsV1DaemonSetForAllNamespaces(),
+ appsV1api.listAppsV1StatefulSetForAllNamespaces(),
+ appsV1api.listAppsV1ReplicaSetForAllNamespaces(),
+ batchV1api.listBatchV1JobForAllNamespaces(),
+ batchV1api.listBatchV1CronJobForAllNamespaces(),
+ ];
+ }
+
+ const summaryList = {
+ DeploymentList: [],
+ DaemonSetList: [],
+ StatefulSetList: [],
+ ReplicaSetList: [],
+ JobList: [],
+ CronJobList: [],
+ };
+
+ return Promise.allSettled(promises).then((results) => {
+ if (results.every((res) => res.status === 'rejected')) {
+ const error = results[0].reason;
+ const errorMessage = error?.response?.data?.message ?? error;
+ throw new Error(errorMessage);
+ }
+ for (const promiseResult of results) {
+ if (promiseResult.status === 'fulfilled' && promiseResult?.value?.data) {
+ const { kind, items } = promiseResult.value.data;
+
+ if (items?.length > 0) {
+ summaryList[kind] = mapWorkloadItems(items, kind);
+ }
+ }
+ }
+
+ 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));
+ });
+ },
+};
diff --git a/app/assets/javascripts/environments/helpers/k8s_integration_helper.js b/app/assets/javascripts/environments/helpers/k8s_integration_helper.js
index e49f1451759..164a2d98e90 100644
--- a/app/assets/javascripts/environments/helpers/k8s_integration_helper.js
+++ b/app/assets/javascripts/environments/helpers/k8s_integration_helper.js
@@ -142,7 +142,7 @@ export function getCronJobsStatuses(items) {
}
export function humanizeClusterErrors(reason) {
- const errorReason = reason.toLowerCase();
+ const errorReason = String(reason).toLowerCase();
const errorMessage = CLUSTER_AGENT_ERROR_MESSAGES[errorReason];
return errorMessage || CLUSTER_AGENT_ERROR_MESSAGES.other;
}