Commit 0a8d441b authored by Martin Wortschack's avatar Martin Wortschack

Merge branch 'show-kubernetes-namespace-on-job-show-page' into 'master'

Show Kubernetes namespace on job show page

Closes #31756

See merge request gitlab-org/gitlab!20983
parents d3ee57b1 66dc32d2
......@@ -12,6 +12,11 @@ export default {
type: Object,
required: true,
},
deploymentCluster: {
type: Object,
required: false,
default: null,
},
iconStatus: {
type: Object,
required: true,
......@@ -61,14 +66,14 @@ export default {
: '';
},
hasCluster() {
return this.hasLastDeployment && this.lastDeployment.cluster;
return Boolean(this.deploymentCluster) && Boolean(this.deploymentCluster.name);
},
clusterNameOrLink() {
if (!this.hasCluster) {
return '';
}
const { name, path } = this.lastDeployment.cluster;
const { name, path } = this.deploymentCluster;
const escapedName = _.escape(name);
const escapedPath = _.escape(path);
......@@ -86,6 +91,9 @@ export default {
false,
);
},
kubernetesNamespace() {
return this.hasCluster ? this.deploymentCluster.kubernetes_namespace : null;
},
},
methods: {
deploymentLink(name) {
......@@ -109,75 +117,153 @@ export default {
);
},
lastEnvironmentMessage() {
const { environmentLink, clusterNameOrLink, hasCluster } = this;
const message = hasCluster
? __('This job is deployed to %{environmentLink} using cluster %{clusterNameOrLink}.')
: __('This job is deployed to %{environmentLink}.');
return sprintf(message, { environmentLink, clusterNameOrLink }, false);
const { environmentLink, clusterNameOrLink, hasCluster, kubernetesNamespace } = this;
if (hasCluster) {
if (kubernetesNamespace) {
return sprintf(
__(
'This job is deployed to %{environmentLink} using cluster %{clusterNameOrLink} and namespace %{kubernetesNamespace}.',
),
{ environmentLink, clusterNameOrLink, kubernetesNamespace },
false,
);
}
// we know the cluster but not the namespace
return sprintf(
__('This job is deployed to %{environmentLink} using cluster %{clusterNameOrLink}.'),
{ environmentLink, clusterNameOrLink },
false,
);
}
// not a cluster deployment
return sprintf(__('This job is deployed to %{environmentLink}.'), { environmentLink }, false);
},
outOfDateEnvironmentMessage() {
const { hasLastDeployment, hasCluster, environmentLink, clusterNameOrLink } = this;
const {
hasLastDeployment,
hasCluster,
environmentLink,
clusterNameOrLink,
kubernetesNamespace,
} = this;
if (hasLastDeployment) {
const message = hasCluster
? __(
'This job is an out-of-date deployment to %{environmentLink} using cluster %{clusterNameOrLink}. View the %{deploymentLink}.',
)
: __(
'This job is an out-of-date deployment to %{environmentLink}. View the %{deploymentLink}.',
const deploymentLink = this.deploymentLink(__('most recent deployment'));
if (hasCluster) {
if (kubernetesNamespace) {
return sprintf(
__(
'This job is an out-of-date deployment to %{environmentLink} using cluster %{clusterNameOrLink} and namespace %{kubernetesNamespace}. View the %{deploymentLink}.',
),
{ environmentLink, clusterNameOrLink, kubernetesNamespace, deploymentLink },
false,
);
}
// we know the cluster but not the namespace
return sprintf(
__(
'This job is an out-of-date deployment to %{environmentLink} using cluster %{clusterNameOrLink}. View the %{deploymentLink}.',
),
{ environmentLink, clusterNameOrLink, deploymentLink },
false,
);
}
// not a cluster deployment
return sprintf(
message,
{
environmentLink,
clusterNameOrLink,
deploymentLink: this.deploymentLink(__('most recent deployment')),
},
__(
'This job is an out-of-date deployment to %{environmentLink}. View the %{deploymentLink}.',
),
{ environmentLink, deploymentLink },
false,
);
}
const message = hasCluster
? __(
// no last deployment, i.e. this is the first deployment
if (hasCluster) {
if (kubernetesNamespace) {
return sprintf(
__(
'This job is an out-of-date deployment to %{environmentLink} using cluster %{clusterNameOrLink} and namespace %{kubernetesNamespace}.',
),
{ environmentLink, clusterNameOrLink, kubernetesNamespace },
false,
);
}
// we know the cluster but not the namespace
return sprintf(
__(
'This job is an out-of-date deployment to %{environmentLink} using cluster %{clusterNameOrLink}.',
)
: __('This job is an out-of-date deployment to %{environmentLink}.');
),
{ environmentLink, clusterNameOrLink },
false,
);
}
// not a cluster deployment
return sprintf(
message,
{
environmentLink,
clusterNameOrLink,
},
__('This job is an out-of-date deployment to %{environmentLink}.'),
{ environmentLink },
false,
);
},
creatingEnvironmentMessage() {
const { hasLastDeployment, hasCluster, environmentLink, clusterNameOrLink } = this;
const {
hasLastDeployment,
hasCluster,
environmentLink,
clusterNameOrLink,
kubernetesNamespace,
} = this;
if (hasLastDeployment) {
const message = hasCluster
? __(
'This job is creating a deployment to %{environmentLink} using cluster %{clusterNameOrLink}. This will overwrite the %{deploymentLink}.',
)
: __(
'This job is creating a deployment to %{environmentLink}. This will overwrite the %{deploymentLink}.',
const deploymentLink = this.deploymentLink(__('latest deployment'));
if (hasCluster) {
if (kubernetesNamespace) {
return sprintf(
__(
'This job is creating a deployment to %{environmentLink} using cluster %{clusterNameOrLink} and namespace %{kubernetesNamespace}. This will overwrite the %{deploymentLink}.',
),
{ environmentLink, clusterNameOrLink, kubernetesNamespace, deploymentLink },
false,
);
}
// we know the cluster but not the namespace
return sprintf(
__(
'This job is creating a deployment to %{environmentLink} using cluster %{clusterNameOrLink}. This will overwrite the %{deploymentLink}.',
),
{ environmentLink, clusterNameOrLink, deploymentLink },
false,
);
}
// not a cluster deployment
return sprintf(
message,
{
environmentLink,
clusterNameOrLink,
deploymentLink: this.deploymentLink(__('latest deployment')),
},
__(
'This job is creating a deployment to %{environmentLink}. This will overwrite the %{deploymentLink}.',
),
{ environmentLink, deploymentLink },
false,
);
}
// no last deployment, i.e. this is the first deployment
if (hasCluster) {
if (kubernetesNamespace) {
return sprintf(
__(
'This job is creating a deployment to %{environmentLink} using cluster %{clusterNameOrLink} and namespace %{kubernetesNamespace}.',
),
{ environmentLink, clusterNameOrLink, kubernetesNamespace },
false,
);
}
// we know the cluster but not the namespace
return sprintf(
__(
'This job is creating a deployment to %{environmentLink} using cluster %{clusterNameOrLink}.',
),
{ environmentLink, clusterNameOrLink },
false,
);
}
// not a cluster deployment
return sprintf(
__('This job is creating a deployment to %{environmentLink}.'),
{ environmentLink },
......
......@@ -256,6 +256,7 @@ export default {
v-if="hasEnvironment"
class="js-job-environment"
:deployment-status="job.deployment_status"
:deployment-cluster="job.deployment_cluster"
:icon-status="job.status"
/>
......
......@@ -30,6 +30,7 @@ class Deployment < ApplicationRecord
validate :valid_ref, on: :create
delegate :name, to: :environment, prefix: true
delegate :kubernetes_namespace, to: :deployment_cluster, allow_nil: true
scope :for_environment, -> (environment) { where(environment_id: environment) }
scope :for_environment_name, -> (name) do
......
......@@ -22,6 +22,12 @@ class BuildDetailsEntity < JobEntity
end
end
expose :deployment_cluster, if: -> (build) { build&.deployment&.cluster } do |build, options|
# Until data is copied over from deployments.cluster_id, this entity must represent Deployment instead of DeploymentCluster
# https://gitlab.com/gitlab-org/gitlab/issues/202628
DeploymentClusterEntity.represent(build.deployment, options)
end
expose :artifact, if: -> (*) { can?(current_user, :read_build, build) } do
expose :download_path, if: -> (*) { build.artifacts? } do |build|
download_project_job_artifacts_path(project, build)
......
# frozen_string_literal: true
class ClusterBasicEntity < Grape::Entity
include RequestAwareEntity
expose :name
expose :path, if: -> (cluster) { can?(request.current_user, :read_cluster, cluster) } do |cluster|
cluster.present(current_user: request.current_user).show_path
end
end
# frozen_string_literal: true
class DeploymentClusterEntity < Grape::Entity
include RequestAwareEntity
# Until data is copied over from deployments.cluster_id, this entity must represent Deployment instead of DeploymentCluster
# https://gitlab.com/gitlab-org/gitlab/issues/202628
expose :name do |deployment|
deployment.cluster.name
end
expose :path, if: -> (deployment) { can?(request.current_user, :read_cluster, deployment.cluster) } do |deployment|
deployment.cluster.present(current_user: request.current_user).show_path
end
expose :kubernetes_namespace, if: -> (deployment) { can?(request.current_user, :read_cluster, deployment.cluster) } do |deployment|
deployment.kubernetes_namespace
end
end
......@@ -41,7 +41,11 @@ class DeploymentEntity < Grape::Entity
JobEntity.represent(deployment.playable_build, options.merge(only: [:play_path, :retry_path]))
end
expose :cluster, using: ClusterBasicEntity
expose :cluster do |deployment, options|
# Until data is copied over from deployments.cluster_id, this entity must represent Deployment instead of DeploymentCluster
# https://gitlab.com/gitlab-org/gitlab/issues/202628
DeploymentClusterEntity.represent(deployment, options) unless deployment.cluster.nil?
end
private
......
---
title: Show Kubernetes namespace on job show page
merge_request: 20983
author:
type: added
......@@ -19587,6 +19587,12 @@ msgstr ""
msgid "This job has not started yet"
msgstr ""
msgid "This job is an out-of-date deployment to %{environmentLink} using cluster %{clusterNameOrLink} and namespace %{kubernetesNamespace}."
msgstr ""
msgid "This job is an out-of-date deployment to %{environmentLink} using cluster %{clusterNameOrLink} and namespace %{kubernetesNamespace}. View the %{deploymentLink}."
msgstr ""
msgid "This job is an out-of-date deployment to %{environmentLink} using cluster %{clusterNameOrLink}."
msgstr ""
......@@ -19602,6 +19608,15 @@ msgstr ""
msgid "This job is archived. Only the complete pipeline can be retried."
msgstr ""
msgid "This job is creating a deployment to %{environmentLink} using cluster %{clusterNameOrLink} and namespace %{kubernetesNamespace}."
msgstr ""
msgid "This job is creating a deployment to %{environmentLink} using cluster %{clusterNameOrLink} and namespace %{kubernetesNamespace}. This will overwrite the %{deploymentLink}."
msgstr ""
msgid "This job is creating a deployment to %{environmentLink} using cluster %{clusterNameOrLink}."
msgstr ""
msgid "This job is creating a deployment to %{environmentLink} using cluster %{clusterNameOrLink}. This will overwrite the %{deploymentLink}."
msgstr ""
......@@ -19611,6 +19626,9 @@ msgstr ""
msgid "This job is creating a deployment to %{environmentLink}. This will overwrite the %{deploymentLink}."
msgstr ""
msgid "This job is deployed to %{environmentLink} using cluster %{clusterNameOrLink} and namespace %{kubernetesNamespace}."
msgstr ""
msgid "This job is deployed to %{environmentLink} using cluster %{clusterNameOrLink}."
msgstr ""
......
# frozen_string_literal: true
FactoryBot.define do
factory :deployment_cluster, class: 'DeploymentCluster' do
cluster
deployment
kubernetes_namespace { 'the-namespace' }
end
end
......@@ -50,7 +50,7 @@
"cluster": {
"oneOf": [
{ "type": "null" },
{ "$ref": "cluster_basic.json" }
{ "$ref": "deployment_cluster.json" }
]
},
"manual_actions": {
......
......@@ -10,6 +10,12 @@
{ "type": "null" },
{ "type": "string" }
]
},
"kubernetes_namespace": {
"oneOf": [
{ "type": "null" },
{ "type": "string" }
]
}
},
"additionalProperties": false
......
......@@ -15,6 +15,12 @@
"terminal_path": { "type": "string" },
"trigger": { "$ref": "trigger.json" },
"deployment_status": { "$ref": "deployment_status.json" },
"deployment_cluster": {
"oneOf": [
{ "$ref": "../deployment_cluster.json" },
{ "type": "null" }
]
},
"runner": { "$ref": "runner.json" },
"runners": { "$ref": "runners.json" },
"has_trace": { "type": "boolean" },
......
......@@ -4,6 +4,7 @@ import mountComponent from '../../helpers/vue_mount_component_helper';
const TEST_CLUSTER_NAME = 'test_cluster';
const TEST_CLUSTER_PATH = 'path/to/test_cluster';
const TEST_KUBERNETES_NAMESPACE = 'this-is-a-kubernetes-namespace';
describe('Environments block', () => {
const Component = Vue.extend(component);
......@@ -28,17 +29,18 @@ describe('Environments block', () => {
last_deployment: { ...lastDeployment },
});
const createEnvironmentWithCluster = () => ({
...environment,
last_deployment: {
...lastDeployment,
cluster: { name: TEST_CLUSTER_NAME, path: TEST_CLUSTER_PATH },
},
const createDeploymentWithCluster = () => ({ name: TEST_CLUSTER_NAME, path: TEST_CLUSTER_PATH });
const createDeploymentWithClusterAndKubernetesNamespace = () => ({
name: TEST_CLUSTER_NAME,
path: TEST_CLUSTER_PATH,
kubernetes_namespace: TEST_KUBERNETES_NAMESPACE,
});
const createComponent = (deploymentStatus = {}) => {
const createComponent = (deploymentStatus = {}, deploymentCluster = {}) => {
vm = mountComponent(Component, {
deploymentStatus,
deploymentCluster,
iconStatus: status,
});
};
......@@ -62,15 +64,36 @@ describe('Environments block', () => {
expect(findText()).toEqual('This job is deployed to environment.');
});
it('renders info with cluster', () => {
createComponent({
status: 'last',
environment: createEnvironmentWithCluster(),
describe('when there is a cluster', () => {
it('renders info with cluster', () => {
createComponent(
{
status: 'last',
environment: createEnvironmentWithLastDeployment(),
},
createDeploymentWithCluster(),
);
expect(findText()).toEqual(
`This job is deployed to environment using cluster ${TEST_CLUSTER_NAME}.`,
);
});
expect(findText()).toEqual(
`This job is deployed to environment using cluster ${TEST_CLUSTER_NAME}.`,
);
describe('when there is a kubernetes namespace', () => {
it('renders info with cluster', () => {
createComponent(
{
status: 'last',
environment: createEnvironmentWithLastDeployment(),
},
createDeploymentWithClusterAndKubernetesNamespace(),
);
expect(findText()).toEqual(
`This job is deployed to environment using cluster ${TEST_CLUSTER_NAME} and namespace ${TEST_KUBERNETES_NAMESPACE}.`,
);
});
});
});
});
......@@ -89,15 +112,36 @@ describe('Environments block', () => {
expect(findJobDeploymentLink().getAttribute('href')).toEqual('bar');
});
it('renders info with cluster', () => {
createComponent({
status: 'out_of_date',
environment: createEnvironmentWithCluster(),
describe('when there is a cluster', () => {
it('renders info with cluster', () => {
createComponent(
{
status: 'out_of_date',
environment: createEnvironmentWithLastDeployment(),
},
createDeploymentWithCluster(),
);
expect(findText()).toEqual(
`This job is an out-of-date deployment to environment using cluster ${TEST_CLUSTER_NAME}. View the most recent deployment.`,
);
});
expect(findText()).toEqual(
`This job is an out-of-date deployment to environment using cluster ${TEST_CLUSTER_NAME}. View the most recent deployment.`,
);
describe('when there is a kubernetes namespace', () => {
it('renders info with cluster', () => {
createComponent(
{
status: 'out_of_date',
environment: createEnvironmentWithLastDeployment(),
},
createDeploymentWithClusterAndKubernetesNamespace(),
);
expect(findText()).toEqual(
`This job is an out-of-date deployment to environment using cluster ${TEST_CLUSTER_NAME} and namespace ${TEST_KUBERNETES_NAMESPACE}. View the most recent deployment.`,
);
});
});
});
});
......@@ -143,7 +187,7 @@ describe('Environments block', () => {
});
describe('without last deployment', () => {
it('renders info about failed deployment', () => {
it('renders info about deployment being created', () => {
createComponent({
status: 'creating',
environment,
......@@ -151,6 +195,22 @@ describe('Environments block', () => {
expect(findText()).toEqual('This job is creating a deployment to environment.');
});
describe('when there is a cluster', () => {
it('inclues information about the cluster', () => {
createComponent(
{
status: 'creating',
environment,
},
createDeploymentWithCluster(),
);
expect(findText()).toEqual(
`This job is creating a deployment to environment using cluster ${TEST_CLUSTER_NAME}.`,
);
});
});
});
describe('without environment', () => {
......@@ -167,10 +227,13 @@ describe('Environments block', () => {
describe('with a cluster', () => {
it('renders the cluster link', () => {
createComponent({
status: 'last',
environment: createEnvironmentWithCluster(),
});
createComponent(
{
status: 'last',
environment: createEnvironmentWithLastDeployment(),
},
createDeploymentWithCluster(),
);
expect(findText()).toEqual(
`This job is deployed to environment using cluster ${TEST_CLUSTER_NAME}.`,
......@@ -181,18 +244,13 @@ describe('Environments block', () => {
describe('when the cluster is missing the path', () => {
it('renders the name without a link', () => {
const cluster = {
name: 'the-cluster',
};
createComponent({
status: 'last',
environment: Object.assign({}, environment, {
last_deployment: {
...lastDeployment,
cluster,
},
}),
});
createComponent(
{
status: 'last',
environment: createEnvironmentWithLastDeployment(),
},
{ name: 'the-cluster' },
);
expect(findText()).toContain('using cluster the-cluster.');
......
......@@ -18,6 +18,7 @@ describe Deployment do
it { is_expected.to delegate_method(:commit).to(:project) }
it { is_expected.to delegate_method(:commit_title).to(:commit).as(:try) }
it { is_expected.to delegate_method(:manual_actions).to(:deployable).as(:try) }
it { is_expected.to delegate_method(:kubernetes_namespace).to(:deployment_cluster).as(:kubernetes_namespace) }
it { is_expected.to validate_presence_of(:ref) }
it { is_expected.to validate_presence_of(:sha) }
......
......@@ -2,9 +2,9 @@
require 'spec_helper'
describe ClusterBasicEntity do
describe DeploymentClusterEntity do
describe '#as_json' do
subject { described_class.new(cluster, request: request).as_json }
subject { described_class.new(deployment, request: request).as_json }
let(:maintainer) { create(:user) }
let(:developer) { create(:user) }
......@@ -12,26 +12,30 @@ describe ClusterBasicEntity do
let(:request) { double(:request, current_user: current_user) }
let(:project) { create(:project) }
let(:cluster) { create(:cluster, name: 'the-cluster', projects: [project]) }
let(:deployment) { create(:deployment, cluster: cluster) }
let!(:deployment_cluster) { create(:deployment_cluster, cluster: cluster, deployment: deployment) }
before do
project.add_maintainer(maintainer)
project.add_developer(developer)
end
it 'matches cluster_basic entity schema' do
expect(subject.as_json).to match_schema('cluster_basic')
it 'matches deployment_cluster entity schema' do
expect(subject.as_json).to match_schema('deployment_cluster')
end
it 'exposes the cluster details' do
expect(subject[:name]).to eq('the-cluster')
expect(subject[:path]).to eq("/#{project.full_path}/-/clusters/#{cluster.id}")
expect(subject[:kubernetes_namespace]).to eq(deployment_cluster.kubernetes_namespace)
end
context 'when the user does not have permission to view the cluster' do
let(:current_user) { developer }
it 'does not include the path' do
it 'does not include the path nor the namespace' do
expect(subject[:path]).to be_nil
expect(subject[:kubernetes_namespace]).to be_nil
end
end
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