Commit 66dc32d2 authored by Hordur Freyr Yngvason's avatar Hordur Freyr Yngvason Committed by Martin Wortschack

Show Kubernetes namespace on job show page

Adds support for showing kubernetes namespaces on deployment jobs where
the namespace has been recorded.

See https://gitlab.com/gitlab-org/gitlab/issues/31756

Also fixes a bug where we were using the last successful deployment
instead of the deployment associated with the build to determine which
cluster to display.
parent afde97a1
......@@ -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