diff options
Diffstat (limited to 'spec/services')
16 files changed, 963 insertions, 610 deletions
diff --git a/spec/services/ci/process_build_service_spec.rb b/spec/services/ci/process_build_service_spec.rb index 9a53b32394d..704685417bb 100644 --- a/spec/services/ci/process_build_service_spec.rb +++ b/spec/services/ci/process_build_service_spec.rb @@ -98,47 +98,19 @@ describe Ci::ProcessBuildService, '#execute' do let(:build) { create(:ci_build, :created, :schedulable, user: user, project: project) } - context 'when ci_enable_scheduled_build is enabled' do - before do - stub_feature_flags(ci_enable_scheduled_build: true) - end - - context 'when current status is success' do - let(:current_status) { 'success' } - - it 'changes the build status' do - expect { subject }.to change { build.status }.to('scheduled') - end - end - - context 'when current status is failed' do - let(:current_status) { 'failed' } + context 'when current status is success' do + let(:current_status) { 'success' } - it 'does not change the build status' do - expect { subject }.to change { build.status }.to('skipped') - end + it 'changes the build status' do + expect { subject }.to change { build.status }.to('scheduled') end end - context 'when ci_enable_scheduled_build is disabled' do - before do - stub_feature_flags(ci_enable_scheduled_build: false) - end - - context 'when current status is success' do - let(:current_status) { 'success' } - - it 'changes the build status' do - expect { subject }.to change { build.status }.to('manual') - end - end - - context 'when current status is failed' do - let(:current_status) { 'failed' } + context 'when current status is failed' do + let(:current_status) { 'failed' } - it 'does not change the build status' do - expect { subject }.to change { build.status }.to('skipped') - end + it 'does not change the build status' do + expect { subject }.to change { build.status }.to('skipped') end end end diff --git a/spec/services/ci/register_job_service_spec.rb b/spec/services/ci/register_job_service_spec.rb index a6565709641..56e2a405bcd 100644 --- a/spec/services/ci/register_job_service_spec.rb +++ b/spec/services/ci/register_job_service_spec.rb @@ -478,6 +478,20 @@ module Ci it_behaves_like 'validation is not active' end end + + context 'when build is degenerated' do + let!(:pending_job) { create(:ci_build, :pending, :degenerated, pipeline: pipeline) } + + subject { execute(specific_runner, {}) } + + it 'does not pick the build and drops the build' do + expect(subject).to be_nil + + pending_job.reload + expect(pending_job).to be_failed + expect(pending_job).to be_archived_failure + end + end end describe '#register_success' do diff --git a/spec/services/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb index 368abded448..e779675744c 100644 --- a/spec/services/ci/retry_build_service_spec.rb +++ b/spec/services/ci/retry_build_service_spec.rb @@ -32,7 +32,7 @@ describe Ci::RetryBuildService do IGNORE_ACCESSORS = %i[type lock_version target_url base_tags trace_sections - commit_id deployments erased_by_id last_deployment project_id + commit_id deployment erased_by_id project_id runner_id tag_taggings taggings tags trigger_request_id user_id auto_canceled_by_id retried failure_reason artifacts_file_store artifacts_metadata_store diff --git a/spec/services/ci/run_scheduled_build_service_spec.rb b/spec/services/ci/run_scheduled_build_service_spec.rb index 2c921dac238..be2aad33ef4 100644 --- a/spec/services/ci/run_scheduled_build_service_spec.rb +++ b/spec/services/ci/run_scheduled_build_service_spec.rb @@ -7,10 +7,6 @@ describe Ci::RunScheduledBuildService do subject { described_class.new(project, user).execute(build) } - before do - stub_feature_flags(ci_enable_scheduled_build: true) - end - context 'when user can update build' do before do project.add_developer(user) diff --git a/spec/services/clusters/create_service_spec.rb b/spec/services/clusters/create_service_spec.rb index 3959295c13e..274880f2c49 100644 --- a/spec/services/clusters/create_service_spec.rb +++ b/spec/services/clusters/create_service_spec.rb @@ -5,18 +5,43 @@ describe Clusters::CreateService do let(:project) { create(:project) } let(:user) { create(:user) } - subject { described_class.new(user, params).execute(project: project, access_token: access_token) } + subject { described_class.new(user, params).execute(access_token: access_token) } context 'when provider is gcp' do context 'when project has no clusters' do context 'when correct params' do - include_context 'valid cluster create params' + let(:params) do + { + name: 'test-cluster', + provider_type: :gcp, + provider_gcp_attributes: { + gcp_project_id: 'gcp-project', + zone: 'us-central1-a', + num_nodes: 1, + machine_type: 'machine_type-a', + legacy_abac: 'true' + }, + clusterable: project + } + end include_examples 'create cluster service success' end context 'when invalid params' do - include_context 'invalid cluster create params' + let(:params) do + { + name: 'test-cluster', + provider_type: :gcp, + provider_gcp_attributes: { + gcp_project_id: '!!!!!!!', + zone: 'us-central1-a', + num_nodes: 1, + machine_type: 'machine_type-a' + }, + clusterable: project + } + end include_examples 'create cluster service error' end diff --git a/spec/services/clusters/gcp/finalize_creation_service_spec.rb b/spec/services/clusters/gcp/finalize_creation_service_spec.rb index 303d45495ef..7fbb6cf2cf5 100644 --- a/spec/services/clusters/gcp/finalize_creation_service_spec.rb +++ b/spec/services/clusters/gcp/finalize_creation_service_spec.rb @@ -1,156 +1,176 @@ +# frozen_string_literal: true + require 'spec_helper' -describe Clusters::Gcp::FinalizeCreationService do +describe Clusters::Gcp::FinalizeCreationService, '#execute' do include GoogleApi::CloudPlatformHelpers include KubernetesHelpers - describe '#execute' do - let(:cluster) { create(:cluster, :project, :providing_by_gcp) } - let(:provider) { cluster.provider } - let(:platform) { cluster.platform } - let(:gcp_project_id) { provider.gcp_project_id } - let(:zone) { provider.zone } - let(:cluster_name) { cluster.name } + let(:cluster) { create(:cluster, :project, :providing_by_gcp) } + let(:provider) { cluster.provider } + let(:platform) { cluster.platform } + let(:endpoint) { '111.111.111.111' } + let(:api_url) { 'https://' + endpoint } + let(:username) { 'sample-username' } + let(:password) { 'sample-password' } + let(:secret_name) { 'gitlab-token' } + let(:token) { 'sample-token' } + let(:namespace) { "#{cluster.project.path}-#{cluster.project.id}" } - subject { described_class.new.execute(provider) } + subject { described_class.new.execute(provider) } - shared_examples 'success' do - it 'configures provider and kubernetes' do - subject + shared_examples 'success' do + it 'configures provider and kubernetes' do + subject - expect(provider).to be_created - end + expect(provider).to be_created end - shared_examples 'error' do - it 'sets an error to provider object' do - subject + it 'properly configures database models' do + subject - expect(provider.reload).to be_errored - end + cluster.reload + + expect(provider.endpoint).to eq(endpoint) + expect(platform.api_url).to eq(api_url) + expect(platform.ca_cert).to eq(Base64.decode64(load_sample_cert)) + expect(platform.username).to eq(username) + expect(platform.password).to eq(password) + expect(platform.token).to eq(token) + end + + it 'creates kubernetes namespace model' do + subject + + kubernetes_namespace = cluster.reload.kubernetes_namespace + expect(kubernetes_namespace).to be_persisted + expect(kubernetes_namespace.namespace).to eq(namespace) + expect(kubernetes_namespace.service_account_name).to eq("#{namespace}-service-account") + expect(kubernetes_namespace.service_account_token).to be_present end + end + + shared_examples 'error' do + it 'sets an error to provider object' do + subject - context 'when succeeded to fetch gke cluster info' do - let(:endpoint) { '111.111.111.111' } - let(:api_url) { 'https://' + endpoint } - let(:username) { 'sample-username' } - let(:password) { 'sample-password' } - let(:secret_name) { 'gitlab-token' } + expect(provider.reload).to be_errored + end + end + shared_examples 'kubernetes information not successfully fetched' do + context 'when failed to fetch gke cluster info' do before do - stub_cloud_platform_get_zone_cluster( - gcp_project_id, zone, cluster_name, - { - endpoint: endpoint, - username: username, - password: password - } - ) + stub_cloud_platform_get_zone_cluster_error(provider.gcp_project_id, provider.zone, cluster.name) end - context 'service account and token created' do - before do - stub_kubeclient_discover(api_url) - stub_kubeclient_create_service_account(api_url) - stub_kubeclient_create_secret(api_url) - end - - shared_context 'kubernetes token successfully fetched' do - let(:token) { 'sample-token' } - - before do - stub_kubeclient_get_secret( - api_url, - { - metadata_name: secret_name, - token: Base64.encode64(token) - } ) - end - end - - context 'provider legacy_abac is enabled' do - include_context 'kubernetes token successfully fetched' - - it_behaves_like 'success' - - it 'properly configures database models' do - subject - - cluster.reload - - expect(provider.endpoint).to eq(endpoint) - expect(platform.api_url).to eq(api_url) - expect(platform.ca_cert).to eq(Base64.decode64(load_sample_cert)) - expect(platform.username).to eq(username) - expect(platform.password).to eq(password) - expect(platform).to be_abac - expect(platform.authorization_type).to eq('abac') - expect(platform.token).to eq(token) - end - end - - context 'provider legacy_abac is disabled' do - before do - provider.legacy_abac = false - end - - include_context 'kubernetes token successfully fetched' - - context 'cluster role binding created' do - before do - stub_kubeclient_create_cluster_role_binding(api_url) - end - - it_behaves_like 'success' - - it 'properly configures database models' do - subject - - cluster.reload - - expect(provider.endpoint).to eq(endpoint) - expect(platform.api_url).to eq(api_url) - expect(platform.ca_cert).to eq(Base64.decode64(load_sample_cert)) - expect(platform.username).to eq(username) - expect(platform.password).to eq(password) - expect(platform).to be_rbac - expect(platform.token).to eq(token) - end - end - end - - context 'when token is empty' do - before do - stub_kubeclient_get_secret(api_url, token: '', metadata_name: secret_name) - end - - it_behaves_like 'error' - end - - context 'when failed to fetch kubernetes token' do - before do - stub_kubeclient_get_secret_error(api_url, secret_name) - end - - it_behaves_like 'error' - end - - context 'when service account fails to create' do - before do - stub_kubeclient_create_service_account_error(api_url) - end - - it_behaves_like 'error' - end + it_behaves_like 'error' + end + + context 'when token is empty' do + let(:token) { '' } + + it_behaves_like 'error' + end + + context 'when failed to fetch kubernetes token' do + before do + stub_kubeclient_get_secret_error(api_url, secret_name, namespace: 'default') end + + it_behaves_like 'error' end - context 'when failed to fetch gke cluster info' do + context 'when service account fails to create' do before do - stub_cloud_platform_get_zone_cluster_error(gcp_project_id, zone, cluster_name) + stub_kubeclient_create_service_account_error(api_url, namespace: 'default') end it_behaves_like 'error' end end + + shared_context 'kubernetes information successfully fetched' do + before do + stub_cloud_platform_get_zone_cluster( + provider.gcp_project_id, provider.zone, cluster.name, + { + endpoint: endpoint, + username: username, + password: password + } + ) + + stub_kubeclient_discover(api_url) + stub_kubeclient_get_namespace(api_url) + stub_kubeclient_create_namespace(api_url) + stub_kubeclient_create_service_account(api_url) + stub_kubeclient_create_secret(api_url) + + stub_kubeclient_get_secret( + api_url, + { + metadata_name: secret_name, + token: Base64.encode64(token), + namespace: 'default' + } + ) + + stub_kubeclient_get_namespace(api_url, namespace: namespace) + stub_kubeclient_create_service_account(api_url, namespace: namespace) + stub_kubeclient_create_secret(api_url, namespace: namespace) + + stub_kubeclient_get_secret( + api_url, + { + metadata_name: "#{namespace}-token", + token: Base64.encode64(token), + namespace: namespace + } + ) + end + end + + context 'With a legacy ABAC cluster' do + before do + provider.legacy_abac = true + end + + include_context 'kubernetes information successfully fetched' + + it_behaves_like 'success' + + it 'uses ABAC authorization type' do + subject + cluster.reload + + expect(platform).to be_abac + expect(platform.authorization_type).to eq('abac') + end + + it_behaves_like 'kubernetes information not successfully fetched' + end + + context 'With an RBAC cluster' do + before do + provider.legacy_abac = false + + stub_kubeclient_create_cluster_role_binding(api_url) + stub_kubeclient_create_role_binding(api_url, namespace: namespace) + end + + include_context 'kubernetes information successfully fetched' + + it_behaves_like 'success' + + it 'uses RBAC authorization type' do + subject + cluster.reload + + expect(platform).to be_rbac + expect(platform.authorization_type).to eq('rbac') + end + + it_behaves_like 'kubernetes information not successfully fetched' + end end diff --git a/spec/services/clusters/gcp/kubernetes/create_or_update_namespace_service_spec.rb b/spec/services/clusters/gcp/kubernetes/create_or_update_namespace_service_spec.rb new file mode 100644 index 00000000000..fc922218ad0 --- /dev/null +++ b/spec/services/clusters/gcp/kubernetes/create_or_update_namespace_service_spec.rb @@ -0,0 +1,115 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService, '#execute' do + include KubernetesHelpers + + let(:cluster) { create(:cluster, :project, :provided_by_gcp) } + let(:platform) { cluster.platform } + let(:api_url) { 'https://kubernetes.example.com' } + let(:project) { cluster.project } + let(:cluster_project) { cluster.cluster_project } + + subject do + described_class.new( + cluster: cluster, + kubernetes_namespace: kubernetes_namespace + ).execute + end + + shared_context 'kubernetes requests' do + before do + stub_kubeclient_discover(api_url) + stub_kubeclient_get_namespace(api_url) + stub_kubeclient_create_service_account(api_url) + stub_kubeclient_create_secret(api_url) + + stub_kubeclient_get_namespace(api_url, namespace: namespace) + stub_kubeclient_create_service_account(api_url, namespace: namespace) + stub_kubeclient_create_secret(api_url, namespace: namespace) + + stub_kubeclient_get_secret( + api_url, + { + metadata_name: "#{namespace}-token", + token: Base64.encode64('sample-token'), + namespace: namespace + } + ) + end + end + + context 'when kubernetes namespace is not persisted' do + let(:namespace) { "#{project.path}-#{project.id}" } + + let(:kubernetes_namespace) do + build(:cluster_kubernetes_namespace, + cluster: cluster, + project: cluster_project.project, + cluster_project: cluster_project) + end + + include_context 'kubernetes requests' + + it 'creates a Clusters::KubernetesNamespace' do + expect do + subject + end.to change(Clusters::KubernetesNamespace, :count).by(1) + end + + it 'creates project service account' do + expect_any_instance_of(Clusters::Gcp::Kubernetes::CreateServiceAccountService).to receive(:execute).once + + subject + end + + it 'configures kubernetes token' do + subject + + kubernetes_namespace.reload + expect(kubernetes_namespace.namespace).to eq(namespace) + expect(kubernetes_namespace.service_account_name).to eq("#{namespace}-service-account") + expect(kubernetes_namespace.encrypted_service_account_token).to be_present + end + end + + context 'when there is a Kubernetes Namespace associated' do + let(:namespace) { 'new-namespace' } + + let(:kubernetes_namespace) do + create(:cluster_kubernetes_namespace, + cluster: cluster, + project: cluster_project.project, + cluster_project: cluster_project) + end + + include_context 'kubernetes requests' + + before do + platform.update_column(:namespace, 'new-namespace') + end + + it 'does not create any Clusters::KubernetesNamespace' do + subject + + expect(cluster.kubernetes_namespace).to eq(kubernetes_namespace) + end + + it 'creates project service account' do + expect_any_instance_of(Clusters::Gcp::Kubernetes::CreateServiceAccountService).to receive(:execute).once + + subject + end + + it 'updates Clusters::KubernetesNamespace' do + subject + + kubernetes_namespace.reload + + expect(kubernetes_namespace.namespace).to eq(namespace) + expect(kubernetes_namespace.service_account_name).to eq("#{namespace}-service-account") + expect(kubernetes_namespace.encrypted_service_account_token).to be_present + end + end +end diff --git a/spec/services/clusters/gcp/kubernetes/create_service_account_service_spec.rb b/spec/services/clusters/gcp/kubernetes/create_service_account_service_spec.rb index b096f1fa4fb..588edff85d4 100644 --- a/spec/services/clusters/gcp/kubernetes/create_service_account_service_spec.rb +++ b/spec/services/clusters/gcp/kubernetes/create_service_account_service_spec.rb @@ -1,94 +1,165 @@ # frozen_string_literal: true - require 'spec_helper' describe Clusters::Gcp::Kubernetes::CreateServiceAccountService do include KubernetesHelpers - let(:service) { described_class.new(kubeclient, rbac: rbac) } + let(:api_url) { 'http://111.111.111.111' } + let(:platform_kubernetes) { cluster.platform_kubernetes } + let(:cluster_project) { cluster.cluster_project } + let(:project) { cluster_project.project } + let(:cluster) do + create(:cluster, + :project, :provided_by_gcp, + platform_kubernetes: create(:cluster_platform_kubernetes, :configured)) + end + + let(:kubeclient) do + Gitlab::Kubernetes::KubeClient.new( + api_url, + auth_options: { username: 'admin', password: 'xxx' } + ) + end - describe '#execute' do - let(:rbac) { false } - let(:api_url) { 'http://111.111.111.111' } - let(:username) { 'admin' } - let(:password) { 'xxx' } + shared_examples 'creates service account and token' do + it 'creates a kubernetes service account' do + subject + + expect(WebMock).to have_requested(:post, api_url + "/api/v1/namespaces/#{namespace}/serviceaccounts").with( + body: hash_including( + kind: 'ServiceAccount', + metadata: { name: service_account_name, namespace: namespace } + ) + ) + end - let(:kubeclient) do - Gitlab::Kubernetes::KubeClient.new( - api_url, - auth_options: { username: username, password: password } + it 'creates a kubernetes secret' do + subject + + expect(WebMock).to have_requested(:post, api_url + "/api/v1/namespaces/#{namespace}/secrets").with( + body: hash_including( + kind: 'Secret', + metadata: { + name: token_name, + namespace: namespace, + annotations: { + 'kubernetes.io/service-account.name': service_account_name + } + }, + type: 'kubernetes.io/service-account-token' + ) ) end + end + + before do + stub_kubeclient_discover(api_url) + stub_kubeclient_get_namespace(api_url, namespace: namespace) + stub_kubeclient_create_service_account(api_url, namespace: namespace ) + stub_kubeclient_create_secret(api_url, namespace: namespace) + end + + describe '.gitlab_creator' do + let(:namespace) { 'default' } + let(:service_account_name) { 'gitlab' } + let(:token_name) { 'gitlab-token' } + + subject { described_class.gitlab_creator(kubeclient, rbac: rbac).execute } + + context 'with ABAC cluster' do + let(:rbac) { false } + + it_behaves_like 'creates service account and token' + end - subject { service.execute } + context 'with RBAC cluster' do + let(:rbac) { true } - context 'when params are correct' do before do - stub_kubeclient_discover(api_url) - stub_kubeclient_create_service_account(api_url) - stub_kubeclient_create_secret(api_url) - end + cluster.platform_kubernetes.rbac! - shared_examples 'creates service account and token' do - it 'creates a kubernetes service account' do - subject + stub_kubeclient_create_cluster_role_binding(api_url) + end - expect(WebMock).to have_requested(:post, api_url + '/api/v1/namespaces/default/serviceaccounts').with( - body: hash_including( - kind: 'ServiceAccount', - metadata: { name: 'gitlab', namespace: 'default' } - ) - ) - end - - it 'creates a kubernetes secret of type ServiceAccountToken' do - subject - - expect(WebMock).to have_requested(:post, api_url + '/api/v1/namespaces/default/secrets').with( - body: hash_including( - kind: 'Secret', - metadata: { - name: 'gitlab-token', - namespace: 'default', - annotations: { - 'kubernetes.io/service-account.name': 'gitlab' - } - }, - type: 'kubernetes.io/service-account-token' - ) + it_behaves_like 'creates service account and token' + + it 'should create a cluster role binding with cluster-admin access' do + subject + + expect(WebMock).to have_requested(:post, api_url + "/apis/rbac.authorization.k8s.io/v1/clusterrolebindings").with( + body: hash_including( + kind: 'ClusterRoleBinding', + metadata: { name: 'gitlab-admin' }, + roleRef: { + apiGroup: 'rbac.authorization.k8s.io', + kind: 'ClusterRole', + name: 'cluster-admin' + }, + subjects: [ + { + kind: 'ServiceAccount', + name: service_account_name, + namespace: namespace + } + ] ) - end + ) end + end + end + + describe '.namespace_creator' do + let(:namespace) { "#{project.path}-#{project.id}" } + let(:service_account_name) { "#{namespace}-service-account" } + let(:token_name) { "#{namespace}-token" } + + subject do + described_class.namespace_creator( + kubeclient, + service_account_name: service_account_name, + service_account_namespace: namespace, + rbac: rbac + ).execute + end + + context 'with ABAC cluster' do + let(:rbac) { false } + + it_behaves_like 'creates service account and token' + end + + context 'With RBAC enabled cluster' do + let(:rbac) { true } + + before do + cluster.platform_kubernetes.rbac! - context 'abac enabled cluster' do - it_behaves_like 'creates service account and token' + stub_kubeclient_create_role_binding(api_url, namespace: namespace) end - context 'rbac enabled cluster' do - let(:rbac) { true } - - before do - stub_kubeclient_create_cluster_role_binding(api_url) - end - - it_behaves_like 'creates service account and token' - - it 'creates a kubernetes cluster role binding' do - subject - - expect(WebMock).to have_requested(:post, api_url + '/apis/rbac.authorization.k8s.io/v1/clusterrolebindings').with( - body: hash_including( - kind: 'ClusterRoleBinding', - metadata: { name: 'gitlab-admin' }, - roleRef: { - apiGroup: 'rbac.authorization.k8s.io', - kind: 'ClusterRole', - name: 'cluster-admin' - }, - subjects: [{ kind: 'ServiceAccount', namespace: 'default', name: 'gitlab' }] - ) + it_behaves_like 'creates service account and token' + + it 'creates a namespaced role binding with edit access' do + subject + + expect(WebMock).to have_requested(:post, api_url + "/apis/rbac.authorization.k8s.io/v1/namespaces/#{namespace}/rolebindings").with( + body: hash_including( + kind: 'RoleBinding', + metadata: { name: "gitlab-#{namespace}", namespace: "#{namespace}" }, + roleRef: { + apiGroup: 'rbac.authorization.k8s.io', + kind: 'ClusterRole', + name: 'edit' + }, + subjects: [ + { + kind: 'ServiceAccount', + name: service_account_name, + namespace: namespace + } + ] ) - end + ) end end end diff --git a/spec/services/clusters/gcp/kubernetes/fetch_kubernetes_token_service_spec.rb b/spec/services/clusters/gcp/kubernetes/fetch_kubernetes_token_service_spec.rb index 2355827fa5a..4d1a6bb7b3a 100644 --- a/spec/services/clusters/gcp/kubernetes/fetch_kubernetes_token_service_spec.rb +++ b/spec/services/clusters/gcp/kubernetes/fetch_kubernetes_token_service_spec.rb @@ -1,56 +1,48 @@ # frozen_string_literal: true -require 'fast_spec_helper' +require 'spec_helper' describe Clusters::Gcp::Kubernetes::FetchKubernetesTokenService do + include KubernetesHelpers + describe '#execute' do let(:api_url) { 'http://111.111.111.111' } - let(:username) { 'admin' } - let(:password) { 'xxx' } + let(:namespace) { 'my-namespace' } + let(:service_account_token_name) { 'gitlab-token' } let(:kubeclient) do Gitlab::Kubernetes::KubeClient.new( api_url, - auth_options: { username: username, password: password } + auth_options: { username: 'admin', password: 'xxx' } ) end - subject { described_class.new(kubeclient).execute } + subject { described_class.new(kubeclient, service_account_token_name, namespace).execute } context 'when params correct' do let(:decoded_token) { 'xxx.token.xxx' } let(:token) { Base64.encode64(decoded_token) } - let(:secret_json) do - { - 'metadata': { - name: 'gitlab-token' - }, - 'data': { - 'token': token - } - } - end - - before do - allow_any_instance_of(Kubeclient::Client) - .to receive(:get_secret).and_return(secret_json) - end - context 'when gitlab-token exists' do - let(:metadata_name) { 'gitlab-token' } + before do + stub_kubeclient_discover(api_url) + stub_kubeclient_get_secret( + api_url, + { + metadata_name: service_account_token_name, + namespace: namespace, + token: token + } + ) + end it { is_expected.to eq(decoded_token) } end context 'when gitlab-token does not exist' do - let(:secret_json) { {} } - - it { is_expected.to be_nil } - end - - context 'when token is nil' do - let(:token) { nil } + before do + allow(kubeclient).to receive(:get_secret).and_raise(Kubeclient::HttpError.new(404, 'Not found', nil)) + end it { is_expected.to be_nil } end diff --git a/spec/services/clusters/update_service_spec.rb b/spec/services/clusters/update_service_spec.rb index dcd75b6912d..a1b20c61116 100644 --- a/spec/services/clusters/update_service_spec.rb +++ b/spec/services/clusters/update_service_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe Clusters::UpdateService do + include KubernetesHelpers + describe '#execute' do subject { described_class.new(cluster.user, params).execute(cluster) } @@ -34,6 +36,11 @@ describe Clusters::UpdateService do } end + before do + allow(ClusterPlatformConfigureWorker).to receive(:perform_async) + stub_kubeclient_get_namespace('https://kubernetes.example.com', namespace: 'my-namespace') + end + it 'updates namespace' do is_expected.to eq(true) expect(cluster.platform.namespace).to eq('custom-namespace') diff --git a/spec/services/create_deployment_service_spec.rb b/spec/services/create_deployment_service_spec.rb deleted file mode 100644 index b9bfbb11511..00000000000 --- a/spec/services/create_deployment_service_spec.rb +++ /dev/null @@ -1,335 +0,0 @@ -require 'spec_helper' - -describe CreateDeploymentService do - let(:user) { create(:user) } - let(:options) { nil } - - let(:job) do - create(:ci_build, - ref: 'master', - tag: false, - environment: 'production', - options: { environment: options }) - end - - let(:project) { job.project } - - let!(:environment) do - create(:environment, project: project, name: 'production') - end - - let(:service) { described_class.new(job) } - - before do - allow_any_instance_of(Deployment).to receive(:create_ref) - end - - describe '#execute' do - subject { service.execute } - - context 'when environment exists' do - it 'creates a deployment' do - expect(subject).to be_persisted - end - end - - context 'when environment does not exist' do - let(:environment) {} - - it 'does not create a deployment' do - expect do - expect(subject).to be_nil - end.not_to change { Deployment.count } - end - end - - context 'when start action is defined' do - let(:options) { { action: 'start' } } - - context 'and environment is stopped' do - before do - environment.stop - end - - it 'makes environment available' do - subject - - expect(environment.reload).to be_available - end - - it 'creates a deployment' do - expect(subject).to be_persisted - end - end - end - - context 'when stop action is defined' do - let(:options) { { action: 'stop' } } - - context 'and environment is available' do - before do - environment.start - end - - it 'makes environment stopped' do - subject - - expect(environment.reload).to be_stopped - end - - it 'does not create a deployment' do - expect(subject).to be_nil - end - end - end - - context 'when variables are used' do - let(:options) do - { name: 'review-apps/$CI_COMMIT_REF_NAME', - url: 'http://$CI_COMMIT_REF_NAME.review-apps.gitlab.com' } - end - - before do - environment.update(name: 'review-apps/master') - job.update(environment: 'review-apps/$CI_COMMIT_REF_NAME') - end - - it 'creates a new deployment' do - expect(subject).to be_persisted - end - - it 'does not create a new environment' do - expect { subject }.not_to change { Environment.count } - end - - it 'updates external url' do - subject - - expect(subject.environment.name).to eq('review-apps/master') - expect(subject.environment.external_url).to eq('http://master.review-apps.gitlab.com') - end - end - - context 'when project was removed' do - let(:environment) {} - - before do - job.update(project: nil) - end - - it 'does not create deployment or environment' do - expect { subject }.not_to raise_error - - expect(Environment.count).to be_zero - expect(Deployment.count).to be_zero - end - end - end - - describe '#expanded_environment_url' do - subject { service.send(:expanded_environment_url) } - - context 'when yaml environment uses $CI_COMMIT_REF_NAME' do - let(:job) do - create(:ci_build, - ref: 'master', - options: { environment: { url: 'http://review/$CI_COMMIT_REF_NAME' } }) - end - - it { is_expected.to eq('http://review/master') } - end - - context 'when yaml environment uses $CI_ENVIRONMENT_SLUG' do - let(:job) do - create(:ci_build, - ref: 'master', - environment: 'production', - options: { environment: { url: 'http://review/$CI_ENVIRONMENT_SLUG' } }) - end - - let!(:environment) do - create(:environment, - project: job.project, - name: 'production', - slug: 'prod-slug', - external_url: 'http://review/old') - end - - it { is_expected.to eq('http://review/prod-slug') } - end - - context 'when yaml environment uses yaml_variables containing symbol keys' do - let(:job) do - create(:ci_build, - yaml_variables: [{ key: :APP_HOST, value: 'host' }], - options: { environment: { url: 'http://review/$APP_HOST' } }) - end - - it { is_expected.to eq('http://review/host') } - end - - context 'when yaml environment does not have url' do - let(:job) { create(:ci_build, environment: 'staging') } - - let!(:environment) do - create(:environment, project: job.project, name: job.environment) - end - - it 'returns the external_url from persisted environment' do - is_expected.to be_nil - end - end - end - - describe 'processing of builds' do - shared_examples 'does not create deployment' do - it 'does not create a new deployment' do - expect { subject }.not_to change { Deployment.count } - end - - it 'does not call a service' do - expect_any_instance_of(described_class).not_to receive(:execute) - - subject - end - end - - shared_examples 'creates deployment' do - it 'creates a new deployment' do - expect { subject }.to change { Deployment.count }.by(1) - end - - it 'calls a service' do - expect_any_instance_of(described_class).to receive(:execute) - - subject - end - - it 'is set as deployable' do - subject - - expect(Deployment.last.deployable).to eq(deployable) - end - - it 'updates environment URL' do - subject - - expect(Deployment.last.environment.external_url).not_to be_nil - end - end - - context 'without environment specified' do - let(:job) { create(:ci_build) } - - it_behaves_like 'does not create deployment' do - subject { job.success } - end - end - - context 'when environment is specified' do - let(:deployable) { job } - - let(:options) do - { environment: { name: 'production', url: 'http://gitlab.com' } } - end - - context 'when job succeeds' do - it_behaves_like 'creates deployment' do - subject { job.success } - end - end - - context 'when job fails' do - it_behaves_like 'does not create deployment' do - subject { job.drop } - end - end - - context 'when job is retried' do - it_behaves_like 'creates deployment' do - before do - stub_not_protect_default_branch - - project.add_developer(user) - end - - let(:deployable) { Ci::Build.retry(job, user) } - - subject { deployable.success } - end - end - end - end - - describe "merge request metrics" do - let(:merge_request) { create(:merge_request, target_branch: 'master', source_branch: 'feature', source_project: project) } - - context "while updating the 'first_deployed_to_production_at' time" do - before do - merge_request.metrics.update!(merged_at: Time.now) - end - - context "for merge requests merged before the current deploy" do - it "sets the time if the deploy's environment is 'production'" do - time = Time.now - Timecop.freeze(time) { service.execute } - - expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_like_time(time) - end - - it "doesn't set the time if the deploy's environment is not 'production'" do - job.update(environment: 'staging') - service = described_class.new(job) - service.execute - - expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_nil - end - - it 'does not raise errors if the merge request does not have a metrics record' do - merge_request.metrics.destroy - - expect(merge_request.reload.metrics).to be_nil - expect { service.execute }.not_to raise_error - end - end - - context "for merge requests merged before the previous deploy" do - context "if the 'first_deployed_to_production_at' time is already set" do - it "does not overwrite the older 'first_deployed_to_production_at' time" do - # Previous deploy - time = Time.now - Timecop.freeze(time) { service.execute } - - expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_like_time(time) - - # Current deploy - service = described_class.new(job) - Timecop.freeze(time + 12.hours) { service.execute } - - expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_like_time(time) - end - end - - context "if the 'first_deployed_to_production_at' time is not already set" do - it "does not overwrite the older 'first_deployed_to_production_at' time" do - # Previous deploy - time = 5.minutes.from_now - Timecop.freeze(time) { service.execute } - - expect(merge_request.reload.metrics.merged_at).to be < merge_request.reload.metrics.first_deployed_to_production_at - - merge_request.reload.metrics.update(first_deployed_to_production_at: nil) - - expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_nil - - # Current deploy - service = described_class.new(job) - Timecop.freeze(time + 12.hours) { service.execute } - - expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_nil - end - end - end - end - end -end diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb index 07aa8449a66..bd519e7f077 100644 --- a/spec/services/issues/update_service_spec.rb +++ b/spec/services/issues/update_service_spec.rb @@ -343,7 +343,42 @@ describe Issues::UpdateService, :mailer do end end - context 'when the milestone change' do + context 'when the milestone is removed' do + let!(:non_subscriber) { create(:user) } + + let!(:subscriber) do + create(:user) do |u| + issue.toggle_subscription(u, project) + project.add_developer(u) + end + end + + it_behaves_like 'system notes for milestones' + + it 'sends notifications for subscribers of changed milestone' do + issue.milestone = create(:milestone) + + issue.save + + perform_enqueued_jobs do + update_issue(milestone_id: "") + end + + should_email(subscriber) + should_not_email(non_subscriber) + end + end + + context 'when the milestone is changed' do + let!(:non_subscriber) { create(:user) } + + let!(:subscriber) do + create(:user) do |u| + issue.toggle_subscription(u, project) + project.add_developer(u) + end + end + it 'marks todos as done' do update_issue(milestone: create(:milestone)) @@ -351,6 +386,15 @@ describe Issues::UpdateService, :mailer do end it_behaves_like 'system notes for milestones' + + it 'sends notifications for subscribers of changed milestone' do + perform_enqueued_jobs do + update_issue(milestone: create(:milestone)) + end + + should_email(subscriber) + should_not_email(non_subscriber) + end end context 'when the labels change' do @@ -374,7 +418,7 @@ describe Issues::UpdateService, :mailer do let!(:non_subscriber) { create(:user) } let!(:subscriber) do - create(:user).tap do |u| + create(:user) do |u| label.toggle_subscription(u, project) project.add_developer(u) end diff --git a/spec/services/merge_requests/reload_diffs_service_spec.rb b/spec/services/merge_requests/reload_diffs_service_spec.rb index 21f369a3818..546c9f277c5 100644 --- a/spec/services/merge_requests/reload_diffs_service_spec.rb +++ b/spec/services/merge_requests/reload_diffs_service_spec.rb @@ -60,6 +60,17 @@ describe MergeRequests::ReloadDiffsService, :use_clean_rails_memory_store_cachin subject.execute end + + it 'avoids N+1 queries', :request_store do + current_user + merge_request + + control_count = ActiveRecord::QueryRecorder.new do + subject.execute + end.count + + expect { subject.execute }.not_to exceed_query_limit(control_count) + end end end end diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb index 55dfab81c26..1b599ba11b6 100644 --- a/spec/services/merge_requests/update_service_spec.rb +++ b/spec/services/merge_requests/update_service_spec.rb @@ -315,7 +315,42 @@ describe MergeRequests::UpdateService, :mailer do end end - context 'when the milestone change' do + context 'when the milestone is removed' do + let!(:non_subscriber) { create(:user) } + + let!(:subscriber) do + create(:user) do |u| + merge_request.toggle_subscription(u, project) + project.add_developer(u) + end + end + + it_behaves_like 'system notes for milestones' + + it 'sends notifications for subscribers of changed milestone' do + merge_request.milestone = create(:milestone) + + merge_request.save + + perform_enqueued_jobs do + update_merge_request(milestone_id: "") + end + + should_email(subscriber) + should_not_email(non_subscriber) + end + end + + context 'when the milestone is changed' do + let!(:non_subscriber) { create(:user) } + + let!(:subscriber) do + create(:user) do |u| + merge_request.toggle_subscription(u, project) + project.add_developer(u) + end + end + it 'marks pending todos as done' do update_merge_request({ milestone: create(:milestone) }) @@ -323,6 +358,15 @@ describe MergeRequests::UpdateService, :mailer do end it_behaves_like 'system notes for milestones' + + it 'sends notifications for subscribers of changed milestone' do + perform_enqueued_jobs do + update_merge_request(milestone: create(:milestone)) + end + + should_email(subscriber) + should_not_email(non_subscriber) + end end context 'when the labels change' do diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index 68a361fa882..2d8da7673dc 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -13,6 +13,54 @@ describe NotificationService, :mailer do end end + shared_examples 'altered milestone notification on issue' do + it 'sends the email to the correct people' do + should_email(subscriber_to_new_milestone) + issue.assignees.each do |a| + should_email(a) + end + should_email(@u_watcher) + should_email(@u_guest_watcher) + should_email(@u_participant_mentioned) + should_email(@subscriber) + should_email(@subscribed_participant) + should_email(@watcher_and_subscriber) + should_not_email(@u_guest_custom) + should_not_email(@u_committer) + should_not_email(@unsubscriber) + should_not_email(@u_participating) + should_not_email(@u_lazy_participant) + should_not_email(issue.author) + should_not_email(@u_disabled) + should_not_email(@u_custom_global) + should_not_email(@u_mentioned) + end + end + + shared_examples 'altered milestone notification on merge request' do + it 'sends the email to the correct people' do + should_email(subscriber_to_new_milestone) + merge_request.assignees.each do |a| + should_email(a) + end + should_email(@u_watcher) + should_email(@u_guest_watcher) + should_email(@u_participant_mentioned) + should_email(@subscriber) + should_email(@subscribed_participant) + should_email(@watcher_and_subscriber) + should_not_email(@u_guest_custom) + should_not_email(@u_committer) + should_not_email(@unsubscriber) + should_not_email(@u_participating) + should_not_email(@u_lazy_participant) + should_not_email(merge_request.author) + should_not_email(@u_disabled) + should_not_email(@u_custom_global) + should_not_email(@u_mentioned) + end + end + shared_examples 'notifications for new mentions' do it 'sends no emails when no new mentions are present' do send_notifications @@ -952,6 +1000,96 @@ describe NotificationService, :mailer do end end + describe '#removed_milestone_issue' do + it_behaves_like 'altered milestone notification on issue' do + let(:milestone) { create(:milestone, project: project, issues: [issue]) } + let!(:subscriber_to_new_milestone) { create(:user) { |u| issue.toggle_subscription(u, project) } } + + before do + notification.removed_milestone_issue(issue, issue.author) + end + end + + context 'confidential issues' do + let(:author) { create(:user) } + let(:assignee) { create(:user) } + let(:non_member) { create(:user) } + let(:member) { create(:user) } + let(:guest) { create(:user) } + let(:admin) { create(:admin) } + let(:confidential_issue) { create(:issue, :confidential, project: project, title: 'Confidential issue', author: author, assignees: [assignee]) } + let(:milestone) { create(:milestone, project: project, issues: [confidential_issue]) } + + it "emails subscribers of the issue's milestone that can read the issue" do + project.add_developer(member) + project.add_guest(guest) + + confidential_issue.subscribe(non_member, project) + confidential_issue.subscribe(author, project) + confidential_issue.subscribe(assignee, project) + confidential_issue.subscribe(member, project) + confidential_issue.subscribe(guest, project) + confidential_issue.subscribe(admin, project) + + reset_delivered_emails! + + notification.removed_milestone_issue(confidential_issue, @u_disabled) + + should_not_email(non_member) + should_not_email(guest) + should_email(author) + should_email(assignee) + should_email(member) + should_email(admin) + end + end + end + + describe '#changed_milestone_issue' do + it_behaves_like 'altered milestone notification on issue' do + let(:new_milestone) { create(:milestone, project: project, issues: [issue]) } + let!(:subscriber_to_new_milestone) { create(:user) { |u| issue.toggle_subscription(u, project) } } + + before do + notification.changed_milestone_issue(issue, new_milestone, issue.author) + end + end + + context 'confidential issues' do + let(:author) { create(:user) } + let(:assignee) { create(:user) } + let(:non_member) { create(:user) } + let(:member) { create(:user) } + let(:guest) { create(:user) } + let(:admin) { create(:admin) } + let(:confidential_issue) { create(:issue, :confidential, project: project, title: 'Confidential issue', author: author, assignees: [assignee]) } + let(:new_milestone) { create(:milestone, project: project, issues: [confidential_issue]) } + + it "emails subscribers of the issue's milestone that can read the issue" do + project.add_developer(member) + project.add_guest(guest) + + confidential_issue.subscribe(non_member, project) + confidential_issue.subscribe(author, project) + confidential_issue.subscribe(assignee, project) + confidential_issue.subscribe(member, project) + confidential_issue.subscribe(guest, project) + confidential_issue.subscribe(admin, project) + + reset_delivered_emails! + + notification.changed_milestone_issue(confidential_issue, new_milestone, @u_disabled) + + should_not_email(non_member) + should_not_email(guest) + should_email(author) + should_email(assignee) + should_email(member) + should_email(admin) + end + end + end + describe '#close_issue' do before do update_custom_notification(:close_issue, @u_guest_custom, resource: project) @@ -1304,6 +1442,28 @@ describe NotificationService, :mailer do end end + describe '#removed_milestone_merge_request' do + it_behaves_like 'altered milestone notification on merge request' do + let(:milestone) { create(:milestone, project: project, merge_requests: [merge_request]) } + let!(:subscriber_to_new_milestone) { create(:user) { |u| merge_request.toggle_subscription(u, project) } } + + before do + notification.removed_milestone_merge_request(merge_request, merge_request.author) + end + end + end + + describe '#changed_milestone_merge_request' do + it_behaves_like 'altered milestone notification on merge request' do + let(:new_milestone) { create(:milestone, project: project, merge_requests: [merge_request]) } + let!(:subscriber_to_new_milestone) { create(:user) { |u| merge_request.toggle_subscription(u, project) } } + + before do + notification.changed_milestone_merge_request(merge_request, new_milestone, merge_request.author) + end + end + end + describe '#merge_request_unmergeable' do it "sends email to merge request author" do notification.merge_request_unmergeable(merge_request) diff --git a/spec/services/update_deployment_service_spec.rb b/spec/services/update_deployment_service_spec.rb new file mode 100644 index 00000000000..3c55dd9659a --- /dev/null +++ b/spec/services/update_deployment_service_spec.rb @@ -0,0 +1,217 @@ +require 'spec_helper' + +describe UpdateDeploymentService do + let(:user) { create(:user) } + let(:options) { { name: 'production' } } + + let(:job) do + create(:ci_build, + ref: 'master', + tag: false, + environment: 'production', + options: { environment: options }, + project: project) + end + + let(:project) { create(:project, :repository) } + let(:environment) { deployment.environment } + let(:deployment) { job.deployment } + let(:service) { described_class.new(deployment) } + + before do + job.success! # Create/Succeed deployment + end + + describe '#execute' do + subject { service.execute } + + let(:store) { Gitlab::EtagCaching::Store.new } + + it 'invalidates the environment etag cache' do + old_value = store.get(environment.etag_cache_key) + + subject + + expect(store.get(environment.etag_cache_key)).not_to eq(old_value) + end + + it 'creates ref' do + expect_any_instance_of(Repository) + .to receive(:create_ref) + .with(deployment.ref, deployment.send(:ref_path)) + + subject + end + + it 'updates merge request metrics' do + expect_any_instance_of(Deployment) + .to receive(:update_merge_request_metrics!) + + subject + end + + context 'when start action is defined' do + let(:options) { { name: 'production', action: 'start' } } + + context 'and environment is stopped' do + before do + environment.stop + end + + it 'makes environment available' do + subject + + expect(environment.reload).to be_available + end + end + end + + context 'when variables are used' do + let(:options) do + { name: 'review-apps/$CI_COMMIT_REF_NAME', + url: 'http://$CI_COMMIT_REF_NAME.review-apps.gitlab.com' } + end + + before do + environment.update(name: 'review-apps/master') + job.update(environment: 'review-apps/$CI_COMMIT_REF_NAME') + end + + it 'does not create a new environment' do + expect { subject }.not_to change { Environment.count } + end + + it 'updates external url' do + subject + + expect(subject.environment.name).to eq('review-apps/master') + expect(subject.environment.external_url).to eq('http://master.review-apps.gitlab.com') + end + end + end + + describe '#expanded_environment_url' do + subject { service.send(:expanded_environment_url) } + + context 'when yaml environment uses $CI_COMMIT_REF_NAME' do + let(:job) do + create(:ci_build, + ref: 'master', + environment: 'production', + project: project, + options: { environment: { name: 'production', url: 'http://review/$CI_COMMIT_REF_NAME' } }) + end + + it { is_expected.to eq('http://review/master') } + end + + context 'when yaml environment uses $CI_ENVIRONMENT_SLUG' do + let(:job) do + create(:ci_build, + ref: 'master', + environment: 'prod-slug', + project: project, + options: { environment: { name: 'prod-slug', url: 'http://review/$CI_ENVIRONMENT_SLUG' } }) + end + + it { is_expected.to eq('http://review/prod-slug') } + end + + context 'when yaml environment uses yaml_variables containing symbol keys' do + let(:job) do + create(:ci_build, + yaml_variables: [{ key: :APP_HOST, value: 'host' }], + environment: 'production', + project: project, + options: { environment: { name: 'production', url: 'http://review/$APP_HOST' } }) + end + + it { is_expected.to eq('http://review/host') } + end + + context 'when yaml environment does not have url' do + let(:job) { create(:ci_build, environment: 'staging', project: project) } + + it 'returns the external_url from persisted environment' do + is_expected.to be_nil + end + end + end + + describe "merge request metrics" do + let(:merge_request) { create(:merge_request, target_branch: 'master', source_branch: 'feature', source_project: project) } + + context "while updating the 'first_deployed_to_production_at' time" do + before do + merge_request.metrics.update!(merged_at: 1.hour.ago) + end + + context "for merge requests merged before the current deploy" do + it "sets the time if the deploy's environment is 'production'" do + service.execute + + expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_like_time(deployment.finished_at) + end + + context 'when job deploys to staging' do + let(:job) do + create(:ci_build, + ref: 'master', + tag: false, + environment: 'staging', + options: { environment: { name: 'staging' } }, + project: project) + end + + it "doesn't set the time if the deploy's environment is not 'production'" do + service.execute + + expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_nil + end + end + + it 'does not raise errors if the merge request does not have a metrics record' do + merge_request.metrics.destroy + + expect(merge_request.reload.metrics).to be_nil + expect { service.execute }.not_to raise_error + end + end + + context "for merge requests merged before the previous deploy" do + context "if the 'first_deployed_to_production_at' time is already set" do + it "does not overwrite the older 'first_deployed_to_production_at' time" do + # Previous deploy + service.execute + + expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_like_time(deployment.finished_at) + + # Current deploy + Timecop.travel(12.hours.from_now) do + service.execute + + expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_like_time(deployment.finished_at) + end + end + end + + context "if the 'first_deployed_to_production_at' time is not already set" do + it "does not overwrite the older 'first_deployed_to_production_at' time" do + # Previous deploy + time = 5.minutes.from_now + Timecop.freeze(time) { service.execute } + + expect(merge_request.reload.metrics.merged_at).to be < merge_request.reload.metrics.first_deployed_to_production_at + + previous_time = merge_request.reload.metrics.first_deployed_to_production_at + + # Current deploy + Timecop.freeze(time + 12.hours) { service.execute } + + expect(merge_request.reload.metrics.first_deployed_to_production_at).to eq(previous_time) + end + end + end + end + end +end |