From 1a30e153a81b08cf2a992f8ad491aecaa7290eab Mon Sep 17 00:00:00 2001 From: Dennis Tang Date: Tue, 15 May 2018 12:30:18 +0200 Subject: [PATCH 01/46] combine "create" and "existing" GKE cluster views into one new page # Conflicts: # app/controllers/projects/clusters/gcp_controller.rb # app/views/projects/clusters/gcp/_form.html.haml --- .../projects/clusters/gcp/login/index.js | 3 - .../pages/projects/clusters/new/index.js | 3 - .../javascripts/pages/projects/index.js | 11 ++ .../projects/clusters/gcp_controller.rb | 76 ------------ .../projects/clusters/user_controller.rb | 40 ------- .../projects/clusters_controller.rb | 109 +++++++++++++++++- .../projects/clusters/gcp/_form.html.haml | 8 +- .../projects/clusters/gcp/_header.html.haml | 2 +- .../projects/clusters/gcp/login.html.haml | 21 ---- app/views/projects/clusters/gcp/new.html.haml | 10 -- app/views/projects/clusters/new.html.haml | 37 ++++-- .../projects/clusters/user/_form.html.haml | 6 +- .../projects/clusters/user/_header.html.haml | 2 +- .../projects/clusters/user/new.html.haml | 11 -- config/routes/project.rb | 9 +- 15 files changed, 160 insertions(+), 188 deletions(-) delete mode 100644 app/assets/javascripts/pages/projects/clusters/gcp/login/index.js delete mode 100644 app/assets/javascripts/pages/projects/clusters/new/index.js delete mode 100644 app/controllers/projects/clusters/gcp_controller.rb delete mode 100644 app/controllers/projects/clusters/user_controller.rb delete mode 100644 app/views/projects/clusters/gcp/login.html.haml delete mode 100644 app/views/projects/clusters/gcp/new.html.haml delete mode 100644 app/views/projects/clusters/user/new.html.haml diff --git a/app/assets/javascripts/pages/projects/clusters/gcp/login/index.js b/app/assets/javascripts/pages/projects/clusters/gcp/login/index.js deleted file mode 100644 index 0c2d7d7c96a..00000000000 --- a/app/assets/javascripts/pages/projects/clusters/gcp/login/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import gcpSignupOffer from '~/clusters/components/gcp_signup_offer'; - -gcpSignupOffer(); diff --git a/app/assets/javascripts/pages/projects/clusters/new/index.js b/app/assets/javascripts/pages/projects/clusters/new/index.js deleted file mode 100644 index 0c2d7d7c96a..00000000000 --- a/app/assets/javascripts/pages/projects/clusters/new/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import gcpSignupOffer from '~/clusters/components/gcp_signup_offer'; - -gcpSignupOffer(); diff --git a/app/assets/javascripts/pages/projects/index.js b/app/assets/javascripts/pages/projects/index.js index de1e13de7e9..0229f15bfb8 100644 --- a/app/assets/javascripts/pages/projects/index.js +++ b/app/assets/javascripts/pages/projects/index.js @@ -1,7 +1,18 @@ +import gcpSignupOffer from '~/clusters/components/gcp_signup_offer'; +// TODO: Uncommment after https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17806 is merged. +// import initGkeDropdowns from '~/projects/gke_cluster_dropdowns'; import Project from './project'; import ShortcutsNavigation from '../../shortcuts_navigation'; document.addEventListener('DOMContentLoaded', () => { + const page = document.body.dataset.page; + const newClusterViews = ['projects:clusters:new', 'projects:clusters:create_cluster']; + + if (newClusterViews.indexOf(page) > -1) { + gcpSignupOffer(); + // initGkeDropdowns(); + } + new Project(); // eslint-disable-line no-new new ShortcutsNavigation(); // eslint-disable-line no-new }); diff --git a/app/controllers/projects/clusters/gcp_controller.rb b/app/controllers/projects/clusters/gcp_controller.rb deleted file mode 100644 index 6a017c2010b..00000000000 --- a/app/controllers/projects/clusters/gcp_controller.rb +++ /dev/null @@ -1,76 +0,0 @@ -class Projects::Clusters::GcpController < Projects::ApplicationController - before_action :authorize_read_cluster! - before_action :authorize_create_cluster!, only: [:new, :create] - before_action :authorize_google_api, except: :login - - def login - begin - state = generate_session_key_redirect(gcp_new_namespace_project_clusters_path.to_s) - - @authorize_url = GoogleApi::CloudPlatform::Client.new( - nil, callback_google_api_auth_url, - state: state).authorize_url - rescue GoogleApi::Auth::ConfigMissingError - # no-op - end - end - - def new - @cluster = ::Clusters::Cluster.new.tap do |cluster| - cluster.build_provider_gcp - end - end - - def create - @cluster = ::Clusters::CreateService - .new(project, current_user, create_params) - .execute(token_in_session) - - if @cluster.persisted? - redirect_to project_cluster_path(project, @cluster) - else - render :new - end - end - - private - - def create_params - params.require(:cluster).permit( - :enabled, - :name, - :environment_scope, - provider_gcp_attributes: [ - :gcp_project_id, - :zone, - :num_nodes, - :machine_type - ]).merge( - provider_type: :gcp, - platform_type: :kubernetes - ) - end - - def authorize_google_api - unless GoogleApi::CloudPlatform::Client.new(token_in_session, nil) - .validate_token(expires_at_in_session) - redirect_to action: 'login' - end - end - - def token_in_session - @token_in_session ||= - session[GoogleApi::CloudPlatform::Client.session_key_for_token] - end - - def expires_at_in_session - @expires_at_in_session ||= - session[GoogleApi::CloudPlatform::Client.session_key_for_expires_at] - end - - def generate_session_key_redirect(uri) - GoogleApi::CloudPlatform::Client.new_session_key_for_redirect_uri do |key| - session[key] = uri - end - end -end diff --git a/app/controllers/projects/clusters/user_controller.rb b/app/controllers/projects/clusters/user_controller.rb deleted file mode 100644 index d0db64b2fa9..00000000000 --- a/app/controllers/projects/clusters/user_controller.rb +++ /dev/null @@ -1,40 +0,0 @@ -class Projects::Clusters::UserController < Projects::ApplicationController - before_action :authorize_read_cluster! - before_action :authorize_create_cluster!, only: [:new, :create] - - def new - @cluster = ::Clusters::Cluster.new.tap do |cluster| - cluster.build_platform_kubernetes - end - end - - def create - @cluster = ::Clusters::CreateService - .new(project, current_user, create_params) - .execute - - if @cluster.persisted? - redirect_to project_cluster_path(project, @cluster) - else - render :new - end - end - - private - - def create_params - params.require(:cluster).permit( - :enabled, - :name, - :environment_scope, - platform_kubernetes_attributes: [ - :namespace, - :api_url, - :token, - :ca_cert - ]).merge( - provider_type: :user, - platform_type: :kubernetes - ) - end -end diff --git a/app/controllers/projects/clusters_controller.rb b/app/controllers/projects/clusters_controller.rb index aeaba3a0acf..4c31732113c 100644 --- a/app/controllers/projects/clusters_controller.rb +++ b/app/controllers/projects/clusters_controller.rb @@ -1,5 +1,5 @@ class Projects::ClustersController < Projects::ApplicationController - before_action :cluster, except: [:index, :new] + before_action :cluster, except: [:index, :new, :create_cluster] before_action :authorize_read_cluster! before_action :authorize_create_cluster!, only: [:new] before_action :authorize_update_cluster!, only: [:update] @@ -14,6 +14,9 @@ class Projects::ClustersController < Projects::ApplicationController end def new + generate_gcp_authorize_url + tap_new_cluster + tap_existing_cluster end def status @@ -64,6 +67,52 @@ class Projects::ClustersController < Projects::ApplicationController end end + def tap_new_cluster + if GoogleApi::CloudPlatform::Client.new(token_in_session, nil) + .validate_token(expires_at_in_session) + @new_cluster = ::Clusters::Cluster.new.tap do |cluster| + cluster.build_provider_gcp + end + end + end + + def tap_existing_cluster + @existing_cluster = ::Clusters::Cluster.new.tap do |cluster| + cluster.build_platform_kubernetes + end + end + + def create_cluster + case params[:type] + when 'new' + cluster_params = create_new_cluster_params + when 'existing' + cluster_params = create_existing_cluster_params + end + + @cluster = ::Clusters::CreateService + .new(project, current_user, cluster_params) + .execute(token_in_session) + + if @cluster.persisted? + redirect_to project_cluster_path(project, @cluster) + else + generate_gcp_authorize_url + active_tab = params[:type] + + case params[:type] + when 'new' + @new_cluster = @cluster + tap_existing_cluster + when 'existing' + @existing_cluster = @cluster + tap_new_cluster + end + + render :new, locals: { active_tab: active_tab } + end + end + private def cluster @@ -108,6 +157,64 @@ class Projects::ClustersController < Projects::ApplicationController end end + def create_new_cluster_params + params.require(:cluster).permit( + :enabled, + :name, + :environment_scope, + provider_gcp_attributes: [ + :gcp_project_id, + :zone, + :num_nodes, + :machine_type + ]).merge( + provider_type: :gcp, + platform_type: :kubernetes + ) + end + + def create_existing_cluster_params + params.require(:cluster).permit( + :enabled, + :name, + :environment_scope, + platform_kubernetes_attributes: [ + :namespace, + :api_url, + :token, + :ca_cert + ]).merge( + provider_type: :user, + platform_type: :kubernetes + ) + end + + def generate_gcp_authorize_url + state = generate_session_key_redirect(new_project_cluster_path(@project).to_s) + + @authorize_url = GoogleApi::CloudPlatform::Client.new( + nil, callback_google_api_auth_url, + state: state).authorize_url + rescue GoogleApi::Auth::ConfigMissingError + # no-op + end + + def token_in_session + @token_in_session ||= + session[GoogleApi::CloudPlatform::Client.session_key_for_token] + end + + def expires_at_in_session + @expires_at_in_session ||= + session[GoogleApi::CloudPlatform::Client.session_key_for_expires_at] + end + + def generate_session_key_redirect(uri) + GoogleApi::CloudPlatform::Client.new_session_key_for_redirect_uri do |key| + session[key] = uri + end + end + def authorize_update_cluster! access_denied! unless can?(current_user, :update_cluster, cluster) end diff --git a/app/views/projects/clusters/gcp/_form.html.haml b/app/views/projects/clusters/gcp/_form.html.haml index ab62735173f..417998cf996 100644 --- a/app/views/projects/clusters/gcp/_form.html.haml +++ b/app/views/projects/clusters/gcp/_form.html.haml @@ -4,8 +4,10 @@ - link_to_help_page = link_to(s_('ClusterIntegration|help page'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer') = s_('ClusterIntegration|Read our %{link_to_help_page} on Kubernetes cluster integration.').html_safe % { link_to_help_page: link_to_help_page} -= form_for @cluster, html: { class: 'js-gke-cluster-creation prepend-top-20', data: { token: @token_in_session } }, url: gcp_namespace_project_clusters_path(@project.namespace, @project), as: :cluster do |field| - = form_errors(@cluster) +%p= link_to('Select a different Google account', @authorize_url) + += form_for @new_cluster, html: { class: 'js-gke-cluster-creation prepend-top-20', data: { token: @token_in_session } }, url: new_namespace_project_clusters_path(@project.namespace, @project, { type: 'new' }), as: :cluster do |field| + = form_errors(@new_cluster) .form-group = field.label :name, s_('ClusterIntegration|Kubernetes cluster name') = field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Kubernetes cluster name') @@ -13,7 +15,7 @@ = field.label :environment_scope, s_('ClusterIntegration|Environment scope') = field.text_field :environment_scope, class: 'form-control', readonly: !has_multiple_clusters?(@project), placeholder: s_('ClusterIntegration|Environment scope') - = field.fields_for :provider_gcp, @cluster.provider_gcp do |provider_gcp_field| + = field.fields_for :provider_gcp, @new_cluster.provider_gcp do |provider_gcp_field| .form-group = provider_gcp_field.label :gcp_project_id, s_('ClusterIntegration|Google Cloud Platform project ID') .js-gcp-project-id-dropdown-entry-point{ data: { docsUrl: 'https://console.cloud.google.com/home/dashboard' } } diff --git a/app/views/projects/clusters/gcp/_header.html.haml b/app/views/projects/clusters/gcp/_header.html.haml index fa989943492..a2ad3cd64df 100644 --- a/app/views/projects/clusters/gcp/_header.html.haml +++ b/app/views/projects/clusters/gcp/_header.html.haml @@ -1,4 +1,4 @@ -%h4.prepend-top-20 +%h4 = s_('ClusterIntegration|Enter the details for your Kubernetes cluster') %p = s_('ClusterIntegration|Please make sure that your Google account meets the following requirements:') diff --git a/app/views/projects/clusters/gcp/login.html.haml b/app/views/projects/clusters/gcp/login.html.haml deleted file mode 100644 index f1771349a53..00000000000 --- a/app/views/projects/clusters/gcp/login.html.haml +++ /dev/null @@ -1,21 +0,0 @@ -- breadcrumb_title 'Kubernetes' -- page_title _("Login") - -= render_gcp_signup_offer - -.row.prepend-top-default - .col-sm-4 - = render 'projects/clusters/sidebar' - .col-sm-8 - = render 'projects/clusters/dropdown', dropdown_text: s_('ClusterIntegration|Create Kubernetes cluster on Google Kubernetes Engine') - = render 'header' -.row - .col-sm-8.offset-sm-4.signin-with-google - - if @authorize_url - = link_to @authorize_url do - = image_tag('auth_buttons/signin_with_google.png', width: '191px') - = _('or') - = link_to('create a new Google account', 'https://accounts.google.com/SignUpWithoutGmail?service=cloudconsole&continue=https%3A%2F%2Fconsole.cloud.google.com%2Ffreetrial%3Futm_campaign%3D2018_cpanel%26utm_source%3Dgitlab%26utm_medium%3Dreferral', target: '_blank', rel: 'noopener noreferrer') - - else - - link = link_to(s_('ClusterIntegration|properly configured'), help_page_path("integration/google"), target: '_blank', rel: 'noopener noreferrer') - = s_('Google authentication is not %{link_to_documentation}. Ask your GitLab administrator if you want to use this service.').html_safe % { link_to_documentation: link } diff --git a/app/views/projects/clusters/gcp/new.html.haml b/app/views/projects/clusters/gcp/new.html.haml deleted file mode 100644 index ea78d66d883..00000000000 --- a/app/views/projects/clusters/gcp/new.html.haml +++ /dev/null @@ -1,10 +0,0 @@ -- breadcrumb_title 'Kubernetes' -- page_title _("New Kubernetes Cluster") - -.row.prepend-top-default - .col-sm-4 - = render 'projects/clusters/sidebar' - .col-sm-8 - = render 'projects/clusters/dropdown', dropdown_text: s_('ClusterIntegration|Create Kubernetes cluster on Google Kubernetes Engine') - = render 'header' - = render 'form' diff --git a/app/views/projects/clusters/new.html.haml b/app/views/projects/clusters/new.html.haml index 828e2a84753..6646edbc621 100644 --- a/app/views/projects/clusters/new.html.haml +++ b/app/views/projects/clusters/new.html.haml @@ -1,15 +1,38 @@ +-# TODO: Combine gcp/new and user/new views - breadcrumb_title 'Kubernetes' - page_title _("Kubernetes Cluster") +- active_tab = local_assigns.fetch(:active_tab, 'new') += javascript_include_tag 'https://apis.google.com/js/api.js' + = render_gcp_signup_offer .row.prepend-top-default - .col-sm-4 + .col-sm-3 = render 'sidebar' - .col-sm-8 - %h4.prepend-top-0= s_('ClusterIntegration|Choose how to set up Kubernetes cluster integration') + .col-lg-9.js-toggle-container + %ul.nav-links.gitlab-tabs{ role: 'tablist' } + %li{ class: active_when(active_tab == 'new'), role: 'presentation' } + %a{ href: '#create-new-cluster-pane', id: 'create-new-cluster-tab', data: { toggle: 'tab' }, role: 'tab' } + %span Create new Cluster on GKE + %li{ class: active_when(active_tab == 'existing'), role: 'presentation' } + %a{ href: '#add-existing-cluster-pane', id: 'add-existing-cluster-tab', data: { toggle: 'tab' }, role: 'tab' } + %span Add existing cluster - %p= s_('ClusterIntegration|Create a new Kubernetes cluster on Google Kubernetes Engine right from GitLab') - = link_to s_('ClusterIntegration|Create on Google Kubernetes Engine'), gcp_new_namespace_project_clusters_path(@project.namespace, @project), class: 'btn append-bottom-20' - %p= s_('ClusterIntegration|Enter the details for an existing Kubernetes cluster') - = link_to s_('ClusterIntegration|Add an existing Kubernetes cluster'), user_new_namespace_project_clusters_path(@project.namespace, @project), class: 'btn append-bottom-20' + .tab-content.gitlab-tab-content + .tab-pane{ id: 'create-new-cluster-pane', class: active_when(active_tab == 'new'), role: 'tabpanel' } + = render 'projects/clusters/gcp/header' + - if @token_in_session + = render 'projects/clusters/gcp/form' + - elsif @authorize_url + = link_to @authorize_url do + = image_tag('auth_buttons/signin_with_google.png', width: '191px') + = _('or') + = link_to('create a new Google account', 'https://accounts.google.com/SignUpWithoutGmail?service=cloudconsole&continue=https%3A%2F%2Fconsole.cloud.google.com%2Ffreetrial%3Futm_campaign%3D2018_cpanel%26utm_source%3Dgitlab%26utm_medium%3Dreferral', target: '_blank', rel: 'noopener noreferrer') + - else + - link = link_to(s_('ClusterIntegration|properly configured'), help_page_path("integration/google"), target: '_blank', rel: 'noopener noreferrer') + = s_('Google authentication is not %{link_to_documentation}. Ask your GitLab administrator if you want to use this service.').html_safe % { link_to_documentation: link } + + .tab-pane{ id: 'add-existing-cluster-pane', class: active_when(active_tab == 'existing'), role: 'tabpanel' } + = render 'projects/clusters/user/header' + = render 'projects/clusters/user/form' diff --git a/app/views/projects/clusters/user/_form.html.haml b/app/views/projects/clusters/user/_form.html.haml index 2e92524ce8f..56169cb2799 100644 --- a/app/views/projects/clusters/user/_form.html.haml +++ b/app/views/projects/clusters/user/_form.html.haml @@ -1,5 +1,5 @@ -= form_for @cluster, url: user_namespace_project_clusters_path(@project.namespace, @project), as: :cluster do |field| - = form_errors(@cluster) += form_for @existing_cluster, url: new_namespace_project_clusters_path(@project.namespace, @project, { type: 'existing' }), as: :cluster do |field| + = form_errors(@existing_cluster) .form-group = field.label :name, s_('ClusterIntegration|Kubernetes cluster name') = field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Kubernetes cluster name') @@ -7,7 +7,7 @@ = field.label :environment_scope, s_('ClusterIntegration|Environment scope') = field.text_field :environment_scope, class: 'form-control', readonly: !has_multiple_clusters?(@project), placeholder: s_('ClusterIntegration|Environment scope') - = field.fields_for :platform_kubernetes, @cluster.platform_kubernetes do |platform_kubernetes_field| + = field.fields_for :platform_kubernetes, @existing_cluster.platform_kubernetes do |platform_kubernetes_field| .form-group = platform_kubernetes_field.label :api_url, s_('ClusterIntegration|API URL') = platform_kubernetes_field.text_field :api_url, class: 'form-control', placeholder: s_('ClusterIntegration|API URL') diff --git a/app/views/projects/clusters/user/_header.html.haml b/app/views/projects/clusters/user/_header.html.haml index 37f6a788518..749177fa6c1 100644 --- a/app/views/projects/clusters/user/_header.html.haml +++ b/app/views/projects/clusters/user/_header.html.haml @@ -1,4 +1,4 @@ -%h4.prepend-top-20 +%h4 = s_('ClusterIntegration|Enter the details for your Kubernetes cluster') %p - link_to_help_page = link_to(s_('ClusterIntegration|documentation'), help_page_path('user/project/clusters/index', anchor: 'adding-an-existing-kubernetes-cluster'), target: '_blank', rel: 'noopener noreferrer') diff --git a/app/views/projects/clusters/user/new.html.haml b/app/views/projects/clusters/user/new.html.haml deleted file mode 100644 index 7fb75cd9cc7..00000000000 --- a/app/views/projects/clusters/user/new.html.haml +++ /dev/null @@ -1,11 +0,0 @@ -- breadcrumb_title 'Kubernetes' -- page_title _("New Kubernetes cluster") - -.row.prepend-top-default - .col-sm-4 - = render 'projects/clusters/sidebar' - .col-sm-8 - = render 'projects/clusters/dropdown', dropdown_text: s_('ClusterIntegration|Add an existing Kubernetes cluster') - = render 'header' - .prepend-top-20 - = render 'form' diff --git a/config/routes/project.rb b/config/routes/project.rb index 5a1be1a8b73..4584c8d74a8 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -206,14 +206,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do resources :clusters, except: [:edit, :create] do collection do - scope :providers do - get '/user/new', to: 'clusters/user#new' - post '/user', to: 'clusters/user#create' - - get '/gcp/new', to: 'clusters/gcp#new' - get '/gcp/login', to: 'clusters/gcp#login' - post '/gcp', to: 'clusters/gcp#create' - end + post '/new', to: 'clusters#create_cluster' end member do From 4a30d68a9684d282395305e07c62238566a9f2e3 Mon Sep 17 00:00:00 2001 From: Dennis Tang Date: Thu, 17 May 2018 22:49:10 +0200 Subject: [PATCH 02/46] update documentation --- doc/user/project/clusters/index.md | 126 +++++++++++++++-------------- 1 file changed, 64 insertions(+), 62 deletions(-) diff --git a/doc/user/project/clusters/index.md b/doc/user/project/clusters/index.md index edb875bc7e6..2f9fe087c3b 100644 --- a/doc/user/project/clusters/index.md +++ b/doc/user/project/clusters/index.md @@ -23,39 +23,41 @@ You need Master [permissions] and above to access the Kubernetes page. Before proceeding, make sure the following requirements are met: -- The [Google authentication integration](../../../integration/google.md) must +* The [Google authentication integration](../../../integration/google.md) must be enabled in GitLab at the instance level. If that's not the case, ask your GitLab administrator to enable it. -- Your associated Google account must have the right privileges to manage +* Your associated Google account must have the right privileges to manage clusters on GKE. That would mean that a [billing account](https://cloud.google.com/billing/docs/how-to/manage-billing-account) must be set up and that you have to have permissions to access it. -- You must have Master [permissions] in order to be able to access the +* You must have Master [permissions] in order to be able to access the **Kubernetes** page. -- You must have [Cloud Billing API](https://cloud.google.com/billing/) enabled -- You must have [Resource Manager +* You must have [Cloud Billing API](https://cloud.google.com/billing/) enabled +* You must have [Resource Manager API](https://cloud.google.com/resource-manager/) If all of the above requirements are met, you can proceed to create and add a new Kubernetes cluster that will be hosted on GKE to your project: -1. Navigate to your project's **CI/CD > Kubernetes** page. -1. Click on **Add Kubernetes cluster**. -1. Click on **Create with GKE**. -1. Connect your Google account if you haven't done already by clicking the - **Sign in with Google** button. -1. Fill in the requested values: - - **Cluster name** (required) - The name you wish to give the cluster. - - **GCP project ID** (required) - The ID of the project you created in your GCP - console that will host the Kubernetes cluster. This must **not** be confused - with the project name. Learn more about [Google Cloud Platform projects](https://cloud.google.com/resource-manager/docs/creating-managing-projects). - - **Zone** - The [zone](https://cloud.google.com/compute/docs/regions-zones/) - under which the cluster will be created. - - **Number of nodes** - The number of nodes you wish the cluster to have. - - **Machine type** - The [machine type](https://cloud.google.com/compute/docs/machine-types) - of the Virtual Machine instance that the cluster will be based on. - - **Environment scope** - The [associated environment](#setting-the-environment-scope) to this cluster. -1. Finally, click the **Create Kubernetes cluster** button. +1. Navigate to your project's **CI/CD > Kubernetes** page. +1. Click on **Add Kubernetes cluster**. +1. Ensure the **Create new cluster on GKE** tab is active, otherwise, select it. +1. Connect your Google account if you haven't done already by clicking the + **Sign in with Google** button. +1. Fill in the requested values: + +* **Cluster name** (required) - The name you wish to give the cluster. +* **GCP project ID** (required) - The ID of the project you created in your GCP + console that will host the Kubernetes cluster. This must **not** be confused + with the project name. Learn more about [Google Cloud Platform projects](https://cloud.google.com/resource-manager/docs/creating-managing-projects). +* **Zone** - The [zone](https://cloud.google.com/compute/docs/regions-zones/) + under which the cluster will be created. +* **Number of nodes** - The number of nodes you wish the cluster to have. +* **Machine type** - The [machine type](https://cloud.google.com/compute/docs/machine-types) + of the Virtual Machine instance that the cluster will be based on. +* **Environment scope** - The [associated environment](#setting-the-environment-scope) to this cluster. + +1. Finally, click the **Create Kubernetes cluster** button. After a few moments, your cluster should be created. If something goes wrong, you will be notified. @@ -70,20 +72,20 @@ You need Master [permissions] and above to access the Kubernetes page. To add an existing Kubernetes cluster to your project: -1. Navigate to your project's **CI/CD > Kubernetes** page. -1. Click on **Add Kubernetes cluster**. -1. Click on **Add an existing Kubernetes cluster** and fill in the details: - - **Kubernetes cluster name** (required) - The name you wish to give the cluster. - - **Environment scope** (required)- The +1. Navigate to your project's **CI/CD > Kubernetes** page. +1. Click on **Add Kubernetes cluster**. +1. Click on the **Add existing cluster** tab and fill in the following details: + * **Kubernetes cluster name** (required) - The name you wish to give the cluster. + * **Environment scope** (required)- The [associated environment](#setting-the-environment-scope) to this cluster. - - **API URL** (required) - + * **API URL** (required) - It's the URL that GitLab uses to access the Kubernetes API. Kubernetes exposes several APIs, we want the "base" URL that is common to all of them, e.g., `https://kubernetes.example.com` rather than `https://kubernetes.example.com/api/v1`. - - **CA certificate** (optional) - + * **CA certificate** (optional) - If the API is using a self-signed TLS certificate, you'll also need to include the `ca.crt` contents here. - - **Token** - + * **Token** - GitLab authenticates against Kubernetes using service tokens, which are scoped to a particular `namespace`. If you don't have a service token yet, you can follow the @@ -91,17 +93,17 @@ To add an existing Kubernetes cluster to your project: to create one. You can also view or create service tokens in the [Kubernetes dashboard](https://kubernetes.io/docs/tasks/access-application-cluster/web-ui-dashboard/#config) (under **Config > Secrets**). - - **Project namespace** (optional) - The following apply: - - By default you don't have to fill it in; by leaving it blank, GitLab will + * **Project namespace** (optional) - The following apply: + * By default you don't have to fill it in; by leaving it blank, GitLab will create one for you. - - Each project should have a unique namespace. - - The project namespace is not necessarily the namespace of the secret, if + * Each project should have a unique namespace. + * The project namespace is not necessarily the namespace of the secret, if you're using a secret with broader permissions, like the secret from `default`. - - You should **not** use `default` as the project namespace. - - If you or someone created a secret specifically for the project, usually + * You should **not** use `default` as the project namespace. + * If you or someone created a secret specifically for the project, usually with limited permissions, the secret's namespace and project namespace may be the same. -1. Finally, click the **Create Kubernetes cluster** button. +1. Finally, click the **Add Kubernetes cluster** button. After a few moments, your cluster should be created. If something goes wrong, you will be notified. @@ -150,12 +152,12 @@ GitLab provides a one-click install for various applications which will be added directly to your configured cluster. Those applications are needed for [Review Apps](../../../ci/review_apps/index.md) and [deployments](../../../ci/environments.md). -| Application | GitLab version | Description | -| ----------- | :------------: | ----------- | -| [Helm Tiller](https://docs.helm.sh/) | 10.2+ | Helm is a package manager for Kubernetes and is required to install all the other applications. It will be automatically installed as a dependency when you try to install a different app. It is installed in its own pod inside the cluster which can run the `helm` CLI in a safe environment. | -| [Ingress](https://kubernetes.io/docs/concepts/services-networking/ingress/) | 10.2+ | Ingress can provide load balancing, SSL termination, and name-based virtual hosting. It acts as a web proxy for your applications and is useful if you want to use [Auto DevOps] or deploy your own web apps. | -| [Prometheus](https://prometheus.io/docs/introduction/overview/) | 10.4+ | Prometheus is an open-source monitoring and alerting system useful to supervise your deployed applications | -| [GitLab Runner](https://docs.gitlab.com/runner/) | 10.6+ | GitLab Runner is the open source project that is used to run your jobs and send the results back to GitLab. It is used in conjunction with [GitLab CI/CD](https://about.gitlab.com/features/gitlab-ci-cd/), the open-source continuous integration service included with GitLab that coordinates the jobs. When installing the GitLab Runner via the applications, it will run in **privileged mode** by default. Make sure you read the [security implications](#security-implications) before doing so. | +| Application | GitLab version | Description | +| --------------------------------------------------------------------------- | :------------: | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [Helm Tiller](https://docs.helm.sh/) | 10.2+ | Helm is a package manager for Kubernetes and is required to install all the other applications. It will be automatically installed as a dependency when you try to install a different app. It is installed in its own pod inside the cluster which can run the `helm` CLI in a safe environment. | +| [Ingress](https://kubernetes.io/docs/concepts/services-networking/ingress/) | 10.2+ | Ingress can provide load balancing, SSL termination, and name-based virtual hosting. It acts as a web proxy for your applications and is useful if you want to use [Auto DevOps] or deploy your own web apps. | +| [Prometheus](https://prometheus.io/docs/introduction/overview/) | 10.4+ | Prometheus is an open-source monitoring and alerting system useful to supervise your deployed applications | +| [GitLab Runner](https://docs.gitlab.com/runner/) | 10.6+ | GitLab Runner is the open source project that is used to run your jobs and send the results back to GitLab. It is used in conjunction with [GitLab CI/CD](https://about.gitlab.com/features/gitlab-ci-cd/), the open-source continuous integration service included with GitLab that coordinates the jobs. When installing the GitLab Runner via the applications, it will run in **privileged mode** by default. Make sure you read the [security implications](#security-implications) before doing so. | ## Getting the external IP address @@ -244,11 +246,11 @@ Also, jobs that don't have an environment keyword set will not be able to access For example, let's say the following Kubernetes clusters exist in a project: -| Cluster | Environment scope | -| ---------- | ------------------- | -| Development| `*` | -| Staging | `staging/*` | -| Production | `production/*` | +| Cluster | Environment scope | +| ----------- | ----------------- | +| Development | `*` | +| Staging | `staging/*` | +| Production | `production/*` | And the following environments are set in [`.gitlab-ci.yml`](../../../ci/yaml/README.md): @@ -278,9 +280,9 @@ deploy to production: The result will then be: -- The development cluster will be used for the "test" job. -- The staging cluster will be used for the "deploy to staging" job. -- The production cluster will be used for the "deploy to production" job. +* The development cluster will be used for the "test" job. +* The staging cluster will be used for the "deploy to staging" job. +* The production cluster will be used for the "deploy to production" job. ## Multiple Kubernetes clusters @@ -300,22 +302,22 @@ The Kubernetes cluster integration exposes the following [deployment variables](../../../ci/variables/README.md#deployment-variables) in the GitLab CI/CD build environment. -| Variable | Description | -| -------- | ----------- | -| `KUBE_URL` | Equal to the API URL. | -| `KUBE_TOKEN` | The Kubernetes token. | -| `KUBE_NAMESPACE` | The Kubernetes namespace is auto-generated if not specified. The default value is `-`. You can overwrite it to use different one if needed, otherwise the `KUBE_NAMESPACE` variable will receive the default value. | -| `KUBE_CA_PEM_FILE` | Only present if a custom CA bundle was specified. Path to a file containing PEM data. | -| `KUBE_CA_PEM` | (**deprecated**) Only if a custom CA bundle was specified. Raw PEM data. | -| `KUBECONFIG` | Path to a file containing `kubeconfig` for this deployment. CA bundle would be embedded if specified. | +| Variable | Description | +| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `KUBE_URL` | Equal to the API URL. | +| `KUBE_TOKEN` | The Kubernetes token. | +| `KUBE_NAMESPACE` | The Kubernetes namespace is auto-generated if not specified. The default value is `-`. You can overwrite it to use different one if needed, otherwise the `KUBE_NAMESPACE` variable will receive the default value. | +| `KUBE_CA_PEM_FILE` | Only present if a custom CA bundle was specified. Path to a file containing PEM data. | +| `KUBE_CA_PEM` | (**deprecated**) Only if a custom CA bundle was specified. Raw PEM data. | +| `KUBECONFIG` | Path to a file containing `kubeconfig` for this deployment. CA bundle would be embedded if specified. | ## Enabling or disabling the Kubernetes cluster integration After you have successfully added your cluster information, you can enable the Kubernetes cluster integration: -1. Click the "Enabled/Disabled" switch -1. Hit **Save** for the changes to take effect +1. Click the "Enabled/Disabled" switch +1. Hit **Save** for the changes to take effect You can now start using your Kubernetes cluster for your deployments. @@ -394,4 +396,4 @@ the deployment variables above, ensuring any pods you create are labelled with [permissions]: ../../permissions.md [ee]: https://about.gitlab.com/products/ -[Auto DevOps]: ../../../topics/autodevops/index.md +[auto devops]: ../../../topics/autodevops/index.md From 1b40d3249327b3a91f45930a0eaee842e609d98b Mon Sep 17 00:00:00 2001 From: Dennis Tang Date: Thu, 17 May 2018 22:51:09 +0200 Subject: [PATCH 03/46] add changelog entry --- changelogs/unreleased/43446-new-cluster-page-tabs.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 changelogs/unreleased/43446-new-cluster-page-tabs.yml diff --git a/changelogs/unreleased/43446-new-cluster-page-tabs.yml b/changelogs/unreleased/43446-new-cluster-page-tabs.yml new file mode 100644 index 00000000000..e8c73257b16 --- /dev/null +++ b/changelogs/unreleased/43446-new-cluster-page-tabs.yml @@ -0,0 +1,5 @@ +--- +title: Create new or add existing Kubernetes cluster from a single page +merge_request: 18963 +author: +type: changed From 250a8aee258b432bc51b8f177159f4abbc31a8b9 Mon Sep 17 00:00:00 2001 From: Dennis Tang Date: Wed, 23 May 2018 17:30:00 +0200 Subject: [PATCH 04/46] revisions --- .../projects/clusters_controller.rb | 53 +++++++------------ config/routes/project.rb | 2 +- 2 files changed, 21 insertions(+), 34 deletions(-) diff --git a/app/controllers/projects/clusters_controller.rb b/app/controllers/projects/clusters_controller.rb index 4c31732113c..305bef4d080 100644 --- a/app/controllers/projects/clusters_controller.rb +++ b/app/controllers/projects/clusters_controller.rb @@ -1,6 +1,9 @@ class Projects::ClustersController < Projects::ApplicationController - before_action :cluster, except: [:index, :new, :create_cluster] + before_action :cluster, except: [:index, :new, :create] before_action :authorize_read_cluster! + before_action :generate_gcp_authorize_url, only: [:new] + before_action :new_cluster, only: [:new] + before_action :existing_cluster, only: [:new] before_action :authorize_create_cluster!, only: [:new] before_action :authorize_update_cluster!, only: [:update] before_action :authorize_admin_cluster!, only: [:destroy] @@ -14,9 +17,6 @@ class Projects::ClustersController < Projects::ApplicationController end def new - generate_gcp_authorize_url - tap_new_cluster - tap_existing_cluster end def status @@ -67,22 +67,7 @@ class Projects::ClustersController < Projects::ApplicationController end end - def tap_new_cluster - if GoogleApi::CloudPlatform::Client.new(token_in_session, nil) - .validate_token(expires_at_in_session) - @new_cluster = ::Clusters::Cluster.new.tap do |cluster| - cluster.build_provider_gcp - end - end - end - - def tap_existing_cluster - @existing_cluster = ::Clusters::Cluster.new.tap do |cluster| - cluster.build_platform_kubernetes - end - end - - def create_cluster + def create case params[:type] when 'new' cluster_params = create_new_cluster_params @@ -120,19 +105,6 @@ class Projects::ClustersController < Projects::ApplicationController .present(current_user: current_user) end - def create_params - params.require(:cluster).permit( - :enabled, - :name, - :provider_type, - provider_gcp_attributes: [ - :gcp_project_id, - :zone, - :num_nodes, - :machine_type - ]) - end - def update_params if cluster.managed? params.require(:cluster).permit( @@ -199,6 +171,21 @@ class Projects::ClustersController < Projects::ApplicationController # no-op end + def new_cluster + if GoogleApi::CloudPlatform::Client.new(token_in_session, nil) + .validate_token(expires_at_in_session) + @new_cluster = ::Clusters::Cluster.new.tap do |cluster| + cluster.build_provider_gcp + end + end + end + + def existing_cluster + @existing_cluster = ::Clusters::Cluster.new.tap do |cluster| + cluster.build_platform_kubernetes + end + end + def token_in_session @token_in_session ||= session[GoogleApi::CloudPlatform::Client.session_key_for_token] diff --git a/config/routes/project.rb b/config/routes/project.rb index 4584c8d74a8..6609f093b20 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -206,7 +206,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do resources :clusters, except: [:edit, :create] do collection do - post '/new', to: 'clusters#create_cluster' + post '/new', to: 'clusters#create' end member do From db30e7f3b3883999d72dbf95b41d9a1de8b7ffc0 Mon Sep 17 00:00:00 2001 From: Dennis Tang Date: Thu, 24 May 2018 11:18:04 +0200 Subject: [PATCH 05/46] use implied route for clusters#create --- app/views/projects/clusters/gcp/_form.html.haml | 2 +- app/views/projects/clusters/user/_form.html.haml | 2 +- config/routes/project.rb | 6 +----- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/app/views/projects/clusters/gcp/_form.html.haml b/app/views/projects/clusters/gcp/_form.html.haml index 417998cf996..ac00e39c2e3 100644 --- a/app/views/projects/clusters/gcp/_form.html.haml +++ b/app/views/projects/clusters/gcp/_form.html.haml @@ -6,7 +6,7 @@ %p= link_to('Select a different Google account', @authorize_url) -= form_for @new_cluster, html: { class: 'js-gke-cluster-creation prepend-top-20', data: { token: @token_in_session } }, url: new_namespace_project_clusters_path(@project.namespace, @project, { type: 'new' }), as: :cluster do |field| += form_for @new_cluster, html: { class: 'js-gke-cluster-creation prepend-top-20', data: { token: @token_in_session } }, url: namespace_project_clusters_path(@project.namespace, @project, { type: 'new' }), as: :cluster do |field| = form_errors(@new_cluster) .form-group = field.label :name, s_('ClusterIntegration|Kubernetes cluster name') diff --git a/app/views/projects/clusters/user/_form.html.haml b/app/views/projects/clusters/user/_form.html.haml index 56169cb2799..bf1fd1f8898 100644 --- a/app/views/projects/clusters/user/_form.html.haml +++ b/app/views/projects/clusters/user/_form.html.haml @@ -1,4 +1,4 @@ -= form_for @existing_cluster, url: new_namespace_project_clusters_path(@project.namespace, @project, { type: 'existing' }), as: :cluster do |field| += form_for @existing_cluster, url: namespace_project_clusters_path(@project.namespace, @project, { type: 'existing' }), as: :cluster do |field| = form_errors(@existing_cluster) .form-group = field.label :name, s_('ClusterIntegration|Kubernetes cluster name') diff --git a/config/routes/project.rb b/config/routes/project.rb index 6609f093b20..8ecebcf263a 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -204,11 +204,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do end end - resources :clusters, except: [:edit, :create] do - collection do - post '/new', to: 'clusters#create' - end - + resources :clusters, except: [:edit] do member do get :status, format: :json From 82430b2d460b5a956fcba0eda40fe4028405897f Mon Sep 17 00:00:00 2001 From: Dennis Tang Date: Thu, 24 May 2018 11:18:17 +0200 Subject: [PATCH 06/46] cleanup --- app/controllers/projects/clusters_controller.rb | 7 +++---- app/views/projects/clusters/new.html.haml | 1 - 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/app/controllers/projects/clusters_controller.rb b/app/controllers/projects/clusters_controller.rb index 305bef4d080..c44949f2125 100644 --- a/app/controllers/projects/clusters_controller.rb +++ b/app/controllers/projects/clusters_controller.rb @@ -83,18 +83,17 @@ class Projects::ClustersController < Projects::ApplicationController redirect_to project_cluster_path(project, @cluster) else generate_gcp_authorize_url - active_tab = params[:type] case params[:type] when 'new' @new_cluster = @cluster - tap_existing_cluster + existing_cluster when 'existing' @existing_cluster = @cluster - tap_new_cluster + new_cluster end - render :new, locals: { active_tab: active_tab } + render :new, locals: { active_tab: params[:type] } end end diff --git a/app/views/projects/clusters/new.html.haml b/app/views/projects/clusters/new.html.haml index 6646edbc621..24084ceeb74 100644 --- a/app/views/projects/clusters/new.html.haml +++ b/app/views/projects/clusters/new.html.haml @@ -1,4 +1,3 @@ --# TODO: Combine gcp/new and user/new views - breadcrumb_title 'Kubernetes' - page_title _("Kubernetes Cluster") - active_tab = local_assigns.fetch(:active_tab, 'new') From 772490c8807a6e6d2e662f7f63085ca2944741f4 Mon Sep 17 00:00:00 2001 From: Dennis Tang Date: Sat, 26 May 2018 01:10:58 +0200 Subject: [PATCH 07/46] fix tabs for bootstrap 4 --- app/views/projects/clusters/new.html.haml | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/app/views/projects/clusters/new.html.haml b/app/views/projects/clusters/new.html.haml index 24084ceeb74..718e3564ded 100644 --- a/app/views/projects/clusters/new.html.haml +++ b/app/views/projects/clusters/new.html.haml @@ -3,19 +3,18 @@ - active_tab = local_assigns.fetch(:active_tab, 'new') = javascript_include_tag 'https://apis.google.com/js/api.js' - = render_gcp_signup_offer .row.prepend-top-default .col-sm-3 = render 'sidebar' .col-lg-9.js-toggle-container - %ul.nav-links.gitlab-tabs{ role: 'tablist' } - %li{ class: active_when(active_tab == 'new'), role: 'presentation' } - %a{ href: '#create-new-cluster-pane', id: 'create-new-cluster-tab', data: { toggle: 'tab' }, role: 'tab' } + %ul.nav-links.nav-tabs.gitlab-tabs.nav{ role: 'tablist' } + %li.nav-item{ role: 'presentation' } + %a.nav-link{ href: '#create-new-cluster-pane', id: 'create-new-cluster-tab', class: active_when(active_tab == 'new'), data: { toggle: 'tab' }, role: 'tab' } %span Create new Cluster on GKE - %li{ class: active_when(active_tab == 'existing'), role: 'presentation' } - %a{ href: '#add-existing-cluster-pane', id: 'add-existing-cluster-tab', data: { toggle: 'tab' }, role: 'tab' } + %li.nav-item{ role: 'presentation' } + %a.nav-link{ href: '#add-existing-cluster-pane', id: 'add-existing-cluster-tab', class: active_when(active_tab == 'existing'), data: { toggle: 'tab' }, role: 'tab' } %span Add existing cluster .tab-content.gitlab-tab-content From 2532dc740f93efada473296716a3b903427963a4 Mon Sep 17 00:00:00 2001 From: Dennis Tang Date: Sat, 26 May 2018 01:11:05 +0200 Subject: [PATCH 08/46] enable new gke dropdowns --- app/assets/javascripts/pages/projects/index.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/pages/projects/index.js b/app/assets/javascripts/pages/projects/index.js index 0229f15bfb8..43c00cdc72c 100644 --- a/app/assets/javascripts/pages/projects/index.js +++ b/app/assets/javascripts/pages/projects/index.js @@ -1,6 +1,5 @@ import gcpSignupOffer from '~/clusters/components/gcp_signup_offer'; -// TODO: Uncommment after https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17806 is merged. -// import initGkeDropdowns from '~/projects/gke_cluster_dropdowns'; +import initGkeDropdowns from '~/projects/gke_cluster_dropdowns'; import Project from './project'; import ShortcutsNavigation from '../../shortcuts_navigation'; @@ -10,7 +9,7 @@ document.addEventListener('DOMContentLoaded', () => { if (newClusterViews.indexOf(page) > -1) { gcpSignupOffer(); - // initGkeDropdowns(); + initGkeDropdowns(); } new Project(); // eslint-disable-line no-new From 70c65e827b7974529409e5f9af5c1d094c08ed19 Mon Sep 17 00:00:00 2001 From: Dennis Tang Date: Sat, 26 May 2018 01:36:56 +0200 Subject: [PATCH 09/46] fix case where token may expire --- app/controllers/projects/clusters_controller.rb | 8 ++++++-- app/views/projects/clusters/new.html.haml | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/app/controllers/projects/clusters_controller.rb b/app/controllers/projects/clusters_controller.rb index c44949f2125..74e1a78360d 100644 --- a/app/controllers/projects/clusters_controller.rb +++ b/app/controllers/projects/clusters_controller.rb @@ -171,8 +171,7 @@ class Projects::ClustersController < Projects::ApplicationController end def new_cluster - if GoogleApi::CloudPlatform::Client.new(token_in_session, nil) - .validate_token(expires_at_in_session) + if valid_gcp_token @new_cluster = ::Clusters::Cluster.new.tap do |cluster| cluster.build_provider_gcp end @@ -185,6 +184,11 @@ class Projects::ClustersController < Projects::ApplicationController end end + def valid_gcp_token + @valid_gcp_token = GoogleApi::CloudPlatform::Client.new(token_in_session, nil) + .validate_token(expires_at_in_session) + end + def token_in_session @token_in_session ||= session[GoogleApi::CloudPlatform::Client.session_key_for_token] diff --git a/app/views/projects/clusters/new.html.haml b/app/views/projects/clusters/new.html.haml index 718e3564ded..694d34ae4e7 100644 --- a/app/views/projects/clusters/new.html.haml +++ b/app/views/projects/clusters/new.html.haml @@ -20,7 +20,7 @@ .tab-content.gitlab-tab-content .tab-pane{ id: 'create-new-cluster-pane', class: active_when(active_tab == 'new'), role: 'tabpanel' } = render 'projects/clusters/gcp/header' - - if @token_in_session + - if @valid_gcp_token = render 'projects/clusters/gcp/form' - elsif @authorize_url = link_to @authorize_url do From 319ebaefbec6c07053df2d4dd12a528937e5e638 Mon Sep 17 00:00:00 2001 From: Dennis Tang Date: Sat, 26 May 2018 01:37:39 +0200 Subject: [PATCH 10/46] update view name for gke dropdown check --- app/assets/javascripts/pages/projects/index.js | 2 +- app/controllers/projects/clusters_controller.rb | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/pages/projects/index.js b/app/assets/javascripts/pages/projects/index.js index 43c00cdc72c..ba8bc5bc5fa 100644 --- a/app/assets/javascripts/pages/projects/index.js +++ b/app/assets/javascripts/pages/projects/index.js @@ -5,7 +5,7 @@ import ShortcutsNavigation from '../../shortcuts_navigation'; document.addEventListener('DOMContentLoaded', () => { const page = document.body.dataset.page; - const newClusterViews = ['projects:clusters:new', 'projects:clusters:create_cluster']; + const newClusterViews = ['projects:clusters:new', 'projects:clusters:create']; if (newClusterViews.indexOf(page) > -1) { gcpSignupOffer(); diff --git a/app/controllers/projects/clusters_controller.rb b/app/controllers/projects/clusters_controller.rb index 74e1a78360d..03a1fe5aaec 100644 --- a/app/controllers/projects/clusters_controller.rb +++ b/app/controllers/projects/clusters_controller.rb @@ -2,6 +2,7 @@ class Projects::ClustersController < Projects::ApplicationController before_action :cluster, except: [:index, :new, :create] before_action :authorize_read_cluster! before_action :generate_gcp_authorize_url, only: [:new] + before_action :validate_gcp_token, only: [:new] before_action :new_cluster, only: [:new] before_action :existing_cluster, only: [:new] before_action :authorize_create_cluster!, only: [:new] @@ -83,6 +84,7 @@ class Projects::ClustersController < Projects::ApplicationController redirect_to project_cluster_path(project, @cluster) else generate_gcp_authorize_url + validate_gcp_token case params[:type] when 'new' @@ -171,10 +173,8 @@ class Projects::ClustersController < Projects::ApplicationController end def new_cluster - if valid_gcp_token - @new_cluster = ::Clusters::Cluster.new.tap do |cluster| - cluster.build_provider_gcp - end + @new_cluster = ::Clusters::Cluster.new.tap do |cluster| + cluster.build_provider_gcp end end @@ -184,7 +184,7 @@ class Projects::ClustersController < Projects::ApplicationController end end - def valid_gcp_token + def validate_gcp_token @valid_gcp_token = GoogleApi::CloudPlatform::Client.new(token_in_session, nil) .validate_token(expires_at_in_session) end From 3ba97f5d1b56fa39653ac29254d50fc9cf5acd02 Mon Sep 17 00:00:00 2001 From: Dennis Tang Date: Mon, 28 May 2018 14:52:52 +0200 Subject: [PATCH 11/46] update tests --- spec/features/projects/clusters/user_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/features/projects/clusters/user_spec.rb b/spec/features/projects/clusters/user_spec.rb index 698b64a659c..766ea58cc17 100644 --- a/spec/features/projects/clusters/user_spec.rb +++ b/spec/features/projects/clusters/user_spec.rb @@ -17,7 +17,7 @@ feature 'User Cluster', :js do visit project_clusters_path(project) click_link 'Add Kubernetes cluster' - click_link 'Add an existing Kubernetes cluster' + click_link 'Add existing cluster' end context 'when user filled form with valid parameters' do From b4308842deb79c1364302188a41c6e37c14b62ec Mon Sep 17 00:00:00 2001 From: Dennis Tang Date: Mon, 28 May 2018 18:15:12 +0200 Subject: [PATCH 12/46] fix more rspec tests --- app/views/projects/clusters/new.html.haml | 2 +- spec/features/projects/clusters/gcp_spec.rb | 10 +++++----- spec/features/projects/clusters_spec.rb | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/views/projects/clusters/new.html.haml b/app/views/projects/clusters/new.html.haml index 694d34ae4e7..756e0ea45ff 100644 --- a/app/views/projects/clusters/new.html.haml +++ b/app/views/projects/clusters/new.html.haml @@ -24,7 +24,7 @@ = render 'projects/clusters/gcp/form' - elsif @authorize_url = link_to @authorize_url do - = image_tag('auth_buttons/signin_with_google.png', width: '191px') + = image_tag('auth_buttons/signin_with_google.png', width: '191px', class: 'signin-with-google') = _('or') = link_to('create a new Google account', 'https://accounts.google.com/SignUpWithoutGmail?service=cloudconsole&continue=https%3A%2F%2Fconsole.cloud.google.com%2Ffreetrial%3Futm_campaign%3D2018_cpanel%26utm_source%3Dgitlab%26utm_medium%3Dreferral', target: '_blank', rel: 'noopener noreferrer') - else diff --git a/spec/features/projects/clusters/gcp_spec.rb b/spec/features/projects/clusters/gcp_spec.rb index 5071a87fa5b..2eb79c9b282 100644 --- a/spec/features/projects/clusters/gcp_spec.rb +++ b/spec/features/projects/clusters/gcp_spec.rb @@ -16,9 +16,9 @@ feature 'Gcp Cluster', :js do let(:project_id) { 'test-project-1234' } before do - allow_any_instance_of(Projects::Clusters::GcpController) + allow_any_instance_of(Projects::ClustersController) .to receive(:token_in_session).and_return('token') - allow_any_instance_of(Projects::Clusters::GcpController) + allow_any_instance_of(Projects::ClustersController) .to receive(:expires_at_in_session).and_return(1.hour.since.to_i.to_s) end @@ -27,7 +27,7 @@ feature 'Gcp Cluster', :js do visit project_clusters_path(project) click_link 'Add Kubernetes cluster' - click_link 'Create on Google Kubernetes Engine' + click_link 'Create new Cluster on GKE' end context 'when user filled form with valid parameters' do @@ -139,7 +139,7 @@ feature 'Gcp Cluster', :js do visit project_clusters_path(project) click_link 'Add Kubernetes cluster' - click_link 'Create on Google Kubernetes Engine' + click_link 'Create new Cluster on GKE' end it 'user sees a login page' do @@ -165,7 +165,7 @@ feature 'Gcp Cluster', :js do it 'user sees offer on cluster GCP login page' do click_link 'Add Kubernetes cluster' - click_link 'Create on Google Kubernetes Engine' + click_link 'Create new Cluster on GKE' expect(page).to have_css('.gcp-signup-offer') end diff --git a/spec/features/projects/clusters_spec.rb b/spec/features/projects/clusters_spec.rb index a251a2f4e52..64241102c8b 100644 --- a/spec/features/projects/clusters_spec.rb +++ b/spec/features/projects/clusters_spec.rb @@ -83,7 +83,7 @@ feature 'Clusters', :js do visit project_clusters_path(project) click_link 'Add Kubernetes cluster' - click_link 'Create on Google Kubernetes Engine' + click_link 'Create new Cluster on GKE' end it 'user sees a login page' do From ff87949be4277021adabd98955276b925b7df98a Mon Sep 17 00:00:00 2001 From: Dennis Tang Date: Thu, 31 May 2018 13:47:25 +0200 Subject: [PATCH 13/46] remove link to GCP zone docs --- app/views/projects/clusters/gcp/_form.html.haml | 1 - 1 file changed, 1 deletion(-) diff --git a/app/views/projects/clusters/gcp/_form.html.haml b/app/views/projects/clusters/gcp/_form.html.haml index d8247b30575..7dbff4e8ec5 100644 --- a/app/views/projects/clusters/gcp/_form.html.haml +++ b/app/views/projects/clusters/gcp/_form.html.haml @@ -29,7 +29,6 @@ .form-group = provider_gcp_field.label :zone, s_('ClusterIntegration|Zone') - = link_to(s_('ClusterIntegration|See zones'), 'https://cloud.google.com/compute/docs/regions-zones/regions-zones', target: '_blank', rel: 'noopener noreferrer') .js-gcp-zone-dropdown-entry-point = provider_gcp_field.hidden_field :zone .dropdown From e58473faf57400a476798d150fcb31d9b14063f6 Mon Sep 17 00:00:00 2001 From: Dennis Tang Date: Thu, 31 May 2018 13:47:32 +0200 Subject: [PATCH 14/46] use helper method instead of global --- app/views/projects/clusters/gcp/_form.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/clusters/gcp/_form.html.haml b/app/views/projects/clusters/gcp/_form.html.haml index 7dbff4e8ec5..4a73ac24072 100644 --- a/app/views/projects/clusters/gcp/_form.html.haml +++ b/app/views/projects/clusters/gcp/_form.html.haml @@ -6,7 +6,7 @@ %p= link_to('Select a different Google account', @authorize_url) -= form_for @new_cluster, html: { class: 'js-gke-cluster-creation prepend-top-20', data: { token: @token_in_session } }, url: namespace_project_clusters_path(@project.namespace, @project, { type: 'new' }), as: :cluster do |field| += form_for @new_cluster, html: { class: 'js-gke-cluster-creation prepend-top-20', data: { token: token_in_session } }, url: namespace_project_clusters_path(@project.namespace, @project, { type: 'new' }), as: :cluster do |field| = form_errors(@new_cluster) .form-group = field.label :name, s_('ClusterIntegration|Kubernetes cluster name') From cfaf547f27b8961d509bfb51ccb7c3bd4808d264 Mon Sep 17 00:00:00 2001 From: Dennis Tang Date: Thu, 31 May 2018 13:47:43 +0200 Subject: [PATCH 15/46] fix responsive layout for GCP form --- app/views/projects/clusters/new.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/projects/clusters/new.html.haml b/app/views/projects/clusters/new.html.haml index 756e0ea45ff..ebbfe2d7fd5 100644 --- a/app/views/projects/clusters/new.html.haml +++ b/app/views/projects/clusters/new.html.haml @@ -6,9 +6,9 @@ = render_gcp_signup_offer .row.prepend-top-default - .col-sm-3 + .col-md-3 = render 'sidebar' - .col-lg-9.js-toggle-container + .col-md-9.js-toggle-container %ul.nav-links.nav-tabs.gitlab-tabs.nav{ role: 'tablist' } %li.nav-item{ role: 'presentation' } %a.nav-link{ href: '#create-new-cluster-pane', id: 'create-new-cluster-tab', class: active_when(active_tab == 'new'), data: { toggle: 'tab' }, role: 'tab' } From 9d2ca0841f4e9c420b00ef9ec7877eed49af0558 Mon Sep 17 00:00:00 2001 From: Dennis Tang Date: Thu, 31 May 2018 13:48:04 +0200 Subject: [PATCH 16/46] consolidate clusters controller specs --- .../projects/clusters/gcp_controller_spec.rb | 182 --------------- .../projects/clusters/user_controller_spec.rb | 89 -------- .../projects/clusters_controller_spec.rb | 215 ++++++++++++++++++ 3 files changed, 215 insertions(+), 271 deletions(-) delete mode 100644 spec/controllers/projects/clusters/gcp_controller_spec.rb delete mode 100644 spec/controllers/projects/clusters/user_controller_spec.rb diff --git a/spec/controllers/projects/clusters/gcp_controller_spec.rb b/spec/controllers/projects/clusters/gcp_controller_spec.rb deleted file mode 100644 index 271ba37aed4..00000000000 --- a/spec/controllers/projects/clusters/gcp_controller_spec.rb +++ /dev/null @@ -1,182 +0,0 @@ -require 'spec_helper' - -describe Projects::Clusters::GcpController do - include AccessMatchersForController - include GoogleApi::CloudPlatformHelpers - - set(:project) { create(:project) } - - describe 'GET login' do - describe 'functionality' do - let(:user) { create(:user) } - - before do - project.add_master(user) - sign_in(user) - end - - context 'when omniauth has been configured' do - let(:key) { 'secret-key' } - let(:session_key_for_redirect_uri) do - GoogleApi::CloudPlatform::Client.session_key_for_redirect_uri(key) - end - - before do - allow(SecureRandom).to receive(:hex).and_return(key) - end - - it 'has authorize_url' do - go - - expect(assigns(:authorize_url)).to include(key) - expect(session[session_key_for_redirect_uri]).to eq(gcp_new_project_clusters_path(project)) - end - end - - context 'when omniauth has not configured' do - before do - stub_omniauth_setting(providers: []) - end - - it 'does not have authorize_url' do - go - - expect(assigns(:authorize_url)).to be_nil - end - end - end - - describe 'security' do - it { expect { go }.to be_allowed_for(:admin) } - it { expect { go }.to be_allowed_for(:owner).of(project) } - it { expect { go }.to be_allowed_for(:master).of(project) } - it { expect { go }.to be_denied_for(:developer).of(project) } - it { expect { go }.to be_denied_for(:reporter).of(project) } - it { expect { go }.to be_denied_for(:guest).of(project) } - it { expect { go }.to be_denied_for(:user) } - it { expect { go }.to be_denied_for(:external) } - end - - def go - get :login, namespace_id: project.namespace, project_id: project - end - end - - describe 'GET new' do - describe 'functionality' do - let(:user) { create(:user) } - - before do - project.add_master(user) - sign_in(user) - end - - context 'when access token is valid' do - before do - stub_google_api_validate_token - end - - it 'has new object' do - go - - expect(assigns(:cluster)).to be_an_instance_of(Clusters::Cluster) - end - end - - context 'when access token is expired' do - before do - stub_google_api_expired_token - end - - it { expect(go).to redirect_to(gcp_login_project_clusters_path(project)) } - end - - context 'when access token is not stored in session' do - it { expect(go).to redirect_to(gcp_login_project_clusters_path(project)) } - end - end - - describe 'security' do - it { expect { go }.to be_allowed_for(:admin) } - it { expect { go }.to be_allowed_for(:owner).of(project) } - it { expect { go }.to be_allowed_for(:master).of(project) } - it { expect { go }.to be_denied_for(:developer).of(project) } - it { expect { go }.to be_denied_for(:reporter).of(project) } - it { expect { go }.to be_denied_for(:guest).of(project) } - it { expect { go }.to be_denied_for(:user) } - it { expect { go }.to be_denied_for(:external) } - end - - def go - get :new, namespace_id: project.namespace, project_id: project - end - end - - describe 'POST create' do - let(:params) do - { - cluster: { - name: 'new-cluster', - provider_gcp_attributes: { - gcp_project_id: '111' - } - } - } - end - - describe 'functionality' do - let(:user) { create(:user) } - - before do - project.add_master(user) - sign_in(user) - end - - context 'when access token is valid' do - before do - stub_google_api_validate_token - end - - it 'creates a new cluster' do - expect(ClusterProvisionWorker).to receive(:perform_async) - expect { go }.to change { Clusters::Cluster.count } - .and change { Clusters::Providers::Gcp.count } - expect(response).to redirect_to(project_cluster_path(project, project.clusters.first)) - expect(project.clusters.first).to be_gcp - expect(project.clusters.first).to be_kubernetes - end - end - - context 'when access token is expired' do - before do - stub_google_api_expired_token - end - - it 'redirects to login page' do - expect(go).to redirect_to(gcp_login_project_clusters_path(project)) - end - end - - context 'when access token is not stored in session' do - it 'redirects to login page' do - expect(go).to redirect_to(gcp_login_project_clusters_path(project)) - end - end - end - - describe 'security' do - it { expect { go }.to be_allowed_for(:admin) } - it { expect { go }.to be_allowed_for(:owner).of(project) } - it { expect { go }.to be_allowed_for(:master).of(project) } - it { expect { go }.to be_denied_for(:developer).of(project) } - it { expect { go }.to be_denied_for(:reporter).of(project) } - it { expect { go }.to be_denied_for(:guest).of(project) } - it { expect { go }.to be_denied_for(:user) } - it { expect { go }.to be_denied_for(:external) } - end - - def go - post :create, params.merge(namespace_id: project.namespace, project_id: project) - end - end -end diff --git a/spec/controllers/projects/clusters/user_controller_spec.rb b/spec/controllers/projects/clusters/user_controller_spec.rb deleted file mode 100644 index 913976d187f..00000000000 --- a/spec/controllers/projects/clusters/user_controller_spec.rb +++ /dev/null @@ -1,89 +0,0 @@ -require 'spec_helper' - -describe Projects::Clusters::UserController do - include AccessMatchersForController - - set(:project) { create(:project) } - - describe 'GET new' do - describe 'functionality' do - let(:user) { create(:user) } - - before do - project.add_master(user) - sign_in(user) - end - - it 'has new object' do - go - - expect(assigns(:cluster)).to be_an_instance_of(Clusters::Cluster) - end - end - - describe 'security' do - it { expect { go }.to be_allowed_for(:admin) } - it { expect { go }.to be_allowed_for(:owner).of(project) } - it { expect { go }.to be_allowed_for(:master).of(project) } - it { expect { go }.to be_denied_for(:developer).of(project) } - it { expect { go }.to be_denied_for(:reporter).of(project) } - it { expect { go }.to be_denied_for(:guest).of(project) } - it { expect { go }.to be_denied_for(:user) } - it { expect { go }.to be_denied_for(:external) } - end - - def go - get :new, namespace_id: project.namespace, project_id: project - end - end - - describe 'POST create' do - let(:params) do - { - cluster: { - name: 'new-cluster', - platform_kubernetes_attributes: { - api_url: 'http://my-url', - token: 'test', - namespace: 'aaa' - } - } - } - end - - describe 'functionality' do - let(:user) { create(:user) } - - before do - project.add_master(user) - sign_in(user) - end - - context 'when creates a cluster' do - it 'creates a new cluster' do - expect(ClusterProvisionWorker).to receive(:perform_async) - expect { go }.to change { Clusters::Cluster.count } - .and change { Clusters::Platforms::Kubernetes.count } - expect(response).to redirect_to(project_cluster_path(project, project.clusters.first)) - expect(project.clusters.first).to be_user - expect(project.clusters.first).to be_kubernetes - end - end - end - - describe 'security' do - it { expect { go }.to be_allowed_for(:admin) } - it { expect { go }.to be_allowed_for(:owner).of(project) } - it { expect { go }.to be_allowed_for(:master).of(project) } - it { expect { go }.to be_denied_for(:developer).of(project) } - it { expect { go }.to be_denied_for(:reporter).of(project) } - it { expect { go }.to be_denied_for(:guest).of(project) } - it { expect { go }.to be_denied_for(:user) } - it { expect { go }.to be_denied_for(:external) } - end - - def go - post :create, params.merge(namespace_id: project.namespace, project_id: project) - end - end -end diff --git a/spec/controllers/projects/clusters_controller_spec.rb b/spec/controllers/projects/clusters_controller_spec.rb index 380e50c8cac..d634e2002de 100644 --- a/spec/controllers/projects/clusters_controller_spec.rb +++ b/spec/controllers/projects/clusters_controller_spec.rb @@ -2,6 +2,7 @@ require 'spec_helper' describe Projects::ClustersController do include AccessMatchersForController + include GoogleApi::CloudPlatformHelpers set(:project) { create(:project) } @@ -73,6 +74,220 @@ describe Projects::ClustersController do end end + describe 'GET new' do + describe 'functionality for new cluster' do + let(:user) { create(:user) } + + before do + project.add_master(user) + sign_in(user) + end + + context 'when omniauth has been configured' do + let(:key) { 'secret-key' } + let(:session_key_for_redirect_uri) do + GoogleApi::CloudPlatform::Client.session_key_for_redirect_uri(key) + end + + before do + allow(SecureRandom).to receive(:hex).and_return(key) + end + + it 'has authorize_url' do + go + + expect(assigns(:authorize_url)).to include(key) + expect(session[session_key_for_redirect_uri]).to eq(new_project_cluster_path(project)) + end + end + + context 'when omniauth has not configured' do + before do + stub_omniauth_setting(providers: []) + end + + it 'does not have authorize_url' do + go + + expect(assigns(:authorize_url)).to be_nil + end + end + + context 'when access token is valid' do + before do + stub_google_api_validate_token + end + + it 'has new object' do + go + + expect(assigns(:new_cluster)).to be_an_instance_of(Clusters::Cluster) + end + end + + context 'when access token is expired' do + before do + stub_google_api_expired_token + end + + it { expect(@valid_gcp_token).to be_falsey } + end + + context 'when access token is not stored in session' do + it { expect(@valid_gcp_token).to be_falsey } + end + end + + describe 'functionality for existing cluster' do + let(:user) { create(:user) } + + before do + project.add_master(user) + sign_in(user) + end + + it 'has new object' do + go + + expect(assigns(:existing_cluster)).to be_an_instance_of(Clusters::Cluster) + end + end + + describe 'security' do + it { expect { go }.to be_allowed_for(:admin) } + it { expect { go }.to be_allowed_for(:owner).of(project) } + it { expect { go }.to be_allowed_for(:master).of(project) } + it { expect { go }.to be_denied_for(:developer).of(project) } + it { expect { go }.to be_denied_for(:reporter).of(project) } + it { expect { go }.to be_denied_for(:guest).of(project) } + it { expect { go }.to be_denied_for(:user) } + it { expect { go }.to be_denied_for(:external) } + end + + def go + get :new, namespace_id: project.namespace, project_id: project + end + end + + describe 'POST create for new cluster' do + let(:params) do + { + type: 'new', + cluster: { + name: 'new-cluster', + provider_gcp_attributes: { + gcp_project_id: '111', + zone: 'us-central1-a', + num_nodes: 3, + machine_type: 'n1-standard-1' + } + } + } + end + + describe 'functionality' do + let(:user) { create(:user) } + + before do + project.add_master(user) + sign_in(user) + end + + context 'when access token is valid' do + before do + stub_google_api_validate_token + end + + it 'creates a new cluster' do + expect(ClusterProvisionWorker).to receive(:perform_async) + expect { go }.to change { Clusters::Cluster.count } + .and change { Clusters::Providers::Gcp.count } + expect(response).to redirect_to(project_cluster_path(project, project.clusters.first)) + expect(project.clusters.first).to be_gcp + expect(project.clusters.first).to be_kubernetes + end + end + + context 'when access token is expired' do + before do + stub_google_api_expired_token + end + + it { expect(@valid_gcp_token).to be_falsey } + end + + context 'when access token is not stored in session' do + it { expect(@valid_gcp_token).to be_falsey } + end + end + + describe 'security', :focus => true do + it { expect { go }.to be_allowed_for(:admin) } + it { expect { go }.to be_allowed_for(:owner).of(project) } + it { expect { go }.to be_allowed_for(:master).of(project) } + # it { expect { go }.to be_denied_for(:developer).of(project) } + # it { expect { go }.to be_denied_for(:reporter).of(project) } + # it { expect { go }.to be_denied_for(:guest).of(project) } + # it { expect { go }.to be_denied_for(:user) } + # it { expect { go }.to be_denied_for(:external) } + end + + def go + post :create, params.merge(namespace_id: project.namespace, project_id: project) + end + end + + describe 'POST create for existing cluster' do + let(:params) do + { + type: 'existing', + cluster: { + name: 'new-cluster', + platform_kubernetes_attributes: { + api_url: 'http://my-url', + token: 'test', + namespace: 'aaa' + } + } + } + end + + describe 'functionality' do + let(:user) { create(:user) } + + before do + project.add_master(user) + sign_in(user) + end + + context 'when creates a cluster' do + it 'creates a new cluster' do + expect(ClusterProvisionWorker).to receive(:perform_async) + expect { go }.to change { Clusters::Cluster.count } + .and change { Clusters::Platforms::Kubernetes.count } + expect(response).to redirect_to(project_cluster_path(project, project.clusters.first)) + expect(project.clusters.first).to be_user + expect(project.clusters.first).to be_kubernetes + end + end + end + + describe 'security' do + it { expect { go }.to be_allowed_for(:admin) } + it { expect { go }.to be_allowed_for(:owner).of(project) } + it { expect { go }.to be_allowed_for(:master).of(project) } + it { expect { go }.to be_denied_for(:developer).of(project) } + it { expect { go }.to be_denied_for(:reporter).of(project) } + it { expect { go }.to be_denied_for(:guest).of(project) } + it { expect { go }.to be_denied_for(:user) } + it { expect { go }.to be_denied_for(:external) } + end + + def go + post :create, params.merge(namespace_id: project.namespace, project_id: project) + end + end + describe 'GET status' do let(:cluster) { create(:cluster, :providing_by_gcp, projects: [project]) } From 081ec31021d259bf99a23057d6670481cddd2011 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 29 May 2018 15:03:14 +0200 Subject: [PATCH 17/46] Fix various bugs related to relative_url_root in development --- app/views/peek/_bar.html.haml | 2 +- lib/gitlab/gon_helper.rb | 2 +- lib/gitlab/webpack/dev_server_middleware.rb | 5 +++++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/app/views/peek/_bar.html.haml b/app/views/peek/_bar.html.haml index cb0cccb8f8a..89d3b931f88 100644 --- a/app/views/peek/_bar.html.haml +++ b/app/views/peek/_bar.html.haml @@ -2,6 +2,6 @@ #js-peek{ data: { env: Peek.env, request_id: Peek.request_id, - peek_url: peek_routes.results_url, + peek_url: "#{peek_routes_path}/results", profile_url: url_for(safe_params.merge(lineprofiler: 'true')) }, class: Peek.env } diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb index 0d31934347f..deaa14c8434 100644 --- a/lib/gitlab/gon_helper.rb +++ b/lib/gitlab/gon_helper.rb @@ -11,7 +11,7 @@ module Gitlab gon.asset_host = ActionController::Base.asset_host gon.webpack_public_path = webpack_public_path gon.relative_url_root = Gitlab.config.gitlab.relative_url_root - gon.shortcuts_path = help_page_path('shortcuts') + gon.shortcuts_path = Gitlab::Routing.url_helpers.help_page_path('shortcuts') gon.user_color_scheme = Gitlab::ColorSchemes.for_user(current_user).css_class gon.sentry_dsn = Gitlab::CurrentSettings.clientside_sentry_dsn if Gitlab::CurrentSettings.clientside_sentry_enabled gon.gitlab_url = Gitlab.config.gitlab.url diff --git a/lib/gitlab/webpack/dev_server_middleware.rb b/lib/gitlab/webpack/dev_server_middleware.rb index b9a75eaac63..529f7d6a8d6 100644 --- a/lib/gitlab/webpack/dev_server_middleware.rb +++ b/lib/gitlab/webpack/dev_server_middleware.rb @@ -15,6 +15,11 @@ module Gitlab def perform_request(env) if @proxy_path && env['PATH_INFO'].start_with?("/#{@proxy_path}") + if relative_url_root = Rails.application.config.relative_url_root + env['SCRIPT_NAME'] = "" + env['REQUEST_PATH'].sub!(/\A#{Regexp.escape(relative_url_root)}/, '') + end + super(env) else @app.call(env) From 92b4317dcfb6d0123f628ef7484dfdd8088ce62b Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Wed, 30 May 2018 23:30:55 +0100 Subject: [PATCH 18/46] Remove whitespace --- .../components/states/mr_widget_merged.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue index 8c7e0664559..eb581b807a2 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue @@ -168,8 +168,8 @@ - {{ mr.shortMergeCommitSha }} Date: Tue, 15 May 2018 15:39:33 +0200 Subject: [PATCH 19/46] Removed API endpoint and specs --- .codeclimate.yml | 2 - .rubocop_todo.yml | 5 - app/models/application_setting.rb | 11 - .../project_features_compatibility.rb | 2 +- .../unreleased/fj-36819-remove-v3-api.yml | 5 + doc/api/README.md | 15 +- doc/api/applications.md | 2 +- doc/api/environments.md | 2 +- doc/api/projects.md | 2 +- doc/api/v3_to_v4.md | 7 +- doc/integration/shibboleth.md | 2 +- lib/api/api.rb | 44 +- lib/api/v3/award_emoji.rb | 130 -- lib/api/v3/boards.rb | 72 - lib/api/v3/branches.rb | 76 - lib/api/v3/broadcast_messages.rb | 31 - lib/api/v3/builds.rb | 250 --- lib/api/v3/commits.rb | 199 --- lib/api/v3/deploy_keys.rb | 143 -- lib/api/v3/deployments.rb | 43 - lib/api/v3/entities.rb | 311 ---- lib/api/v3/environments.rb | 87 - lib/api/v3/files.rb | 138 -- lib/api/v3/groups.rb | 187 --- lib/api/v3/helpers.rb | 49 - lib/api/v3/issues.rb | 240 --- lib/api/v3/labels.rb | 34 - lib/api/v3/members.rb | 136 -- lib/api/v3/merge_request_diffs.rb | 44 - lib/api/v3/merge_requests.rb | 301 ---- lib/api/v3/milestones.rb | 65 - lib/api/v3/notes.rb | 148 -- lib/api/v3/pipelines.rb | 38 - lib/api/v3/project_hooks.rb | 111 -- lib/api/v3/project_snippets.rb | 143 -- lib/api/v3/projects.rb | 475 ------ lib/api/v3/repositories.rb | 110 -- lib/api/v3/runners.rb | 66 - lib/api/v3/services.rb | 670 -------- lib/api/v3/settings.rb | 147 -- lib/api/v3/snippets.rb | 141 -- lib/api/v3/subscriptions.rb | 53 - lib/api/v3/system_hooks.rb | 32 - lib/api/v3/tags.rb | 40 - lib/api/v3/templates.rb | 122 -- lib/api/v3/time_tracking_endpoints.rb | 116 -- lib/api/v3/todos.rb | 30 - lib/api/v3/triggers.rb | 112 -- lib/api/v3/users.rb | 204 --- lib/api/v3/variables.rb | 29 - lib/gitlab/gitlab_import/client.rb | 10 +- lib/gitlab/gitlab_import/importer.rb | 2 +- lib/gitlab/gitlab_import/project_creator.rb | 2 +- qa/spec/runtime/api_request_spec.rb | 2 +- .../api/schemas/public_api/v3/issues.json | 78 - .../schemas/public_api/v3/merge_requests.json | 91 - .../lib/gitlab/gitlab_import/importer_spec.rb | 4 +- spec/models/application_setting_spec.rb | 16 - spec/requests/api/v3/award_emoji_spec.rb | 297 ---- spec/requests/api/v3/boards_spec.rb | 114 -- spec/requests/api/v3/branches_spec.rb | 120 -- .../api/v3/broadcast_messages_spec.rb | 32 - spec/requests/api/v3/builds_spec.rb | 550 ------ spec/requests/api/v3/commits_spec.rb | 603 ------- spec/requests/api/v3/deploy_keys_spec.rb | 179 -- spec/requests/api/v3/deployments_spec.rb | 69 - spec/requests/api/v3/environments_spec.rb | 163 -- spec/requests/api/v3/files_spec.rb | 283 ---- spec/requests/api/v3/groups_spec.rb | 566 ------- spec/requests/api/v3/issues_spec.rb | 1298 -------------- spec/requests/api/v3/labels_spec.rb | 169 -- spec/requests/api/v3/members_spec.rb | 350 ---- .../api/v3/merge_request_diffs_spec.rb | 48 - spec/requests/api/v3/merge_requests_spec.rb | 767 --------- spec/requests/api/v3/milestones_spec.rb | 238 --- spec/requests/api/v3/notes_spec.rb | 431 ----- spec/requests/api/v3/pipelines_spec.rb | 201 --- spec/requests/api/v3/project_hooks_spec.rb | 219 --- spec/requests/api/v3/project_snippets_spec.rb | 226 --- spec/requests/api/v3/projects_spec.rb | 1495 ----------------- spec/requests/api/v3/repositories_spec.rb | 366 ---- spec/requests/api/v3/runners_spec.rb | 152 -- spec/requests/api/v3/services_spec.rb | 26 - spec/requests/api/v3/settings_spec.rb | 63 - spec/requests/api/v3/snippets_spec.rb | 186 -- spec/requests/api/v3/system_hooks_spec.rb | 56 - spec/requests/api/v3/tags_spec.rb | 88 - spec/requests/api/v3/templates_spec.rb | 201 --- spec/requests/api/v3/todos_spec.rb | 77 - spec/requests/api/v3/triggers_spec.rb | 235 --- spec/requests/api/v3/users_spec.rb | 362 ---- .../api/v3/time_tracking_shared_examples.rb | 128 -- spec/support/gitlab_stubs/project_8.json | 68 +- spec/support/gitlab_stubs/projects.json | 283 +++- spec/support/gitlab_stubs/session.json | 18 - spec/support/helpers/api_helpers.rb | 11 - spec/support/helpers/stub_gitlab_calls.rb | 20 +- 97 files changed, 356 insertions(+), 16029 deletions(-) create mode 100644 changelogs/unreleased/fj-36819-remove-v3-api.yml delete mode 100644 lib/api/v3/award_emoji.rb delete mode 100644 lib/api/v3/boards.rb delete mode 100644 lib/api/v3/branches.rb delete mode 100644 lib/api/v3/broadcast_messages.rb delete mode 100644 lib/api/v3/builds.rb delete mode 100644 lib/api/v3/commits.rb delete mode 100644 lib/api/v3/deploy_keys.rb delete mode 100644 lib/api/v3/deployments.rb delete mode 100644 lib/api/v3/entities.rb delete mode 100644 lib/api/v3/environments.rb delete mode 100644 lib/api/v3/files.rb delete mode 100644 lib/api/v3/groups.rb delete mode 100644 lib/api/v3/helpers.rb delete mode 100644 lib/api/v3/issues.rb delete mode 100644 lib/api/v3/labels.rb delete mode 100644 lib/api/v3/members.rb delete mode 100644 lib/api/v3/merge_request_diffs.rb delete mode 100644 lib/api/v3/merge_requests.rb delete mode 100644 lib/api/v3/milestones.rb delete mode 100644 lib/api/v3/notes.rb delete mode 100644 lib/api/v3/pipelines.rb delete mode 100644 lib/api/v3/project_hooks.rb delete mode 100644 lib/api/v3/project_snippets.rb delete mode 100644 lib/api/v3/projects.rb delete mode 100644 lib/api/v3/repositories.rb delete mode 100644 lib/api/v3/runners.rb delete mode 100644 lib/api/v3/services.rb delete mode 100644 lib/api/v3/settings.rb delete mode 100644 lib/api/v3/snippets.rb delete mode 100644 lib/api/v3/subscriptions.rb delete mode 100644 lib/api/v3/system_hooks.rb delete mode 100644 lib/api/v3/tags.rb delete mode 100644 lib/api/v3/templates.rb delete mode 100644 lib/api/v3/time_tracking_endpoints.rb delete mode 100644 lib/api/v3/todos.rb delete mode 100644 lib/api/v3/triggers.rb delete mode 100644 lib/api/v3/users.rb delete mode 100644 lib/api/v3/variables.rb delete mode 100644 spec/fixtures/api/schemas/public_api/v3/issues.json delete mode 100644 spec/fixtures/api/schemas/public_api/v3/merge_requests.json delete mode 100644 spec/requests/api/v3/award_emoji_spec.rb delete mode 100644 spec/requests/api/v3/boards_spec.rb delete mode 100644 spec/requests/api/v3/branches_spec.rb delete mode 100644 spec/requests/api/v3/broadcast_messages_spec.rb delete mode 100644 spec/requests/api/v3/builds_spec.rb delete mode 100644 spec/requests/api/v3/commits_spec.rb delete mode 100644 spec/requests/api/v3/deploy_keys_spec.rb delete mode 100644 spec/requests/api/v3/deployments_spec.rb delete mode 100644 spec/requests/api/v3/environments_spec.rb delete mode 100644 spec/requests/api/v3/files_spec.rb delete mode 100644 spec/requests/api/v3/groups_spec.rb delete mode 100644 spec/requests/api/v3/issues_spec.rb delete mode 100644 spec/requests/api/v3/labels_spec.rb delete mode 100644 spec/requests/api/v3/members_spec.rb delete mode 100644 spec/requests/api/v3/merge_request_diffs_spec.rb delete mode 100644 spec/requests/api/v3/merge_requests_spec.rb delete mode 100644 spec/requests/api/v3/milestones_spec.rb delete mode 100644 spec/requests/api/v3/notes_spec.rb delete mode 100644 spec/requests/api/v3/pipelines_spec.rb delete mode 100644 spec/requests/api/v3/project_hooks_spec.rb delete mode 100644 spec/requests/api/v3/project_snippets_spec.rb delete mode 100644 spec/requests/api/v3/projects_spec.rb delete mode 100644 spec/requests/api/v3/repositories_spec.rb delete mode 100644 spec/requests/api/v3/runners_spec.rb delete mode 100644 spec/requests/api/v3/services_spec.rb delete mode 100644 spec/requests/api/v3/settings_spec.rb delete mode 100644 spec/requests/api/v3/snippets_spec.rb delete mode 100644 spec/requests/api/v3/system_hooks_spec.rb delete mode 100644 spec/requests/api/v3/tags_spec.rb delete mode 100644 spec/requests/api/v3/templates_spec.rb delete mode 100644 spec/requests/api/v3/todos_spec.rb delete mode 100644 spec/requests/api/v3/triggers_spec.rb delete mode 100644 spec/requests/api/v3/users_spec.rb delete mode 100644 spec/support/api/v3/time_tracking_shared_examples.rb delete mode 100644 spec/support/gitlab_stubs/session.json diff --git a/.codeclimate.yml b/.codeclimate.yml index 8699a903f2a..9998ddba643 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -8,8 +8,6 @@ engines: languages: - ruby - javascript - exclude_paths: - - "lib/api/v3/*" ratings: paths: - Gemfile.lock diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 16b0b5c95e2..1fb352306d7 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -173,7 +173,6 @@ Lint/UriEscapeUnescape: - 'spec/requests/api/files_spec.rb' - 'spec/requests/api/internal_spec.rb' - 'spec/requests/api/issues_spec.rb' - - 'spec/requests/api/v3/issues_spec.rb' # Offense count: 1 # Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns. @@ -333,8 +332,6 @@ RSpec/ScatteredSetup: - 'spec/lib/gitlab/bitbucket_import/importer_spec.rb' - 'spec/lib/gitlab/git/env_spec.rb' - 'spec/requests/api/jobs_spec.rb' - - 'spec/requests/api/v3/builds_spec.rb' - - 'spec/requests/api/v3/projects_spec.rb' - 'spec/services/projects/create_service_spec.rb' # Offense count: 1 @@ -618,7 +615,6 @@ Style/OrAssignment: Exclude: - 'app/models/concerns/token_authenticatable.rb' - 'lib/api/commit_statuses.rb' - - 'lib/api/v3/members.rb' - 'lib/gitlab/project_transfer.rb' # Offense count: 50 @@ -781,7 +777,6 @@ Style/TernaryParentheses: - 'app/finders/projects_finder.rb' - 'app/helpers/namespaces_helper.rb' - 'features/support/capybara.rb' - - 'lib/api/v3/projects.rb' - 'lib/gitlab/ci/build/artifacts/metadata/entry.rb' - 'spec/requests/api/pipeline_schedules_spec.rb' - 'spec/support/capybara.rb' diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index b12f7a2c83f..3d58a14882f 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -360,17 +360,6 @@ class ApplicationSetting < ActiveRecord::Base Array(read_attribute(:repository_storages)) end - # DEPRECATED - # repository_storage is still required in the API. Remove in 9.0 - # Still used in API v3 - def repository_storage - repository_storages.first - end - - def repository_storage=(value) - self.repository_storages = [value] - end - def default_project_visibility=(level) super(Gitlab::VisibilityLevel.level_value(level)) end diff --git a/app/models/concerns/project_features_compatibility.rb b/app/models/concerns/project_features_compatibility.rb index b3fec99c816..1f7d78a2efe 100644 --- a/app/models/concerns/project_features_compatibility.rb +++ b/app/models/concerns/project_features_compatibility.rb @@ -1,4 +1,4 @@ -# Makes api V3 compatible with old project features permissions methods +# Makes api V4 compatible with old project features permissions methods # # After migrating issues_enabled merge_requests_enabled builds_enabled snippets_enabled and wiki_enabled # fields to a new table "project_features", support for the old fields is still needed in the API. diff --git a/changelogs/unreleased/fj-36819-remove-v3-api.yml b/changelogs/unreleased/fj-36819-remove-v3-api.yml new file mode 100644 index 00000000000..e5355252458 --- /dev/null +++ b/changelogs/unreleased/fj-36819-remove-v3-api.yml @@ -0,0 +1,5 @@ +--- +title: Removed API v3 from the codebase +merge_request: 18970 +author: +type: removed diff --git a/doc/api/README.md b/doc/api/README.md index 194907accc7..1c756dc855f 100644 --- a/doc/api/README.md +++ b/doc/api/README.md @@ -90,24 +90,23 @@ specification. ## Compatibility Guidelines The HTTP API is versioned using a single number, the current one being 4. This -number symbolises the same as the major version number as described by +number symbolises the same as the major version number as described by [SemVer](https://semver.org/). This mean that backward incompatible changes will require this version number to change. However, the minor version is -not explicit. This allows for a stable API endpoint, but also means new +not explicit. This allows for a stable API endpoint, but also means new features can be added to the API in the same version number. New features and bug fixes are released in tandem with a new GitLab, and apart from incidental patch and security releases, are released on the 22nd each -month. Backward incompatible changes (e.g. endpoints removal, parameters -removal etc.), as well as removal of entire API versions are done in tandem -with a major point release of GitLab itself. All deprecations and changes -between two versions should be listed in the documentation. For the changes +month. Backward incompatible changes (e.g. endpoints removal, parameters +removal etc.), as well as removal of entire API versions are done in tandem +with a major point release of GitLab itself. All deprecations and changes +between two versions should be listed in the documentation. For the changes between v3 and v4; please read the [v3 to v4 documentation](v3_to_v4.md) #### Current status -Currently two API versions are available, v3 and v4. v3 is deprecated and -will soon be removed. Deletion is scheduled for +Currently only API version v4 is available. Version v3 was removed in [GitLab 11.0](https://gitlab.com/gitlab-org/gitlab-ce/issues/36819). ## Basic usage diff --git a/doc/api/applications.md b/doc/api/applications.md index 933867ed0bb..6d244594b71 100644 --- a/doc/api/applications.md +++ b/doc/api/applications.md @@ -23,7 +23,7 @@ POST /applications | `scopes` | string | yes | The scopes of the application | ```bash -curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --data "name=MyApplication&redirect_uri=http://redirect.uri&scopes=" https://gitlab.example.com/api/v3/applications +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --data "name=MyApplication&redirect_uri=http://redirect.uri&scopes=" https://gitlab.example.com/api/v4/applications ``` Example response: diff --git a/doc/api/environments.md b/doc/api/environments.md index 6e20781f51a..29da4590a59 100644 --- a/doc/api/environments.md +++ b/doc/api/environments.md @@ -123,7 +123,7 @@ POST /projects/:id/environments/:environment_id/stop | `environment_id` | integer | yes | The ID of the environment | ```bash -curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/environments/1/stop" +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/environments/1/stop" ``` Example response: diff --git a/doc/api/projects.md b/doc/api/projects.md index 79cf5e1cc10..d3e95926322 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -1169,7 +1169,7 @@ The `file=` parameter must point to a file on your filesystem and be preceded by `@`. For example: ```bash -curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --form "file=@dk.png" https://gitlab.example.com/api/v3/projects/5/uploads +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --form "file=@dk.png" https://gitlab.example.com/api/v4/projects/5/uploads ``` Returned object: diff --git a/doc/api/v3_to_v4.md b/doc/api/v3_to_v4.md index 9835fab7c98..98eae66469f 100644 --- a/doc/api/v3_to_v4.md +++ b/doc/api/v3_to_v4.md @@ -2,10 +2,9 @@ Since GitLab 9.0, API V4 is the preferred version to be used. -API V3 will be unsupported from GitLab 9.5, to be released on August -22, 2017. It will be removed in GitLab 9.5 or later. In the meantime, we advise -you to make any necessary changes to applications that use V3. The V3 API -documentation is still +API V3 was unsupported from GitLab 9.5, released on August +22, 2017. API v3 was removed in [GitLab 11.0](https://gitlab.com/gitlab-org/gitlab-ce/issues/36819). +The V3 API documentation is still [available](https://gitlab.com/gitlab-org/gitlab-ce/blob/8-16-stable/doc/api/README.md). Below are the changes made between V3 and V4. diff --git a/doc/integration/shibboleth.md b/doc/integration/shibboleth.md index 8611d4f7315..0e43b4a39a4 100644 --- a/doc/integration/shibboleth.md +++ b/doc/integration/shibboleth.md @@ -107,7 +107,7 @@ you will not get a shibboleth session! RewriteEngine on #Don't escape encoded characters in api requests - RewriteCond %{REQUEST_URI} ^/api/v3/.* + RewriteCond %{REQUEST_URI} ^/api/v4/.* RewriteCond %{REQUEST_URI} !/Shibboleth.sso RewriteCond %{REQUEST_URI} !/shibboleth-sp RewriteRule .* http://127.0.0.1:8181%{REQUEST_URI} [P,QSA,NE] diff --git a/lib/api/api.rb b/lib/api/api.rb index 206fabe5c43..7ea575a9661 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -22,48 +22,14 @@ module API allow_access_with_scope :api prefix :api - version %w(v3 v4), using: :path - version 'v3', using: :path do - helpers ::API::V3::Helpers - helpers ::API::Helpers::CommonHelpers - - mount ::API::V3::AwardEmoji - mount ::API::V3::Boards - mount ::API::V3::Branches - mount ::API::V3::BroadcastMessages - mount ::API::V3::Builds - mount ::API::V3::Commits - mount ::API::V3::DeployKeys - mount ::API::V3::Environments - mount ::API::V3::Files - mount ::API::V3::Groups - mount ::API::V3::Issues - mount ::API::V3::Labels - mount ::API::V3::Members - mount ::API::V3::MergeRequestDiffs - mount ::API::V3::MergeRequests - mount ::API::V3::Notes - mount ::API::V3::Pipelines - mount ::API::V3::ProjectHooks - mount ::API::V3::Milestones - mount ::API::V3::Projects - mount ::API::V3::ProjectSnippets - mount ::API::V3::Repositories - mount ::API::V3::Runners - mount ::API::V3::Services - mount ::API::V3::Settings - mount ::API::V3::Snippets - mount ::API::V3::Subscriptions - mount ::API::V3::SystemHooks - mount ::API::V3::Tags - mount ::API::V3::Templates - mount ::API::V3::Todos - mount ::API::V3::Triggers - mount ::API::V3::Users - mount ::API::V3::Variables + route :any, '*path' do + error!('API V3 is no longer supported. Use API V4 instead.', 410) + end end + version 'v4', using: :path + before do header['X-Frame-Options'] = 'SAMEORIGIN' header['X-Content-Type-Options'] = 'nosniff' diff --git a/lib/api/v3/award_emoji.rb b/lib/api/v3/award_emoji.rb deleted file mode 100644 index b96b2d70b12..00000000000 --- a/lib/api/v3/award_emoji.rb +++ /dev/null @@ -1,130 +0,0 @@ -module API - module V3 - class AwardEmoji < Grape::API - include PaginationParams - - before { authenticate! } - AWARDABLES = %w[issue merge_request snippet].freeze - - resource :projects, requirements: { id: %r{[^/]+} } do - AWARDABLES.each do |awardable_type| - awardable_string = awardable_type.pluralize - awardable_id_string = "#{awardable_type}_id" - - params do - requires :id, type: String, desc: 'The ID of a project' - requires :"#{awardable_id_string}", type: Integer, desc: "The ID of an Issue, Merge Request or Snippet" - end - - [ - ":id/#{awardable_string}/:#{awardable_id_string}/award_emoji", - ":id/#{awardable_string}/:#{awardable_id_string}/notes/:note_id/award_emoji" - ].each do |endpoint| - - desc 'Get a list of project +awardable+ award emoji' do - detail 'This feature was introduced in 8.9' - success Entities::AwardEmoji - end - params do - use :pagination - end - get endpoint do - if can_read_awardable? - awards = awardable.award_emoji - present paginate(awards), with: Entities::AwardEmoji - else - not_found!("Award Emoji") - end - end - - desc 'Get a specific award emoji' do - detail 'This feature was introduced in 8.9' - success Entities::AwardEmoji - end - params do - requires :award_id, type: Integer, desc: 'The ID of the award' - end - get "#{endpoint}/:award_id" do - if can_read_awardable? - present awardable.award_emoji.find(params[:award_id]), with: Entities::AwardEmoji - else - not_found!("Award Emoji") - end - end - - desc 'Award a new Emoji' do - detail 'This feature was introduced in 8.9' - success Entities::AwardEmoji - end - params do - requires :name, type: String, desc: 'The name of a award_emoji (without colons)' - end - post endpoint do - not_found!('Award Emoji') unless can_read_awardable? && can_award_awardable? - - award = awardable.create_award_emoji(params[:name], current_user) - - if award.persisted? - present award, with: Entities::AwardEmoji - else - not_found!("Award Emoji #{award.errors.messages}") - end - end - - desc 'Delete a +awardables+ award emoji' do - detail 'This feature was introduced in 8.9' - success Entities::AwardEmoji - end - params do - requires :award_id, type: Integer, desc: 'The ID of an award emoji' - end - delete "#{endpoint}/:award_id" do - award = awardable.award_emoji.find(params[:award_id]) - - unauthorized! unless award.user == current_user || current_user.admin? - - award.destroy - present award, with: Entities::AwardEmoji - end - end - end - end - - helpers do - def can_read_awardable? - can?(current_user, read_ability(awardable), awardable) - end - - def can_award_awardable? - awardable.user_can_award?(current_user, params[:name]) - end - - def awardable - @awardable ||= - begin - if params.include?(:note_id) - note_id = params.delete(:note_id) - - awardable.notes.find(note_id) - elsif params.include?(:issue_id) - user_project.issues.find(params[:issue_id]) - elsif params.include?(:merge_request_id) - user_project.merge_requests.find(params[:merge_request_id]) - else - user_project.snippets.find(params[:snippet_id]) - end - end - end - - def read_ability(awardable) - case awardable - when Note - read_ability(awardable.noteable) - else - :"read_#{awardable.class.to_s.underscore}" - end - end - end - end - end -end diff --git a/lib/api/v3/boards.rb b/lib/api/v3/boards.rb deleted file mode 100644 index 94acc67171e..00000000000 --- a/lib/api/v3/boards.rb +++ /dev/null @@ -1,72 +0,0 @@ -module API - module V3 - class Boards < Grape::API - before { authenticate! } - - params do - requires :id, type: String, desc: 'The ID of a project' - end - resource :projects, requirements: { id: %r{[^/]+} } do - desc 'Get all project boards' do - detail 'This feature was introduced in 8.13' - success ::API::Entities::Board - end - get ':id/boards' do - authorize!(:read_board, user_project) - present user_project.boards, with: ::API::Entities::Board - end - - params do - requires :board_id, type: Integer, desc: 'The ID of a board' - end - segment ':id/boards/:board_id' do - helpers do - def project_board - board = user_project.boards.first - - if params[:board_id] == board.id - board - else - not_found!('Board') - end - end - - def board_lists - project_board.lists.destroyable - end - end - - desc 'Get the lists of a project board' do - detail 'Does not include `done` list. This feature was introduced in 8.13' - success ::API::Entities::List - end - get '/lists' do - authorize!(:read_board, user_project) - present board_lists, with: ::API::Entities::List - end - - desc 'Delete a board list' do - detail 'This feature was introduced in 8.13' - success ::API::Entities::List - end - params do - requires :list_id, type: Integer, desc: 'The ID of a board list' - end - delete "/lists/:list_id" do - authorize!(:admin_list, user_project) - - list = board_lists.find(params[:list_id]) - - service = ::Boards::Lists::DestroyService.new(user_project, current_user) - - if service.execute(list) - present list, with: ::API::Entities::List - else - render_api_error!({ error: 'List could not be deleted!' }, 400) - end - end - end - end - end - end -end diff --git a/lib/api/v3/branches.rb b/lib/api/v3/branches.rb deleted file mode 100644 index 25176c5b38e..00000000000 --- a/lib/api/v3/branches.rb +++ /dev/null @@ -1,76 +0,0 @@ -require 'mime/types' - -module API - module V3 - class Branches < Grape::API - before { authenticate! } - before { authorize! :download_code, user_project } - - params do - requires :id, type: String, desc: 'The ID of a project' - end - resource :projects, requirements: { id: %r{[^/]+} } do - desc 'Get a project repository branches' do - success ::API::Entities::Branch - end - get ":id/repository/branches" do - Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42276') - - repository = user_project.repository - branches = repository.branches.sort_by(&:name) - merged_branch_names = repository.merged_branch_names(branches.map(&:name)) - - present branches, with: ::API::Entities::Branch, project: user_project, merged_branch_names: merged_branch_names - end - - desc 'Delete a branch' - params do - requires :branch, type: String, desc: 'The name of the branch' - end - delete ":id/repository/branches/:branch", requirements: { branch: /.+/ } do - authorize_push_project - - result = DeleteBranchService.new(user_project, current_user) - .execute(params[:branch]) - - if result[:status] == :success - status(200) - { - branch_name: params[:branch] - } - else - render_api_error!(result[:message], result[:return_code]) - end - end - - desc 'Delete all merged branches' - delete ":id/repository/merged_branches" do - DeleteMergedBranchesService.new(user_project, current_user).async_execute - - status(200) - end - - desc 'Create branch' do - success ::API::Entities::Branch - end - params do - requires :branch_name, type: String, desc: 'The name of the branch' - requires :ref, type: String, desc: 'Create branch from commit sha or existing branch' - end - post ":id/repository/branches" do - authorize_push_project - result = CreateBranchService.new(user_project, current_user) - .execute(params[:branch_name], params[:ref]) - - if result[:status] == :success - present result[:branch], - with: ::API::Entities::Branch, - project: user_project - else - render_api_error!(result[:message], 400) - end - end - end - end - end -end diff --git a/lib/api/v3/broadcast_messages.rb b/lib/api/v3/broadcast_messages.rb deleted file mode 100644 index 417e4ad0b26..00000000000 --- a/lib/api/v3/broadcast_messages.rb +++ /dev/null @@ -1,31 +0,0 @@ -module API - module V3 - class BroadcastMessages < Grape::API - include PaginationParams - - before { authenticate! } - before { authenticated_as_admin! } - - resource :broadcast_messages do - helpers do - def find_message - BroadcastMessage.find(params[:id]) - end - end - - desc 'Delete a broadcast message' do - detail 'This feature was introduced in GitLab 8.12.' - success ::API::Entities::BroadcastMessage - end - params do - requires :id, type: Integer, desc: 'Broadcast message ID' - end - delete ':id' do - message = find_message - - present message.destroy, with: ::API::Entities::BroadcastMessage - end - end - end - end -end diff --git a/lib/api/v3/builds.rb b/lib/api/v3/builds.rb deleted file mode 100644 index b49448e1e67..00000000000 --- a/lib/api/v3/builds.rb +++ /dev/null @@ -1,250 +0,0 @@ -module API - module V3 - class Builds < Grape::API - include PaginationParams - - before { authenticate! } - - params do - requires :id, type: String, desc: 'The ID of a project' - end - resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do - helpers do - params :optional_scope do - optional :scope, types: [String, Array[String]], desc: 'The scope of builds to show', - values: %w(pending running failed success canceled skipped), - coerce_with: ->(scope) { - if scope.is_a?(String) - [scope] - elsif scope.is_a?(::Hash) - scope.values - else - ['unknown'] - end - } - end - end - - desc 'Get a project builds' do - success ::API::V3::Entities::Build - end - params do - use :optional_scope - use :pagination - end - get ':id/builds' do - builds = user_project.builds.order('id DESC') - builds = filter_builds(builds, params[:scope]) - - builds = builds.preload(:user, :job_artifacts_archive, :runner, pipeline: :project) - present paginate(builds), with: ::API::V3::Entities::Build - end - - desc 'Get builds for a specific commit of a project' do - success ::API::V3::Entities::Build - end - params do - requires :sha, type: String, desc: 'The SHA id of a commit' - use :optional_scope - use :pagination - end - get ':id/repository/commits/:sha/builds' do - authorize_read_builds! - - break not_found! unless user_project.commit(params[:sha]) - - pipelines = user_project.pipelines.where(sha: params[:sha]) - builds = user_project.builds.where(pipeline: pipelines).order('id DESC') - builds = filter_builds(builds, params[:scope]) - - present paginate(builds), with: ::API::V3::Entities::Build - end - - desc 'Get a specific build of a project' do - success ::API::V3::Entities::Build - end - params do - requires :build_id, type: Integer, desc: 'The ID of a build' - end - get ':id/builds/:build_id' do - authorize_read_builds! - - build = get_build!(params[:build_id]) - - present build, with: ::API::V3::Entities::Build - end - - desc 'Download the artifacts file from build' do - detail 'This feature was introduced in GitLab 8.5' - end - params do - requires :build_id, type: Integer, desc: 'The ID of a build' - end - get ':id/builds/:build_id/artifacts' do - authorize_read_builds! - - build = get_build!(params[:build_id]) - - present_carrierwave_file!(build.artifacts_file) - end - - desc 'Download the artifacts file from build' do - detail 'This feature was introduced in GitLab 8.10' - end - params do - requires :ref_name, type: String, desc: 'The ref from repository' - requires :job, type: String, desc: 'The name for the build' - end - get ':id/builds/artifacts/:ref_name/download', - requirements: { ref_name: /.+/ } do - authorize_read_builds! - - builds = user_project.latest_successful_builds_for(params[:ref_name]) - latest_build = builds.find_by!(name: params[:job]) - - present_carrierwave_file!(latest_build.artifacts_file) - end - - # TODO: We should use `present_disk_file!` and leave this implementation for backward compatibility (when build trace - # is saved in the DB instead of file). But before that, we need to consider how to replace the value of - # `runners_token` with some mask (like `xxxxxx`) when sending trace file directly by workhorse. - desc 'Get a trace of a specific build of a project' - params do - requires :build_id, type: Integer, desc: 'The ID of a build' - end - get ':id/builds/:build_id/trace' do - authorize_read_builds! - - build = get_build!(params[:build_id]) - - header 'Content-Disposition', "infile; filename=\"#{build.id}.log\"" - content_type 'text/plain' - env['api.format'] = :binary - - trace = build.trace.raw - body trace - end - - desc 'Cancel a specific build of a project' do - success ::API::V3::Entities::Build - end - params do - requires :build_id, type: Integer, desc: 'The ID of a build' - end - post ':id/builds/:build_id/cancel' do - authorize_update_builds! - - build = get_build!(params[:build_id]) - authorize!(:update_build, build) - - build.cancel - - present build, with: ::API::V3::Entities::Build - end - - desc 'Retry a specific build of a project' do - success ::API::V3::Entities::Build - end - params do - requires :build_id, type: Integer, desc: 'The ID of a build' - end - post ':id/builds/:build_id/retry' do - authorize_update_builds! - - build = get_build!(params[:build_id]) - authorize!(:update_build, build) - break forbidden!('Build is not retryable') unless build.retryable? - - build = Ci::Build.retry(build, current_user) - - present build, with: ::API::V3::Entities::Build - end - - desc 'Erase build (remove artifacts and build trace)' do - success ::API::V3::Entities::Build - end - params do - requires :build_id, type: Integer, desc: 'The ID of a build' - end - post ':id/builds/:build_id/erase' do - authorize_update_builds! - - build = get_build!(params[:build_id]) - authorize!(:erase_build, build) - break forbidden!('Build is not erasable!') unless build.erasable? - - build.erase(erased_by: current_user) - present build, with: ::API::V3::Entities::Build - end - - desc 'Keep the artifacts to prevent them from being deleted' do - success ::API::V3::Entities::Build - end - params do - requires :build_id, type: Integer, desc: 'The ID of a build' - end - post ':id/builds/:build_id/artifacts/keep' do - authorize_update_builds! - - build = get_build!(params[:build_id]) - authorize!(:update_build, build) - break not_found!(build) unless build.artifacts? - - build.keep_artifacts! - - status 200 - present build, with: ::API::V3::Entities::Build - end - - desc 'Trigger a manual build' do - success ::API::V3::Entities::Build - detail 'This feature was added in GitLab 8.11' - end - params do - requires :build_id, type: Integer, desc: 'The ID of a Build' - end - post ":id/builds/:build_id/play" do - authorize_read_builds! - - build = get_build!(params[:build_id]) - authorize!(:update_build, build) - bad_request!("Unplayable Job") unless build.playable? - - build.play(current_user) - - status 200 - present build, with: ::API::V3::Entities::Build - end - end - - helpers do - def find_build(id) - user_project.builds.find_by(id: id.to_i) - end - - def get_build!(id) - find_build(id) || not_found! - end - - def filter_builds(builds, scope) - return builds if scope.nil? || scope.empty? - - available_statuses = ::CommitStatus::AVAILABLE_STATUSES - - unknown = scope - available_statuses - render_api_error!('Scope contains invalid value(s)', 400) unless unknown.empty? - - builds.where(status: available_statuses && scope) - end - - def authorize_read_builds! - authorize! :read_build, user_project - end - - def authorize_update_builds! - authorize! :update_build, user_project - end - end - end - end -end diff --git a/lib/api/v3/commits.rb b/lib/api/v3/commits.rb deleted file mode 100644 index 4f6ea8f502e..00000000000 --- a/lib/api/v3/commits.rb +++ /dev/null @@ -1,199 +0,0 @@ -require 'mime/types' - -module API - module V3 - class Commits < Grape::API - include PaginationParams - - before { authenticate! } - before { authorize! :download_code, user_project } - - params do - requires :id, type: String, desc: 'The ID of a project' - end - resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do - desc 'Get a project repository commits' do - success ::API::Entities::Commit - end - params do - optional :ref_name, type: String, desc: 'The name of a repository branch or tag, if not given the default branch is used' - optional :since, type: DateTime, desc: 'Only commits after or in this date will be returned' - optional :until, type: DateTime, desc: 'Only commits before or in this date will be returned' - optional :page, type: Integer, default: 0, desc: 'The page for pagination' - optional :per_page, type: Integer, default: 20, desc: 'The number of results per page' - optional :path, type: String, desc: 'The file path' - end - get ":id/repository/commits" do - ref = params[:ref_name] || user_project.try(:default_branch) || 'master' - offset = params[:page] * params[:per_page] - - commits = user_project.repository.commits(ref, - path: params[:path], - limit: params[:per_page], - offset: offset, - after: params[:since], - before: params[:until]) - - present commits, with: ::API::Entities::Commit - end - - desc 'Commit multiple file changes as one commit' do - success ::API::Entities::CommitDetail - detail 'This feature was introduced in GitLab 8.13' - end - params do - requires :branch_name, type: String, desc: 'The name of branch' - requires :commit_message, type: String, desc: 'Commit message' - requires :actions, type: Array[Hash], desc: 'Actions to perform in commit' - optional :author_email, type: String, desc: 'Author email for commit' - optional :author_name, type: String, desc: 'Author name for commit' - end - post ":id/repository/commits" do - authorize! :push_code, user_project - - attrs = declared_params.dup - branch = attrs.delete(:branch_name) - attrs.merge!(start_branch: branch, branch_name: branch) - - result = ::Files::MultiService.new(user_project, current_user, attrs).execute - - if result[:status] == :success - commit_detail = user_project.repository.commits(result[:result], limit: 1).first - present commit_detail, with: ::API::Entities::CommitDetail - else - render_api_error!(result[:message], 400) - end - end - - desc 'Get a specific commit of a project' do - success ::API::Entities::CommitDetail - failure [[404, 'Not Found']] - end - params do - requires :sha, type: String, desc: 'A commit sha, or the name of a branch or tag' - optional :stats, type: Boolean, default: true, desc: 'Include commit stats' - end - get ":id/repository/commits/:sha", requirements: API::COMMIT_ENDPOINT_REQUIREMENTS do - commit = user_project.commit(params[:sha]) - - not_found! "Commit" unless commit - - present commit, with: ::API::Entities::CommitDetail, stats: params[:stats] - end - - desc 'Get the diff for a specific commit of a project' do - failure [[404, 'Not Found']] - end - params do - requires :sha, type: String, desc: 'A commit sha, or the name of a branch or tag' - end - get ":id/repository/commits/:sha/diff", requirements: API::COMMIT_ENDPOINT_REQUIREMENTS do - commit = user_project.commit(params[:sha]) - - not_found! "Commit" unless commit - - commit.raw_diffs.to_a - end - - desc "Get a commit's comments" do - success ::API::Entities::CommitNote - failure [[404, 'Not Found']] - end - params do - use :pagination - requires :sha, type: String, desc: 'A commit sha, or the name of a branch or tag' - end - get ':id/repository/commits/:sha/comments', requirements: API::COMMIT_ENDPOINT_REQUIREMENTS do - commit = user_project.commit(params[:sha]) - - not_found! 'Commit' unless commit - notes = commit.notes.order(:created_at) - - present paginate(notes), with: ::API::Entities::CommitNote - end - - desc 'Cherry pick commit into a branch' do - detail 'This feature was introduced in GitLab 8.15' - success ::API::Entities::Commit - end - params do - requires :sha, type: String, desc: 'A commit sha to be cherry picked' - requires :branch, type: String, desc: 'The name of the branch' - end - post ':id/repository/commits/:sha/cherry_pick', requirements: API::COMMIT_ENDPOINT_REQUIREMENTS do - authorize! :push_code, user_project - - commit = user_project.commit(params[:sha]) - not_found!('Commit') unless commit - - branch = user_project.repository.find_branch(params[:branch]) - not_found!('Branch') unless branch - - commit_params = { - commit: commit, - start_branch: params[:branch], - branch_name: params[:branch] - } - - result = ::Commits::CherryPickService.new(user_project, current_user, commit_params).execute - - if result[:status] == :success - branch = user_project.repository.find_branch(params[:branch]) - present user_project.repository.commit(branch.dereferenced_target), with: ::API::Entities::Commit - else - render_api_error!(result[:message], 400) - end - end - - desc 'Post comment to commit' do - success ::API::Entities::CommitNote - end - params do - requires :sha, type: String, regexp: /\A\h{6,40}\z/, desc: "The commit's SHA" - requires :note, type: String, desc: 'The text of the comment' - optional :path, type: String, desc: 'The file path' - given :path do - requires :line, type: Integer, desc: 'The line number' - requires :line_type, type: String, values: %w(new old), default: 'new', desc: 'The type of the line' - end - end - post ':id/repository/commits/:sha/comments', requirements: API::COMMIT_ENDPOINT_REQUIREMENTS do - commit = user_project.commit(params[:sha]) - not_found! 'Commit' unless commit - - opts = { - note: params[:note], - noteable_type: 'Commit', - commit_id: commit.id - } - - if params[:path] - commit.raw_diffs(limits: false).each do |diff| - next unless diff.new_path == params[:path] - - lines = Gitlab::Diff::Parser.new.parse(diff.diff.each_line) - - lines.each do |line| - next unless line.new_pos == params[:line] && line.type == params[:line_type] - - break opts[:line_code] = Gitlab::Git.diff_line_code(diff.new_path, line.new_pos, line.old_pos) - end - - break if opts[:line_code] - end - - opts[:type] = LegacyDiffNote.name if opts[:line_code] - end - - note = ::Notes::CreateService.new(user_project, current_user, opts).execute - - if note.save - present note, with: ::API::Entities::CommitNote - else - render_api_error!("Failed to save note #{note.errors.messages}", 400) - end - end - end - end - end -end diff --git a/lib/api/v3/deploy_keys.rb b/lib/api/v3/deploy_keys.rb deleted file mode 100644 index 47e54ca85a5..00000000000 --- a/lib/api/v3/deploy_keys.rb +++ /dev/null @@ -1,143 +0,0 @@ -module API - module V3 - class DeployKeys < Grape::API - before { authenticate! } - - helpers do - def add_deploy_keys_project(project, attrs = {}) - project.deploy_keys_projects.create(attrs) - end - - def find_by_deploy_key(project, key_id) - project.deploy_keys_projects.find_by!(deploy_key: key_id) - end - end - - get "deploy_keys" do - authenticated_as_admin! - - keys = DeployKey.all - present keys, with: ::API::Entities::SSHKey - end - - params do - requires :id, type: String, desc: 'The ID of the project' - end - resource :projects, requirements: { id: %r{[^/]+} } do - before { authorize_admin_project } - - %w(keys deploy_keys).each do |path| - desc "Get a specific project's deploy keys" do - success ::API::Entities::DeployKeysProject - end - get ":id/#{path}" do - keys = user_project.deploy_keys_projects.preload(:deploy_key) - - present keys, with: ::API::Entities::DeployKeysProject - end - - desc 'Get single deploy key' do - success ::API::Entities::DeployKeysProject - end - params do - requires :key_id, type: Integer, desc: 'The ID of the deploy key' - end - get ":id/#{path}/:key_id" do - key = find_by_deploy_key(user_project, params[:key_id]) - - present key, with: ::API::Entities::DeployKeysProject - end - - desc 'Add new deploy key to currently authenticated user' do - success ::API::Entities::DeployKeysProject - end - params do - requires :key, type: String, desc: 'The new deploy key' - requires :title, type: String, desc: 'The name of the deploy key' - optional :can_push, type: Boolean, desc: "Can deploy key push to the project's repository" - end - post ":id/#{path}" do - params[:key].strip! - - # Check for an existing key joined to this project - key = user_project.deploy_keys_projects - .joins(:deploy_key) - .find_by(keys: { key: params[:key] }) - - if key - present key, with: ::API::Entities::DeployKeysProject - break - end - - # Check for available deploy keys in other projects - key = current_user.accessible_deploy_keys.find_by(key: params[:key]) - if key - added_key = add_deploy_keys_project(user_project, deploy_key: key, can_push: !!params[:can_push]) - - present added_key, with: ::API::Entities::DeployKeysProject - break - end - - # Create a new deploy key - key_attributes = { can_push: !!params[:can_push], - deploy_key_attributes: declared_params.except(:can_push) } - key = add_deploy_keys_project(user_project, key_attributes) - - if key.valid? - present key, with: ::API::Entities::DeployKeysProject - else - render_validation_error!(key) - end - end - - desc 'Enable a deploy key for a project' do - detail 'This feature was added in GitLab 8.11' - success ::API::Entities::SSHKey - end - params do - requires :key_id, type: Integer, desc: 'The ID of the deploy key' - end - post ":id/#{path}/:key_id/enable" do - key = ::Projects::EnableDeployKeyService.new(user_project, - current_user, declared_params).execute - - if key - present key, with: ::API::Entities::SSHKey - else - not_found!('Deploy Key') - end - end - - desc 'Disable a deploy key for a project' do - detail 'This feature was added in GitLab 8.11' - success ::API::Entities::SSHKey - end - params do - requires :key_id, type: Integer, desc: 'The ID of the deploy key' - end - delete ":id/#{path}/:key_id/disable" do - key = user_project.deploy_keys_projects.find_by(deploy_key_id: params[:key_id]) - key.destroy - - present key.deploy_key, with: ::API::Entities::SSHKey - end - - desc 'Delete deploy key for a project' do - success Key - end - params do - requires :key_id, type: Integer, desc: 'The ID of the deploy key' - end - delete ":id/#{path}/:key_id" do - key = user_project.deploy_keys_projects.find_by(deploy_key_id: params[:key_id]) - if key - key.destroy - else - not_found!('Deploy Key') - end - end - end - end - end - end -end diff --git a/lib/api/v3/deployments.rb b/lib/api/v3/deployments.rb deleted file mode 100644 index 1d4972eda26..00000000000 --- a/lib/api/v3/deployments.rb +++ /dev/null @@ -1,43 +0,0 @@ -module API - module V3 - # Deployments RESTful API endpoints - class Deployments < Grape::API - include PaginationParams - - before { authenticate! } - - params do - requires :id, type: String, desc: 'The project ID' - end - resource :projects, requirements: { id: %r{[^/]+} } do - desc 'Get all deployments of the project' do - detail 'This feature was introduced in GitLab 8.11.' - success ::API::V3::Deployments - end - params do - use :pagination - end - get ':id/deployments' do - authorize! :read_deployment, user_project - - present paginate(user_project.deployments), with: ::API::V3::Deployments - end - - desc 'Gets a specific deployment' do - detail 'This feature was introduced in GitLab 8.11.' - success ::API::V3::Deployments - end - params do - requires :deployment_id, type: Integer, desc: 'The deployment ID' - end - get ':id/deployments/:deployment_id' do - authorize! :read_deployment, user_project - - deployment = user_project.deployments.find(params[:deployment_id]) - - present deployment, with: ::API::V3::Deployments - end - end - end - end -end diff --git a/lib/api/v3/entities.rb b/lib/api/v3/entities.rb deleted file mode 100644 index 28fcf6c6e84..00000000000 --- a/lib/api/v3/entities.rb +++ /dev/null @@ -1,311 +0,0 @@ -module API - module V3 - module Entities - class ProjectSnippet < Grape::Entity - expose :id, :title, :file_name - expose :author, using: ::API::Entities::UserBasic - expose :updated_at, :created_at - expose(:expires_at) { |snippet| nil } - - expose :web_url do |snippet, options| - Gitlab::UrlBuilder.build(snippet) - end - end - - class Note < Grape::Entity - expose :id - expose :note, as: :body - expose :attachment_identifier, as: :attachment - expose :author, using: ::API::Entities::UserBasic - expose :created_at, :updated_at - expose :system?, as: :system - expose :noteable_id, :noteable_type - # upvote? and downvote? are deprecated, always return false - expose(:upvote?) { |note| false } - expose(:downvote?) { |note| false } - end - - class PushEventPayload < Grape::Entity - expose :commit_count, :action, :ref_type, :commit_from, :commit_to - expose :ref, :commit_title - end - - class Event < Grape::Entity - expose :project_id, :action_name - expose :target_id, :target_type, :author_id - expose :target_title - expose :created_at - expose :note, using: Entities::Note, if: ->(event, options) { event.note? } - expose :author, using: ::API::Entities::UserBasic, if: ->(event, options) { event.author } - - expose :push_event_payload, - as: :push_data, - using: PushEventPayload, - if: -> (event, _) { event.push? } - - expose :author_username do |event, options| - event.author&.username - end - end - - class AwardEmoji < Grape::Entity - expose :id - expose :name - expose :user, using: ::API::Entities::UserBasic - expose :created_at, :updated_at - expose :awardable_id, :awardable_type - end - - class Project < Grape::Entity - expose :id, :description, :default_branch, :tag_list - expose :public?, as: :public - expose :archived?, as: :archived - expose :visibility_level, :ssh_url_to_repo, :http_url_to_repo, :web_url - expose :owner, using: ::API::Entities::UserBasic, unless: ->(project, options) { project.group } - expose :name, :name_with_namespace - expose :path, :path_with_namespace - expose :resolve_outdated_diff_discussions - expose :container_registry_enabled - - # Expose old field names with the new permissions methods to keep API compatible - expose(:issues_enabled) { |project, options| project.feature_available?(:issues, options[:current_user]) } - expose(:merge_requests_enabled) { |project, options| project.feature_available?(:merge_requests, options[:current_user]) } - expose(:wiki_enabled) { |project, options| project.feature_available?(:wiki, options[:current_user]) } - expose(:builds_enabled) { |project, options| project.feature_available?(:builds, options[:current_user]) } - expose(:snippets_enabled) { |project, options| project.feature_available?(:snippets, options[:current_user]) } - - expose :created_at, :last_activity_at - expose :shared_runners_enabled - expose :lfs_enabled?, as: :lfs_enabled - expose :creator_id - expose :namespace, using: 'API::Entities::Namespace' - expose :forked_from_project, using: ::API::Entities::BasicProjectDetails, if: lambda { |project, options| project.forked? } - expose :avatar_url do |user, options| - user.avatar_url(only_path: false) - end - expose :star_count, :forks_count - expose :open_issues_count, if: lambda { |project, options| project.feature_available?(:issues, options[:current_user]) && project.default_issues_tracker? } - expose :runners_token, if: lambda { |_project, options| options[:user_can_admin_project] } - expose :public_builds - expose :shared_with_groups do |project, options| - ::API::Entities::SharedGroup.represent(project.project_group_links.all, options) - end - expose :only_allow_merge_if_pipeline_succeeds, as: :only_allow_merge_if_build_succeeds - expose :request_access_enabled - expose :only_allow_merge_if_all_discussions_are_resolved - - expose :statistics, using: '::API::V3::Entities::ProjectStatistics', if: :statistics - end - - class ProjectWithAccess < Project - expose :permissions do - expose :project_access, using: ::API::Entities::ProjectAccess do |project, options| - project.project_members.find_by(user_id: options[:current_user].id) - end - - expose :group_access, using: ::API::Entities::GroupAccess do |project, options| - if project.group - project.group.group_members.find_by(user_id: options[:current_user].id) - end - end - end - end - - class MergeRequest < Grape::Entity - expose :id, :iid - expose(:project_id) { |entity| entity.project.id } - expose :title, :description - expose :state, :created_at, :updated_at - expose :target_branch, :source_branch - expose :upvotes, :downvotes - expose :author, :assignee, using: ::API::Entities::UserBasic - expose :source_project_id, :target_project_id - expose :label_names, as: :labels - expose :work_in_progress?, as: :work_in_progress - expose :milestone, using: ::API::Entities::Milestone - expose :merge_when_pipeline_succeeds, as: :merge_when_build_succeeds - expose :merge_status - expose :diff_head_sha, as: :sha - expose :merge_commit_sha - expose :subscribed do |merge_request, options| - merge_request.subscribed?(options[:current_user], options[:project]) - end - expose :user_notes_count - expose :should_remove_source_branch?, as: :should_remove_source_branch - expose :force_remove_source_branch?, as: :force_remove_source_branch - - expose :squash - - expose :web_url do |merge_request, options| - Gitlab::UrlBuilder.build(merge_request) - end - end - - class Group < Grape::Entity - expose :id, :name, :path, :description, :visibility_level - expose :lfs_enabled?, as: :lfs_enabled - expose :avatar_url do |user, options| - user.avatar_url(only_path: false) - end - expose :web_url - expose :request_access_enabled - expose :full_name, :full_path - - if ::Group.supports_nested_groups? - expose :parent_id - end - - expose :statistics, if: :statistics do - with_options format_with: -> (value) { value.to_i } do - expose :storage_size - expose :repository_size - expose :lfs_objects_size - expose :build_artifacts_size - end - end - end - - class GroupDetail < Group - expose :projects, using: Entities::Project - expose :shared_projects, using: Entities::Project - end - - class ApplicationSetting < Grape::Entity - expose :id - expose :default_projects_limit - expose :signup_enabled - expose :password_authentication_enabled_for_web, as: :password_authentication_enabled - expose :password_authentication_enabled_for_web, as: :signin_enabled - expose :gravatar_enabled - expose :sign_in_text - expose :after_sign_up_text - expose :created_at - expose :updated_at - expose :home_page_url - expose :default_branch_protection - expose :restricted_visibility_levels - expose :max_attachment_size - expose :session_expire_delay - expose :default_project_visibility - expose :default_snippet_visibility - expose :default_group_visibility - expose :domain_whitelist - expose :domain_blacklist_enabled - expose :domain_blacklist - expose :user_oauth_applications - expose :after_sign_out_path - expose :container_registry_token_expire_delay - expose :repository_storage - expose :repository_storages - expose :koding_enabled - expose :koding_url - expose :plantuml_enabled - expose :plantuml_url - expose :terminal_max_session_time - end - - class Environment < ::API::Entities::EnvironmentBasic - expose :project, using: Entities::Project - end - - class Trigger < Grape::Entity - expose :token, :created_at, :updated_at, :last_used - expose :owner, using: ::API::Entities::UserBasic - end - - class TriggerRequest < Grape::Entity - expose :id, :variables - end - - class Build < Grape::Entity - expose :id, :status, :stage, :name, :ref, :tag, :coverage - expose :created_at, :started_at, :finished_at - expose :user, with: ::API::Entities::User - expose :artifacts_file, using: ::API::Entities::JobArtifactFile, if: -> (build, opts) { build.artifacts? } - expose :commit, with: ::API::Entities::Commit - expose :runner, with: ::API::Entities::Runner - expose :pipeline, with: ::API::Entities::PipelineBasic - end - - class BuildArtifactFile < Grape::Entity - expose :filename, :size - end - - class Deployment < Grape::Entity - expose :id, :iid, :ref, :sha, :created_at - expose :user, using: ::API::Entities::UserBasic - expose :environment, using: ::API::Entities::EnvironmentBasic - expose :deployable, using: Entities::Build - end - - class MergeRequestChanges < MergeRequest - expose :diffs, as: :changes, using: ::API::Entities::Diff do |compare, _| - compare.raw_diffs(limits: false).to_a - end - end - - class ProjectStatistics < Grape::Entity - expose :commit_count - expose :storage_size - expose :repository_size - expose :lfs_objects_size - expose :build_artifacts_size - end - - class ProjectService < Grape::Entity - expose :id, :title, :created_at, :updated_at, :active - expose :push_events, :issues_events, :confidential_issues_events - expose :merge_requests_events, :tag_push_events, :note_events - expose :pipeline_events - expose :job_events, as: :build_events - # Expose serialized properties - expose :properties do |service, options| - service.properties.slice(*service.api_field_names) - end - end - - class ProjectHook < ::API::Entities::Hook - expose :project_id, :issues_events, :confidential_issues_events - expose :merge_requests_events, :note_events, :pipeline_events - expose :wiki_page_events - expose :job_events, as: :build_events - end - - class ProjectEntity < Grape::Entity - expose :id, :iid - expose(:project_id) { |entity| entity&.project.try(:id) } - expose :title, :description - expose :state, :created_at, :updated_at - end - - class IssueBasic < ProjectEntity - expose :label_names, as: :labels - expose :milestone, using: ::API::Entities::Milestone - expose :assignees, :author, using: ::API::Entities::UserBasic - - expose :assignee, using: ::API::Entities::UserBasic do |issue, options| - issue.assignees.first - end - - expose :user_notes_count - expose :upvotes, :downvotes - expose :due_date - expose :confidential - - expose :web_url do |issue, options| - Gitlab::UrlBuilder.build(issue) - end - end - - class Issue < IssueBasic - unexpose :assignees - expose :assignee do |issue, options| - ::API::Entities::UserBasic.represent(issue.assignees.first, options) - end - expose :subscribed do |issue, options| - issue.subscribed?(options[:current_user], options[:project] || issue.project) - end - end - end - end -end diff --git a/lib/api/v3/environments.rb b/lib/api/v3/environments.rb deleted file mode 100644 index 6bb4e016a01..00000000000 --- a/lib/api/v3/environments.rb +++ /dev/null @@ -1,87 +0,0 @@ -module API - module V3 - class Environments < Grape::API - include ::API::Helpers::CustomValidators - include PaginationParams - - before { authenticate! } - - params do - requires :id, type: String, desc: 'The project ID' - end - resource :projects, requirements: { id: %r{[^/]+} } do - desc 'Get all environments of the project' do - detail 'This feature was introduced in GitLab 8.11.' - success Entities::Environment - end - params do - use :pagination - end - get ':id/environments' do - authorize! :read_environment, user_project - - present paginate(user_project.environments), with: Entities::Environment - end - - desc 'Creates a new environment' do - detail 'This feature was introduced in GitLab 8.11.' - success Entities::Environment - end - params do - requires :name, type: String, desc: 'The name of the environment to be created' - optional :external_url, type: String, desc: 'URL on which this deployment is viewable' - optional :slug, absence: { message: "is automatically generated and cannot be changed" } - end - post ':id/environments' do - authorize! :create_environment, user_project - - environment = user_project.environments.create(declared_params) - - if environment.persisted? - present environment, with: Entities::Environment - else - render_validation_error!(environment) - end - end - - desc 'Updates an existing environment' do - detail 'This feature was introduced in GitLab 8.11.' - success Entities::Environment - end - params do - requires :environment_id, type: Integer, desc: 'The environment ID' - optional :name, type: String, desc: 'The new environment name' - optional :external_url, type: String, desc: 'The new URL on which this deployment is viewable' - optional :slug, absence: { message: "is automatically generated and cannot be changed" } - end - put ':id/environments/:environment_id' do - authorize! :update_environment, user_project - - environment = user_project.environments.find(params[:environment_id]) - - update_params = declared_params(include_missing: false).extract!(:name, :external_url) - if environment.update(update_params) - present environment, with: Entities::Environment - else - render_validation_error!(environment) - end - end - - desc 'Deletes an existing environment' do - detail 'This feature was introduced in GitLab 8.11.' - success Entities::Environment - end - params do - requires :environment_id, type: Integer, desc: 'The environment ID' - end - delete ':id/environments/:environment_id' do - authorize! :update_environment, user_project - - environment = user_project.environments.find(params[:environment_id]) - - present environment.destroy, with: Entities::Environment - end - end - end - end -end diff --git a/lib/api/v3/files.rb b/lib/api/v3/files.rb deleted file mode 100644 index 7b4b3448b6d..00000000000 --- a/lib/api/v3/files.rb +++ /dev/null @@ -1,138 +0,0 @@ -module API - module V3 - class Files < Grape::API - helpers do - def commit_params(attrs) - { - file_path: attrs[:file_path], - start_branch: attrs[:branch], - branch_name: attrs[:branch], - commit_message: attrs[:commit_message], - file_content: attrs[:content], - file_content_encoding: attrs[:encoding], - author_email: attrs[:author_email], - author_name: attrs[:author_name] - } - end - - def commit_response(attrs) - { - file_path: attrs[:file_path], - branch: attrs[:branch] - } - end - - params :simple_file_params do - requires :file_path, type: String, desc: 'The path to new file. Ex. lib/class.rb' - requires :branch_name, type: String, desc: 'The name of branch' - requires :commit_message, type: String, desc: 'Commit Message' - optional :author_email, type: String, desc: 'The email of the author' - optional :author_name, type: String, desc: 'The name of the author' - end - - params :extended_file_params do - use :simple_file_params - requires :content, type: String, desc: 'File content' - optional :encoding, type: String, values: %w[base64], desc: 'File encoding' - end - end - - params do - requires :id, type: String, desc: 'The project ID' - end - resource :projects, requirements: { id: %r{[^/]+} } do - desc 'Get a file from repository' - params do - requires :file_path, type: String, desc: 'The path to the file. Ex. lib/class.rb' - requires :ref, type: String, desc: 'The name of branch, tag, or commit' - end - get ":id/repository/files" do - authorize! :download_code, user_project - - commit = user_project.commit(params[:ref]) - not_found!('Commit') unless commit - - repo = user_project.repository - blob = repo.blob_at(commit.sha, params[:file_path]) - not_found!('File') unless blob - - blob.load_all_data! - status(200) - - { - file_name: blob.name, - file_path: blob.path, - size: blob.size, - encoding: "base64", - content: Base64.strict_encode64(blob.data), - ref: params[:ref], - blob_id: blob.id, - commit_id: commit.id, - last_commit_id: repo.last_commit_id_for_path(commit.sha, params[:file_path]) - } - end - - desc 'Create new file in repository' - params do - use :extended_file_params - end - post ":id/repository/files" do - authorize! :push_code, user_project - - file_params = declared_params(include_missing: false) - file_params[:branch] = file_params.delete(:branch_name) - - result = ::Files::CreateService.new(user_project, current_user, commit_params(file_params)).execute - - if result[:status] == :success - status(201) - commit_response(file_params) - else - render_api_error!(result[:message], 400) - end - end - - desc 'Update existing file in repository' - params do - use :extended_file_params - end - put ":id/repository/files" do - authorize! :push_code, user_project - - file_params = declared_params(include_missing: false) - file_params[:branch] = file_params.delete(:branch_name) - - result = ::Files::UpdateService.new(user_project, current_user, commit_params(file_params)).execute - - if result[:status] == :success - status(200) - commit_response(file_params) - else - http_status = result[:http_status] || 400 - render_api_error!(result[:message], http_status) - end - end - - desc 'Delete an existing file in repository' - params do - use :simple_file_params - end - delete ":id/repository/files" do - authorize! :push_code, user_project - - file_params = declared_params(include_missing: false) - file_params[:branch] = file_params.delete(:branch_name) - - result = ::Files::DeleteService.new(user_project, current_user, commit_params(file_params)).execute - - if result[:status] == :success - status(200) - commit_response(file_params) - else - render_api_error!(result[:message], 400) - end - end - end - end - end -end diff --git a/lib/api/v3/groups.rb b/lib/api/v3/groups.rb deleted file mode 100644 index 4fa7d196e50..00000000000 --- a/lib/api/v3/groups.rb +++ /dev/null @@ -1,187 +0,0 @@ -module API - module V3 - class Groups < Grape::API - include PaginationParams - - before { authenticate! } - - helpers do - params :optional_params do - optional :description, type: String, desc: 'The description of the group' - optional :visibility_level, type: Integer, desc: 'The visibility level of the group' - optional :lfs_enabled, type: Boolean, desc: 'Enable/disable LFS for the projects in this group' - optional :request_access_enabled, type: Boolean, desc: 'Allow users to request member access' - end - - params :statistics_params do - optional :statistics, type: Boolean, default: false, desc: 'Include project statistics' - end - - def present_groups(groups, options = {}) - options = options.reverse_merge( - with: Entities::Group, - current_user: current_user - ) - - groups = groups.with_statistics if options[:statistics] - present paginate(groups), options - end - end - - resource :groups do - desc 'Get a groups list' do - success Entities::Group - end - params do - use :statistics_params - optional :skip_groups, type: Array[Integer], desc: 'Array of group ids to exclude from list' - optional :all_available, type: Boolean, desc: 'Show all group that you have access to' - optional :search, type: String, desc: 'Search for a specific group' - optional :order_by, type: String, values: %w[name path], default: 'name', desc: 'Order by name or path' - optional :sort, type: String, values: %w[asc desc], default: 'asc', desc: 'Sort by asc (ascending) or desc (descending)' - use :pagination - end - get do - groups = if current_user.admin - Group.all - elsif params[:all_available] - GroupsFinder.new(current_user).execute - else - current_user.groups - end - - groups = groups.search(params[:search]) if params[:search].present? - groups = groups.where.not(id: params[:skip_groups]) if params[:skip_groups].present? - groups = groups.reorder(params[:order_by] => params[:sort]) - - present_groups groups, statistics: params[:statistics] && current_user.admin? - end - - desc 'Get list of owned groups for authenticated user' do - success Entities::Group - end - params do - use :pagination - use :statistics_params - end - get '/owned' do - present_groups current_user.owned_groups, statistics: params[:statistics] - end - - desc 'Create a group. Available only for users who can create groups.' do - success Entities::Group - end - params do - requires :name, type: String, desc: 'The name of the group' - requires :path, type: String, desc: 'The path of the group' - - if ::Group.supports_nested_groups? - optional :parent_id, type: Integer, desc: 'The parent group id for creating nested group' - end - - use :optional_params - end - post do - authorize! :create_group - - group = ::Groups::CreateService.new(current_user, declared_params(include_missing: false)).execute - - if group.persisted? - present group, with: Entities::Group, current_user: current_user - else - render_api_error!("Failed to save group #{group.errors.messages}", 400) - end - end - end - - params do - requires :id, type: String, desc: 'The ID of a group' - end - resource :groups, requirements: { id: %r{[^/]+} } do - desc 'Update a group. Available only for users who can administrate groups.' do - success Entities::Group - end - params do - optional :name, type: String, desc: 'The name of the group' - optional :path, type: String, desc: 'The path of the group' - use :optional_params - at_least_one_of :name, :path, :description, :visibility_level, - :lfs_enabled, :request_access_enabled - end - put ':id' do - group = find_group!(params[:id]) - authorize! :admin_group, group - - if ::Groups::UpdateService.new(group, current_user, declared_params(include_missing: false)).execute - present group, with: Entities::GroupDetail, current_user: current_user - else - render_validation_error!(group) - end - end - - desc 'Get a single group, with containing projects.' do - success Entities::GroupDetail - end - get ":id" do - group = find_group!(params[:id]) - present group, with: Entities::GroupDetail, current_user: current_user - end - - desc 'Remove a group.' - delete ":id" do - group = find_group!(params[:id]) - authorize! :admin_group, group - ::Groups::DestroyService.new(group, current_user).async_execute - - accepted! - end - - desc 'Get a list of projects in this group.' do - success Entities::Project - end - params do - optional :archived, type: Boolean, default: false, desc: 'Limit by archived status' - optional :visibility, type: String, values: %w[public internal private], - desc: 'Limit by visibility' - optional :search, type: String, desc: 'Return list of authorized projects matching the search criteria' - optional :order_by, type: String, values: %w[id name path created_at updated_at last_activity_at], - default: 'created_at', desc: 'Return projects ordered by field' - optional :sort, type: String, values: %w[asc desc], default: 'desc', - desc: 'Return projects sorted in ascending and descending order' - optional :simple, type: Boolean, default: false, - desc: 'Return only the ID, URL, name, and path of each project' - optional :owned, type: Boolean, default: false, desc: 'Limit by owned by authenticated user' - optional :starred, type: Boolean, default: false, desc: 'Limit by starred status' - - use :pagination - end - get ":id/projects" do - group = find_group!(params[:id]) - projects = GroupProjectsFinder.new(group: group, current_user: current_user).execute - projects = filter_projects(projects) - entity = params[:simple] ? ::API::Entities::BasicProjectDetails : Entities::Project - present paginate(projects), with: entity, current_user: current_user - end - - desc 'Transfer a project to the group namespace. Available only for admin.' do - success Entities::GroupDetail - end - params do - requires :project_id, type: String, desc: 'The ID or path of the project' - end - post ":id/projects/:project_id", requirements: { project_id: /.+/ } do - authenticated_as_admin! - group = find_group!(params[:id]) - project = find_project!(params[:project_id]) - result = ::Projects::TransferService.new(project, current_user).execute(group) - - if result - present group, with: Entities::GroupDetail, current_user: current_user - else - render_api_error!("Failed to transfer project #{project.errors.messages}", 400) - end - end - end - end - end -end diff --git a/lib/api/v3/helpers.rb b/lib/api/v3/helpers.rb deleted file mode 100644 index 4e63aa01c1a..00000000000 --- a/lib/api/v3/helpers.rb +++ /dev/null @@ -1,49 +0,0 @@ -module API - module V3 - module Helpers - def find_project_issue(id) - IssuesFinder.new(current_user, project_id: user_project.id).find(id) - end - - def find_project_merge_request(id) - MergeRequestsFinder.new(current_user, project_id: user_project.id).find(id) - end - - def find_merge_request_with_access(id, access_level = :read_merge_request) - merge_request = user_project.merge_requests.find(id) - authorize! access_level, merge_request - merge_request - end - - # project helpers - - def filter_projects(projects) - if params[:membership] - projects = projects.merge(current_user.authorized_projects) - end - - if params[:owned] - projects = projects.merge(current_user.owned_projects) - end - - if params[:starred] - projects = projects.merge(current_user.starred_projects) - end - - if params[:search].present? - projects = projects.search(params[:search]) - end - - if params[:visibility].present? - projects = projects.where(visibility_level: Gitlab::VisibilityLevel.level_value(params[:visibility])) - end - - unless params[:archived].nil? - projects = projects.where(archived: to_boolean(params[:archived])) - end - - projects.reorder(params[:order_by] => params[:sort]) - end - end - end -end diff --git a/lib/api/v3/issues.rb b/lib/api/v3/issues.rb deleted file mode 100644 index b59947d81d9..00000000000 --- a/lib/api/v3/issues.rb +++ /dev/null @@ -1,240 +0,0 @@ -module API - module V3 - class Issues < Grape::API - include PaginationParams - - before { authenticate! } - - helpers do - def find_issues(args = {}) - args = params.merge(args) - args = convert_parameters_from_legacy_format(args) - - args.delete(:id) - args[:milestone_title] = args.delete(:milestone) - - match_all_labels = args.delete(:match_all_labels) - labels = args.delete(:labels) - args[:label_name] = labels if match_all_labels - - # IssuesFinder expects iids - args[:iids] = args.delete(:iid) if args.key?(:iid) - - issues = IssuesFinder.new(current_user, args).execute.inc_notes_with_associations - - if !match_all_labels && labels.present? - issues = issues.includes(:labels).where('labels.title' => labels.split(',')) - end - - issues.reorder(args[:order_by] => args[:sort]) - end - - params :issues_params do - optional :labels, type: String, desc: 'Comma-separated list of label names' - optional :milestone, type: String, desc: 'Milestone title' - optional :order_by, type: String, values: %w[created_at updated_at], default: 'created_at', - desc: 'Return issues ordered by `created_at` or `updated_at` fields.' - optional :sort, type: String, values: %w[asc desc], default: 'desc', - desc: 'Return issues sorted in `asc` or `desc` order.' - optional :milestone, type: String, desc: 'Return issues for a specific milestone' - use :pagination - end - - params :issue_params do - optional :description, type: String, desc: 'The description of an issue' - optional :assignee_id, type: Integer, desc: 'The ID of a user to assign issue' - optional :milestone_id, type: Integer, desc: 'The ID of a milestone to assign issue' - optional :labels, type: String, desc: 'Comma-separated list of label names' - optional :due_date, type: String, desc: 'Date time string in the format YEAR-MONTH-DAY' - optional :confidential, type: Boolean, desc: 'Boolean parameter if the issue should be confidential' - end - end - - resource :issues do - desc "Get currently authenticated user's issues" do - success ::API::V3::Entities::Issue - end - params do - optional :state, type: String, values: %w[opened closed all], default: 'all', - desc: 'Return opened, closed, or all issues' - use :issues_params - end - get do - issues = find_issues(scope: 'authored') - - present paginate(issues), with: ::API::V3::Entities::Issue, current_user: current_user - end - end - - params do - requires :id, type: String, desc: 'The ID of a group' - end - resource :groups, requirements: { id: %r{[^/]+} } do - desc 'Get a list of group issues' do - success ::API::V3::Entities::Issue - end - params do - optional :state, type: String, values: %w[opened closed all], default: 'all', - desc: 'Return opened, closed, or all issues' - use :issues_params - end - get ":id/issues" do - group = find_group!(params[:id]) - - issues = find_issues(group_id: group.id, match_all_labels: true) - - present paginate(issues), with: ::API::V3::Entities::Issue, current_user: current_user - end - end - - params do - requires :id, type: String, desc: 'The ID of a project' - end - resource :projects, requirements: { id: %r{[^/]+} } do - include TimeTrackingEndpoints - - desc 'Get a list of project issues' do - detail 'iid filter is deprecated have been removed on V4' - success ::API::V3::Entities::Issue - end - params do - optional :state, type: String, values: %w[opened closed all], default: 'all', - desc: 'Return opened, closed, or all issues' - optional :iid, type: Integer, desc: 'Return the issue having the given `iid`' - use :issues_params - end - get ":id/issues" do - project = find_project!(params[:id]) - - issues = find_issues(project_id: project.id) - - present paginate(issues), with: ::API::V3::Entities::Issue, current_user: current_user, project: user_project - end - - desc 'Get a single project issue' do - success ::API::V3::Entities::Issue - end - params do - requires :issue_id, type: Integer, desc: 'The ID of a project issue' - end - get ":id/issues/:issue_id" do - issue = find_project_issue(params[:issue_id]) - present issue, with: ::API::V3::Entities::Issue, current_user: current_user, project: user_project - end - - desc 'Create a new project issue' do - success ::API::V3::Entities::Issue - end - params do - requires :title, type: String, desc: 'The title of an issue' - optional :created_at, type: DateTime, - desc: 'Date time when the issue was created. Available only for admins and project owners.' - optional :merge_request_for_resolving_discussions, type: Integer, - desc: 'The IID of a merge request for which to resolve discussions' - use :issue_params - end - post ':id/issues' do - Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42131') - - # Setting created_at time only allowed for admins and project owners - unless current_user.admin? || user_project.owner == current_user - params.delete(:created_at) - end - - issue_params = declared_params(include_missing: false) - issue_params = issue_params.merge(merge_request_to_resolve_discussions_of: issue_params.delete(:merge_request_for_resolving_discussions)) - issue_params = convert_parameters_from_legacy_format(issue_params) - - issue = ::Issues::CreateService.new(user_project, - current_user, - issue_params.merge(request: request, api: true)).execute - render_spam_error! if issue.spam? - - if issue.valid? - present issue, with: ::API::V3::Entities::Issue, current_user: current_user, project: user_project - else - render_validation_error!(issue) - end - end - - desc 'Update an existing issue' do - success ::API::V3::Entities::Issue - end - params do - requires :issue_id, type: Integer, desc: 'The ID of a project issue' - optional :title, type: String, desc: 'The title of an issue' - optional :updated_at, type: DateTime, - desc: 'Date time when the issue was updated. Available only for admins and project owners.' - optional :state_event, type: String, values: %w[reopen close], desc: 'State of the issue' - use :issue_params - at_least_one_of :title, :description, :assignee_id, :milestone_id, - :labels, :created_at, :due_date, :confidential, :state_event - end - put ':id/issues/:issue_id' do - Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42132') - - issue = user_project.issues.find(params.delete(:issue_id)) - authorize! :update_issue, issue - - # Setting created_at time only allowed for admins and project owners - unless current_user.admin? || user_project.owner == current_user - params.delete(:updated_at) - end - - update_params = declared_params(include_missing: false).merge(request: request, api: true) - update_params = convert_parameters_from_legacy_format(update_params) - - issue = ::Issues::UpdateService.new(user_project, - current_user, - update_params).execute(issue) - - render_spam_error! if issue.spam? - - if issue.valid? - present issue, with: ::API::V3::Entities::Issue, current_user: current_user, project: user_project - else - render_validation_error!(issue) - end - end - - desc 'Move an existing issue' do - success ::API::V3::Entities::Issue - end - params do - requires :issue_id, type: Integer, desc: 'The ID of a project issue' - requires :to_project_id, type: Integer, desc: 'The ID of the new project' - end - post ':id/issues/:issue_id/move' do - Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42133') - - issue = user_project.issues.find_by(id: params[:issue_id]) - not_found!('Issue') unless issue - - new_project = Project.find_by(id: params[:to_project_id]) - not_found!('Project') unless new_project - - begin - issue = ::Issues::MoveService.new(user_project, current_user).execute(issue, new_project) - present issue, with: ::API::V3::Entities::Issue, current_user: current_user, project: user_project - rescue ::Issues::MoveService::MoveError => error - render_api_error!(error.message, 400) - end - end - - desc 'Delete a project issue' - params do - requires :issue_id, type: Integer, desc: 'The ID of a project issue' - end - delete ":id/issues/:issue_id" do - issue = user_project.issues.find_by(id: params[:issue_id]) - not_found!('Issue') unless issue - - authorize!(:destroy_issue, issue) - - status(200) - issue.destroy - end - end - end - end -end diff --git a/lib/api/v3/labels.rb b/lib/api/v3/labels.rb deleted file mode 100644 index 4157462ec2a..00000000000 --- a/lib/api/v3/labels.rb +++ /dev/null @@ -1,34 +0,0 @@ -module API - module V3 - class Labels < Grape::API - before { authenticate! } - - params do - requires :id, type: String, desc: 'The ID of a project' - end - resource :projects, requirements: { id: %r{[^/]+} } do - desc 'Get all labels of the project' do - success ::API::Entities::Label - end - get ':id/labels' do - present available_labels_for(user_project), with: ::API::Entities::Label, current_user: current_user, project: user_project - end - - desc 'Delete an existing label' do - success ::API::Entities::Label - end - params do - requires :name, type: String, desc: 'The name of the label to be deleted' - end - delete ':id/labels' do - authorize! :admin_label, user_project - - label = user_project.labels.find_by(title: params[:name]) - not_found!('Label') unless label - - present label.destroy, with: ::API::Entities::Label, current_user: current_user, project: user_project - end - end - end - end -end diff --git a/lib/api/v3/members.rb b/lib/api/v3/members.rb deleted file mode 100644 index 88dd598f1e9..00000000000 --- a/lib/api/v3/members.rb +++ /dev/null @@ -1,136 +0,0 @@ -module API - module V3 - class Members < Grape::API - include PaginationParams - - before { authenticate! } - - helpers ::API::Helpers::MembersHelpers - - %w[group project].each do |source_type| - params do - requires :id, type: String, desc: "The #{source_type} ID" - end - resource source_type.pluralize, requirements: { id: %r{[^/]+} } do - desc 'Gets a list of group or project members viewable by the authenticated user.' do - success ::API::Entities::Member - end - params do - optional :query, type: String, desc: 'A query string to search for members' - use :pagination - end - get ":id/members" do - source = find_source(source_type, params[:id]) - - members = source.members.where.not(user_id: nil).includes(:user) - members = members.joins(:user).merge(User.search(params[:query])) if params[:query].present? - members = paginate(members) - - present members, with: ::API::Entities::Member - end - - desc 'Gets a member of a group or project.' do - success ::API::Entities::Member - end - params do - requires :user_id, type: Integer, desc: 'The user ID of the member' - end - get ":id/members/:user_id" do - source = find_source(source_type, params[:id]) - - members = source.members - member = members.find_by!(user_id: params[:user_id]) - - present member, with: ::API::Entities::Member - end - - desc 'Adds a member to a group or project.' do - success ::API::Entities::Member - end - params do - requires :user_id, type: Integer, desc: 'The user ID of the new member' - requires :access_level, type: Integer, desc: 'A valid access level (defaults: `30`, developer access level)' - optional :expires_at, type: DateTime, desc: 'Date string in the format YEAR-MONTH-DAY' - end - post ":id/members" do - source = find_source(source_type, params[:id]) - authorize_admin_source!(source_type, source) - - member = source.members.find_by(user_id: params[:user_id]) - - # We need this explicit check because `source.add_user` doesn't - # currently return the member created so it would return 201 even if - # the member already existed... - # The `source_type == 'group'` check is to ensure back-compatibility - # but 409 behavior should be used for both project and group members in 9.0! - conflict!('Member already exists') if source_type == 'group' && member - - unless member - member = source.add_user(params[:user_id], params[:access_level], current_user: current_user, expires_at: params[:expires_at]) - end - - if member.persisted? && member.valid? - present member, with: ::API::Entities::Member - else - # This is to ensure back-compatibility but 400 behavior should be used - # for all validation errors in 9.0! - render_api_error!('Access level is not known', 422) if member.errors.key?(:access_level) - render_validation_error!(member) - end - end - - desc 'Updates a member of a group or project.' do - success ::API::Entities::Member - end - params do - requires :user_id, type: Integer, desc: 'The user ID of the new member' - requires :access_level, type: Integer, desc: 'A valid access level' - optional :expires_at, type: DateTime, desc: 'Date string in the format YEAR-MONTH-DAY' - end - put ":id/members/:user_id" do - source = find_source(source_type, params.delete(:id)) - authorize_admin_source!(source_type, source) - - member = source.members.find_by!(user_id: params.delete(:user_id)) - - if member.update_attributes(declared_params(include_missing: false)) - present member, with: ::API::Entities::Member - else - # This is to ensure back-compatibility but 400 behavior should be used - # for all validation errors in 9.0! - render_api_error!('Access level is not known', 422) if member.errors.key?(:access_level) - render_validation_error!(member) - end - end - - desc 'Removes a user from a group or project.' - params do - requires :user_id, type: Integer, desc: 'The user ID of the member' - end - delete ":id/members/:user_id" do - source = find_source(source_type, params[:id]) - - # This is to ensure back-compatibility but find_by! should be used - # in that casse in 9.0! - member = source.members.find_by(user_id: params[:user_id]) - - # This is to ensure back-compatibility but this should be removed in - # favor of find_by! in 9.0! - not_found!("Member: user_id:#{params[:user_id]}") if source_type == 'group' && member.nil? - - # This is to ensure back-compatibility but 204 behavior should be used - # for all DELETE endpoints in 9.0! - if member.nil? - status(200 ) - { message: "Access revoked", id: params[:user_id].to_i } - else - ::Members::DestroyService.new(current_user).execute(member) - - present member, with: ::API::Entities::Member - end - end - end - end - end - end -end diff --git a/lib/api/v3/merge_request_diffs.rb b/lib/api/v3/merge_request_diffs.rb deleted file mode 100644 index 22866fc2845..00000000000 --- a/lib/api/v3/merge_request_diffs.rb +++ /dev/null @@ -1,44 +0,0 @@ -module API - module V3 - # MergeRequestDiff API - class MergeRequestDiffs < Grape::API - before { authenticate! } - - params do - requires :id, type: String, desc: 'The ID of a project' - end - resource :projects, requirements: { id: %r{[^/]+} } do - desc 'Get a list of merge request diff versions' do - detail 'This feature was introduced in GitLab 8.12.' - success ::API::Entities::MergeRequestDiff - end - - params do - requires :merge_request_id, type: Integer, desc: 'The ID of a merge request' - end - - get ":id/merge_requests/:merge_request_id/versions" do - merge_request = find_merge_request_with_access(params[:merge_request_id]) - - present merge_request.merge_request_diffs.order_id_desc, with: ::API::Entities::MergeRequestDiff - end - - desc 'Get a single merge request diff version' do - detail 'This feature was introduced in GitLab 8.12.' - success ::API::Entities::MergeRequestDiffFull - end - - params do - requires :merge_request_id, type: Integer, desc: 'The ID of a merge request' - requires :version_id, type: Integer, desc: 'The ID of a merge request diff version' - end - - get ":id/merge_requests/:merge_request_id/versions/:version_id" do - merge_request = find_merge_request_with_access(params[:merge_request_id]) - - present merge_request.merge_request_diffs.find(params[:version_id]), with: ::API::Entities::MergeRequestDiffFull - end - end - end - end -end diff --git a/lib/api/v3/merge_requests.rb b/lib/api/v3/merge_requests.rb deleted file mode 100644 index af5afd1c334..00000000000 --- a/lib/api/v3/merge_requests.rb +++ /dev/null @@ -1,301 +0,0 @@ -module API - module V3 - class MergeRequests < Grape::API - include PaginationParams - - DEPRECATION_MESSAGE = 'This endpoint is deprecated and has been removed on V4'.freeze - - before { authenticate! } - - params do - requires :id, type: String, desc: 'The ID of a project' - end - resource :projects, requirements: { id: %r{[^/]+} } do - include TimeTrackingEndpoints - - helpers do - def handle_merge_request_errors!(errors) - if errors[:project_access].any? - error!(errors[:project_access], 422) - elsif errors[:branch_conflict].any? - error!(errors[:branch_conflict], 422) - elsif errors[:validate_fork].any? - error!(errors[:validate_fork], 422) - elsif errors[:validate_branches].any? - conflict!(errors[:validate_branches]) - elsif errors[:base].any? - error!(errors[:base], 422) - end - - render_api_error!(errors, 400) - end - - def issue_entity(project) - if project.has_external_issue_tracker? - ::API::Entities::ExternalIssue - else - ::API::V3::Entities::Issue - end - end - - params :optional_params do - optional :description, type: String, desc: 'The description of the merge request' - optional :assignee_id, type: Integer, desc: 'The ID of a user to assign the merge request' - optional :milestone_id, type: Integer, desc: 'The ID of a milestone to assign the merge request' - optional :labels, type: String, desc: 'Comma-separated list of label names' - optional :remove_source_branch, type: Boolean, desc: 'Remove source branch when merging' - optional :squash, type: Boolean, desc: 'Squash commits when merging' - end - end - - desc 'List merge requests' do - detail 'iid filter is deprecated have been removed on V4' - success ::API::V3::Entities::MergeRequest - end - params do - optional :state, type: String, values: %w[opened closed merged all], default: 'all', - desc: 'Return opened, closed, merged, or all merge requests' - optional :order_by, type: String, values: %w[created_at updated_at], default: 'created_at', - desc: 'Return merge requests ordered by `created_at` or `updated_at` fields.' - optional :sort, type: String, values: %w[asc desc], default: 'desc', - desc: 'Return merge requests sorted in `asc` or `desc` order.' - optional :iid, type: Array[Integer], desc: 'The IID of the merge requests' - use :pagination - end - get ":id/merge_requests" do - authorize! :read_merge_request, user_project - - merge_requests = user_project.merge_requests.inc_notes_with_associations - merge_requests = filter_by_iid(merge_requests, params[:iid]) if params[:iid].present? - - merge_requests = - case params[:state] - when 'opened' then merge_requests.opened - when 'closed' then merge_requests.closed - when 'merged' then merge_requests.merged - else merge_requests - end - - merge_requests = merge_requests.reorder(params[:order_by] => params[:sort]) - present paginate(merge_requests), with: ::API::V3::Entities::MergeRequest, current_user: current_user, project: user_project - end - - desc 'Create a merge request' do - success ::API::V3::Entities::MergeRequest - end - params do - requires :title, type: String, desc: 'The title of the merge request' - requires :source_branch, type: String, desc: 'The source branch' - requires :target_branch, type: String, desc: 'The target branch' - optional :target_project_id, type: Integer, - desc: 'The target project of the merge request defaults to the :id of the project' - use :optional_params - end - post ":id/merge_requests" do - Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42126') - - authorize! :create_merge_request_from, user_project - - mr_params = declared_params(include_missing: false) - mr_params[:force_remove_source_branch] = mr_params.delete(:remove_source_branch) if mr_params[:remove_source_branch].present? - - merge_request = ::MergeRequests::CreateService.new(user_project, current_user, mr_params).execute - - if merge_request.valid? - present merge_request, with: ::API::V3::Entities::MergeRequest, current_user: current_user, project: user_project - else - handle_merge_request_errors! merge_request.errors - end - end - - desc 'Delete a merge request' - params do - requires :merge_request_id, type: Integer, desc: 'The ID of a merge request' - end - delete ":id/merge_requests/:merge_request_id" do - merge_request = find_project_merge_request(params[:merge_request_id]) - - authorize!(:destroy_merge_request, merge_request) - - status(200) - merge_request.destroy - end - - params do - requires :merge_request_id, type: Integer, desc: 'The ID of a merge request' - end - { ":id/merge_request/:merge_request_id" => :deprecated, ":id/merge_requests/:merge_request_id" => :ok }.each do |path, status| - desc 'Get a single merge request' do - if status == :deprecated - detail DEPRECATION_MESSAGE - end - - success ::API::V3::Entities::MergeRequest - end - get path do - merge_request = find_merge_request_with_access(params[:merge_request_id]) - - present merge_request, with: ::API::V3::Entities::MergeRequest, current_user: current_user, project: user_project - end - - desc 'Get the commits of a merge request' do - success ::API::Entities::Commit - end - get "#{path}/commits" do - merge_request = find_merge_request_with_access(params[:merge_request_id]) - - present merge_request.commits, with: ::API::Entities::Commit - end - - desc 'Show the merge request changes' do - success ::API::Entities::MergeRequestChanges - end - get "#{path}/changes" do - merge_request = find_merge_request_with_access(params[:merge_request_id]) - - present merge_request, with: ::API::Entities::MergeRequestChanges, current_user: current_user - end - - desc 'Update a merge request' do - success ::API::V3::Entities::MergeRequest - end - params do - optional :title, type: String, allow_blank: false, desc: 'The title of the merge request' - optional :target_branch, type: String, allow_blank: false, desc: 'The target branch' - optional :state_event, type: String, values: %w[close reopen merge], - desc: 'Status of the merge request' - use :optional_params - at_least_one_of :title, :target_branch, :description, :assignee_id, - :milestone_id, :labels, :state_event, - :remove_source_branch, :squash - end - put path do - Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42127') - - merge_request = find_merge_request_with_access(params.delete(:merge_request_id), :update_merge_request) - - mr_params = declared_params(include_missing: false) - mr_params[:force_remove_source_branch] = mr_params.delete(:remove_source_branch) if mr_params[:remove_source_branch].present? - - merge_request = ::MergeRequests::UpdateService.new(user_project, current_user, mr_params).execute(merge_request) - - if merge_request.valid? - present merge_request, with: ::API::V3::Entities::MergeRequest, current_user: current_user, project: user_project - else - handle_merge_request_errors! merge_request.errors - end - end - - desc 'Merge a merge request' do - success ::API::V3::Entities::MergeRequest - end - params do - optional :merge_commit_message, type: String, desc: 'Custom merge commit message' - optional :should_remove_source_branch, type: Boolean, - desc: 'When true, the source branch will be deleted if possible' - optional :merge_when_build_succeeds, type: Boolean, - desc: 'When true, this merge request will be merged when the build succeeds' - optional :sha, type: String, desc: 'When present, must have the HEAD SHA of the source branch' - optional :squash, type: Boolean, desc: 'When true, the commits will be squashed into a single commit on merge' - end - put "#{path}/merge" do - merge_request = find_project_merge_request(params[:merge_request_id]) - - # Merge request can not be merged - # because user dont have permissions to push into target branch - unauthorized! unless merge_request.can_be_merged_by?(current_user) - - not_allowed! unless merge_request.mergeable_state? - - render_api_error!('Branch cannot be merged', 406) unless merge_request.mergeable? - - if params[:sha] && merge_request.diff_head_sha != params[:sha] - render_api_error!("SHA does not match HEAD of source branch: #{merge_request.diff_head_sha}", 409) - end - - merge_request.update(squash: params[:squash]) if params[:squash] - - merge_params = { - commit_message: params[:merge_commit_message], - should_remove_source_branch: params[:should_remove_source_branch] - } - - if params[:merge_when_build_succeeds] && merge_request.head_pipeline && merge_request.head_pipeline.active? - ::MergeRequests::MergeWhenPipelineSucceedsService - .new(merge_request.target_project, current_user, merge_params) - .execute(merge_request) - else - ::MergeRequests::MergeService - .new(merge_request.target_project, current_user, merge_params) - .execute(merge_request) - end - - present merge_request, with: ::API::V3::Entities::MergeRequest, current_user: current_user, project: user_project - end - - desc 'Cancel merge if "Merge When Build succeeds" is enabled' do - success ::API::V3::Entities::MergeRequest - end - post "#{path}/cancel_merge_when_build_succeeds" do - merge_request = find_project_merge_request(params[:merge_request_id]) - - unauthorized! unless merge_request.can_cancel_merge_when_pipeline_succeeds?(current_user) - - ::MergeRequest::MergeWhenPipelineSucceedsService - .new(merge_request.target_project, current_user) - .cancel(merge_request) - end - - desc 'Get the comments of a merge request' do - detail 'Duplicate. DEPRECATED and HAS BEEN REMOVED in V4' - success ::API::Entities::MRNote - end - params do - use :pagination - end - get "#{path}/comments" do - merge_request = find_merge_request_with_access(params[:merge_request_id]) - present paginate(merge_request.notes.fresh), with: ::API::Entities::MRNote - end - - desc 'Post a comment to a merge request' do - detail 'Duplicate. DEPRECATED and HAS BEEN REMOVED in V4' - success ::API::Entities::MRNote - end - params do - requires :note, type: String, desc: 'The text of the comment' - end - post "#{path}/comments" do - merge_request = find_merge_request_with_access(params[:merge_request_id], :create_note) - - opts = { - note: params[:note], - noteable_type: 'MergeRequest', - noteable_id: merge_request.id - } - - note = ::Notes::CreateService.new(user_project, current_user, opts).execute - - if note.save - present note, with: ::API::Entities::MRNote - else - render_api_error!("Failed to save note #{note.errors.messages}", 400) - end - end - - desc 'List issues that will be closed on merge' do - success ::API::Entities::MRNote - end - params do - use :pagination - end - get "#{path}/closes_issues" do - merge_request = find_merge_request_with_access(params[:merge_request_id]) - issues = ::Kaminari.paginate_array(merge_request.closes_issues(current_user)) - present paginate(issues), with: issue_entity(user_project), current_user: current_user - end - end - end - end - end -end diff --git a/lib/api/v3/milestones.rb b/lib/api/v3/milestones.rb deleted file mode 100644 index 9be4cf9d22a..00000000000 --- a/lib/api/v3/milestones.rb +++ /dev/null @@ -1,65 +0,0 @@ -module API - module V3 - class Milestones < Grape::API - include PaginationParams - - before { authenticate! } - - helpers do - def filter_milestones_state(milestones, state) - case state - when 'active' then milestones.active - when 'closed' then milestones.closed - else milestones - end - end - end - - params do - requires :id, type: String, desc: 'The ID of a project' - end - resource :projects, requirements: { id: %r{[^/]+} } do - desc 'Get a list of project milestones' do - success ::API::Entities::Milestone - end - params do - optional :state, type: String, values: %w[active closed all], default: 'all', - desc: 'Return "active", "closed", or "all" milestones' - optional :iid, type: Array[Integer], desc: 'The IID of the milestone' - use :pagination - end - get ":id/milestones" do - authorize! :read_milestone, user_project - - milestones = user_project.milestones - milestones = filter_milestones_state(milestones, params[:state]) - milestones = filter_by_iid(milestones, params[:iid]) if params[:iid].present? - milestones = milestones.order_id_desc - - present paginate(milestones), with: ::API::Entities::Milestone - end - - desc 'Get all issues for a single project milestone' do - success ::API::V3::Entities::Issue - end - params do - requires :milestone_id, type: Integer, desc: 'The ID of a project milestone' - use :pagination - end - get ':id/milestones/:milestone_id/issues' do - authorize! :read_milestone, user_project - - milestone = user_project.milestones.find(params[:milestone_id]) - - finder_params = { - project_id: user_project.id, - milestone_title: milestone.title - } - - issues = IssuesFinder.new(current_user, finder_params).execute - present paginate(issues), with: ::API::V3::Entities::Issue, current_user: current_user, project: user_project - end - end - end - end -end diff --git a/lib/api/v3/notes.rb b/lib/api/v3/notes.rb deleted file mode 100644 index d49772b92f2..00000000000 --- a/lib/api/v3/notes.rb +++ /dev/null @@ -1,148 +0,0 @@ -module API - module V3 - class Notes < Grape::API - include PaginationParams - - before { authenticate! } - - NOTEABLE_TYPES = [Issue, MergeRequest, Snippet].freeze - - params do - requires :id, type: String, desc: 'The ID of a project' - end - resource :projects, requirements: { id: %r{[^/]+} } do - NOTEABLE_TYPES.each do |noteable_type| - noteables_str = noteable_type.to_s.underscore.pluralize - - desc 'Get a list of project +noteable+ notes' do - success ::API::V3::Entities::Note - end - params do - requires :noteable_id, type: Integer, desc: 'The ID of the noteable' - use :pagination - end - get ":id/#{noteables_str}/:noteable_id/notes" do - noteable = user_project.public_send(noteables_str.to_sym).find(params[:noteable_id]) # rubocop:disable GitlabSecurity/PublicSend - - if can?(current_user, noteable_read_ability_name(noteable), noteable) - # We exclude notes that are cross-references and that cannot be viewed - # by the current user. By doing this exclusion at this level and not - # at the DB query level (which we cannot in that case), the current - # page can have less elements than :per_page even if - # there's more than one page. - notes = - # paginate() only works with a relation. This could lead to a - # mismatch between the pagination headers info and the actual notes - # array returned, but this is really a edge-case. - paginate(noteable.notes) - .reject { |n| n.cross_reference_not_visible_for?(current_user) } - present notes, with: ::API::V3::Entities::Note - else - not_found!("Notes") - end - end - - desc 'Get a single +noteable+ note' do - success ::API::V3::Entities::Note - end - params do - requires :note_id, type: Integer, desc: 'The ID of a note' - requires :noteable_id, type: Integer, desc: 'The ID of the noteable' - end - get ":id/#{noteables_str}/:noteable_id/notes/:note_id" do - noteable = user_project.public_send(noteables_str.to_sym).find(params[:noteable_id]) # rubocop:disable GitlabSecurity/PublicSend - note = noteable.notes.find(params[:note_id]) - can_read_note = can?(current_user, noteable_read_ability_name(noteable), noteable) && !note.cross_reference_not_visible_for?(current_user) - - if can_read_note - present note, with: ::API::V3::Entities::Note - else - not_found!("Note") - end - end - - desc 'Create a new +noteable+ note' do - success ::API::V3::Entities::Note - end - params do - requires :noteable_id, type: Integer, desc: 'The ID of the noteable' - requires :body, type: String, desc: 'The content of a note' - optional :created_at, type: String, desc: 'The creation date of the note' - end - post ":id/#{noteables_str}/:noteable_id/notes" do - opts = { - note: params[:body], - noteable_type: noteables_str.classify, - noteable_id: params[:noteable_id] - } - - noteable = user_project.public_send(noteables_str.to_sym).find(params[:noteable_id]) # rubocop:disable GitlabSecurity/PublicSend - - if can?(current_user, noteable_read_ability_name(noteable), noteable) - if params[:created_at] && (current_user.admin? || user_project.owner == current_user) - opts[:created_at] = params[:created_at] - end - - note = ::Notes::CreateService.new(user_project, current_user, opts).execute - if note.valid? - present note, with: ::API::V3::Entities.const_get(note.class.name) - else - not_found!("Note #{note.errors.messages}") - end - else - not_found!("Note") - end - end - - desc 'Update an existing +noteable+ note' do - success ::API::V3::Entities::Note - end - params do - requires :noteable_id, type: Integer, desc: 'The ID of the noteable' - requires :note_id, type: Integer, desc: 'The ID of a note' - requires :body, type: String, desc: 'The content of a note' - end - put ":id/#{noteables_str}/:noteable_id/notes/:note_id" do - note = user_project.notes.find(params[:note_id]) - - authorize! :admin_note, note - - opts = { - note: params[:body] - } - - note = ::Notes::UpdateService.new(user_project, current_user, opts).execute(note) - - if note.valid? - present note, with: ::API::V3::Entities::Note - else - render_api_error!("Failed to save note #{note.errors.messages}", 400) - end - end - - desc 'Delete a +noteable+ note' do - success ::API::V3::Entities::Note - end - params do - requires :noteable_id, type: Integer, desc: 'The ID of the noteable' - requires :note_id, type: Integer, desc: 'The ID of a note' - end - delete ":id/#{noteables_str}/:noteable_id/notes/:note_id" do - note = user_project.notes.find(params[:note_id]) - authorize! :admin_note, note - - ::Notes::DestroyService.new(user_project, current_user).execute(note) - - present note, with: ::API::V3::Entities::Note - end - end - end - - helpers do - def noteable_read_ability_name(noteable) - "read_#{noteable.class.to_s.underscore}".to_sym - end - end - end - end -end diff --git a/lib/api/v3/pipelines.rb b/lib/api/v3/pipelines.rb deleted file mode 100644 index 6d31c12f572..00000000000 --- a/lib/api/v3/pipelines.rb +++ /dev/null @@ -1,38 +0,0 @@ -module API - module V3 - class Pipelines < Grape::API - include PaginationParams - - before { authenticate! } - - params do - requires :id, type: String, desc: 'The project ID' - end - resource :projects, requirements: { id: %r{[^/]+} } do - desc 'Get all Pipelines of the project' do - detail 'This feature was introduced in GitLab 8.11.' - success ::API::Entities::Pipeline - end - params do - use :pagination - optional :scope, type: String, values: %w(running branches tags), - desc: 'Either running, branches, or tags' - end - get ':id/pipelines' do - Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42123') - - authorize! :read_pipeline, user_project - - pipelines = PipelinesFinder.new(user_project, scope: params[:scope]).execute - present paginate(pipelines), with: ::API::Entities::Pipeline - end - end - - helpers do - def pipeline - @pipeline ||= user_project.pipelines.find(params[:pipeline_id]) - end - end - end - end -end diff --git a/lib/api/v3/project_hooks.rb b/lib/api/v3/project_hooks.rb deleted file mode 100644 index 631944150c7..00000000000 --- a/lib/api/v3/project_hooks.rb +++ /dev/null @@ -1,111 +0,0 @@ -module API - module V3 - class ProjectHooks < Grape::API - include PaginationParams - - before { authenticate! } - before { authorize_admin_project } - - helpers do - params :project_hook_properties do - requires :url, type: String, desc: "The URL to send the request to" - optional :push_events, type: Boolean, desc: "Trigger hook on push events" - optional :issues_events, type: Boolean, desc: "Trigger hook on issues events" - optional :confidential_issues_events, type: Boolean, desc: "Trigger hook on confidential issues events" - optional :merge_requests_events, type: Boolean, desc: "Trigger hook on merge request events" - optional :tag_push_events, type: Boolean, desc: "Trigger hook on tag push events" - optional :note_events, type: Boolean, desc: "Trigger hook on note(comment) events" - optional :build_events, type: Boolean, desc: "Trigger hook on build events" - optional :pipeline_events, type: Boolean, desc: "Trigger hook on pipeline events" - optional :wiki_page_events, type: Boolean, desc: "Trigger hook on wiki events" - optional :enable_ssl_verification, type: Boolean, desc: "Do SSL verification when triggering the hook" - optional :token, type: String, desc: "Secret token to validate received payloads; this will not be returned in the response" - end - end - - params do - requires :id, type: String, desc: 'The ID of a project' - end - resource :projects, requirements: { id: %r{[^/]+} } do - desc 'Get project hooks' do - success ::API::V3::Entities::ProjectHook - end - params do - use :pagination - end - get ":id/hooks" do - hooks = paginate user_project.hooks - - present hooks, with: ::API::V3::Entities::ProjectHook - end - - desc 'Get a project hook' do - success ::API::V3::Entities::ProjectHook - end - params do - requires :hook_id, type: Integer, desc: 'The ID of a project hook' - end - get ":id/hooks/:hook_id" do - hook = user_project.hooks.find(params[:hook_id]) - present hook, with: ::API::V3::Entities::ProjectHook - end - - desc 'Add hook to project' do - success ::API::V3::Entities::ProjectHook - end - params do - use :project_hook_properties - end - post ":id/hooks" do - attrs = declared_params(include_missing: false) - attrs[:job_events] = attrs.delete(:build_events) if attrs.key?(:build_events) - hook = user_project.hooks.new(attrs) - - if hook.save - present hook, with: ::API::V3::Entities::ProjectHook - else - error!("Invalid url given", 422) if hook.errors[:url].present? - - not_found!("Project hook #{hook.errors.messages}") - end - end - - desc 'Update an existing project hook' do - success ::API::V3::Entities::ProjectHook - end - params do - requires :hook_id, type: Integer, desc: "The ID of the hook to update" - use :project_hook_properties - end - put ":id/hooks/:hook_id" do - hook = user_project.hooks.find(params.delete(:hook_id)) - - attrs = declared_params(include_missing: false) - attrs[:job_events] = attrs.delete(:build_events) if attrs.key?(:build_events) - if hook.update_attributes(attrs) - present hook, with: ::API::V3::Entities::ProjectHook - else - error!("Invalid url given", 422) if hook.errors[:url].present? - - not_found!("Project hook #{hook.errors.messages}") - end - end - - desc 'Deletes project hook' do - success ::API::V3::Entities::ProjectHook - end - params do - requires :hook_id, type: Integer, desc: 'The ID of the hook to delete' - end - delete ":id/hooks/:hook_id" do - begin - present user_project.hooks.destroy(params[:hook_id]), with: ::API::V3::Entities::ProjectHook - rescue - # ProjectHook can raise Error if hook_id not found - not_found!("Error deleting hook #{params[:hook_id]}") - end - end - end - end - end -end diff --git a/lib/api/v3/project_snippets.rb b/lib/api/v3/project_snippets.rb deleted file mode 100644 index 6ba425ba8c7..00000000000 --- a/lib/api/v3/project_snippets.rb +++ /dev/null @@ -1,143 +0,0 @@ -module API - module V3 - class ProjectSnippets < Grape::API - include PaginationParams - - before { authenticate! } - - params do - requires :id, type: String, desc: 'The ID of a project' - end - resource :projects, requirements: { id: %r{[^/]+} } do - helpers do - def handle_project_member_errors(errors) - if errors[:project_access].any? - error!(errors[:project_access], 422) - end - - not_found! - end - - def snippets_for_current_user - SnippetsFinder.new(current_user, project: user_project).execute - end - end - - desc 'Get all project snippets' do - success ::API::V3::Entities::ProjectSnippet - end - params do - use :pagination - end - get ":id/snippets" do - present paginate(snippets_for_current_user), with: ::API::V3::Entities::ProjectSnippet - end - - desc 'Get a single project snippet' do - success ::API::V3::Entities::ProjectSnippet - end - params do - requires :snippet_id, type: Integer, desc: 'The ID of a project snippet' - end - get ":id/snippets/:snippet_id" do - snippet = snippets_for_current_user.find(params[:snippet_id]) - present snippet, with: ::API::V3::Entities::ProjectSnippet - end - - desc 'Create a new project snippet' do - success ::API::V3::Entities::ProjectSnippet - end - params do - requires :title, type: String, desc: 'The title of the snippet' - requires :file_name, type: String, desc: 'The file name of the snippet' - requires :code, type: String, desc: 'The content of the snippet' - requires :visibility_level, type: Integer, - values: [Gitlab::VisibilityLevel::PRIVATE, - Gitlab::VisibilityLevel::INTERNAL, - Gitlab::VisibilityLevel::PUBLIC], - desc: 'The visibility level of the snippet' - end - post ":id/snippets" do - authorize! :create_project_snippet, user_project - snippet_params = declared_params.merge(request: request, api: true) - snippet_params[:content] = snippet_params.delete(:code) - - snippet = CreateSnippetService.new(user_project, current_user, snippet_params).execute - - render_spam_error! if snippet.spam? - - if snippet.persisted? - present snippet, with: ::API::V3::Entities::ProjectSnippet - else - render_validation_error!(snippet) - end - end - - desc 'Update an existing project snippet' do - success ::API::V3::Entities::ProjectSnippet - end - params do - requires :snippet_id, type: Integer, desc: 'The ID of a project snippet' - optional :title, type: String, desc: 'The title of the snippet' - optional :file_name, type: String, desc: 'The file name of the snippet' - optional :code, type: String, desc: 'The content of the snippet' - optional :visibility_level, type: Integer, - values: [Gitlab::VisibilityLevel::PRIVATE, - Gitlab::VisibilityLevel::INTERNAL, - Gitlab::VisibilityLevel::PUBLIC], - desc: 'The visibility level of the snippet' - at_least_one_of :title, :file_name, :code, :visibility_level - end - put ":id/snippets/:snippet_id" do - snippet = snippets_for_current_user.find_by(id: params.delete(:snippet_id)) - not_found!('Snippet') unless snippet - - authorize! :update_project_snippet, snippet - - snippet_params = declared_params(include_missing: false) - .merge(request: request, api: true) - - snippet_params[:content] = snippet_params.delete(:code) if snippet_params[:code].present? - - UpdateSnippetService.new(user_project, current_user, snippet, - snippet_params).execute - - render_spam_error! if snippet.spam? - - if snippet.valid? - present snippet, with: ::API::V3::Entities::ProjectSnippet - else - render_validation_error!(snippet) - end - end - - desc 'Delete a project snippet' - params do - requires :snippet_id, type: Integer, desc: 'The ID of a project snippet' - end - delete ":id/snippets/:snippet_id" do - snippet = snippets_for_current_user.find_by(id: params[:snippet_id]) - not_found!('Snippet') unless snippet - - authorize! :admin_project_snippet, snippet - snippet.destroy - - status(200) - end - - desc 'Get a raw project snippet' - params do - requires :snippet_id, type: Integer, desc: 'The ID of a project snippet' - end - get ":id/snippets/:snippet_id/raw" do - snippet = snippets_for_current_user.find_by(id: params[:snippet_id]) - not_found!('Snippet') unless snippet - - env['api.format'] = :txt - content_type 'text/plain' - present snippet.content - end - end - end - end -end diff --git a/lib/api/v3/projects.rb b/lib/api/v3/projects.rb deleted file mode 100644 index eb3dd113524..00000000000 --- a/lib/api/v3/projects.rb +++ /dev/null @@ -1,475 +0,0 @@ -module API - module V3 - class Projects < Grape::API - include PaginationParams - - before { authenticate_non_get! } - - after_validation do - set_only_allow_merge_if_pipeline_succeeds! - end - - helpers do - params :optional_params do - optional :description, type: String, desc: 'The description of the project' - optional :issues_enabled, type: Boolean, desc: 'Flag indication if the issue tracker is enabled' - optional :merge_requests_enabled, type: Boolean, desc: 'Flag indication if merge requests are enabled' - optional :wiki_enabled, type: Boolean, desc: 'Flag indication if the wiki is enabled' - optional :builds_enabled, type: Boolean, desc: 'Flag indication if builds are enabled' - optional :snippets_enabled, type: Boolean, desc: 'Flag indication if snippets are enabled' - optional :shared_runners_enabled, type: Boolean, desc: 'Flag indication if shared runners are enabled for that project' - optional :resolve_outdated_diff_discussions, type: Boolean, desc: 'Automatically resolve merge request diffs discussions on lines changed with a push' - optional :container_registry_enabled, type: Boolean, desc: 'Flag indication if the container registry is enabled for that project' - optional :lfs_enabled, type: Boolean, desc: 'Flag indication if Git LFS is enabled for that project' - optional :public, type: Boolean, desc: 'Create a public project. The same as visibility_level = 20.' - optional :visibility_level, type: Integer, values: [ - Gitlab::VisibilityLevel::PRIVATE, - Gitlab::VisibilityLevel::INTERNAL, - Gitlab::VisibilityLevel::PUBLIC - ], desc: 'Create a public project. The same as visibility_level = 20.' - optional :public_builds, type: Boolean, desc: 'Perform public builds' - optional :request_access_enabled, type: Boolean, desc: 'Allow users to request member access' - optional :only_allow_merge_if_build_succeeds, type: Boolean, desc: 'Only allow to merge if builds succeed' - optional :only_allow_merge_if_pipeline_succeeds, type: Boolean, desc: 'Only allow to merge if builds succeed' - optional :only_allow_merge_if_all_discussions_are_resolved, type: Boolean, desc: 'Only allow to merge if all discussions are resolved' - end - - def map_public_to_visibility_level(attrs) - publik = attrs.delete(:public) - if !publik.nil? && !attrs[:visibility_level].present? - # Since setting the public attribute to private could mean either - # private or internal, use the more conservative option, private. - attrs[:visibility_level] = (publik == true) ? Gitlab::VisibilityLevel::PUBLIC : Gitlab::VisibilityLevel::PRIVATE - end - - attrs - end - - def set_only_allow_merge_if_pipeline_succeeds! - if params.key?(:only_allow_merge_if_build_succeeds) - params[:only_allow_merge_if_pipeline_succeeds] = params.delete(:only_allow_merge_if_build_succeeds) - end - end - end - - resource :projects do - helpers do - params :collection_params do - use :sort_params - use :filter_params - use :pagination - - optional :simple, type: Boolean, default: false, - desc: 'Return only the ID, URL, name, and path of each project' - end - - params :sort_params do - optional :order_by, type: String, values: %w[id name path created_at updated_at last_activity_at], - default: 'created_at', desc: 'Return projects ordered by field' - optional :sort, type: String, values: %w[asc desc], default: 'desc', - desc: 'Return projects sorted in ascending and descending order' - end - - params :filter_params do - optional :archived, type: Boolean, default: nil, desc: 'Limit by archived status' - optional :visibility, type: String, values: %w[public internal private], - desc: 'Limit by visibility' - optional :search, type: String, desc: 'Return list of authorized projects matching the search criteria' - end - - params :statistics_params do - optional :statistics, type: Boolean, default: false, desc: 'Include project statistics' - end - - params :create_params do - optional :namespace_id, type: Integer, desc: 'Namespace ID for the new project. Default to the user namespace.' - optional :import_url, type: String, desc: 'URL from which the project is imported' - end - - def present_projects(projects, options = {}) - options = options.reverse_merge( - with: ::API::V3::Entities::Project, - current_user: current_user, - simple: params[:simple] - ) - - projects = filter_projects(projects) - projects = projects.with_statistics if options[:statistics] - options[:with] = ::API::Entities::BasicProjectDetails if options[:simple] - - present paginate(projects), options - end - end - - desc 'Get a list of visible projects for authenticated user' do - success ::API::Entities::BasicProjectDetails - end - params do - use :collection_params - end - get '/visible' do - entity = current_user ? ::API::V3::Entities::ProjectWithAccess : ::API::Entities::BasicProjectDetails - present_projects ProjectsFinder.new(current_user: current_user).execute, with: entity - end - - desc 'Get a projects list for authenticated user' do - success ::API::Entities::BasicProjectDetails - end - params do - use :collection_params - end - get do - authenticate! - - present_projects current_user.authorized_projects.order_id_desc, - with: ::API::V3::Entities::ProjectWithAccess - end - - desc 'Get an owned projects list for authenticated user' do - success ::API::Entities::BasicProjectDetails - end - params do - use :collection_params - use :statistics_params - end - get '/owned' do - authenticate! - - present_projects current_user.owned_projects, - with: ::API::V3::Entities::ProjectWithAccess, - statistics: params[:statistics] - end - - desc 'Gets starred project for the authenticated user' do - success ::API::Entities::BasicProjectDetails - end - params do - use :collection_params - end - get '/starred' do - authenticate! - - present_projects ProjectsFinder.new(current_user: current_user, params: { starred: true }).execute - end - - desc 'Get all projects for admin user' do - success ::API::Entities::BasicProjectDetails - end - params do - use :collection_params - use :statistics_params - end - get '/all' do - authenticated_as_admin! - - present_projects Project.all, with: ::API::V3::Entities::ProjectWithAccess, statistics: params[:statistics] - end - - desc 'Search for projects the current user has access to' do - success ::API::V3::Entities::Project - end - params do - requires :query, type: String, desc: 'The project name to be searched' - use :sort_params - use :pagination - end - get "/search/:query", requirements: { query: %r{[^/]+} } do - search_service = ::Search::GlobalService.new(current_user, search: params[:query]).execute - projects = search_service.objects('projects', params[:page], false) - projects = projects.reorder(params[:order_by] => params[:sort]) - - present paginate(projects), with: ::API::V3::Entities::Project - end - - desc 'Create new project' do - success ::API::V3::Entities::Project - end - params do - optional :name, type: String, desc: 'The name of the project' - optional :path, type: String, desc: 'The path of the repository' - at_least_one_of :name, :path - use :optional_params - use :create_params - end - post do - attrs = map_public_to_visibility_level(declared_params(include_missing: false)) - project = ::Projects::CreateService.new(current_user, attrs).execute - - if project.saved? - present project, with: ::API::V3::Entities::Project, - user_can_admin_project: can?(current_user, :admin_project, project) - else - if project.errors[:limit_reached].present? - error!(project.errors[:limit_reached], 403) - end - - render_validation_error!(project) - end - end - - desc 'Create new project for a specified user. Only available to admin users.' do - success ::API::V3::Entities::Project - end - params do - requires :name, type: String, desc: 'The name of the project' - requires :user_id, type: Integer, desc: 'The ID of a user' - optional :default_branch, type: String, desc: 'The default branch of the project' - use :optional_params - use :create_params - end - post "user/:user_id" do - authenticated_as_admin! - user = User.find_by(id: params.delete(:user_id)) - not_found!('User') unless user - - attrs = map_public_to_visibility_level(declared_params(include_missing: false)) - project = ::Projects::CreateService.new(user, attrs).execute - - if project.saved? - present project, with: ::API::V3::Entities::Project, - user_can_admin_project: can?(current_user, :admin_project, project) - else - render_validation_error!(project) - end - end - end - - params do - requires :id, type: String, desc: 'The ID of a project' - end - resource :projects, requirements: { id: %r{[^/]+} } do - desc 'Get a single project' do - success ::API::V3::Entities::ProjectWithAccess - end - get ":id" do - entity = current_user ? ::API::V3::Entities::ProjectWithAccess : ::API::Entities::BasicProjectDetails - present user_project, with: entity, current_user: current_user, - user_can_admin_project: can?(current_user, :admin_project, user_project) - end - - desc 'Get events for a single project' do - success ::API::V3::Entities::Event - end - params do - use :pagination - end - get ":id/events" do - present paginate(user_project.events.recent), with: ::API::V3::Entities::Event - end - - desc 'Fork new project for the current user or provided namespace.' do - success ::API::V3::Entities::Project - end - params do - optional :namespace, type: String, desc: 'The ID or name of the namespace that the project will be forked into' - end - post 'fork/:id' do - fork_params = declared_params(include_missing: false) - namespace_id = fork_params[:namespace] - - if namespace_id.present? - fork_params[:namespace] = find_namespace(namespace_id) - - unless fork_params[:namespace] && can?(current_user, :create_projects, fork_params[:namespace]) - not_found!('Target Namespace') - end - end - - forked_project = ::Projects::ForkService.new(user_project, current_user, fork_params).execute - - if forked_project.errors.any? - conflict!(forked_project.errors.messages) - else - present forked_project, with: ::API::V3::Entities::Project, - user_can_admin_project: can?(current_user, :admin_project, forked_project) - end - end - - desc 'Update an existing project' do - success ::API::V3::Entities::Project - end - params do - optional :name, type: String, desc: 'The name of the project' - optional :default_branch, type: String, desc: 'The default branch of the project' - optional :path, type: String, desc: 'The path of the repository' - use :optional_params - at_least_one_of :name, :description, :issues_enabled, :merge_requests_enabled, - :wiki_enabled, :builds_enabled, :snippets_enabled, - :shared_runners_enabled, :resolve_outdated_diff_discussions, - :container_registry_enabled, :lfs_enabled, :public, :visibility_level, - :public_builds, :request_access_enabled, :only_allow_merge_if_build_succeeds, - :only_allow_merge_if_all_discussions_are_resolved, :path, - :default_branch - end - put ':id' do - authorize_admin_project - attrs = map_public_to_visibility_level(declared_params(include_missing: false)) - authorize! :rename_project, user_project if attrs[:name].present? - authorize! :change_visibility_level, user_project if attrs[:visibility_level].present? - - result = ::Projects::UpdateService.new(user_project, current_user, attrs).execute - - if result[:status] == :success - present user_project, with: ::API::V3::Entities::Project, - user_can_admin_project: can?(current_user, :admin_project, user_project) - else - render_validation_error!(user_project) - end - end - - desc 'Archive a project' do - success ::API::V3::Entities::Project - end - post ':id/archive' do - authorize!(:archive_project, user_project) - - user_project.archive! - - present user_project, with: ::API::V3::Entities::Project - end - - desc 'Unarchive a project' do - success ::API::V3::Entities::Project - end - post ':id/unarchive' do - authorize!(:archive_project, user_project) - - user_project.unarchive! - - present user_project, with: ::API::V3::Entities::Project - end - - desc 'Star a project' do - success ::API::V3::Entities::Project - end - post ':id/star' do - if current_user.starred?(user_project) - not_modified! - else - current_user.toggle_star(user_project) - user_project.reload - - present user_project, with: ::API::V3::Entities::Project - end - end - - desc 'Unstar a project' do - success ::API::V3::Entities::Project - end - delete ':id/star' do - if current_user.starred?(user_project) - current_user.toggle_star(user_project) - user_project.reload - - present user_project, with: ::API::V3::Entities::Project - else - not_modified! - end - end - - desc 'Remove a project' - delete ":id" do - authorize! :remove_project, user_project - - status(200) - ::Projects::DestroyService.new(user_project, current_user, {}).async_execute - end - - desc 'Mark this project as forked from another' - params do - requires :forked_from_id, type: String, desc: 'The ID of the project it was forked from' - end - post ":id/fork/:forked_from_id" do - authenticated_as_admin! - - forked_from_project = find_project!(params[:forked_from_id]) - not_found!("Source Project") unless forked_from_project - - if user_project.forked_from_project.nil? - user_project.create_forked_project_link(forked_to_project_id: user_project.id, forked_from_project_id: forked_from_project.id) - - ::Projects::ForksCountService.new(forked_from_project).refresh_cache - else - render_api_error!("Project already forked", 409) - end - end - - desc 'Remove a forked_from relationship' - delete ":id/fork" do - authorize! :remove_fork_project, user_project - - if user_project.forked? - status(200) - user_project.forked_project_link.destroy - else - not_modified! - end - end - - desc 'Share the project with a group' do - success ::API::Entities::ProjectGroupLink - end - params do - requires :group_id, type: Integer, desc: 'The ID of a group' - requires :group_access, type: Integer, values: Gitlab::Access.values, desc: 'The group access level' - optional :expires_at, type: Date, desc: 'Share expiration date' - end - post ":id/share" do - authorize! :admin_project, user_project - group = Group.find_by_id(params[:group_id]) - - unless group && can?(current_user, :read_group, group) - not_found!('Group') - end - - unless user_project.allowed_to_share_with_group? - break render_api_error!("The project sharing with group is disabled", 400) - end - - link = user_project.project_group_links.new(declared_params(include_missing: false)) - - if link.save - present link, with: ::API::Entities::ProjectGroupLink - else - render_api_error!(link.errors.full_messages.first, 409) - end - end - - params do - requires :group_id, type: Integer, desc: 'The ID of the group' - end - delete ":id/share/:group_id" do - authorize! :admin_project, user_project - - link = user_project.project_group_links.find_by(group_id: params[:group_id]) - not_found!('Group Link') unless link - - link.destroy - no_content! - end - - desc 'Upload a file' - params do - requires :file, type: File, desc: 'The file to be uploaded' - end - post ":id/uploads" do - UploadService.new(user_project, params[:file]).execute - end - - desc 'Get the users list of a project' do - success ::API::Entities::UserBasic - end - params do - optional :search, type: String, desc: 'Return list of users matching the search criteria' - use :pagination - end - get ':id/users' do - users = user_project.team.users - users = users.search(params[:search]) if params[:search].present? - - present paginate(users), with: ::API::Entities::UserBasic - end - end - end - end -end diff --git a/lib/api/v3/repositories.rb b/lib/api/v3/repositories.rb deleted file mode 100644 index f701d64e886..00000000000 --- a/lib/api/v3/repositories.rb +++ /dev/null @@ -1,110 +0,0 @@ -require 'mime/types' - -module API - module V3 - class Repositories < Grape::API - before { authorize! :download_code, user_project } - - params do - requires :id, type: String, desc: 'The ID of a project' - end - resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do - helpers do - def handle_project_member_errors(errors) - if errors[:project_access].any? - error!(errors[:project_access], 422) - end - - not_found! - end - end - - desc 'Get a project repository tree' do - success ::API::Entities::TreeObject - end - params do - optional :ref_name, type: String, desc: 'The name of a repository branch or tag, if not given the default branch is used' - optional :path, type: String, desc: 'The path of the tree' - optional :recursive, type: Boolean, default: false, desc: 'Used to get a recursive tree' - end - get ':id/repository/tree' do - ref = params[:ref_name] || user_project.try(:default_branch) || 'master' - path = params[:path] || nil - - commit = user_project.commit(ref) - not_found!('Tree') unless commit - - tree = user_project.repository.tree(commit.id, path, recursive: params[:recursive]) - - present tree.sorted_entries, with: ::API::Entities::TreeObject - end - - desc 'Get a raw file contents' - params do - requires :sha, type: String, desc: 'The commit, branch name, or tag name' - requires :filepath, type: String, desc: 'The path to the file to display' - end - get [":id/repository/blobs/:sha", ":id/repository/commits/:sha/blob"], requirements: API::COMMIT_ENDPOINT_REQUIREMENTS do - repo = user_project.repository - commit = repo.commit(params[:sha]) - not_found! "Commit" unless commit - blob = Gitlab::Git::Blob.find(repo, commit.id, params[:filepath]) - not_found! "File" unless blob - send_git_blob repo, blob - end - - desc 'Get a raw blob contents by blob sha' - params do - requires :sha, type: String, desc: 'The commit, branch name, or tag name' - end - get ':id/repository/raw_blobs/:sha', requirements: API::COMMIT_ENDPOINT_REQUIREMENTS do - repo = user_project.repository - begin - blob = Gitlab::Git::Blob.raw(repo, params[:sha]) - rescue - not_found! 'Blob' - end - not_found! 'Blob' unless blob - send_git_blob repo, blob - end - - desc 'Get an archive of the repository' - params do - optional :sha, type: String, desc: 'The commit sha of the archive to be downloaded' - optional :format, type: String, desc: 'The archive format' - end - get ':id/repository/archive', requirements: { format: Gitlab::PathRegex.archive_formats_regex } do - begin - send_git_archive user_project.repository, ref: params[:sha], format: params[:format], append_sha: true - rescue - not_found!('File') - end - end - - desc 'Compare two branches, tags, or commits' do - success ::API::Entities::Compare - end - params do - requires :from, type: String, desc: 'The commit, branch name, or tag name to start comparison' - requires :to, type: String, desc: 'The commit, branch name, or tag name to stop comparison' - end - get ':id/repository/compare' do - compare = Gitlab::Git::Compare.new(user_project.repository.raw_repository, params[:from], params[:to]) - present compare, with: ::API::Entities::Compare - end - - desc 'Get repository contributors' do - success ::API::Entities::Contributor - end - get ':id/repository/contributors' do - begin - present user_project.repository.contributors, - with: ::API::Entities::Contributor - rescue - not_found! - end - end - end - end - end -end diff --git a/lib/api/v3/runners.rb b/lib/api/v3/runners.rb deleted file mode 100644 index 8a5c46805bd..00000000000 --- a/lib/api/v3/runners.rb +++ /dev/null @@ -1,66 +0,0 @@ -module API - module V3 - class Runners < Grape::API - include PaginationParams - - before { authenticate! } - - resource :runners do - desc 'Remove a runner' do - success ::API::Entities::Runner - end - params do - requires :id, type: Integer, desc: 'The ID of the runner' - end - delete ':id' do - runner = Ci::Runner.find(params[:id]) - not_found!('Runner') unless runner - - authenticate_delete_runner!(runner) - - status(200) - runner.destroy - end - end - - params do - requires :id, type: String, desc: 'The ID of a project' - end - resource :projects, requirements: { id: %r{[^/]+} } do - before { authorize_admin_project } - - desc "Disable project's runner" do - success ::API::Entities::Runner - end - params do - requires :runner_id, type: Integer, desc: 'The ID of the runner' - end - delete ':id/runners/:runner_id' do - runner_project = user_project.runner_projects.find_by(runner_id: params[:runner_id]) - not_found!('Runner') unless runner_project - - runner = runner_project.runner - forbidden!("Only one project associated with the runner. Please remove the runner instead") if runner.projects.count == 1 - - runner_project.destroy - - present runner, with: ::API::Entities::Runner - end - end - - helpers do - def authenticate_delete_runner!(runner) - return if current_user.admin? - - forbidden!("Runner is shared") if runner.is_shared? - forbidden!("Runner associated with more than one project") if runner.projects.count > 1 - forbidden!("No access granted") unless user_can_access_runner?(runner) - end - - def user_can_access_runner?(runner) - current_user.ci_owned_runners.exists?(runner.id) - end - end - end - end -end diff --git a/lib/api/v3/services.rb b/lib/api/v3/services.rb deleted file mode 100644 index 20ca1021c71..00000000000 --- a/lib/api/v3/services.rb +++ /dev/null @@ -1,670 +0,0 @@ -module API - module V3 - class Services < Grape::API - services = { - 'asana' => [ - { - required: true, - name: :api_key, - type: String, - desc: 'User API token' - }, - { - required: false, - name: :restrict_to_branch, - type: String, - desc: 'Comma-separated list of branches which will be automatically inspected. Leave blank to include all branches' - } - ], - 'assembla' => [ - { - required: true, - name: :token, - type: String, - desc: 'The authentication token' - }, - { - required: false, - name: :subdomain, - type: String, - desc: 'Subdomain setting' - } - ], - 'bamboo' => [ - { - required: true, - name: :bamboo_url, - type: String, - desc: 'Bamboo root URL like https://bamboo.example.com' - }, - { - required: true, - name: :build_key, - type: String, - desc: 'Bamboo build plan key like' - }, - { - required: true, - name: :username, - type: String, - desc: 'A user with API access, if applicable' - }, - { - required: true, - name: :password, - type: String, - desc: 'Passord of the user' - } - ], - 'bugzilla' => [ - { - required: true, - name: :new_issue_url, - type: String, - desc: 'New issue URL' - }, - { - required: true, - name: :issues_url, - type: String, - desc: 'Issues URL' - }, - { - required: true, - name: :project_url, - type: String, - desc: 'Project URL' - }, - { - required: false, - name: :description, - type: String, - desc: 'Description' - }, - { - required: false, - name: :title, - type: String, - desc: 'Title' - } - ], - 'buildkite' => [ - { - required: true, - name: :token, - type: String, - desc: 'Buildkite project GitLab token' - }, - { - required: true, - name: :project_url, - type: String, - desc: 'The buildkite project URL' - }, - { - required: false, - name: :enable_ssl_verification, - type: Boolean, - desc: 'Enable SSL verification for communication' - } - ], - 'builds-email' => [ - { - required: true, - name: :recipients, - type: String, - desc: 'Comma-separated list of recipient email addresses' - }, - { - required: false, - name: :add_pusher, - type: Boolean, - desc: 'Add pusher to recipients list' - }, - { - required: false, - name: :notify_only_broken_builds, - type: Boolean, - desc: 'Notify only broken builds' - } - ], - 'campfire' => [ - { - required: true, - name: :token, - type: String, - desc: 'Campfire token' - }, - { - required: false, - name: :subdomain, - type: String, - desc: 'Campfire subdomain' - }, - { - required: false, - name: :room, - type: String, - desc: 'Campfire room' - } - ], - 'custom-issue-tracker' => [ - { - required: true, - name: :new_issue_url, - type: String, - desc: 'New issue URL' - }, - { - required: true, - name: :issues_url, - type: String, - desc: 'Issues URL' - }, - { - required: true, - name: :project_url, - type: String, - desc: 'Project URL' - }, - { - required: false, - name: :description, - type: String, - desc: 'Description' - }, - { - required: false, - name: :title, - type: String, - desc: 'Title' - } - ], - 'drone-ci' => [ - { - required: true, - name: :token, - type: String, - desc: 'Drone CI token' - }, - { - required: true, - name: :drone_url, - type: String, - desc: 'Drone CI URL' - }, - { - required: false, - name: :enable_ssl_verification, - type: Boolean, - desc: 'Enable SSL verification for communication' - } - ], - 'emails-on-push' => [ - { - required: true, - name: :recipients, - type: String, - desc: 'Comma-separated list of recipient email addresses' - }, - { - required: false, - name: :disable_diffs, - type: Boolean, - desc: 'Disable code diffs' - }, - { - required: false, - name: :send_from_committer_email, - type: Boolean, - desc: 'Send from committer' - } - ], - 'external-wiki' => [ - { - required: true, - name: :external_wiki_url, - type: String, - desc: 'The URL of the external Wiki' - } - ], - 'flowdock' => [ - { - required: true, - name: :token, - type: String, - desc: 'Flowdock token' - } - ], - 'gemnasium' => [ - { - required: true, - name: :api_key, - type: String, - desc: 'Your personal API key on gemnasium.com' - }, - { - required: true, - name: :token, - type: String, - desc: "The project's slug on gemnasium.com" - } - ], - 'hipchat' => [ - { - required: true, - name: :token, - type: String, - desc: 'The room token' - }, - { - required: false, - name: :room, - type: String, - desc: 'The room name or ID' - }, - { - required: false, - name: :color, - type: String, - desc: 'The room color' - }, - { - required: false, - name: :notify, - type: Boolean, - desc: 'Enable notifications' - }, - { - required: false, - name: :api_version, - type: String, - desc: 'Leave blank for default (v2)' - }, - { - required: false, - name: :server, - type: String, - desc: 'Leave blank for default. https://hipchat.example.com' - } - ], - 'irker' => [ - { - required: true, - name: :recipients, - type: String, - desc: 'Recipients/channels separated by whitespaces' - }, - { - required: false, - name: :default_irc_uri, - type: String, - desc: 'Default: irc://irc.network.net:6697' - }, - { - required: false, - name: :server_host, - type: String, - desc: 'Server host. Default localhost' - }, - { - required: false, - name: :server_port, - type: Integer, - desc: 'Server port. Default 6659' - }, - { - required: false, - name: :colorize_messages, - type: Boolean, - desc: 'Colorize messages' - } - ], - 'jira' => [ - { - required: true, - name: :url, - type: String, - desc: 'The URL to the JIRA project which is being linked to this GitLab project, e.g., https://jira.example.com' - }, - { - required: true, - name: :project_key, - type: String, - desc: 'The short identifier for your JIRA project, all uppercase, e.g., PROJ' - }, - { - required: false, - name: :username, - type: String, - desc: 'The username of the user created to be used with GitLab/JIRA' - }, - { - required: false, - name: :password, - type: String, - desc: 'The password of the user created to be used with GitLab/JIRA' - }, - { - required: false, - name: :jira_issue_transition_id, - type: Integer, - desc: 'The ID of a transition that moves issues to a closed state. You can find this number under the JIRA workflow administration (**Administration > Issues > Workflows**) by selecting **View** under **Operations** of the desired workflow of your project. The ID of each state can be found inside the parenthesis of each transition name under the **Transitions (id)** column ([see screenshot][trans]). By default, this ID is set to `2`' - } - ], - - 'kubernetes' => [ - { - required: true, - name: :namespace, - type: String, - desc: 'The Kubernetes namespace to use' - }, - { - required: true, - name: :api_url, - type: String, - desc: 'The URL to the Kubernetes cluster API, e.g., https://kubernetes.example.com' - }, - { - required: true, - name: :token, - type: String, - desc: 'The service token to authenticate against the Kubernetes cluster with' - }, - { - required: false, - name: :ca_pem, - type: String, - desc: 'A custom certificate authority bundle to verify the Kubernetes cluster with (PEM format)' - } - ], - 'mattermost-slash-commands' => [ - { - required: true, - name: :token, - type: String, - desc: 'The Mattermost token' - } - ], - 'slack-slash-commands' => [ - { - required: true, - name: :token, - type: String, - desc: 'The Slack token' - } - ], - 'packagist' => [ - { - required: true, - name: :username, - type: String, - desc: 'The username' - }, - { - required: true, - name: :token, - type: String, - desc: 'The Packagist API token' - }, - { - required: false, - name: :server, - type: String, - desc: 'The server' - } - ], - 'pipelines-email' => [ - { - required: true, - name: :recipients, - type: String, - desc: 'Comma-separated list of recipient email addresses' - }, - { - required: false, - name: :notify_only_broken_builds, - type: Boolean, - desc: 'Notify only broken builds' - } - ], - 'pivotaltracker' => [ - { - required: true, - name: :token, - type: String, - desc: 'The Pivotaltracker token' - }, - { - required: false, - name: :restrict_to_branch, - type: String, - desc: 'Comma-separated list of branches which will be automatically inspected. Leave blank to include all branches.' - } - ], - 'pushover' => [ - { - required: true, - name: :api_key, - type: String, - desc: 'The application key' - }, - { - required: true, - name: :user_key, - type: String, - desc: 'The user key' - }, - { - required: true, - name: :priority, - type: String, - desc: 'The priority' - }, - { - required: true, - name: :device, - type: String, - desc: 'Leave blank for all active devices' - }, - { - required: true, - name: :sound, - type: String, - desc: 'The sound of the notification' - } - ], - 'redmine' => [ - { - required: true, - name: :new_issue_url, - type: String, - desc: 'The new issue URL' - }, - { - required: true, - name: :project_url, - type: String, - desc: 'The project URL' - }, - { - required: true, - name: :issues_url, - type: String, - desc: 'The issues URL' - }, - { - required: false, - name: :description, - type: String, - desc: 'The description of the tracker' - } - ], - 'slack' => [ - { - required: true, - name: :webhook, - type: String, - desc: 'The Slack webhook. e.g. https://hooks.slack.com/services/...' - }, - { - required: false, - name: :new_issue_url, - type: String, - desc: 'The user name' - }, - { - required: false, - name: :channel, - type: String, - desc: 'The channel name' - } - ], - 'microsoft-teams' => [ - required: true, - name: :webhook, - type: String, - desc: 'The Microsoft Teams webhook. e.g. https://outlook.office.com/webhook/…' - ], - 'mattermost' => [ - { - required: true, - name: :webhook, - type: String, - desc: 'The Mattermost webhook. e.g. http://mattermost_host/hooks/...' - } - ], - 'teamcity' => [ - { - required: true, - name: :teamcity_url, - type: String, - desc: 'TeamCity root URL like https://teamcity.example.com' - }, - { - required: true, - name: :build_type, - type: String, - desc: 'Build configuration ID' - }, - { - required: true, - name: :username, - type: String, - desc: 'A user with permissions to trigger a manual build' - }, - { - required: true, - name: :password, - type: String, - desc: 'The password of the user' - } - ] - } - - trigger_services = { - 'mattermost-slash-commands' => [ - { - name: :token, - type: String, - desc: 'The Mattermost token' - } - ], - 'slack-slash-commands' => [ - { - name: :token, - type: String, - desc: 'The Slack token' - } - ] - }.freeze - - params do - requires :id, type: String, desc: 'The ID of a project' - end - resource :projects, requirements: { id: %r{[^/]+} } do - before { authenticate! } - before { authorize_admin_project } - - helpers do - def service_attributes(service) - service.fields.inject([]) do |arr, hash| - arr << hash[:name].to_sym - end - end - end - - desc "Delete a service for project" - params do - requires :service_slug, type: String, values: services.keys, desc: 'The name of the service' - end - delete ":id/services/:service_slug" do - service = user_project.find_or_initialize_service(params[:service_slug].underscore) - - attrs = service_attributes(service).inject({}) do |hash, key| - hash.merge!(key => nil) - end - - if service.update_attributes(attrs.merge(active: false)) - status(200) - true - else - render_api_error!('400 Bad Request', 400) - end - end - - desc 'Get the service settings for project' do - success Entities::ProjectService - end - params do - requires :service_slug, type: String, values: services.keys, desc: 'The name of the service' - end - get ":id/services/:service_slug" do - service = user_project.find_or_initialize_service(params[:service_slug].underscore) - present service, with: Entities::ProjectService - end - end - - trigger_services.each do |service_slug, settings| - helpers do - def slash_command_service(project, service_slug, params) - project.services.active.where(template: false).find do |service| - service.try(:token) == params[:token] && service.to_param == service_slug.underscore - end - end - end - - params do - requires :id, type: String, desc: 'The ID of a project' - end - resource :projects, requirements: { id: %r{[^/]+} } do - desc "Trigger a slash command for #{service_slug}" do - detail 'Added in GitLab 8.13' - end - params do - settings.each do |setting| - requires setting[:name], type: setting[:type], desc: setting[:desc] - end - end - post ":id/services/#{service_slug.underscore}/trigger" do - project = find_project(params[:id]) - - # This is not accurate, but done to prevent leakage of the project names - not_found!('Service') unless project - - service = slash_command_service(project, service_slug, params) - result = service.try(:trigger, params) - - if result - status result[:status] || 200 - present result - else - not_found!('Service') - end - end - end - end - end - end -end diff --git a/lib/api/v3/settings.rb b/lib/api/v3/settings.rb deleted file mode 100644 index fc56495c8b1..00000000000 --- a/lib/api/v3/settings.rb +++ /dev/null @@ -1,147 +0,0 @@ -module API - module V3 - class Settings < Grape::API - before { authenticated_as_admin! } - - helpers do - def current_settings - @current_setting ||= - (ApplicationSetting.current_without_cache || ApplicationSetting.create_from_defaults) - end - end - - desc 'Get the current application settings' do - success Entities::ApplicationSetting - end - get "application/settings" do - present current_settings, with: Entities::ApplicationSetting - end - - desc 'Modify application settings' do - success Entities::ApplicationSetting - end - params do - optional :default_branch_protection, type: Integer, values: [0, 1, 2], desc: 'Determine if developers can push to master' - optional :default_project_visibility, type: Integer, values: Gitlab::VisibilityLevel.values, desc: 'The default project visibility' - optional :default_snippet_visibility, type: Integer, values: Gitlab::VisibilityLevel.values, desc: 'The default snippet visibility' - optional :default_group_visibility, type: Integer, values: Gitlab::VisibilityLevel.values, desc: 'The default group visibility' - optional :restricted_visibility_levels, type: Array[String], desc: 'Selected levels cannot be used by non-admin users for projects or snippets. If the public level is restricted, user profiles are only visible to logged in users.' - optional :import_sources, type: Array[String], values: %w[github bitbucket gitlab google_code fogbugz git gitlab_project], - desc: 'Enabled sources for code import during project creation. OmniAuth must be configured for GitHub, Bitbucket, and GitLab.com' - optional :disabled_oauth_sign_in_sources, type: Array[String], desc: 'Disable certain OAuth sign-in sources' - optional :enabled_git_access_protocol, type: String, values: %w[ssh http nil], desc: 'Allow only the selected protocols to be used for Git access.' - optional :gravatar_enabled, type: Boolean, desc: 'Flag indicating if the Gravatar service is enabled' - optional :default_projects_limit, type: Integer, desc: 'The maximum number of personal projects' - optional :max_attachment_size, type: Integer, desc: 'Maximum attachment size in MB' - optional :session_expire_delay, type: Integer, desc: 'Session duration in minutes. GitLab restart is required to apply changes.' - optional :user_oauth_applications, type: Boolean, desc: 'Allow users to register any application to use GitLab as an OAuth provider' - optional :user_default_external, type: Boolean, desc: 'Newly registered users will by default be external' - optional :signup_enabled, type: Boolean, desc: 'Flag indicating if sign up is enabled' - optional :send_user_confirmation_email, type: Boolean, desc: 'Send confirmation email on sign-up' - optional :domain_whitelist, type: String, desc: 'ONLY users with e-mail addresses that match these domain(s) will be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com' - optional :domain_blacklist_enabled, type: Boolean, desc: 'Enable domain blacklist for sign ups' - given domain_blacklist_enabled: ->(val) { val } do - requires :domain_blacklist, type: String, desc: 'Users with e-mail addresses that match these domain(s) will NOT be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com' - end - optional :after_sign_up_text, type: String, desc: 'Text shown after sign up' - optional :password_authentication_enabled, type: Boolean, desc: 'Flag indicating if password authentication is enabled for the web interface' - optional :signin_enabled, type: Boolean, desc: 'Flag indicating if password authentication is enabled for the web interface' - mutually_exclusive :password_authentication_enabled, :signin_enabled - optional :require_two_factor_authentication, type: Boolean, desc: 'Require all users to setup Two-factor authentication' - given require_two_factor_authentication: ->(val) { val } do - requires :two_factor_grace_period, type: Integer, desc: 'Amount of time (in hours) that users are allowed to skip forced configuration of two-factor authentication' - end - optional :home_page_url, type: String, desc: 'We will redirect non-logged in users to this page' - optional :after_sign_out_path, type: String, desc: 'We will redirect users to this page after they sign out' - optional :sign_in_text, type: String, desc: 'The sign in text of the GitLab application' - optional :help_page_text, type: String, desc: 'Custom text displayed on the help page' - optional :shared_runners_enabled, type: Boolean, desc: 'Enable shared runners for new projects' - given shared_runners_enabled: ->(val) { val } do - requires :shared_runners_text, type: String, desc: 'Shared runners text ' - end - optional :max_artifacts_size, type: Integer, desc: "Set the maximum file size each build's artifacts can have" - optional :max_pages_size, type: Integer, desc: 'Maximum size of pages in MB' - optional :container_registry_token_expire_delay, type: Integer, desc: 'Authorization token duration (minutes)' - optional :metrics_enabled, type: Boolean, desc: 'Enable the InfluxDB metrics' - given metrics_enabled: ->(val) { val } do - requires :metrics_host, type: String, desc: 'The InfluxDB host' - requires :metrics_port, type: Integer, desc: 'The UDP port to use for connecting to InfluxDB' - requires :metrics_pool_size, type: Integer, desc: 'The amount of InfluxDB connections to open' - requires :metrics_timeout, type: Integer, desc: 'The amount of seconds after which an InfluxDB connection will time out' - requires :metrics_method_call_threshold, type: Integer, desc: 'A method call is only tracked when it takes longer to complete than the given amount of milliseconds.' - requires :metrics_sample_interval, type: Integer, desc: 'The sampling interval in seconds' - requires :metrics_packet_size, type: Integer, desc: 'The amount of points to store in a single UDP packet' - end - optional :sidekiq_throttling_enabled, type: Boolean, desc: 'Enable Sidekiq Job Throttling' - given sidekiq_throttling_enabled: ->(val) { val } do - requires :sidekiq_throttling_queus, type: Array[String], desc: 'Choose which queues you wish to throttle' - requires :sidekiq_throttling_factor, type: Float, desc: 'The factor by which the queues should be throttled. A value between 0.0 and 1.0, exclusive.' - end - optional :recaptcha_enabled, type: Boolean, desc: 'Helps prevent bots from creating accounts' - given recaptcha_enabled: ->(val) { val } do - requires :recaptcha_site_key, type: String, desc: 'Generate site key at http://www.google.com/recaptcha' - requires :recaptcha_private_key, type: String, desc: 'Generate private key at http://www.google.com/recaptcha' - end - optional :akismet_enabled, type: Boolean, desc: 'Helps prevent bots from creating issues' - given akismet_enabled: ->(val) { val } do - requires :akismet_api_key, type: String, desc: 'Generate API key at http://www.akismet.com' - end - optional :admin_notification_email, type: String, desc: 'Abuse reports will be sent to this address if it is set. Abuse reports are always available in the admin area.' - optional :sentry_enabled, type: Boolean, desc: 'Sentry is an error reporting and logging tool which is currently not shipped with GitLab, get it here: https://getsentry.com' - given sentry_enabled: ->(val) { val } do - requires :sentry_dsn, type: String, desc: 'Sentry Data Source Name' - end - optional :repository_storage, type: String, desc: 'Storage paths for new projects' - optional :repository_checks_enabled, type: Boolean, desc: "GitLab will periodically run 'git fsck' in all project and wiki repositories to look for silent disk corruption issues." - optional :koding_enabled, type: Boolean, desc: 'Enable Koding' - given koding_enabled: ->(val) { val } do - requires :koding_url, type: String, desc: 'The Koding team URL' - end - optional :plantuml_enabled, type: Boolean, desc: 'Enable PlantUML' - given plantuml_enabled: ->(val) { val } do - requires :plantuml_url, type: String, desc: 'The PlantUML server URL' - end - optional :version_check_enabled, type: Boolean, desc: 'Let GitLab inform you when an update is available.' - optional :email_author_in_body, type: Boolean, desc: 'Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead.' - optional :html_emails_enabled, type: Boolean, desc: 'By default GitLab sends emails in HTML and plain text formats so mail clients can choose what format to use. Disable this option if you only want to send emails in plain text format.' - optional :housekeeping_enabled, type: Boolean, desc: 'Enable automatic repository housekeeping (git repack, git gc)' - given housekeeping_enabled: ->(val) { val } do - requires :housekeeping_bitmaps_enabled, type: Boolean, desc: "Creating pack file bitmaps makes housekeeping take a little longer but bitmaps should accelerate 'git clone' performance." - requires :housekeeping_incremental_repack_period, type: Integer, desc: "Number of Git pushes after which an incremental 'git repack' is run." - requires :housekeeping_full_repack_period, type: Integer, desc: "Number of Git pushes after which a full 'git repack' is run." - requires :housekeeping_gc_period, type: Integer, desc: "Number of Git pushes after which 'git gc' is run." - end - optional :terminal_max_session_time, type: Integer, desc: 'Maximum time for web terminal websocket connection (in seconds). Set to 0 for unlimited time.' - at_least_one_of :default_branch_protection, :default_project_visibility, :default_snippet_visibility, - :default_group_visibility, :restricted_visibility_levels, :import_sources, - :enabled_git_access_protocol, :gravatar_enabled, :default_projects_limit, - :max_attachment_size, :session_expire_delay, :disabled_oauth_sign_in_sources, - :user_oauth_applications, :user_default_external, :signup_enabled, - :send_user_confirmation_email, :domain_whitelist, :domain_blacklist_enabled, - :after_sign_up_text, :password_authentication_enabled, :signin_enabled, :require_two_factor_authentication, - :home_page_url, :after_sign_out_path, :sign_in_text, :help_page_text, - :shared_runners_enabled, :max_artifacts_size, :max_pages_size, :container_registry_token_expire_delay, - :metrics_enabled, :sidekiq_throttling_enabled, :recaptcha_enabled, - :akismet_enabled, :admin_notification_email, :sentry_enabled, - :repository_storage, :repository_checks_enabled, :koding_enabled, :plantuml_enabled, - :version_check_enabled, :email_author_in_body, :html_emails_enabled, - :housekeeping_enabled, :terminal_max_session_time - end - put "application/settings" do - attrs = declared_params(include_missing: false) - - if attrs.has_key?(:signin_enabled) - attrs[:password_authentication_enabled_for_web] = attrs.delete(:signin_enabled) - elsif attrs.has_key?(:password_authentication_enabled) - attrs[:password_authentication_enabled_for_web] = attrs.delete(:password_authentication_enabled) - end - - if current_settings.update_attributes(attrs) - present current_settings, with: Entities::ApplicationSetting - else - render_validation_error!(current_settings) - end - end - end - end -end diff --git a/lib/api/v3/snippets.rb b/lib/api/v3/snippets.rb deleted file mode 100644 index 1df8a20e74a..00000000000 --- a/lib/api/v3/snippets.rb +++ /dev/null @@ -1,141 +0,0 @@ -module API - module V3 - class Snippets < Grape::API - include PaginationParams - - before { authenticate! } - - resource :snippets do - helpers do - def snippets_for_current_user - SnippetsFinder.new(current_user, author: current_user).execute - end - - def public_snippets - SnippetsFinder.new(current_user, visibility: Snippet::PUBLIC).execute - end - end - - desc 'Get a snippets list for authenticated user' do - detail 'This feature was introduced in GitLab 8.15.' - success ::API::Entities::PersonalSnippet - end - params do - use :pagination - end - get do - present paginate(snippets_for_current_user), with: ::API::Entities::PersonalSnippet - end - - desc 'List all public snippets current_user has access to' do - detail 'This feature was introduced in GitLab 8.15.' - success ::API::Entities::PersonalSnippet - end - params do - use :pagination - end - get 'public' do - present paginate(public_snippets), with: ::API::Entities::PersonalSnippet - end - - desc 'Get a single snippet' do - detail 'This feature was introduced in GitLab 8.15.' - success ::API::Entities::PersonalSnippet - end - params do - requires :id, type: Integer, desc: 'The ID of a snippet' - end - get ':id' do - snippet = snippets_for_current_user.find(params[:id]) - present snippet, with: ::API::Entities::PersonalSnippet - end - - desc 'Create new snippet' do - detail 'This feature was introduced in GitLab 8.15.' - success ::API::Entities::PersonalSnippet - end - params do - requires :title, type: String, desc: 'The title of a snippet' - requires :file_name, type: String, desc: 'The name of a snippet file' - requires :content, type: String, desc: 'The content of a snippet' - optional :visibility_level, type: Integer, - values: Gitlab::VisibilityLevel.values, - default: Gitlab::VisibilityLevel::INTERNAL, - desc: 'The visibility level of the snippet' - end - post do - attrs = declared_params(include_missing: false).merge(request: request, api: true) - snippet = CreateSnippetService.new(nil, current_user, attrs).execute - - if snippet.persisted? - present snippet, with: ::API::Entities::PersonalSnippet - else - render_validation_error!(snippet) - end - end - - desc 'Update an existing snippet' do - detail 'This feature was introduced in GitLab 8.15.' - success ::API::Entities::PersonalSnippet - end - params do - requires :id, type: Integer, desc: 'The ID of a snippet' - optional :title, type: String, desc: 'The title of a snippet' - optional :file_name, type: String, desc: 'The name of a snippet file' - optional :content, type: String, desc: 'The content of a snippet' - optional :visibility_level, type: Integer, - values: Gitlab::VisibilityLevel.values, - desc: 'The visibility level of the snippet' - at_least_one_of :title, :file_name, :content, :visibility_level - end - put ':id' do - snippet = snippets_for_current_user.find_by(id: params.delete(:id)) - break not_found!('Snippet') unless snippet - - authorize! :update_personal_snippet, snippet - - attrs = declared_params(include_missing: false) - - UpdateSnippetService.new(nil, current_user, snippet, attrs).execute - - if snippet.persisted? - present snippet, with: ::API::Entities::PersonalSnippet - else - render_validation_error!(snippet) - end - end - - desc 'Remove snippet' do - detail 'This feature was introduced in GitLab 8.15.' - success ::API::Entities::PersonalSnippet - end - params do - requires :id, type: Integer, desc: 'The ID of a snippet' - end - delete ':id' do - snippet = snippets_for_current_user.find_by(id: params.delete(:id)) - break not_found!('Snippet') unless snippet - - authorize! :destroy_personal_snippet, snippet - snippet.destroy - no_content! - end - - desc 'Get a raw snippet' do - detail 'This feature was introduced in GitLab 8.15.' - end - params do - requires :id, type: Integer, desc: 'The ID of a snippet' - end - get ":id/raw" do - snippet = snippets_for_current_user.find_by(id: params.delete(:id)) - break not_found!('Snippet') unless snippet - - env['api.format'] = :txt - content_type 'text/plain' - present snippet.content - end - end - end - end -end diff --git a/lib/api/v3/subscriptions.rb b/lib/api/v3/subscriptions.rb deleted file mode 100644 index 690768db82f..00000000000 --- a/lib/api/v3/subscriptions.rb +++ /dev/null @@ -1,53 +0,0 @@ -module API - module V3 - class Subscriptions < Grape::API - before { authenticate! } - - subscribable_types = { - 'merge_request' => proc { |id| find_merge_request_with_access(id, :update_merge_request) }, - 'merge_requests' => proc { |id| find_merge_request_with_access(id, :update_merge_request) }, - 'issues' => proc { |id| find_project_issue(id) }, - 'labels' => proc { |id| find_project_label(id) } - } - - params do - requires :id, type: String, desc: 'The ID of a project' - requires :subscribable_id, type: String, desc: 'The ID of a resource' - end - resource :projects, requirements: { id: %r{[^/]+} } do - subscribable_types.each do |type, finder| - type_singularized = type.singularize - entity_class = ::API::Entities.const_get(type_singularized.camelcase) - - desc 'Subscribe to a resource' do - success entity_class - end - post ":id/#{type}/:subscribable_id/subscription" do - resource = instance_exec(params[:subscribable_id], &finder) - - if resource.subscribed?(current_user, user_project) - not_modified! - else - resource.subscribe(current_user, user_project) - present resource, with: entity_class, current_user: current_user, project: user_project - end - end - - desc 'Unsubscribe from a resource' do - success entity_class - end - delete ":id/#{type}/:subscribable_id/subscription" do - resource = instance_exec(params[:subscribable_id], &finder) - - if !resource.subscribed?(current_user, user_project) - not_modified! - else - resource.unsubscribe(current_user, user_project) - present resource, with: entity_class, current_user: current_user, project: user_project - end - end - end - end - end - end -end diff --git a/lib/api/v3/system_hooks.rb b/lib/api/v3/system_hooks.rb deleted file mode 100644 index 5787c06fc12..00000000000 --- a/lib/api/v3/system_hooks.rb +++ /dev/null @@ -1,32 +0,0 @@ -module API - module V3 - class SystemHooks < Grape::API - before do - authenticate! - authenticated_as_admin! - end - - resource :hooks do - desc 'Get the list of system hooks' do - success ::API::Entities::Hook - end - get do - present SystemHook.all, with: ::API::Entities::Hook - end - - desc 'Delete a hook' do - success ::API::Entities::Hook - end - params do - requires :id, type: Integer, desc: 'The ID of the system hook' - end - delete ":id" do - hook = SystemHook.find_by(id: params[:id]) - not_found!('System hook') unless hook - - present hook.destroy, with: ::API::Entities::Hook - end - end - end - end -end diff --git a/lib/api/v3/tags.rb b/lib/api/v3/tags.rb deleted file mode 100644 index 6e37d31d153..00000000000 --- a/lib/api/v3/tags.rb +++ /dev/null @@ -1,40 +0,0 @@ -module API - module V3 - class Tags < Grape::API - before { authorize! :download_code, user_project } - - params do - requires :id, type: String, desc: 'The ID of a project' - end - resource :projects, requirements: { id: %r{[^/]+} } do - desc 'Get a project repository tags' do - success ::API::Entities::Tag - end - get ":id/repository/tags" do - tags = user_project.repository.tags.sort_by(&:name).reverse - present tags, with: ::API::Entities::Tag, project: user_project - end - - desc 'Delete a repository tag' - params do - requires :tag_name, type: String, desc: 'The name of the tag' - end - delete ":id/repository/tags/:tag_name", requirements: { tag_name: /.+/ } do - authorize_push_project - - result = ::Tags::DestroyService.new(user_project, current_user) - .execute(params[:tag_name]) - - if result[:status] == :success - status(200) - { - tag_name: params[:tag_name] - } - else - render_api_error!(result[:message], result[:return_code]) - end - end - end - end - end -end diff --git a/lib/api/v3/templates.rb b/lib/api/v3/templates.rb deleted file mode 100644 index b82b02b5f49..00000000000 --- a/lib/api/v3/templates.rb +++ /dev/null @@ -1,122 +0,0 @@ -module API - module V3 - class Templates < Grape::API - GLOBAL_TEMPLATE_TYPES = { - gitignores: { - klass: Gitlab::Template::GitignoreTemplate, - gitlab_version: 8.8 - }, - gitlab_ci_ymls: { - klass: Gitlab::Template::GitlabCiYmlTemplate, - gitlab_version: 8.9 - }, - dockerfiles: { - klass: Gitlab::Template::DockerfileTemplate, - gitlab_version: 8.15 - } - }.freeze - PROJECT_TEMPLATE_REGEX = - %r{[\<\{\[] - (project|description| - one\sline\s.+\swhat\sit\sdoes\.) # matching the start and end is enough here - [\>\}\]]}xi.freeze - YEAR_TEMPLATE_REGEX = /[<{\[](year|yyyy)[>}\]]/i.freeze - FULLNAME_TEMPLATE_REGEX = - %r{[\<\{\[] - (fullname|name\sof\s(author|copyright\sowner)) - [\>\}\]]}xi.freeze - DEPRECATION_MESSAGE = ' This endpoint is deprecated and has been removed in V4.'.freeze - - helpers do - def parsed_license_template - # We create a fresh Licensee::License object since we'll modify its - # content in place below. - template = Licensee::License.new(params[:name]) - - template.content.gsub!(YEAR_TEMPLATE_REGEX, Time.now.year.to_s) - template.content.gsub!(PROJECT_TEMPLATE_REGEX, params[:project]) if params[:project].present? - - fullname = params[:fullname].presence || current_user.try(:name) - template.content.gsub!(FULLNAME_TEMPLATE_REGEX, fullname) if fullname - template - end - - def render_response(template_type, template) - not_found!(template_type.to_s.singularize) unless template - present template, with: ::API::Entities::Template - end - end - - { "licenses" => :deprecated, "templates/licenses" => :ok }.each do |route, status| - desc 'Get the list of the available license template' do - detailed_desc = 'This feature was introduced in GitLab 8.7.' - detailed_desc << DEPRECATION_MESSAGE unless status == :ok - detail detailed_desc - success ::API::Entities::License - end - params do - optional :popular, type: Boolean, desc: 'If passed, returns only popular licenses' - end - get route do - options = { - featured: declared(params)[:popular].present? ? true : nil - } - present Licensee::License.all(options), with: ::API::Entities::License - end - end - - { "licenses/:name" => :deprecated, "templates/licenses/:name" => :ok }.each do |route, status| - desc 'Get the text for a specific license' do - detailed_desc = 'This feature was introduced in GitLab 8.7.' - detailed_desc << DEPRECATION_MESSAGE unless status == :ok - detail detailed_desc - success ::API::Entities::License - end - params do - requires :name, type: String, desc: 'The name of the template' - end - get route, requirements: { name: /[\w\.-]+/ } do - not_found!('License') unless Licensee::License.find(declared(params)[:name]) - - template = parsed_license_template - - present template, with: ::API::Entities::License - end - end - - GLOBAL_TEMPLATE_TYPES.each do |template_type, properties| - klass = properties[:klass] - gitlab_version = properties[:gitlab_version] - - { template_type => :deprecated, "templates/#{template_type}" => :ok }.each do |route, status| - desc 'Get the list of the available template' do - detailed_desc = "This feature was introduced in GitLab #{gitlab_version}." - detailed_desc << DEPRECATION_MESSAGE unless status == :ok - detail detailed_desc - success ::API::Entities::TemplatesList - end - get route do - present klass.all, with: ::API::Entities::TemplatesList - end - end - - { "#{template_type}/:name" => :deprecated, "templates/#{template_type}/:name" => :ok }.each do |route, status| - desc 'Get the text for a specific template present in local filesystem' do - detailed_desc = "This feature was introduced in GitLab #{gitlab_version}." - detailed_desc << DEPRECATION_MESSAGE unless status == :ok - detail detailed_desc - success ::API::Entities::Template - end - params do - requires :name, type: String, desc: 'The name of the template' - end - get route do - new_template = klass.find(declared(params)[:name]) - - render_response(template_type, new_template) - end - end - end - end - end -end diff --git a/lib/api/v3/time_tracking_endpoints.rb b/lib/api/v3/time_tracking_endpoints.rb deleted file mode 100644 index 1aad39815f9..00000000000 --- a/lib/api/v3/time_tracking_endpoints.rb +++ /dev/null @@ -1,116 +0,0 @@ -module API - module V3 - module TimeTrackingEndpoints - extend ActiveSupport::Concern - - included do - helpers do - def issuable_name - declared_params.key?(:issue_id) ? 'issue' : 'merge_request' - end - - def issuable_key - "#{issuable_name}_id".to_sym - end - - def update_issuable_key - "update_#{issuable_name}".to_sym - end - - def read_issuable_key - "read_#{issuable_name}".to_sym - end - - def load_issuable - @issuable ||= begin - case issuable_name - when 'issue' - find_project_issue(params.delete(issuable_key)) - when 'merge_request' - find_project_merge_request(params.delete(issuable_key)) - end - end - end - - def update_issuable(attrs) - custom_params = declared_params(include_missing: false) - custom_params.merge!(attrs) - - issuable = update_service.new(user_project, current_user, custom_params).execute(load_issuable) - if issuable.valid? - present issuable, with: ::API::Entities::IssuableTimeStats - else - render_validation_error!(issuable) - end - end - - def update_service - issuable_name == 'issue' ? ::Issues::UpdateService : ::MergeRequests::UpdateService - end - end - - issuable_name = name.end_with?('Issues') ? 'issue' : 'merge_request' - issuable_collection_name = issuable_name.pluralize - issuable_key = "#{issuable_name}_id".to_sym - - desc "Set a time estimate for a project #{issuable_name}" - params do - requires issuable_key, type: Integer, desc: "The ID of a project #{issuable_name}" - requires :duration, type: String, desc: 'The duration to be parsed' - end - post ":id/#{issuable_collection_name}/:#{issuable_key}/time_estimate" do - authorize! update_issuable_key, load_issuable - - status :ok - update_issuable(time_estimate: Gitlab::TimeTrackingFormatter.parse(params.delete(:duration))) - end - - desc "Reset the time estimate for a project #{issuable_name}" - params do - requires issuable_key, type: Integer, desc: "The ID of a project #{issuable_name}" - end - post ":id/#{issuable_collection_name}/:#{issuable_key}/reset_time_estimate" do - authorize! update_issuable_key, load_issuable - - status :ok - update_issuable(time_estimate: 0) - end - - desc "Add spent time for a project #{issuable_name}" - params do - requires issuable_key, type: Integer, desc: "The ID of a project #{issuable_name}" - requires :duration, type: String, desc: 'The duration to be parsed' - end - post ":id/#{issuable_collection_name}/:#{issuable_key}/add_spent_time" do - authorize! update_issuable_key, load_issuable - - update_issuable(spend_time: { - duration: Gitlab::TimeTrackingFormatter.parse(params.delete(:duration)), - user_id: current_user.id - }) - end - - desc "Reset spent time for a project #{issuable_name}" - params do - requires issuable_key, type: Integer, desc: "The ID of a project #{issuable_name}" - end - post ":id/#{issuable_collection_name}/:#{issuable_key}/reset_spent_time" do - authorize! update_issuable_key, load_issuable - - status :ok - update_issuable(spend_time: { duration: :reset, user_id: current_user.id }) - end - - desc "Show time stats for a project #{issuable_name}" - params do - requires issuable_key, type: Integer, desc: "The ID of a project #{issuable_name}" - end - get ":id/#{issuable_collection_name}/:#{issuable_key}/time_stats" do - authorize! read_issuable_key, load_issuable - - present load_issuable, with: ::API::Entities::IssuableTimeStats - end - end - end - end -end diff --git a/lib/api/v3/todos.rb b/lib/api/v3/todos.rb deleted file mode 100644 index 3e2c61f6dbd..00000000000 --- a/lib/api/v3/todos.rb +++ /dev/null @@ -1,30 +0,0 @@ -module API - module V3 - class Todos < Grape::API - before { authenticate! } - - resource :todos do - desc 'Mark a todo as done' do - success ::API::Entities::Todo - end - params do - requires :id, type: Integer, desc: 'The ID of the todo being marked as done' - end - delete ':id' do - TodoService.new.mark_todos_as_done_by_ids(params[:id], current_user) - todo = current_user.todos.find(params[:id]) - - present todo, with: ::API::Entities::Todo, current_user: current_user - end - - desc 'Mark all todos as done' - delete do - status(200) - - todos = TodosFinder.new(current_user, params).execute - TodoService.new.mark_todos_as_done(todos, current_user).size - end - end - end - end -end diff --git a/lib/api/v3/triggers.rb b/lib/api/v3/triggers.rb deleted file mode 100644 index 969bb2a05de..00000000000 --- a/lib/api/v3/triggers.rb +++ /dev/null @@ -1,112 +0,0 @@ -module API - module V3 - class Triggers < Grape::API - include PaginationParams - - params do - requires :id, type: String, desc: 'The ID of a project' - end - resource :projects, requirements: { id: %r{[^/]+} } do - desc 'Trigger a GitLab project build' do - success ::API::V3::Entities::TriggerRequest - end - params do - requires :ref, type: String, desc: 'The commit sha or name of a branch or tag' - requires :token, type: String, desc: 'The unique token of trigger' - optional :variables, type: Hash, desc: 'The list of variables to be injected into build' - end - post ":id/(ref/:ref/)trigger/builds", requirements: { ref: /.+/ } do - Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42121') - - # validate variables - params[:variables] = params[:variables].to_h - unless params[:variables].all? { |key, value| key.is_a?(String) && value.is_a?(String) } - render_api_error!('variables needs to be a map of key-valued strings', 400) - end - - project = find_project(params[:id]) - not_found! unless project - - result = Ci::PipelineTriggerService.new(project, nil, params).execute - not_found! unless result - - if result[:http_status] - render_api_error!(result[:message], result[:http_status]) - else - pipeline = result[:pipeline] - - # We switched to Ci::PipelineVariable from Ci::TriggerRequest.variables. - # Ci::TriggerRequest doesn't save variables anymore. - # Here is copying Ci::PipelineVariable to Ci::TriggerRequest.variables for presenting the variables. - # The same endpoint in v4 API pressents Pipeline instead of TriggerRequest, so it doesn't need such a process. - trigger_request = pipeline.trigger_requests.last - trigger_request.variables = params[:variables] - - present trigger_request, with: ::API::V3::Entities::TriggerRequest - end - end - - desc 'Get triggers list' do - success ::API::V3::Entities::Trigger - end - params do - use :pagination - end - get ':id/triggers' do - authenticate! - authorize! :admin_build, user_project - - triggers = user_project.triggers.includes(:trigger_requests) - - present paginate(triggers), with: ::API::V3::Entities::Trigger - end - - desc 'Get specific trigger of a project' do - success ::API::V3::Entities::Trigger - end - params do - requires :token, type: String, desc: 'The unique token of trigger' - end - get ':id/triggers/:token' do - authenticate! - authorize! :admin_build, user_project - - trigger = user_project.triggers.find_by(token: params[:token].to_s) - break not_found!('Trigger') unless trigger - - present trigger, with: ::API::V3::Entities::Trigger - end - - desc 'Create a trigger' do - success ::API::V3::Entities::Trigger - end - post ':id/triggers' do - authenticate! - authorize! :admin_build, user_project - - trigger = user_project.triggers.create - - present trigger, with: ::API::V3::Entities::Trigger - end - - desc 'Delete a trigger' do - success ::API::V3::Entities::Trigger - end - params do - requires :token, type: String, desc: 'The unique token of trigger' - end - delete ':id/triggers/:token' do - authenticate! - authorize! :admin_build, user_project - - trigger = user_project.triggers.find_by(token: params[:token].to_s) - break not_found!('Trigger') unless trigger - - trigger.destroy - - present trigger, with: ::API::V3::Entities::Trigger - end - end - end - end -end diff --git a/lib/api/v3/users.rb b/lib/api/v3/users.rb deleted file mode 100644 index cf106f2552d..00000000000 --- a/lib/api/v3/users.rb +++ /dev/null @@ -1,204 +0,0 @@ -module API - module V3 - class Users < Grape::API - include PaginationParams - include APIGuard - - allow_access_with_scope :read_user, if: -> (request) { request.get? } - - before do - authenticate! - end - - resource :users, requirements: { uid: /[0-9]*/, id: /[0-9]*/ } do - helpers do - params :optional_attributes do - optional :skype, type: String, desc: 'The Skype username' - optional :linkedin, type: String, desc: 'The LinkedIn username' - optional :twitter, type: String, desc: 'The Twitter username' - optional :website_url, type: String, desc: 'The website of the user' - optional :organization, type: String, desc: 'The organization of the user' - optional :projects_limit, type: Integer, desc: 'The number of projects a user can create' - optional :extern_uid, type: String, desc: 'The external authentication provider UID' - optional :provider, type: String, desc: 'The external provider' - optional :bio, type: String, desc: 'The biography of the user' - optional :location, type: String, desc: 'The location of the user' - optional :admin, type: Boolean, desc: 'Flag indicating the user is an administrator' - optional :can_create_group, type: Boolean, desc: 'Flag indicating the user can create groups' - optional :confirm, type: Boolean, default: true, desc: 'Flag indicating the account needs to be confirmed' - optional :external, type: Boolean, desc: 'Flag indicating the user is an external user' - all_or_none_of :extern_uid, :provider - end - end - - desc 'Create a user. Available only for admins.' do - success ::API::Entities::UserPublic - end - params do - requires :email, type: String, desc: 'The email of the user' - optional :password, type: String, desc: 'The password of the new user' - optional :reset_password, type: Boolean, desc: 'Flag indicating the user will be sent a password reset token' - at_least_one_of :password, :reset_password - requires :name, type: String, desc: 'The name of the user' - requires :username, type: String, desc: 'The username of the user' - use :optional_attributes - end - post do - authenticated_as_admin! - - params = declared_params(include_missing: false) - user = ::Users::CreateService.new(current_user, params.merge!(skip_confirmation: !params[:confirm])).execute - - if user.persisted? - present user, with: ::API::Entities::UserPublic - else - conflict!('Email has already been taken') if User - .where(email: user.email) - .count > 0 - - conflict!('Username has already been taken') if User - .where(username: user.username) - .count > 0 - - render_validation_error!(user) - end - end - - desc 'Get the SSH keys of a specified user. Available only for admins.' do - success ::API::Entities::SSHKey - end - params do - requires :id, type: Integer, desc: 'The ID of the user' - use :pagination - end - get ':id/keys' do - authenticated_as_admin! - - user = User.find_by(id: params[:id]) - not_found!('User') unless user - - present paginate(user.keys), with: ::API::Entities::SSHKey - end - - desc 'Get the emails addresses of a specified user. Available only for admins.' do - success ::API::Entities::Email - end - params do - requires :id, type: Integer, desc: 'The ID of the user' - use :pagination - end - get ':id/emails' do - authenticated_as_admin! - user = User.find_by(id: params[:id]) - not_found!('User') unless user - - present user.emails, with: ::API::Entities::Email - end - - desc 'Block a user. Available only for admins.' - params do - requires :id, type: Integer, desc: 'The ID of the user' - end - put ':id/block' do - authenticated_as_admin! - user = User.find_by(id: params[:id]) - not_found!('User') unless user - - if !user.ldap_blocked? - user.block - else - forbidden!('LDAP blocked users cannot be modified by the API') - end - end - - desc 'Unblock a user. Available only for admins.' - params do - requires :id, type: Integer, desc: 'The ID of the user' - end - put ':id/unblock' do - authenticated_as_admin! - user = User.find_by(id: params[:id]) - not_found!('User') unless user - - if user.ldap_blocked? - forbidden!('LDAP blocked users cannot be unblocked by the API') - else - user.activate - end - end - - desc 'Get the contribution events of a specified user' do - detail 'This feature was introduced in GitLab 8.13.' - success ::API::V3::Entities::Event - end - params do - requires :id, type: Integer, desc: 'The ID of the user' - use :pagination - end - get ':id/events' do - user = User.find_by(id: params[:id]) - not_found!('User') unless user - - events = user.events - .merge(ProjectsFinder.new(current_user: current_user).execute) - .references(:project) - .with_associations - .recent - - present paginate(events), with: ::API::V3::Entities::Event - end - - desc 'Delete an existing SSH key from a specified user. Available only for admins.' do - success ::API::Entities::SSHKey - end - params do - requires :id, type: Integer, desc: 'The ID of the user' - requires :key_id, type: Integer, desc: 'The ID of the SSH key' - end - delete ':id/keys/:key_id' do - authenticated_as_admin! - - user = User.find_by(id: params[:id]) - not_found!('User') unless user - - key = user.keys.find_by(id: params[:key_id]) - not_found!('Key') unless key - - present key.destroy, with: ::API::Entities::SSHKey - end - end - - resource :user do - desc "Get the currently authenticated user's SSH keys" do - success ::API::Entities::SSHKey - end - params do - use :pagination - end - get "keys" do - present current_user.keys, with: ::API::Entities::SSHKey - end - - desc "Get the currently authenticated user's email addresses" do - success ::API::Entities::Email - end - get "emails" do - present current_user.emails, with: ::API::Entities::Email - end - - desc 'Delete an SSH key from the currently authenticated user' do - success ::API::Entities::SSHKey - end - params do - requires :key_id, type: Integer, desc: 'The ID of the SSH key' - end - delete "keys/:key_id" do - key = current_user.keys.find_by(id: params[:key_id]) - not_found!('Key') unless key - - present key.destroy, with: ::API::Entities::SSHKey - end - end - end - end -end diff --git a/lib/api/v3/variables.rb b/lib/api/v3/variables.rb deleted file mode 100644 index 83972b1e7ce..00000000000 --- a/lib/api/v3/variables.rb +++ /dev/null @@ -1,29 +0,0 @@ -module API - module V3 - class Variables < Grape::API - include PaginationParams - - before { authenticate! } - before { authorize! :admin_build, user_project } - - params do - requires :id, type: String, desc: 'The ID of a project' - end - - resource :projects, requirements: { id: %r{[^/]+} } do - desc 'Delete an existing variable from a project' do - success ::API::Entities::Variable - end - params do - requires :key, type: String, desc: 'The key of the variable' - end - delete ':id/variables/:key' do - variable = user_project.variables.find_by(key: params[:key]) - not_found!('Variable') unless variable - - present variable.destroy, with: ::API::Entities::Variable - end - end - end - end -end diff --git a/lib/gitlab/gitlab_import/client.rb b/lib/gitlab/gitlab_import/client.rb index 5482504e72e..22719e9a003 100644 --- a/lib/gitlab/gitlab_import/client.rb +++ b/lib/gitlab/gitlab_import/client.rb @@ -29,28 +29,28 @@ module Gitlab end def user - api.get("/api/v3/user").parsed + api.get("/api/v4/user").parsed end def issues(project_identifier) lazy_page_iterator(PER_PAGE) do |page| - api.get("/api/v3/projects/#{project_identifier}/issues?per_page=#{PER_PAGE}&page=#{page}").parsed + api.get("/api/v4/projects/#{project_identifier}/issues?per_page=#{PER_PAGE}&page=#{page}").parsed end end def issue_comments(project_identifier, issue_id) lazy_page_iterator(PER_PAGE) do |page| - api.get("/api/v3/projects/#{project_identifier}/issues/#{issue_id}/notes?per_page=#{PER_PAGE}&page=#{page}").parsed + api.get("/api/v4/projects/#{project_identifier}/issues/#{issue_id}/notes?per_page=#{PER_PAGE}&page=#{page}").parsed end end def project(id) - api.get("/api/v3/projects/#{id}").parsed + api.get("/api/v4/projects/#{id}").parsed end def projects lazy_page_iterator(PER_PAGE) do |page| - api.get("/api/v3/projects?per_page=#{PER_PAGE}&page=#{page}").parsed + api.get("/api/v4/projects?per_page=#{PER_PAGE}&page=#{page}").parsed end end diff --git a/lib/gitlab/gitlab_import/importer.rb b/lib/gitlab/gitlab_import/importer.rb index e44d7934fda..195672f5a12 100644 --- a/lib/gitlab/gitlab_import/importer.rb +++ b/lib/gitlab/gitlab_import/importer.rb @@ -25,7 +25,7 @@ module Gitlab body = @formatter.author_line(issue["author"]["name"]) body += issue["description"] - comments = client.issue_comments(project_identifier, issue["id"]) + comments = client.issue_comments(project_identifier, issue["iid"]) if comments.any? body += @formatter.comments_header diff --git a/lib/gitlab/gitlab_import/project_creator.rb b/lib/gitlab/gitlab_import/project_creator.rb index 3d0418261bb..430b8c10058 100644 --- a/lib/gitlab/gitlab_import/project_creator.rb +++ b/lib/gitlab/gitlab_import/project_creator.rb @@ -17,7 +17,7 @@ module Gitlab path: repo["path"], description: repo["description"], namespace_id: namespace.id, - visibility_level: repo["visibility_level"], + visibility_level: Gitlab::VisibilityLevel.level_value(repo["visibility"]), import_type: "gitlab", import_source: repo["path_with_namespace"], import_url: repo["http_url_to_repo"].sub("://", "://oauth2:#{@session_data[:gitlab_access_token]}@") diff --git a/qa/spec/runtime/api_request_spec.rb b/qa/spec/runtime/api_request_spec.rb index 9a1ed8a7a46..8cf4b040c24 100644 --- a/qa/spec/runtime/api_request_spec.rb +++ b/qa/spec/runtime/api_request_spec.rb @@ -36,7 +36,7 @@ describe QA::Runtime::API::Request do end it 'uses a different api version' do - expect(request.request_path('/users', version: 'v3')).to eq '/api/v3/users' + expect(request.request_path('/users', version: 'other_version')).to eq '/api/other_version/users' end end end diff --git a/spec/fixtures/api/schemas/public_api/v3/issues.json b/spec/fixtures/api/schemas/public_api/v3/issues.json deleted file mode 100644 index 51b0822bc66..00000000000 --- a/spec/fixtures/api/schemas/public_api/v3/issues.json +++ /dev/null @@ -1,78 +0,0 @@ -{ - "type": "array", - "items": { - "type": "object", - "properties" : { - "id": { "type": "integer" }, - "iid": { "type": "integer" }, - "project_id": { "type": "integer" }, - "title": { "type": "string" }, - "description": { "type": ["string", "null"] }, - "state": { "type": "string" }, - "created_at": { "type": "date" }, - "updated_at": { "type": "date" }, - "labels": { - "type": "array", - "items": { - "type": "string" - } - }, - "milestone": { - "type": "object", - "properties": { - "id": { "type": "integer" }, - "iid": { "type": "integer" }, - "project_id": { "type": ["integer", "null"] }, - "group_id": { "type": ["integer", "null"] }, - "title": { "type": "string" }, - "description": { "type": ["string", "null"] }, - "state": { "type": "string" }, - "created_at": { "type": "date" }, - "updated_at": { "type": "date" }, - "due_date": { "type": "date" }, - "start_date": { "type": "date" } - }, - "additionalProperties": false - }, - "assignee": { - "type": ["object", "null"], - "properties": { - "name": { "type": "string" }, - "username": { "type": "string" }, - "id": { "type": "integer" }, - "state": { "type": "string" }, - "avatar_url": { "type": "uri" }, - "web_url": { "type": "uri" } - }, - "additionalProperties": false - }, - "author": { - "type": "object", - "properties": { - "name": { "type": "string" }, - "username": { "type": "string" }, - "id": { "type": "integer" }, - "state": { "type": "string" }, - "avatar_url": { "type": "uri" }, - "web_url": { "type": "uri" } - }, - "additionalProperties": false - }, - "user_notes_count": { "type": "integer" }, - "upvotes": { "type": "integer" }, - "downvotes": { "type": "integer" }, - "due_date": { "type": ["date", "null"] }, - "confidential": { "type": "boolean" }, - "web_url": { "type": "uri" }, - "subscribed": { "type": ["boolean"] } - }, - "required": [ - "id", "iid", "project_id", "title", "description", - "state", "created_at", "updated_at", "labels", - "milestone", "assignee", "author", "user_notes_count", - "upvotes", "downvotes", "due_date", "confidential", - "web_url", "subscribed" - ], - "additionalProperties": false - } -} diff --git a/spec/fixtures/api/schemas/public_api/v3/merge_requests.json b/spec/fixtures/api/schemas/public_api/v3/merge_requests.json deleted file mode 100644 index 9af7a8c9f4f..00000000000 --- a/spec/fixtures/api/schemas/public_api/v3/merge_requests.json +++ /dev/null @@ -1,91 +0,0 @@ -{ - "type": "array", - "items": { - "type": "object", - "properties" : { - "id": { "type": "integer" }, - "iid": { "type": "integer" }, - "project_id": { "type": "integer" }, - "title": { "type": "string" }, - "description": { "type": ["string", "null"] }, - "state": { "type": "string" }, - "created_at": { "type": "date" }, - "updated_at": { "type": "date" }, - "target_branch": { "type": "string" }, - "source_branch": { "type": "string" }, - "upvotes": { "type": "integer" }, - "downvotes": { "type": "integer" }, - "author": { - "type": "object", - "properties": { - "name": { "type": "string" }, - "username": { "type": "string" }, - "id": { "type": "integer" }, - "state": { "type": "string" }, - "avatar_url": { "type": "uri" }, - "web_url": { "type": "uri" } - }, - "additionalProperties": false - }, - "assignee": { - "type": "object", - "properties": { - "name": { "type": "string" }, - "username": { "type": "string" }, - "id": { "type": "integer" }, - "state": { "type": "string" }, - "avatar_url": { "type": "uri" }, - "web_url": { "type": "uri" } - }, - "additionalProperties": false - }, - "source_project_id": { "type": "integer" }, - "target_project_id": { "type": "integer" }, - "labels": { - "type": "array", - "items": { - "type": "string" - } - }, - "work_in_progress": { "type": "boolean" }, - "milestone": { - "type": ["object", "null"], - "properties": { - "id": { "type": "integer" }, - "iid": { "type": "integer" }, - "project_id": { "type": ["integer", "null"] }, - "group_id": { "type": ["integer", "null"] }, - "title": { "type": "string" }, - "description": { "type": ["string", "null"] }, - "state": { "type": "string" }, - "created_at": { "type": "date" }, - "updated_at": { "type": "date" }, - "due_date": { "type": "date" }, - "start_date": { "type": "date" } - }, - "additionalProperties": false - }, - "merge_when_build_succeeds": { "type": "boolean" }, - "merge_status": { "type": "string" }, - "sha": { "type": "string" }, - "merge_commit_sha": { "type": ["string", "null"] }, - "user_notes_count": { "type": "integer" }, - "should_remove_source_branch": { "type": ["boolean", "null"] }, - "force_remove_source_branch": { "type": ["boolean", "null"] }, - "web_url": { "type": "uri" }, - "subscribed": { "type": ["boolean"] }, - "squash": { "type": "boolean" } - }, - "required": [ - "id", "iid", "project_id", "title", "description", - "state", "created_at", "updated_at", "target_branch", - "source_branch", "upvotes", "downvotes", "author", - "assignee", "source_project_id", "target_project_id", - "labels", "work_in_progress", "milestone", "merge_when_build_succeeds", - "merge_status", "sha", "merge_commit_sha", "user_notes_count", - "should_remove_source_branch", "force_remove_source_branch", - "web_url", "subscribed", "squash" - ], - "additionalProperties": false - } -} diff --git a/spec/lib/gitlab/gitlab_import/importer_spec.rb b/spec/lib/gitlab/gitlab_import/importer_spec.rb index e1d935602b5..200edceca8c 100644 --- a/spec/lib/gitlab/gitlab_import/importer_spec.rb +++ b/spec/lib/gitlab/gitlab_import/importer_spec.rb @@ -20,7 +20,7 @@ describe Gitlab::GitlabImport::Importer do } } ]) - stub_request('issues/2579857/notes', []) + stub_request('issues/3/notes', []) end it 'persists issues' do @@ -43,7 +43,7 @@ describe Gitlab::GitlabImport::Importer do end def stub_request(path, body) - url = "https://gitlab.com/api/v3/projects/asd%2Fvim/#{path}?page=1&per_page=100" + url = "https://gitlab.com/api/v4/projects/asd%2Fvim/#{path}?page=1&per_page=100" WebMock.stub_request(:get, url) .to_return( diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index f8f07205623..968267a6d24 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -195,22 +195,6 @@ describe ApplicationSetting do expect(setting.pick_repository_storage).to eq('random') end - - describe '#repository_storage' do - it 'returns the first storage' do - setting.repository_storages = %w(good bad) - - expect(setting.repository_storage).to eq('good') - end - end - - describe '#repository_storage=' do - it 'overwrites repository_storages' do - setting.repository_storage = 'overwritten' - - expect(setting.repository_storages).to eq(['overwritten']) - end - end end end diff --git a/spec/requests/api/v3/award_emoji_spec.rb b/spec/requests/api/v3/award_emoji_spec.rb deleted file mode 100644 index 6dc430676b0..00000000000 --- a/spec/requests/api/v3/award_emoji_spec.rb +++ /dev/null @@ -1,297 +0,0 @@ -require 'spec_helper' - -describe API::V3::AwardEmoji do - set(:user) { create(:user) } - set(:project) { create(:project) } - set(:issue) { create(:issue, project: project) } - set(:award_emoji) { create(:award_emoji, awardable: issue, user: user) } - let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) } - let!(:downvote) { create(:award_emoji, :downvote, awardable: merge_request, user: user) } - set(:note) { create(:note, project: project, noteable: issue) } - - before { project.add_master(user) } - - describe "GET /projects/:id/awardable/:awardable_id/award_emoji" do - context 'on an issue' do - it "returns an array of award_emoji" do - get v3_api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.first['name']).to eq(award_emoji.name) - end - - it "returns a 404 error when issue id not found" do - get v3_api("/projects/#{project.id}/issues/12345/award_emoji", user) - - expect(response).to have_gitlab_http_status(404) - end - end - - context 'on a merge request' do - it "returns an array of award_emoji" do - get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/award_emoji", user) - - expect(response).to have_gitlab_http_status(200) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array - expect(json_response.first['name']).to eq(downvote.name) - end - end - - context 'on a snippet' do - let(:snippet) { create(:project_snippet, :public, project: project) } - let!(:award) { create(:award_emoji, awardable: snippet) } - - it 'returns the awarded emoji' do - get v3_api("/projects/#{project.id}/snippets/#{snippet.id}/award_emoji", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.first['name']).to eq(award.name) - end - end - - context 'when the user has no access' do - it 'returns a status code 404' do - user1 = create(:user) - - get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/award_emoji", user1) - - expect(response).to have_gitlab_http_status(404) - end - end - end - - describe 'GET /projects/:id/awardable/:awardable_id/notes/:note_id/award_emoji' do - let!(:rocket) { create(:award_emoji, awardable: note, name: 'rocket') } - - it 'returns an array of award emoji' do - get v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.first['name']).to eq(rocket.name) - end - end - - describe "GET /projects/:id/awardable/:awardable_id/award_emoji/:award_id" do - context 'on an issue' do - it "returns the award emoji" do - get v3_api("/projects/#{project.id}/issues/#{issue.id}/award_emoji/#{award_emoji.id}", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['name']).to eq(award_emoji.name) - expect(json_response['awardable_id']).to eq(issue.id) - expect(json_response['awardable_type']).to eq("Issue") - end - - it "returns a 404 error if the award is not found" do - get v3_api("/projects/#{project.id}/issues/#{issue.id}/award_emoji/12345", user) - - expect(response).to have_gitlab_http_status(404) - end - end - - context 'on a merge request' do - it 'returns the award emoji' do - get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/award_emoji/#{downvote.id}", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['name']).to eq(downvote.name) - expect(json_response['awardable_id']).to eq(merge_request.id) - expect(json_response['awardable_type']).to eq("MergeRequest") - end - end - - context 'on a snippet' do - let(:snippet) { create(:project_snippet, :public, project: project) } - let!(:award) { create(:award_emoji, awardable: snippet) } - - it 'returns the awarded emoji' do - get v3_api("/projects/#{project.id}/snippets/#{snippet.id}/award_emoji/#{award.id}", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['name']).to eq(award.name) - expect(json_response['awardable_id']).to eq(snippet.id) - expect(json_response['awardable_type']).to eq("Snippet") - end - end - - context 'when the user has no access' do - it 'returns a status code 404' do - user1 = create(:user) - - get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/award_emoji/#{downvote.id}", user1) - - expect(response).to have_gitlab_http_status(404) - end - end - end - - describe 'GET /projects/:id/awardable/:awardable_id/notes/:note_id/award_emoji/:award_id' do - let!(:rocket) { create(:award_emoji, awardable: note, name: 'rocket') } - - it 'returns an award emoji' do - get v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji/#{rocket.id}", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).not_to be_an Array - expect(json_response['name']).to eq(rocket.name) - end - end - - describe "POST /projects/:id/awardable/:awardable_id/award_emoji" do - let(:issue2) { create(:issue, project: project, author: user) } - - context "on an issue" do - it "creates a new award emoji" do - post v3_api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user), name: 'blowfish' - - expect(response).to have_gitlab_http_status(201) - expect(json_response['name']).to eq('blowfish') - expect(json_response['user']['username']).to eq(user.username) - end - - it "returns a 400 bad request error if the name is not given" do - post v3_api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user) - - expect(response).to have_gitlab_http_status(400) - end - - it "returns a 401 unauthorized error if the user is not authenticated" do - post v3_api("/projects/#{project.id}/issues/#{issue.id}/award_emoji"), name: 'thumbsup' - - expect(response).to have_gitlab_http_status(401) - end - - it "returns a 404 error if the user authored issue" do - post v3_api("/projects/#{project.id}/issues/#{issue2.id}/award_emoji", user), name: 'thumbsup' - - expect(response).to have_gitlab_http_status(404) - end - - it "normalizes +1 as thumbsup award" do - post v3_api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user), name: '+1' - - expect(issue.award_emoji.last.name).to eq("thumbsup") - end - - context 'when the emoji already has been awarded' do - it 'returns a 404 status code' do - post v3_api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user), name: 'thumbsup' - post v3_api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user), name: 'thumbsup' - - expect(response).to have_gitlab_http_status(404) - expect(json_response["message"]).to match("has already been taken") - end - end - end - - context 'on a snippet' do - it 'creates a new award emoji' do - snippet = create(:project_snippet, :public, project: project) - - post v3_api("/projects/#{project.id}/snippets/#{snippet.id}/award_emoji", user), name: 'blowfish' - - expect(response).to have_gitlab_http_status(201) - expect(json_response['name']).to eq('blowfish') - expect(json_response['user']['username']).to eq(user.username) - end - end - end - - describe "POST /projects/:id/awardable/:awardable_id/notes/:note_id/award_emoji" do - let(:note2) { create(:note, project: project, noteable: issue, author: user) } - - it 'creates a new award emoji' do - expect do - post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user), name: 'rocket' - end.to change { note.award_emoji.count }.from(0).to(1) - - expect(response).to have_gitlab_http_status(201) - expect(json_response['user']['username']).to eq(user.username) - end - - it "it returns 404 error when user authored note" do - post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note2.id}/award_emoji", user), name: 'thumbsup' - - expect(response).to have_gitlab_http_status(404) - end - - it "normalizes +1 as thumbsup award" do - post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user), name: '+1' - - expect(note.award_emoji.last.name).to eq("thumbsup") - end - - context 'when the emoji already has been awarded' do - it 'returns a 404 status code' do - post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user), name: 'rocket' - post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user), name: 'rocket' - - expect(response).to have_gitlab_http_status(404) - expect(json_response["message"]).to match("has already been taken") - end - end - end - - describe 'DELETE /projects/:id/awardable/:awardable_id/award_emoji/:award_id' do - context 'when the awardable is an Issue' do - it 'deletes the award' do - expect do - delete v3_api("/projects/#{project.id}/issues/#{issue.id}/award_emoji/#{award_emoji.id}", user) - - expect(response).to have_gitlab_http_status(200) - end.to change { issue.award_emoji.count }.from(1).to(0) - end - - it 'returns a 404 error when the award emoji can not be found' do - delete v3_api("/projects/#{project.id}/issues/#{issue.id}/award_emoji/12345", user) - - expect(response).to have_gitlab_http_status(404) - end - end - - context 'when the awardable is a Merge Request' do - it 'deletes the award' do - expect do - delete v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/award_emoji/#{downvote.id}", user) - - expect(response).to have_gitlab_http_status(200) - end.to change { merge_request.award_emoji.count }.from(1).to(0) - end - - it 'returns a 404 error when note id not found' do - delete v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/notes/12345", user) - - expect(response).to have_gitlab_http_status(404) - end - end - - context 'when the awardable is a Snippet' do - let(:snippet) { create(:project_snippet, :public, project: project) } - let!(:award) { create(:award_emoji, awardable: snippet, user: user) } - - it 'deletes the award' do - expect do - delete v3_api("/projects/#{project.id}/snippets/#{snippet.id}/award_emoji/#{award.id}", user) - - expect(response).to have_gitlab_http_status(200) - end.to change { snippet.award_emoji.count }.from(1).to(0) - end - end - end - - describe 'DELETE /projects/:id/awardable/:awardable_id/award_emoji/:award_emoji_id' do - let!(:rocket) { create(:award_emoji, awardable: note, name: 'rocket', user: user) } - - it 'deletes the award' do - expect do - delete v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji/#{rocket.id}", user) - - expect(response).to have_gitlab_http_status(200) - end.to change { note.award_emoji.count }.from(1).to(0) - end - end -end diff --git a/spec/requests/api/v3/boards_spec.rb b/spec/requests/api/v3/boards_spec.rb deleted file mode 100644 index dde4f096193..00000000000 --- a/spec/requests/api/v3/boards_spec.rb +++ /dev/null @@ -1,114 +0,0 @@ -require 'spec_helper' - -describe API::V3::Boards do - set(:user) { create(:user) } - set(:guest) { create(:user) } - set(:non_member) { create(:user) } - set(:project) { create(:project, :public, creator_id: user.id, namespace: user.namespace ) } - - set(:dev_label) do - create(:label, title: 'Development', color: '#FFAABB', project: project) - end - - set(:test_label) do - create(:label, title: 'Testing', color: '#FFAACC', project: project) - end - - set(:dev_list) do - create(:list, label: dev_label, position: 1) - end - - set(:test_list) do - create(:list, label: test_label, position: 2) - end - - set(:board) do - create(:board, project: project, lists: [dev_list, test_list]) - end - - before do - project.add_reporter(user) - project.add_guest(guest) - end - - describe "GET /projects/:id/boards" do - let(:base_url) { "/projects/#{project.id}/boards" } - - context "when unauthenticated" do - it "returns authentication error" do - get v3_api(base_url) - - expect(response).to have_gitlab_http_status(401) - end - end - - context "when authenticated" do - it "returns the project issue board" do - get v3_api(base_url, user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(1) - expect(json_response.first['id']).to eq(board.id) - expect(json_response.first['lists']).to be_an Array - expect(json_response.first['lists'].length).to eq(2) - expect(json_response.first['lists'].last).to have_key('position') - end - end - end - - describe "GET /projects/:id/boards/:board_id/lists" do - let(:base_url) { "/projects/#{project.id}/boards/#{board.id}/lists" } - - it 'returns issue board lists' do - get v3_api(base_url, user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(2) - expect(json_response.first['label']['name']).to eq(dev_label.title) - end - - it 'returns 404 if board not found' do - get v3_api("/projects/#{project.id}/boards/22343/lists", user) - - expect(response).to have_gitlab_http_status(404) - end - end - - describe "DELETE /projects/:id/board/lists/:list_id" do - let(:base_url) { "/projects/#{project.id}/boards/#{board.id}/lists" } - - it "rejects a non member from deleting a list" do - delete v3_api("#{base_url}/#{dev_list.id}", non_member) - - expect(response).to have_gitlab_http_status(403) - end - - it "rejects a user with guest role from deleting a list" do - delete v3_api("#{base_url}/#{dev_list.id}", guest) - - expect(response).to have_gitlab_http_status(403) - end - - it "returns 404 error if list id not found" do - delete v3_api("#{base_url}/44444", user) - - expect(response).to have_gitlab_http_status(404) - end - - context "when the user is project owner" do - set(:owner) { create(:user) } - - before do - project.update(namespace: owner.namespace) - end - - it "deletes the list if an admin requests it" do - delete v3_api("#{base_url}/#{dev_list.id}", owner) - - expect(response).to have_gitlab_http_status(200) - end - end - end -end diff --git a/spec/requests/api/v3/branches_spec.rb b/spec/requests/api/v3/branches_spec.rb deleted file mode 100644 index 1e038595a1f..00000000000 --- a/spec/requests/api/v3/branches_spec.rb +++ /dev/null @@ -1,120 +0,0 @@ -require 'spec_helper' -require 'mime/types' - -describe API::V3::Branches do - set(:user) { create(:user) } - set(:user2) { create(:user) } - set(:project) { create(:project, :repository, creator: user) } - set(:master) { create(:project_member, :master, user: user, project: project) } - set(:guest) { create(:project_member, :guest, user: user2, project: project) } - let!(:branch_name) { 'feature' } - let!(:branch_sha) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } - let!(:branch_with_dot) { CreateBranchService.new(project, user).execute("with.1.2.3", "master") } - - describe "GET /projects/:id/repository/branches" do - it "returns an array of project branches" do - project.repository.expire_all_method_caches - - get v3_api("/projects/#{project.id}/repository/branches", user), per_page: 100 - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - branch_names = json_response.map { |x| x['name'] } - expect(branch_names).to match_array(project.repository.branch_names) - end - end - - describe "DELETE /projects/:id/repository/branches/:branch" do - before do - allow_any_instance_of(Repository).to receive(:rm_branch).and_return(true) - end - - it "removes branch" do - delete v3_api("/projects/#{project.id}/repository/branches/#{branch_name}", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['branch_name']).to eq(branch_name) - end - - it "removes a branch with dots in the branch name" do - delete v3_api("/projects/#{project.id}/repository/branches/with.1.2.3", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['branch_name']).to eq("with.1.2.3") - end - - it 'returns 404 if branch not exists' do - delete v3_api("/projects/#{project.id}/repository/branches/foobar", user) - expect(response).to have_gitlab_http_status(404) - end - end - - describe "DELETE /projects/:id/repository/merged_branches" do - before do - allow_any_instance_of(Repository).to receive(:rm_branch).and_return(true) - end - - it 'returns 200' do - delete v3_api("/projects/#{project.id}/repository/merged_branches", user) - - expect(response).to have_gitlab_http_status(200) - end - - it 'returns a 403 error if guest' do - delete v3_api("/projects/#{project.id}/repository/merged_branches", user2) - - expect(response).to have_gitlab_http_status(403) - end - end - - describe "POST /projects/:id/repository/branches" do - it "creates a new branch" do - post v3_api("/projects/#{project.id}/repository/branches", user), - branch_name: 'feature1', - ref: branch_sha - - expect(response).to have_gitlab_http_status(201) - - expect(json_response['name']).to eq('feature1') - expect(json_response['commit']['id']).to eq(branch_sha) - end - - it "denies for user without push access" do - post v3_api("/projects/#{project.id}/repository/branches", user2), - branch_name: branch_name, - ref: branch_sha - expect(response).to have_gitlab_http_status(403) - end - - it 'returns 400 if branch name is invalid' do - post v3_api("/projects/#{project.id}/repository/branches", user), - branch_name: 'new design', - ref: branch_sha - expect(response).to have_gitlab_http_status(400) - expect(json_response['message']).to eq('Branch name is invalid') - end - - it 'returns 400 if branch already exists' do - post v3_api("/projects/#{project.id}/repository/branches", user), - branch_name: 'new_design1', - ref: branch_sha - expect(response).to have_gitlab_http_status(201) - - post v3_api("/projects/#{project.id}/repository/branches", user), - branch_name: 'new_design1', - ref: branch_sha - - expect(response).to have_gitlab_http_status(400) - expect(json_response['message']).to eq('Branch already exists') - end - - it 'returns 400 if ref name is invalid' do - post v3_api("/projects/#{project.id}/repository/branches", user), - branch_name: 'new_design3', - ref: 'foo' - - expect(response).to have_gitlab_http_status(400) - expect(json_response['message']).to eq('Invalid reference name') - end - end -end diff --git a/spec/requests/api/v3/broadcast_messages_spec.rb b/spec/requests/api/v3/broadcast_messages_spec.rb deleted file mode 100644 index d9641011491..00000000000 --- a/spec/requests/api/v3/broadcast_messages_spec.rb +++ /dev/null @@ -1,32 +0,0 @@ -require 'spec_helper' - -describe API::V3::BroadcastMessages do - set(:user) { create(:user) } - set(:admin) { create(:admin) } - - describe 'DELETE /broadcast_messages/:id' do - set(:message) { create(:broadcast_message) } - - it 'returns a 401 for anonymous users' do - delete v3_api("/broadcast_messages/#{message.id}"), - attributes_for(:broadcast_message) - - expect(response).to have_gitlab_http_status(401) - end - - it 'returns a 403 for users' do - delete v3_api("/broadcast_messages/#{message.id}", user), - attributes_for(:broadcast_message) - - expect(response).to have_gitlab_http_status(403) - end - - it 'deletes the broadcast message for admins' do - expect do - delete v3_api("/broadcast_messages/#{message.id}", admin) - - expect(response).to have_gitlab_http_status(200) - end.to change { BroadcastMessage.count }.by(-1) - end - end -end diff --git a/spec/requests/api/v3/builds_spec.rb b/spec/requests/api/v3/builds_spec.rb deleted file mode 100644 index 485d7c2cc43..00000000000 --- a/spec/requests/api/v3/builds_spec.rb +++ /dev/null @@ -1,550 +0,0 @@ -require 'spec_helper' - -describe API::V3::Builds do - set(:user) { create(:user) } - let(:api_user) { user } - set(:project) { create(:project, :repository, creator: user, public_builds: false) } - let!(:developer) { create(:project_member, :developer, user: user, project: project) } - let(:reporter) { create(:project_member, :reporter, project: project) } - let(:guest) { create(:project_member, :guest, project: project) } - let(:pipeline) { create(:ci_empty_pipeline, project: project, sha: project.commit.id, ref: project.default_branch) } - let(:build) { create(:ci_build, pipeline: pipeline) } - - describe 'GET /projects/:id/builds ' do - let(:query) { '' } - - before do |example| - build - - create(:ci_build, :skipped, pipeline: pipeline) - - unless example.metadata[:skip_before_request] - get v3_api("/projects/#{project.id}/builds?#{query}", api_user) - end - end - - context 'authorized user' do - it 'returns project builds' do - expect(response).to have_gitlab_http_status(200) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array - end - - it 'returns correct values' do - expect(json_response).not_to be_empty - expect(json_response.first['commit']['id']).to eq project.commit.id - end - - it 'returns pipeline data' do - json_build = json_response.first - expect(json_build['pipeline']).not_to be_empty - expect(json_build['pipeline']['id']).to eq build.pipeline.id - expect(json_build['pipeline']['ref']).to eq build.pipeline.ref - expect(json_build['pipeline']['sha']).to eq build.pipeline.sha - expect(json_build['pipeline']['status']).to eq build.pipeline.status - end - - it 'avoids N+1 queries', :skip_before_request do - first_build = create(:ci_build, :artifacts, pipeline: pipeline) - first_build.runner = create(:ci_runner) - first_build.user = create(:user) - first_build.save - - control_count = ActiveRecord::QueryRecorder.new { go }.count - - second_pipeline = create(:ci_empty_pipeline, project: project, sha: project.commit.id, ref: project.default_branch) - second_build = create(:ci_build, :artifacts, pipeline: second_pipeline) - second_build.runner = create(:ci_runner) - second_build.user = create(:user) - second_build.save - - expect { go }.not_to exceed_query_limit(control_count) - end - - context 'filter project with one scope element' do - let(:query) { 'scope=pending' } - - it do - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - end - end - - context 'filter project with scope skipped' do - let(:query) { 'scope=skipped' } - let(:json_build) { json_response.first } - - it 'return builds with status skipped' do - expect(response).to have_gitlab_http_status 200 - expect(json_response).to be_an Array - expect(json_response.length).to eq 1 - expect(json_build['status']).to eq 'skipped' - end - end - - context 'filter project with array of scope elements' do - let(:query) { 'scope[0]=pending&scope[1]=running' } - - it do - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - end - end - - context 'respond 400 when scope contains invalid state' do - let(:query) { 'scope[0]=pending&scope[1]=unknown_status' } - - it { expect(response).to have_gitlab_http_status(400) } - end - end - - context 'unauthorized user' do - let(:api_user) { nil } - - it 'does not return project builds' do - expect(response).to have_gitlab_http_status(401) - end - end - - def go - get v3_api("/projects/#{project.id}/builds?#{query}", api_user) - end - end - - describe 'GET /projects/:id/repository/commits/:sha/builds' do - before do - build - end - - context 'when commit does not exist in repository' do - before do - get v3_api("/projects/#{project.id}/repository/commits/1a271fd1/builds", api_user) - end - - it 'responds with 404' do - expect(response).to have_gitlab_http_status(404) - end - end - - context 'when commit exists in repository' do - context 'when user is authorized' do - context 'when pipeline has jobs' do - before do - create(:ci_pipeline, project: project, sha: project.commit.id) - create(:ci_build, pipeline: pipeline) - create(:ci_build) - - get v3_api("/projects/#{project.id}/repository/commits/#{project.commit.id}/builds", api_user) - end - - it 'returns project jobs for specific commit' do - expect(response).to have_gitlab_http_status(200) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array - expect(json_response.size).to eq 2 - end - - it 'returns pipeline data' do - json_build = json_response.first - expect(json_build['pipeline']).not_to be_empty - expect(json_build['pipeline']['id']).to eq build.pipeline.id - expect(json_build['pipeline']['ref']).to eq build.pipeline.ref - expect(json_build['pipeline']['sha']).to eq build.pipeline.sha - expect(json_build['pipeline']['status']).to eq build.pipeline.status - end - end - - context 'when pipeline has no jobs' do - before do - branch_head = project.commit('feature').id - get v3_api("/projects/#{project.id}/repository/commits/#{branch_head}/builds", api_user) - end - - it 'returns an empty array' do - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response).to be_empty - end - end - end - - context 'when user is not authorized' do - before do - create(:ci_pipeline, project: project, sha: project.commit.id) - create(:ci_build, pipeline: pipeline) - - get v3_api("/projects/#{project.id}/repository/commits/#{project.commit.id}/builds", nil) - end - - it 'does not return project jobs' do - expect(response).to have_gitlab_http_status(401) - expect(json_response.except('message')).to be_empty - end - end - end - end - - describe 'GET /projects/:id/builds/:build_id' do - before do - get v3_api("/projects/#{project.id}/builds/#{build.id}", api_user) - end - - context 'authorized user' do - it 'returns specific job data' do - expect(response).to have_gitlab_http_status(200) - expect(json_response['name']).to eq('test') - end - - it 'returns pipeline data' do - json_build = json_response - expect(json_build['pipeline']).not_to be_empty - expect(json_build['pipeline']['id']).to eq build.pipeline.id - expect(json_build['pipeline']['ref']).to eq build.pipeline.ref - expect(json_build['pipeline']['sha']).to eq build.pipeline.sha - expect(json_build['pipeline']['status']).to eq build.pipeline.status - end - end - - context 'unauthorized user' do - let(:api_user) { nil } - - it 'does not return specific job data' do - expect(response).to have_gitlab_http_status(401) - end - end - end - - describe 'GET /projects/:id/builds/:build_id/artifacts' do - before do - stub_artifacts_object_storage - get v3_api("/projects/#{project.id}/builds/#{build.id}/artifacts", api_user) - end - - context 'job with artifacts' do - context 'when artifacts are stored locally' do - let(:build) { create(:ci_build, :artifacts, pipeline: pipeline) } - - context 'authorized user' do - let(:download_headers) do - { 'Content-Transfer-Encoding' => 'binary', - 'Content-Disposition' => 'attachment; filename=ci_build_artifacts.zip' } - end - - it 'returns specific job artifacts' do - expect(response).to have_http_status(200) - expect(response.headers.to_h).to include(download_headers) - expect(response.body).to match_file(build.artifacts_file.file.file) - end - end - end - - context 'when artifacts are stored remotely' do - let(:build) { create(:ci_build, pipeline: pipeline) } - let!(:artifact) { create(:ci_job_artifact, :archive, :remote_store, job: build) } - - it 'returns location redirect' do - get v3_api("/projects/#{project.id}/builds/#{build.id}/artifacts", api_user) - - expect(response).to have_gitlab_http_status(302) - end - end - - context 'unauthorized user' do - let(:api_user) { nil } - - it 'does not return specific job artifacts' do - expect(response).to have_gitlab_http_status(401) - end - end - end - - it 'does not return job artifacts if not uploaded' do - expect(response).to have_gitlab_http_status(404) - end - end - - describe 'GET /projects/:id/artifacts/:ref_name/download?job=name' do - let(:api_user) { reporter.user } - let(:build) { create(:ci_build, :artifacts, pipeline: pipeline) } - - before do - stub_artifacts_object_storage - build.success - end - - def path_for_ref(ref = pipeline.ref, job = build.name) - v3_api("/projects/#{project.id}/builds/artifacts/#{ref}/download?job=#{job}", api_user) - end - - context 'when not logged in' do - let(:api_user) { nil } - - before do - get path_for_ref - end - - it 'gives 401' do - expect(response).to have_gitlab_http_status(401) - end - end - - context 'when logging as guest' do - let(:api_user) { guest.user } - - before do - get path_for_ref - end - - it 'gives 403' do - expect(response).to have_gitlab_http_status(403) - end - end - - context 'non-existing job' do - shared_examples 'not found' do - it { expect(response).to have_gitlab_http_status(:not_found) } - end - - context 'has no such ref' do - before do - get path_for_ref('TAIL', build.name) - end - - it_behaves_like 'not found' - end - - context 'has no such job' do - before do - get path_for_ref(pipeline.ref, 'NOBUILD') - end - - it_behaves_like 'not found' - end - end - - context 'find proper job' do - shared_examples 'a valid file' do - context 'when artifacts are stored locally' do - let(:download_headers) do - { 'Content-Transfer-Encoding' => 'binary', - 'Content-Disposition' => - "attachment; filename=#{build.artifacts_file.filename}" } - end - - it { expect(response).to have_http_status(200) } - it { expect(response.headers.to_h).to include(download_headers) } - end - - context 'when artifacts are stored remotely' do - let(:build) { create(:ci_build, pipeline: pipeline) } - let!(:artifact) { create(:ci_job_artifact, :archive, :remote_store, job: build) } - - before do - build.reload - - get v3_api("/projects/#{project.id}/builds/#{build.id}/artifacts", api_user) - end - - it 'returns location redirect' do - expect(response).to have_http_status(302) - end - end - end - - context 'with regular branch' do - before do - pipeline.reload - pipeline.update(ref: 'master', - sha: project.commit('master').sha) - - get path_for_ref('master') - end - - it_behaves_like 'a valid file' - end - - context 'with branch name containing slash' do - before do - pipeline.reload - pipeline.update(ref: 'improve/awesome', - sha: project.commit('improve/awesome').sha) - end - - before do - get path_for_ref('improve/awesome') - end - - it_behaves_like 'a valid file' - end - end - end - - describe 'GET /projects/:id/builds/:build_id/trace' do - let(:build) { create(:ci_build, :trace_live, pipeline: pipeline) } - - before do - get v3_api("/projects/#{project.id}/builds/#{build.id}/trace", api_user) - end - - context 'authorized user' do - it 'returns specific job trace' do - expect(response).to have_gitlab_http_status(200) - expect(response.body).to eq(build.trace.raw) - end - end - - context 'unauthorized user' do - let(:api_user) { nil } - - it 'does not return specific job trace' do - expect(response).to have_gitlab_http_status(401) - end - end - end - - describe 'POST /projects/:id/builds/:build_id/cancel' do - before do - post v3_api("/projects/#{project.id}/builds/#{build.id}/cancel", api_user) - end - - context 'authorized user' do - context 'user with :update_build persmission' do - it 'cancels running or pending job' do - expect(response).to have_gitlab_http_status(201) - expect(project.builds.first.status).to eq('canceled') - end - end - - context 'user without :update_build permission' do - let(:api_user) { reporter.user } - - it 'does not cancel job' do - expect(response).to have_gitlab_http_status(403) - end - end - end - - context 'unauthorized user' do - let(:api_user) { nil } - - it 'does not cancel job' do - expect(response).to have_gitlab_http_status(401) - end - end - end - - describe 'POST /projects/:id/builds/:build_id/retry' do - let(:build) { create(:ci_build, :canceled, pipeline: pipeline) } - - before do - post v3_api("/projects/#{project.id}/builds/#{build.id}/retry", api_user) - end - - context 'authorized user' do - context 'user with :update_build permission' do - it 'retries non-running job' do - expect(response).to have_gitlab_http_status(201) - expect(project.builds.first.status).to eq('canceled') - expect(json_response['status']).to eq('pending') - end - end - - context 'user without :update_build permission' do - let(:api_user) { reporter.user } - - it 'does not retry job' do - expect(response).to have_gitlab_http_status(403) - end - end - end - - context 'unauthorized user' do - let(:api_user) { nil } - - it 'does not retry job' do - expect(response).to have_gitlab_http_status(401) - end - end - end - - describe 'POST /projects/:id/builds/:build_id/erase' do - before do - project.add_master(user) - - post v3_api("/projects/#{project.id}/builds/#{build.id}/erase", user) - end - - context 'job is erasable' do - let(:build) { create(:ci_build, :trace_artifact, :artifacts, :success, project: project, pipeline: pipeline) } - - it 'erases job content' do - expect(response.status).to eq 201 - expect(build).not_to have_trace - expect(build.artifacts_file.exists?).to be_falsy - expect(build.artifacts_metadata.exists?).to be_falsy - end - - it 'updates job' do - expect(build.reload.erased_at).to be_truthy - expect(build.reload.erased_by).to eq user - end - end - - context 'job is not erasable' do - let(:build) { create(:ci_build, :trace_live, project: project, pipeline: pipeline) } - - it 'responds with forbidden' do - expect(response.status).to eq 403 - end - end - end - - describe 'POST /projects/:id/builds/:build_id/artifacts/keep' do - before do - post v3_api("/projects/#{project.id}/builds/#{build.id}/artifacts/keep", user) - end - - context 'artifacts did not expire' do - let(:build) do - create(:ci_build, :trace_artifact, :artifacts, :success, - project: project, pipeline: pipeline, artifacts_expire_at: Time.now + 7.days) - end - - it 'keeps artifacts' do - expect(response.status).to eq 200 - expect(build.reload.artifacts_expire_at).to be_nil - end - end - - context 'no artifacts' do - let(:build) { create(:ci_build, project: project, pipeline: pipeline) } - - it 'responds with not found' do - expect(response.status).to eq 404 - end - end - end - - describe 'POST /projects/:id/builds/:build_id/play' do - before do - post v3_api("/projects/#{project.id}/builds/#{build.id}/play", user) - end - - context 'on an playable job' do - let(:build) { create(:ci_build, :manual, project: project, pipeline: pipeline) } - - it 'plays the job' do - expect(response).to have_gitlab_http_status 200 - expect(json_response['user']['id']).to eq(user.id) - expect(json_response['id']).to eq(build.id) - end - end - - context 'on a non-playable job' do - it 'returns a status code 400, Bad Request' do - expect(response).to have_gitlab_http_status 400 - expect(response.body).to match("Unplayable Job") - end - end - end -end diff --git a/spec/requests/api/v3/commits_spec.rb b/spec/requests/api/v3/commits_spec.rb deleted file mode 100644 index 9ef3b859001..00000000000 --- a/spec/requests/api/v3/commits_spec.rb +++ /dev/null @@ -1,603 +0,0 @@ -require 'spec_helper' -require 'mime/types' - -describe API::V3::Commits do - let(:user) { create(:user) } - let(:user2) { create(:user) } - let!(:project) { create(:project, :repository, creator: user, namespace: user.namespace) } - let!(:guest) { create(:project_member, :guest, user: user2, project: project) } - let!(:note) { create(:note_on_commit, author: user, project: project, commit_id: project.repository.commit.id, note: 'a comment on a commit') } - let!(:another_note) { create(:note_on_commit, author: user, project: project, commit_id: project.repository.commit.id, note: 'another comment on a commit') } - - before { project.add_reporter(user) } - - describe "List repository commits" do - context "authorized user" do - before { project.add_reporter(user2) } - - it "returns project commits" do - commit = project.repository.commit - get v3_api("/projects/#{project.id}/repository/commits", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.first['id']).to eq(commit.id) - expect(json_response.first['committer_name']).to eq(commit.committer_name) - expect(json_response.first['committer_email']).to eq(commit.committer_email) - end - end - - context "unauthorized user" do - it "does not return project commits" do - get v3_api("/projects/#{project.id}/repository/commits") - expect(response).to have_gitlab_http_status(401) - end - end - - context "since optional parameter" do - it "returns project commits since provided parameter" do - commits = project.repository.commits("master", limit: 2) - since = commits.second.created_at - - get v3_api("/projects/#{project.id}/repository/commits?since=#{since.utc.iso8601}", user) - - expect(json_response.size).to eq 2 - expect(json_response.first["id"]).to eq(commits.first.id) - expect(json_response.second["id"]).to eq(commits.second.id) - end - end - - context "until optional parameter" do - it "returns project commits until provided parameter" do - commits = project.repository.commits("master", limit: 20) - before = commits.second.created_at - - get v3_api("/projects/#{project.id}/repository/commits?until=#{before.utc.iso8601}", user) - - if commits.size == 20 - expect(json_response.size).to eq(20) - else - expect(json_response.size).to eq(commits.size - 1) - end - - expect(json_response.first["id"]).to eq(commits.second.id) - expect(json_response.second["id"]).to eq(commits.third.id) - end - end - - context "invalid xmlschema date parameters" do - it "returns an invalid parameter error message" do - get v3_api("/projects/#{project.id}/repository/commits?since=invalid-date", user) - - expect(response).to have_gitlab_http_status(400) - expect(json_response['error']).to eq('since is invalid') - end - end - - context "path optional parameter" do - it "returns project commits matching provided path parameter" do - path = 'files/ruby/popen.rb' - - get v3_api("/projects/#{project.id}/repository/commits?path=#{path}", user) - - expect(json_response.size).to eq(3) - expect(json_response.first["id"]).to eq("570e7b2abdd848b95f2f578043fc23bd6f6fd24d") - end - end - end - - describe "POST /projects/:id/repository/commits" do - let!(:url) { "/projects/#{project.id}/repository/commits" } - - it 'returns a 403 unauthorized for user without permissions' do - post v3_api(url, user2) - - expect(response).to have_gitlab_http_status(403) - end - - it 'returns a 400 bad request if no params are given' do - post v3_api(url, user) - - expect(response).to have_gitlab_http_status(400) - end - - describe 'create' do - let(:message) { 'Created file' } - let!(:invalid_c_params) do - { - branch_name: 'master', - commit_message: message, - actions: [ - { - action: 'create', - file_path: 'files/ruby/popen.rb', - content: 'puts 8' - } - ] - } - end - let!(:valid_c_params) do - { - branch_name: 'master', - commit_message: message, - actions: [ - { - action: 'create', - file_path: 'foo/bar/baz.txt', - content: 'puts 8' - } - ] - } - end - - it 'a new file in project repo' do - post v3_api(url, user), valid_c_params - - expect(response).to have_gitlab_http_status(201) - expect(json_response['title']).to eq(message) - expect(json_response['committer_name']).to eq(user.name) - expect(json_response['committer_email']).to eq(user.email) - end - - it 'returns a 400 bad request if file exists' do - post v3_api(url, user), invalid_c_params - - expect(response).to have_gitlab_http_status(400) - end - - context 'with project path containing a dot in URL' do - let!(:user) { create(:user, username: 'foo.bar') } - let(:url) { "/projects/#{CGI.escape(project.full_path)}/repository/commits" } - - it 'a new file in project repo' do - post v3_api(url, user), valid_c_params - - expect(response).to have_gitlab_http_status(201) - end - end - end - - describe 'delete' do - let(:message) { 'Deleted file' } - let!(:invalid_d_params) do - { - branch_name: 'markdown', - commit_message: message, - actions: [ - { - action: 'delete', - file_path: 'doc/api/projects.md' - } - ] - } - end - let!(:valid_d_params) do - { - branch_name: 'markdown', - commit_message: message, - actions: [ - { - action: 'delete', - file_path: 'doc/api/users.md' - } - ] - } - end - - it 'an existing file in project repo' do - post v3_api(url, user), valid_d_params - - expect(response).to have_gitlab_http_status(201) - expect(json_response['title']).to eq(message) - end - - it 'returns a 400 bad request if file does not exist' do - post v3_api(url, user), invalid_d_params - - expect(response).to have_gitlab_http_status(400) - end - end - - describe 'move' do - let(:message) { 'Moved file' } - let!(:invalid_m_params) do - { - branch_name: 'feature', - commit_message: message, - actions: [ - { - action: 'move', - file_path: 'CHANGELOG', - previous_path: 'VERSION', - content: '6.7.0.pre' - } - ] - } - end - let!(:valid_m_params) do - { - branch_name: 'feature', - commit_message: message, - actions: [ - { - action: 'move', - file_path: 'VERSION.txt', - previous_path: 'VERSION', - content: '6.7.0.pre' - } - ] - } - end - - it 'an existing file in project repo' do - post v3_api(url, user), valid_m_params - - expect(response).to have_gitlab_http_status(201) - expect(json_response['title']).to eq(message) - end - - it 'returns a 400 bad request if file does not exist' do - post v3_api(url, user), invalid_m_params - - expect(response).to have_gitlab_http_status(400) - end - end - - describe 'update' do - let(:message) { 'Updated file' } - let!(:invalid_u_params) do - { - branch_name: 'master', - commit_message: message, - actions: [ - { - action: 'update', - file_path: 'foo/bar.baz', - content: 'puts 8' - } - ] - } - end - let!(:valid_u_params) do - { - branch_name: 'master', - commit_message: message, - actions: [ - { - action: 'update', - file_path: 'files/ruby/popen.rb', - content: 'puts 8' - } - ] - } - end - - it 'an existing file in project repo' do - post v3_api(url, user), valid_u_params - - expect(response).to have_gitlab_http_status(201) - expect(json_response['title']).to eq(message) - end - - it 'returns a 400 bad request if file does not exist' do - post v3_api(url, user), invalid_u_params - - expect(response).to have_gitlab_http_status(400) - end - end - - context "multiple operations" do - let(:message) { 'Multiple actions' } - let!(:invalid_mo_params) do - { - branch_name: 'master', - commit_message: message, - actions: [ - { - action: 'create', - file_path: 'files/ruby/popen.rb', - content: 'puts 8' - }, - { - action: 'delete', - file_path: 'doc/v3_api/projects.md' - }, - { - action: 'move', - file_path: 'CHANGELOG', - previous_path: 'VERSION', - content: '6.7.0.pre' - }, - { - action: 'update', - file_path: 'foo/bar.baz', - content: 'puts 8' - } - ] - } - end - let!(:valid_mo_params) do - { - branch_name: 'master', - commit_message: message, - actions: [ - { - action: 'create', - file_path: 'foo/bar/baz.txt', - content: 'puts 8' - }, - { - action: 'delete', - file_path: 'Gemfile.zip' - }, - { - action: 'move', - file_path: 'VERSION.txt', - previous_path: 'VERSION', - content: '6.7.0.pre' - }, - { - action: 'update', - file_path: 'files/ruby/popen.rb', - content: 'puts 8' - } - ] - } - end - - it 'are commited as one in project repo' do - post v3_api(url, user), valid_mo_params - - expect(response).to have_gitlab_http_status(201) - expect(json_response['title']).to eq(message) - end - - it 'return a 400 bad request if there are any issues' do - post v3_api(url, user), invalid_mo_params - - expect(response).to have_gitlab_http_status(400) - end - end - end - - describe "Get a single commit" do - context "authorized user" do - it "returns a commit by sha" do - get v3_api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['id']).to eq(project.repository.commit.id) - expect(json_response['title']).to eq(project.repository.commit.title) - expect(json_response['stats']['additions']).to eq(project.repository.commit.stats.additions) - expect(json_response['stats']['deletions']).to eq(project.repository.commit.stats.deletions) - expect(json_response['stats']['total']).to eq(project.repository.commit.stats.total) - end - - it "returns a 404 error if not found" do - get v3_api("/projects/#{project.id}/repository/commits/invalid_sha", user) - expect(response).to have_gitlab_http_status(404) - end - - it "returns nil for commit without CI" do - get v3_api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['status']).to be_nil - end - - it "returns status for CI" do - pipeline = project.pipelines.create(source: :push, ref: 'master', sha: project.repository.commit.sha, protected: false) - pipeline.update(status: 'success') - - get v3_api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['status']).to eq(pipeline.status) - end - - it "returns status for CI when pipeline is created" do - project.pipelines.create(source: :push, ref: 'master', sha: project.repository.commit.sha, protected: false) - - get v3_api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['status']).to eq("created") - end - - context 'when stat param' do - let(:project_id) { project.id } - let(:commit_id) { project.repository.commit.id } - let(:route) { "/projects/#{project_id}/repository/commits/#{commit_id}" } - - it 'is not present return stats by default' do - get v3_api(route, user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to include 'stats' - end - - it "is false it does not include stats" do - get v3_api(route, user), stats: false - - expect(response).to have_gitlab_http_status(200) - expect(json_response).not_to include 'stats' - end - - it "is true it includes stats" do - get v3_api(route, user), stats: true - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to include 'stats' - end - end - end - - context "unauthorized user" do - it "does not return the selected commit" do - get v3_api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}") - expect(response).to have_gitlab_http_status(401) - end - end - end - - describe "Get the diff of a commit" do - context "authorized user" do - before { project.add_reporter(user2) } - - it "returns the diff of the selected commit" do - get v3_api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/diff", user) - expect(response).to have_gitlab_http_status(200) - - expect(json_response).to be_an Array - expect(json_response.length).to be >= 1 - expect(json_response.first.keys).to include "diff" - end - - it "returns a 404 error if invalid commit" do - get v3_api("/projects/#{project.id}/repository/commits/invalid_sha/diff", user) - expect(response).to have_gitlab_http_status(404) - end - end - - context "unauthorized user" do - it "does not return the diff of the selected commit" do - get v3_api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/diff") - expect(response).to have_gitlab_http_status(401) - end - end - end - - describe 'Get the comments of a commit' do - context 'authorized user' do - it 'returns merge_request comments' do - get v3_api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments", user) - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(2) - expect(json_response.first['note']).to eq('a comment on a commit') - expect(json_response.first['author']['id']).to eq(user.id) - end - - it 'returns a 404 error if merge_request_id not found' do - get v3_api("/projects/#{project.id}/repository/commits/1234ab/comments", user) - expect(response).to have_gitlab_http_status(404) - end - end - - context 'unauthorized user' do - it 'does not return the diff of the selected commit' do - get v3_api("/projects/#{project.id}/repository/commits/1234ab/comments") - expect(response).to have_gitlab_http_status(401) - end - end - end - - describe 'POST :id/repository/commits/:sha/cherry_pick' do - let(:master_pickable_commit) { project.commit('7d3b0f7cff5f37573aea97cebfd5692ea1689924') } - - context 'authorized user' do - it 'cherry picks a commit' do - post v3_api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick", user), branch: 'master' - - expect(response).to have_gitlab_http_status(201) - expect(json_response['title']).to eq(master_pickable_commit.title) - expect(json_response['message']).to eq(master_pickable_commit.cherry_pick_message(user)) - expect(json_response['author_name']).to eq(master_pickable_commit.author_name) - expect(json_response['committer_name']).to eq(user.name) - end - - it 'returns 400 if commit is already included in the target branch' do - post v3_api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick", user), branch: 'markdown' - - expect(response).to have_gitlab_http_status(400) - expect(json_response['message']).to include('Sorry, we cannot cherry-pick this commit automatically.') - end - - it 'returns 400 if you are not allowed to push to the target branch' do - project.add_developer(user2) - protected_branch = create(:protected_branch, project: project, name: 'feature') - - post v3_api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick", user2), branch: protected_branch.name - - expect(response).to have_gitlab_http_status(400) - expect(json_response['message']).to eq('You are not allowed to push into this branch') - end - - it 'returns 400 for missing parameters' do - post v3_api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick", user) - - expect(response).to have_gitlab_http_status(400) - expect(json_response['error']).to eq('branch is missing') - end - - it 'returns 404 if commit is not found' do - post v3_api("/projects/#{project.id}/repository/commits/abcd0123/cherry_pick", user), branch: 'master' - - expect(response).to have_gitlab_http_status(404) - expect(json_response['message']).to eq('404 Commit Not Found') - end - - it 'returns 404 if branch is not found' do - post v3_api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick", user), branch: 'foo' - - expect(response).to have_gitlab_http_status(404) - expect(json_response['message']).to eq('404 Branch Not Found') - end - - it 'returns 400 for missing parameters' do - post v3_api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick", user) - - expect(response).to have_gitlab_http_status(400) - expect(json_response['error']).to eq('branch is missing') - end - end - - context 'unauthorized user' do - it 'does not cherry pick the commit' do - post v3_api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick"), branch: 'master' - - expect(response).to have_gitlab_http_status(401) - end - end - end - - describe 'Post comment to commit' do - context 'authorized user' do - it 'returns comment' do - post v3_api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments", user), note: 'My comment' - expect(response).to have_gitlab_http_status(201) - expect(json_response['note']).to eq('My comment') - expect(json_response['path']).to be_nil - expect(json_response['line']).to be_nil - expect(json_response['line_type']).to be_nil - end - - it 'returns the inline comment' do - post v3_api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments", user), note: 'My comment', path: project.repository.commit.raw_diffs.first.new_path, line: 1, line_type: 'new' - - expect(response).to have_gitlab_http_status(201) - expect(json_response['note']).to eq('My comment') - expect(json_response['path']).to eq(project.repository.commit.raw_diffs.first.new_path) - expect(json_response['line']).to eq(1) - expect(json_response['line_type']).to eq('new') - end - - it 'returns 400 if note is missing' do - post v3_api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments", user) - expect(response).to have_gitlab_http_status(400) - end - - it 'returns 404 if note is attached to non existent commit' do - post v3_api("/projects/#{project.id}/repository/commits/1234ab/comments", user), note: 'My comment' - expect(response).to have_gitlab_http_status(404) - end - end - - context 'unauthorized user' do - it 'does not return the diff of the selected commit' do - post v3_api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments") - expect(response).to have_gitlab_http_status(401) - end - end - end -end diff --git a/spec/requests/api/v3/deploy_keys_spec.rb b/spec/requests/api/v3/deploy_keys_spec.rb deleted file mode 100644 index 501af587ad4..00000000000 --- a/spec/requests/api/v3/deploy_keys_spec.rb +++ /dev/null @@ -1,179 +0,0 @@ -require 'spec_helper' - -describe API::V3::DeployKeys do - let(:user) { create(:user) } - let(:admin) { create(:admin) } - let(:project) { create(:project, creator_id: user.id) } - let(:project2) { create(:project, creator_id: user.id) } - let(:deploy_key) { create(:deploy_key, public: true) } - - let!(:deploy_keys_project) do - create(:deploy_keys_project, project: project, deploy_key: deploy_key) - end - - describe 'GET /deploy_keys' do - context 'when unauthenticated' do - it 'should return authentication error' do - get v3_api('/deploy_keys') - - expect(response.status).to eq(401) - end - end - - context 'when authenticated as non-admin user' do - it 'should return a 403 error' do - get v3_api('/deploy_keys', user) - - expect(response.status).to eq(403) - end - end - - context 'when authenticated as admin' do - it 'should return all deploy keys' do - get v3_api('/deploy_keys', admin) - - expect(response.status).to eq(200) - expect(json_response).to be_an Array - expect(json_response.first['id']).to eq(deploy_keys_project.deploy_key.id) - end - end - end - - %w(deploy_keys keys).each do |path| - describe "GET /projects/:id/#{path}" do - before { deploy_key } - - it 'should return array of ssh keys' do - get v3_api("/projects/#{project.id}/#{path}", admin) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.first['title']).to eq(deploy_key.title) - end - end - - describe "GET /projects/:id/#{path}/:key_id" do - it 'should return a single key' do - get v3_api("/projects/#{project.id}/#{path}/#{deploy_key.id}", admin) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['title']).to eq(deploy_key.title) - end - - it 'should return 404 Not Found with invalid ID' do - get v3_api("/projects/#{project.id}/#{path}/404", admin) - - expect(response).to have_gitlab_http_status(404) - end - end - - describe "POST /projects/:id/deploy_keys" do - it 'should not create an invalid ssh key' do - post v3_api("/projects/#{project.id}/#{path}", admin), { title: 'invalid key' } - - expect(response).to have_gitlab_http_status(400) - expect(json_response['error']).to eq('key is missing') - end - - it 'should not create a key without title' do - post v3_api("/projects/#{project.id}/#{path}", admin), key: 'some key' - - expect(response).to have_gitlab_http_status(400) - expect(json_response['error']).to eq('title is missing') - end - - it 'should create new ssh key' do - key_attrs = attributes_for :another_key - - expect do - post v3_api("/projects/#{project.id}/#{path}", admin), key_attrs - end.to change { project.deploy_keys.count }.by(1) - end - - it 'returns an existing ssh key when attempting to add a duplicate' do - expect do - post v3_api("/projects/#{project.id}/#{path}", admin), { key: deploy_key.key, title: deploy_key.title } - end.not_to change { project.deploy_keys.count } - - expect(response).to have_gitlab_http_status(201) - end - - it 'joins an existing ssh key to a new project' do - expect do - post v3_api("/projects/#{project2.id}/#{path}", admin), { key: deploy_key.key, title: deploy_key.title } - end.to change { project2.deploy_keys.count }.by(1) - - expect(response).to have_gitlab_http_status(201) - end - - it 'accepts can_push parameter' do - key_attrs = attributes_for(:another_key).merge(can_push: true) - - post v3_api("/projects/#{project.id}/#{path}", admin), key_attrs - - expect(response).to have_gitlab_http_status(201) - expect(json_response['can_push']).to eq(true) - end - end - - describe "DELETE /projects/:id/#{path}/:key_id" do - before { deploy_key } - - it 'should delete existing key' do - expect do - delete v3_api("/projects/#{project.id}/#{path}/#{deploy_key.id}", admin) - end.to change { project.deploy_keys.count }.by(-1) - end - - it 'should return 404 Not Found with invalid ID' do - delete v3_api("/projects/#{project.id}/#{path}/404", admin) - - expect(response).to have_gitlab_http_status(404) - end - end - - describe "POST /projects/:id/#{path}/:key_id/enable" do - let(:project2) { create(:project) } - - context 'when the user can admin the project' do - it 'enables the key' do - expect do - post v3_api("/projects/#{project2.id}/#{path}/#{deploy_key.id}/enable", admin) - end.to change { project2.deploy_keys.count }.from(0).to(1) - - expect(response).to have_gitlab_http_status(201) - expect(json_response['id']).to eq(deploy_key.id) - end - end - - context 'when authenticated as non-admin user' do - it 'should return a 404 error' do - post v3_api("/projects/#{project2.id}/#{path}/#{deploy_key.id}/enable", user) - - expect(response).to have_gitlab_http_status(404) - end - end - end - - describe "DELETE /projects/:id/deploy_keys/:key_id/disable" do - context 'when the user can admin the project' do - it 'disables the key' do - expect do - delete v3_api("/projects/#{project.id}/#{path}/#{deploy_key.id}/disable", admin) - end.to change { project.deploy_keys.count }.from(1).to(0) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['id']).to eq(deploy_key.id) - end - end - - context 'when authenticated as non-admin user' do - it 'should return a 404 error' do - delete v3_api("/projects/#{project.id}/#{path}/#{deploy_key.id}/disable", user) - - expect(response).to have_gitlab_http_status(404) - end - end - end - end -end diff --git a/spec/requests/api/v3/deployments_spec.rb b/spec/requests/api/v3/deployments_spec.rb deleted file mode 100644 index ac86fbea498..00000000000 --- a/spec/requests/api/v3/deployments_spec.rb +++ /dev/null @@ -1,69 +0,0 @@ -require 'spec_helper' - -describe API::V3::Deployments do - let(:user) { create(:user) } - let(:non_member) { create(:user) } - let(:project) { deployment.environment.project } - let!(:deployment) { create(:deployment) } - - before do - project.add_master(user) - end - - shared_examples 'a paginated resources' do - before do - # Fires the request - request - end - - it 'has pagination headers' do - expect(response).to include_pagination_headers - end - end - - describe 'GET /projects/:id/deployments' do - context 'as member of the project' do - it_behaves_like 'a paginated resources' do - let(:request) { get v3_api("/projects/#{project.id}/deployments", user) } - end - - it 'returns projects deployments' do - get v3_api("/projects/#{project.id}/deployments", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.size).to eq(1) - expect(json_response.first['iid']).to eq(deployment.iid) - expect(json_response.first['sha']).to match /\A\h{40}\z/ - end - end - - context 'as non member' do - it 'returns a 404 status code' do - get v3_api("/projects/#{project.id}/deployments", non_member) - - expect(response).to have_gitlab_http_status(404) - end - end - end - - describe 'GET /projects/:id/deployments/:deployment_id' do - context 'as a member of the project' do - it 'returns the projects deployment' do - get v3_api("/projects/#{project.id}/deployments/#{deployment.id}", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['sha']).to match /\A\h{40}\z/ - expect(json_response['id']).to eq(deployment.id) - end - end - - context 'as non member' do - it 'returns a 404 status code' do - get v3_api("/projects/#{project.id}/deployments/#{deployment.id}", non_member) - - expect(response).to have_gitlab_http_status(404) - end - end - end -end diff --git a/spec/requests/api/v3/environments_spec.rb b/spec/requests/api/v3/environments_spec.rb deleted file mode 100644 index 68be5256b64..00000000000 --- a/spec/requests/api/v3/environments_spec.rb +++ /dev/null @@ -1,163 +0,0 @@ -require 'spec_helper' - -describe API::V3::Environments do - let(:user) { create(:user) } - let(:non_member) { create(:user) } - let(:project) { create(:project, :private, namespace: user.namespace) } - let!(:environment) { create(:environment, project: project) } - - before do - project.add_master(user) - end - - shared_examples 'a paginated resources' do - before do - # Fires the request - request - end - - it 'has pagination headers' do - expect(response.headers).to include('X-Total') - expect(response.headers).to include('X-Total-Pages') - expect(response.headers).to include('X-Per-Page') - expect(response.headers).to include('X-Page') - expect(response.headers).to include('X-Next-Page') - expect(response.headers).to include('X-Prev-Page') - expect(response.headers).to include('Link') - end - end - - describe 'GET /projects/:id/environments' do - context 'as member of the project' do - it_behaves_like 'a paginated resources' do - let(:request) { get v3_api("/projects/#{project.id}/environments", user) } - end - - it 'returns project environments' do - get v3_api("/projects/#{project.id}/environments", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.size).to eq(1) - expect(json_response.first['name']).to eq(environment.name) - expect(json_response.first['external_url']).to eq(environment.external_url) - expect(json_response.first['project']['id']).to eq(project.id) - expect(json_response.first['project']['visibility_level']).to be_present - end - end - - context 'as non member' do - it 'returns a 404 status code' do - get v3_api("/projects/#{project.id}/environments", non_member) - - expect(response).to have_gitlab_http_status(404) - end - end - end - - describe 'POST /projects/:id/environments' do - context 'as a member' do - it 'creates a environment with valid params' do - post v3_api("/projects/#{project.id}/environments", user), name: "mepmep" - - expect(response).to have_gitlab_http_status(201) - expect(json_response['name']).to eq('mepmep') - expect(json_response['slug']).to eq('mepmep') - expect(json_response['external']).to be nil - end - - it 'requires name to be passed' do - post v3_api("/projects/#{project.id}/environments", user), external_url: 'test.gitlab.com' - - expect(response).to have_gitlab_http_status(400) - end - - it 'returns a 400 if environment already exists' do - post v3_api("/projects/#{project.id}/environments", user), name: environment.name - - expect(response).to have_gitlab_http_status(400) - end - - it 'returns a 400 if slug is specified' do - post v3_api("/projects/#{project.id}/environments", user), name: "foo", slug: "foo" - - expect(response).to have_gitlab_http_status(400) - expect(json_response["error"]).to eq("slug is automatically generated and cannot be changed") - end - end - - context 'a non member' do - it 'rejects the request' do - post v3_api("/projects/#{project.id}/environments", non_member), name: 'gitlab.com' - - expect(response).to have_gitlab_http_status(404) - end - - it 'returns a 400 when the required params are missing' do - post v3_api("/projects/12345/environments", non_member), external_url: 'http://env.git.com' - end - end - end - - describe 'PUT /projects/:id/environments/:environment_id' do - it 'returns a 200 if name and external_url are changed' do - url = 'https://mepmep.whatever.ninja' - put v3_api("/projects/#{project.id}/environments/#{environment.id}", user), - name: 'Mepmep', external_url: url - - expect(response).to have_gitlab_http_status(200) - expect(json_response['name']).to eq('Mepmep') - expect(json_response['external_url']).to eq(url) - end - - it "won't allow slug to be changed" do - slug = environment.slug - api_url = v3_api("/projects/#{project.id}/environments/#{environment.id}", user) - put api_url, slug: slug + "-foo" - - expect(response).to have_gitlab_http_status(400) - expect(json_response["error"]).to eq("slug is automatically generated and cannot be changed") - end - - it "won't update the external_url if only the name is passed" do - url = environment.external_url - put v3_api("/projects/#{project.id}/environments/#{environment.id}", user), - name: 'Mepmep' - - expect(response).to have_gitlab_http_status(200) - expect(json_response['name']).to eq('Mepmep') - expect(json_response['external_url']).to eq(url) - end - - it 'returns a 404 if the environment does not exist' do - put v3_api("/projects/#{project.id}/environments/12345", user) - - expect(response).to have_gitlab_http_status(404) - end - end - - describe 'DELETE /projects/:id/environments/:environment_id' do - context 'as a master' do - it 'returns a 200 for an existing environment' do - delete v3_api("/projects/#{project.id}/environments/#{environment.id}", user) - - expect(response).to have_gitlab_http_status(200) - end - - it 'returns a 404 for non existing id' do - delete v3_api("/projects/#{project.id}/environments/12345", user) - - expect(response).to have_gitlab_http_status(404) - expect(json_response['message']).to eq('404 Not found') - end - end - - context 'a non member' do - it 'rejects the request' do - delete v3_api("/projects/#{project.id}/environments/#{environment.id}", non_member) - - expect(response).to have_gitlab_http_status(404) - end - end - end -end diff --git a/spec/requests/api/v3/files_spec.rb b/spec/requests/api/v3/files_spec.rb deleted file mode 100644 index 26a3d8870a0..00000000000 --- a/spec/requests/api/v3/files_spec.rb +++ /dev/null @@ -1,283 +0,0 @@ -require 'spec_helper' - -describe API::V3::Files do - # I have to remove periods from the end of the name - # This happened when the user's name had a suffix (i.e. "Sr.") - # This seems to be what git does under the hood. For example, this commit: - # - # $ git commit --author='Foo Sr. ' -m 'Where's my trailing period?' - # - # results in this: - # - # $ git show --pretty - # ... - # Author: Foo Sr - # ... - - let(:user) { create(:user) } - let!(:project) { create(:project, :repository, namespace: user.namespace ) } - let(:guest) { create(:user) { |u| project.add_guest(u) } } - let(:file_path) { 'files/ruby/popen.rb' } - let(:params) do - { - file_path: file_path, - ref: 'master' - } - end - let(:author_email) { 'user@example.org' } - let(:author_name) { 'John Doe' } - - before { project.add_developer(user) } - - describe "GET /projects/:id/repository/files" do - let(:route) { "/projects/#{project.id}/repository/files" } - - shared_examples_for 'repository files' do - it "returns file info" do - get v3_api(route, current_user), params - - expect(response).to have_gitlab_http_status(200) - expect(json_response['file_path']).to eq(file_path) - expect(json_response['file_name']).to eq('popen.rb') - expect(json_response['last_commit_id']).to eq('570e7b2abdd848b95f2f578043fc23bd6f6fd24d') - expect(Base64.decode64(json_response['content']).lines.first).to eq("require 'fileutils'\n") - end - - context 'when no params are given' do - it_behaves_like '400 response' do - let(:request) { get v3_api(route, current_user) } - end - end - - context 'when file_path does not exist' do - let(:params) do - { - file_path: 'app/models/application.rb', - ref: 'master' - } - end - - it_behaves_like '404 response' do - let(:request) { get v3_api(route, current_user), params } - let(:message) { '404 File Not Found' } - end - end - - context 'when repository is disabled' do - include_context 'disabled repository' - - it_behaves_like '403 response' do - let(:request) { get v3_api(route, current_user), params } - end - end - end - - context 'when unauthenticated', 'and project is public' do - it_behaves_like 'repository files' do - let(:project) { create(:project, :public, :repository) } - let(:current_user) { nil } - end - end - - context 'when unauthenticated', 'and project is private' do - it_behaves_like '404 response' do - let(:request) { get v3_api(route), params } - let(:message) { '404 Project Not Found' } - end - end - - context 'when authenticated', 'as a developer' do - it_behaves_like 'repository files' do - let(:current_user) { user } - end - end - - context 'when authenticated', 'as a guest' do - it_behaves_like '403 response' do - let(:request) { get v3_api(route, guest), params } - end - end - end - - describe "POST /projects/:id/repository/files" do - let(:valid_params) do - { - file_path: 'newfile.rb', - branch_name: 'master', - content: 'puts 8', - commit_message: 'Added newfile' - } - end - - it "creates a new file in project repo" do - post v3_api("/projects/#{project.id}/repository/files", user), valid_params - - expect(response).to have_gitlab_http_status(201) - expect(json_response['file_path']).to eq('newfile.rb') - last_commit = project.repository.commit.raw - expect(last_commit.author_email).to eq(user.email) - expect(last_commit.author_name).to eq(user.name) - end - - it "returns a 400 bad request if no params given" do - post v3_api("/projects/#{project.id}/repository/files", user) - - expect(response).to have_gitlab_http_status(400) - end - - it "returns a 400 if editor fails to create file" do - allow_any_instance_of(Repository).to receive(:create_file) - .and_raise(Gitlab::Git::CommitError, 'Cannot create file') - - post v3_api("/projects/#{project.id}/repository/files", user), valid_params - - expect(response).to have_gitlab_http_status(400) - end - - context "when specifying an author" do - it "creates a new file with the specified author" do - valid_params.merge!(author_email: author_email, author_name: author_name) - - post v3_api("/projects/#{project.id}/repository/files", user), valid_params - - expect(response).to have_gitlab_http_status(201) - last_commit = project.repository.commit.raw - expect(last_commit.author_email).to eq(author_email) - expect(last_commit.author_name).to eq(author_name) - end - end - - context 'when the repo is empty' do - let!(:project) { create(:project_empty_repo, namespace: user.namespace ) } - - it "creates a new file in project repo" do - post v3_api("/projects/#{project.id}/repository/files", user), valid_params - - expect(response).to have_gitlab_http_status(201) - expect(json_response['file_path']).to eq('newfile.rb') - last_commit = project.repository.commit.raw - expect(last_commit.author_email).to eq(user.email) - expect(last_commit.author_name).to eq(user.name) - end - end - end - - describe "PUT /projects/:id/repository/files" do - let(:valid_params) do - { - file_path: file_path, - branch_name: 'master', - content: 'puts 8', - commit_message: 'Changed file' - } - end - - it "updates existing file in project repo" do - put v3_api("/projects/#{project.id}/repository/files", user), valid_params - - expect(response).to have_gitlab_http_status(200) - expect(json_response['file_path']).to eq(file_path) - last_commit = project.repository.commit.raw - expect(last_commit.author_email).to eq(user.email) - expect(last_commit.author_name).to eq(user.name) - end - - it "returns a 400 bad request if no params given" do - put v3_api("/projects/#{project.id}/repository/files", user) - - expect(response).to have_gitlab_http_status(400) - end - - context "when specifying an author" do - it "updates a file with the specified author" do - valid_params.merge!(author_email: author_email, author_name: author_name, content: "New content") - - put v3_api("/projects/#{project.id}/repository/files", user), valid_params - - expect(response).to have_gitlab_http_status(200) - last_commit = project.repository.commit.raw - expect(last_commit.author_email).to eq(author_email) - expect(last_commit.author_name).to eq(author_name) - end - end - end - - describe "DELETE /projects/:id/repository/files" do - let(:valid_params) do - { - file_path: file_path, - branch_name: 'master', - commit_message: 'Changed file' - } - end - - it "deletes existing file in project repo" do - delete v3_api("/projects/#{project.id}/repository/files", user), valid_params - - expect(response).to have_gitlab_http_status(200) - expect(json_response['file_path']).to eq(file_path) - last_commit = project.repository.commit.raw - expect(last_commit.author_email).to eq(user.email) - expect(last_commit.author_name).to eq(user.name) - end - - it "returns a 400 bad request if no params given" do - delete v3_api("/projects/#{project.id}/repository/files", user) - - expect(response).to have_gitlab_http_status(400) - end - - it "returns a 400 if fails to delete file" do - allow_any_instance_of(Repository).to receive(:delete_file).and_raise(Gitlab::Git::CommitError, 'Cannot delete file') - - delete v3_api("/projects/#{project.id}/repository/files", user), valid_params - - expect(response).to have_gitlab_http_status(400) - end - - context "when specifying an author" do - it "removes a file with the specified author" do - valid_params.merge!(author_email: author_email, author_name: author_name) - - delete v3_api("/projects/#{project.id}/repository/files", user), valid_params - - expect(response).to have_gitlab_http_status(200) - last_commit = project.repository.commit.raw - expect(last_commit.author_email).to eq(author_email) - expect(last_commit.author_name).to eq(author_name) - end - end - end - - describe "POST /projects/:id/repository/files with binary file" do - let(:file_path) { 'test.bin' } - let(:put_params) do - { - file_path: file_path, - branch_name: 'master', - content: 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAAACnej3aAAAAAXRSTlMAQObYZgAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII=', - commit_message: 'Binary file with a \n should not be touched', - encoding: 'base64' - } - end - let(:get_params) do - { - file_path: file_path, - ref: 'master' - } - end - - before do - post v3_api("/projects/#{project.id}/repository/files", user), put_params - end - - it "remains unchanged" do - get v3_api("/projects/#{project.id}/repository/files", user), get_params - - expect(response).to have_gitlab_http_status(200) - expect(json_response['file_path']).to eq(file_path) - expect(json_response['file_name']).to eq(file_path) - expect(json_response['content']).to eq(put_params[:content]) - end - end -end diff --git a/spec/requests/api/v3/groups_spec.rb b/spec/requests/api/v3/groups_spec.rb deleted file mode 100644 index 34d4b8e9565..00000000000 --- a/spec/requests/api/v3/groups_spec.rb +++ /dev/null @@ -1,566 +0,0 @@ -require 'spec_helper' - -describe API::V3::Groups do - include UploadHelpers - - let(:user1) { create(:user, can_create_group: false) } - let(:user2) { create(:user) } - let(:user3) { create(:user) } - let(:admin) { create(:admin) } - let!(:group1) { create(:group, avatar: File.open(uploaded_image_temp_path)) } - let!(:group2) { create(:group, :private) } - let!(:project1) { create(:project, namespace: group1) } - let!(:project2) { create(:project, namespace: group2) } - let!(:project3) { create(:project, namespace: group1, path: 'test', visibility_level: Gitlab::VisibilityLevel::PRIVATE) } - - before do - group1.add_owner(user1) - group2.add_owner(user2) - end - - describe "GET /groups" do - context "when unauthenticated" do - it "returns authentication error" do - get v3_api("/groups") - - expect(response).to have_gitlab_http_status(401) - end - end - - context "when authenticated as user" do - it "normal user: returns an array of groups of user1" do - get v3_api("/groups", user1) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(1) - expect(json_response) - .to satisfy_one { |group| group['name'] == group1.name } - end - - it "does not include statistics" do - get v3_api("/groups", user1), statistics: true - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.first).not_to include 'statistics' - end - end - - context "when authenticated as admin" do - it "admin: returns an array of all groups" do - get v3_api("/groups", admin) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(2) - end - - it "does not include statistics by default" do - get v3_api("/groups", admin) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.first).not_to include('statistics') - end - - it "includes statistics if requested" do - attributes = { - storage_size: 702, - repository_size: 123, - lfs_objects_size: 234, - build_artifacts_size: 345 - }.stringify_keys - - project1.statistics.update!(attributes) - - get v3_api("/groups", admin), statistics: true - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response) - .to satisfy_one { |group| group['statistics'] == attributes } - end - end - - context "when using skip_groups in request" do - it "returns all groups excluding skipped groups" do - get v3_api("/groups", admin), skip_groups: [group2.id] - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(1) - end - end - - context "when using all_available in request" do - let(:response_groups) { json_response.map { |group| group['name'] } } - - it "returns all groups you have access to" do - public_group = create :group, :public - - get v3_api("/groups", user1), all_available: true - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(response_groups).to contain_exactly(public_group.name, group1.name) - end - end - - context "when using sorting" do - let(:group3) { create(:group, name: "a#{group1.name}", path: "z#{group1.path}") } - let(:response_groups) { json_response.map { |group| group['name'] } } - - before do - group3.add_owner(user1) - end - - it "sorts by name ascending by default" do - get v3_api("/groups", user1) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(response_groups).to eq([group3.name, group1.name]) - end - - it "sorts in descending order when passed" do - get v3_api("/groups", user1), sort: "desc" - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(response_groups).to eq([group1.name, group3.name]) - end - - it "sorts by the order_by param" do - get v3_api("/groups", user1), order_by: "path" - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(response_groups).to eq([group1.name, group3.name]) - end - end - end - - describe 'GET /groups/owned' do - context 'when unauthenticated' do - it 'returns authentication error' do - get v3_api('/groups/owned') - - expect(response).to have_gitlab_http_status(401) - end - end - - context 'when authenticated as group owner' do - it 'returns an array of groups the user owns' do - get v3_api('/groups/owned', user2) - - expect(response).to have_gitlab_http_status(200) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array - expect(json_response.first['name']).to eq(group2.name) - end - end - end - - describe "GET /groups/:id" do - context "when authenticated as user" do - it "returns one of user1's groups" do - project = create(:project, namespace: group2, path: 'Foo') - create(:project_group_link, project: project, group: group1) - - get v3_api("/groups/#{group1.id}", user1) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['id']).to eq(group1.id) - expect(json_response['name']).to eq(group1.name) - expect(json_response['path']).to eq(group1.path) - expect(json_response['description']).to eq(group1.description) - expect(json_response['visibility_level']).to eq(group1.visibility_level) - expect(json_response['avatar_url']).to eq(group1.avatar_url(only_path: false)) - expect(json_response['web_url']).to eq(group1.web_url) - expect(json_response['request_access_enabled']).to eq(group1.request_access_enabled) - expect(json_response['full_name']).to eq(group1.full_name) - expect(json_response['full_path']).to eq(group1.full_path) - expect(json_response['parent_id']).to eq(group1.parent_id) - expect(json_response['projects']).to be_an Array - expect(json_response['projects'].length).to eq(2) - expect(json_response['shared_projects']).to be_an Array - expect(json_response['shared_projects'].length).to eq(1) - expect(json_response['shared_projects'][0]['id']).to eq(project.id) - end - - it "does not return a non existing group" do - get v3_api("/groups/1328", user1) - - expect(response).to have_gitlab_http_status(404) - end - - it "does not return a group not attached to user1" do - get v3_api("/groups/#{group2.id}", user1) - - expect(response).to have_gitlab_http_status(404) - end - end - - context "when authenticated as admin" do - it "returns any existing group" do - get v3_api("/groups/#{group2.id}", admin) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['name']).to eq(group2.name) - end - - it "does not return a non existing group" do - get v3_api("/groups/1328", admin) - - expect(response).to have_gitlab_http_status(404) - end - end - - context 'when using group path in URL' do - it 'returns any existing group' do - get v3_api("/groups/#{group1.path}", admin) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['name']).to eq(group1.name) - end - - it 'does not return a non existing group' do - get v3_api('/groups/unknown', admin) - - expect(response).to have_gitlab_http_status(404) - end - - it 'does not return a group not attached to user1' do - get v3_api("/groups/#{group2.path}", user1) - - expect(response).to have_gitlab_http_status(404) - end - end - end - - describe 'PUT /groups/:id' do - let(:new_group_name) { 'New Group'} - - context 'when authenticated as the group owner' do - it 'updates the group' do - put v3_api("/groups/#{group1.id}", user1), name: new_group_name, request_access_enabled: true - - expect(response).to have_gitlab_http_status(200) - expect(json_response['name']).to eq(new_group_name) - expect(json_response['request_access_enabled']).to eq(true) - end - - it 'returns 404 for a non existing group' do - put v3_api('/groups/1328', user1), name: new_group_name - - expect(response).to have_gitlab_http_status(404) - end - end - - context 'when authenticated as the admin' do - it 'updates the group' do - put v3_api("/groups/#{group1.id}", admin), name: new_group_name - - expect(response).to have_gitlab_http_status(200) - expect(json_response['name']).to eq(new_group_name) - end - end - - context 'when authenticated as an user that can see the group' do - it 'does not updates the group' do - put v3_api("/groups/#{group1.id}", user2), name: new_group_name - - expect(response).to have_gitlab_http_status(403) - end - end - - context 'when authenticated as an user that cannot see the group' do - it 'returns 404 when trying to update the group' do - put v3_api("/groups/#{group2.id}", user1), name: new_group_name - - expect(response).to have_gitlab_http_status(404) - end - end - end - - describe "GET /groups/:id/projects" do - context "when authenticated as user" do - it "returns the group's projects" do - get v3_api("/groups/#{group1.id}/projects", user1) - - expect(response).to have_gitlab_http_status(200) - expect(json_response.length).to eq(2) - project_names = json_response.map { |proj| proj['name'] } - expect(project_names).to match_array([project1.name, project3.name]) - expect(json_response.first['visibility_level']).to be_present - end - - it "returns the group's projects with simple representation" do - get v3_api("/groups/#{group1.id}/projects", user1), simple: true - - expect(response).to have_gitlab_http_status(200) - expect(json_response.length).to eq(2) - project_names = json_response.map { |proj| proj['name'] } - expect(project_names).to match_array([project1.name, project3.name]) - expect(json_response.first['visibility_level']).not_to be_present - end - - it 'filters the groups projects' do - public_project = create(:project, :public, path: 'test1', group: group1) - - get v3_api("/groups/#{group1.id}/projects", user1), visibility: 'public' - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an(Array) - expect(json_response.length).to eq(1) - expect(json_response.first['name']).to eq(public_project.name) - end - - it "does not return a non existing group" do - get v3_api("/groups/1328/projects", user1) - - expect(response).to have_gitlab_http_status(404) - end - - it "does not return a group not attached to user1" do - get v3_api("/groups/#{group2.id}/projects", user1) - - expect(response).to have_gitlab_http_status(404) - end - - it "only returns projects to which user has access" do - project3.add_developer(user3) - - get v3_api("/groups/#{group1.id}/projects", user3) - - expect(response).to have_gitlab_http_status(200) - expect(json_response.length).to eq(1) - expect(json_response.first['name']).to eq(project3.name) - end - - it 'only returns the projects owned by user' do - project2.group.add_owner(user3) - - get v3_api("/groups/#{project2.group.id}/projects", user3), owned: true - - expect(response).to have_gitlab_http_status(200) - expect(json_response.length).to eq(1) - expect(json_response.first['name']).to eq(project2.name) - end - - it 'only returns the projects starred by user' do - user1.starred_projects = [project1] - - get v3_api("/groups/#{group1.id}/projects", user1), starred: true - - expect(response).to have_gitlab_http_status(200) - expect(json_response.length).to eq(1) - expect(json_response.first['name']).to eq(project1.name) - end - end - - context "when authenticated as admin" do - it "returns any existing group" do - get v3_api("/groups/#{group2.id}/projects", admin) - - expect(response).to have_gitlab_http_status(200) - expect(json_response.length).to eq(1) - expect(json_response.first['name']).to eq(project2.name) - end - - it "does not return a non existing group" do - get v3_api("/groups/1328/projects", admin) - - expect(response).to have_gitlab_http_status(404) - end - end - - context 'when using group path in URL' do - it 'returns any existing group' do - get v3_api("/groups/#{group1.path}/projects", admin) - - expect(response).to have_gitlab_http_status(200) - project_names = json_response.map { |proj| proj['name'] } - expect(project_names).to match_array([project1.name, project3.name]) - end - - it 'does not return a non existing group' do - get v3_api('/groups/unknown/projects', admin) - - expect(response).to have_gitlab_http_status(404) - end - - it 'does not return a group not attached to user1' do - get v3_api("/groups/#{group2.path}/projects", user1) - - expect(response).to have_gitlab_http_status(404) - end - end - end - - describe "POST /groups" do - context "when authenticated as user without group permissions" do - it "does not create group" do - post v3_api("/groups", user1), attributes_for(:group) - - expect(response).to have_gitlab_http_status(403) - end - end - - context "when authenticated as user with group permissions" do - it "creates group" do - group = attributes_for(:group, { request_access_enabled: false }) - - post v3_api("/groups", user3), group - - expect(response).to have_gitlab_http_status(201) - - expect(json_response["name"]).to eq(group[:name]) - expect(json_response["path"]).to eq(group[:path]) - expect(json_response["request_access_enabled"]).to eq(group[:request_access_enabled]) - end - - it "creates a nested group", :nested_groups do - parent = create(:group) - parent.add_owner(user3) - group = attributes_for(:group, { parent_id: parent.id }) - - post v3_api("/groups", user3), group - - expect(response).to have_gitlab_http_status(201) - - expect(json_response["full_path"]).to eq("#{parent.path}/#{group[:path]}") - expect(json_response["parent_id"]).to eq(parent.id) - end - - it "does not create group, duplicate" do - post v3_api("/groups", user3), { name: 'Duplicate Test', path: group2.path } - - expect(response).to have_gitlab_http_status(400) - expect(response.message).to eq("Bad Request") - end - - it "returns 400 bad request error if name not given" do - post v3_api("/groups", user3), { path: group2.path } - - expect(response).to have_gitlab_http_status(400) - end - - it "returns 400 bad request error if path not given" do - post v3_api("/groups", user3), { name: 'test' } - - expect(response).to have_gitlab_http_status(400) - end - end - end - - describe "DELETE /groups/:id" do - context "when authenticated as user" do - it "removes group" do - Sidekiq::Testing.fake! do - expect { delete v3_api("/groups/#{group1.id}", user1) }.to change(GroupDestroyWorker.jobs, :size).by(1) - end - - expect(response).to have_gitlab_http_status(202) - end - - it "does not remove a group if not an owner" do - user4 = create(:user) - group1.add_master(user4) - - delete v3_api("/groups/#{group1.id}", user3) - - expect(response).to have_gitlab_http_status(403) - end - - it "does not remove a non existing group" do - delete v3_api("/groups/1328", user1) - - expect(response).to have_gitlab_http_status(404) - end - - it "does not remove a group not attached to user1" do - delete v3_api("/groups/#{group2.id}", user1) - - expect(response).to have_gitlab_http_status(404) - end - end - - context "when authenticated as admin" do - it "removes any existing group" do - delete v3_api("/groups/#{group2.id}", admin) - - expect(response).to have_gitlab_http_status(202) - end - - it "does not remove a non existing group" do - delete v3_api("/groups/1328", admin) - - expect(response).to have_gitlab_http_status(404) - end - end - end - - describe "POST /groups/:id/projects/:project_id" do - let(:project) { create(:project) } - let(:project_path) { CGI.escape(project.full_path) } - - before do - allow_any_instance_of(Projects::TransferService) - .to receive(:execute).and_return(true) - end - - context "when authenticated as user" do - it "does not transfer project to group" do - post v3_api("/groups/#{group1.id}/projects/#{project.id}", user2) - - expect(response).to have_gitlab_http_status(403) - end - end - - context "when authenticated as admin" do - it "transfers project to group" do - post v3_api("/groups/#{group1.id}/projects/#{project.id}", admin) - - expect(response).to have_gitlab_http_status(201) - end - - context 'when using project path in URL' do - context 'with a valid project path' do - it "transfers project to group" do - post v3_api("/groups/#{group1.id}/projects/#{project_path}", admin) - - expect(response).to have_gitlab_http_status(201) - end - end - - context 'with a non-existent project path' do - it "does not transfer project to group" do - post v3_api("/groups/#{group1.id}/projects/nogroup%2Fnoproject", admin) - - expect(response).to have_gitlab_http_status(404) - end - end - end - - context 'when using a group path in URL' do - context 'with a valid group path' do - it "transfers project to group" do - post v3_api("/groups/#{group1.path}/projects/#{project_path}", admin) - - expect(response).to have_gitlab_http_status(201) - end - end - - context 'with a non-existent group path' do - it "does not transfer project to group" do - post v3_api("/groups/noexist/projects/#{project_path}", admin) - - expect(response).to have_gitlab_http_status(404) - end - end - end - end - end -end diff --git a/spec/requests/api/v3/issues_spec.rb b/spec/requests/api/v3/issues_spec.rb deleted file mode 100644 index 11b5469be7b..00000000000 --- a/spec/requests/api/v3/issues_spec.rb +++ /dev/null @@ -1,1298 +0,0 @@ -require 'spec_helper' - -describe API::V3::Issues do - set(:user) { create(:user) } - set(:user2) { create(:user) } - set(:non_member) { create(:user) } - set(:guest) { create(:user) } - set(:author) { create(:author) } - set(:assignee) { create(:assignee) } - set(:admin) { create(:user, :admin) } - let!(:project) { create(:project, :public, creator_id: user.id, namespace: user.namespace ) } - let!(:closed_issue) do - create :closed_issue, - author: user, - assignees: [user], - project: project, - state: :closed, - milestone: milestone, - created_at: generate(:past_time), - updated_at: 3.hours.ago - end - let!(:confidential_issue) do - create :issue, - :confidential, - project: project, - author: author, - assignees: [assignee], - created_at: generate(:past_time), - updated_at: 2.hours.ago - end - let!(:issue) do - create :issue, - author: user, - assignees: [user], - project: project, - milestone: milestone, - created_at: generate(:past_time), - updated_at: 1.hour.ago - end - let!(:label) do - create(:label, title: 'label', color: '#FFAABB', project: project) - end - let!(:label_link) { create(:label_link, label: label, target: issue) } - let!(:milestone) { create(:milestone, title: '1.0.0', project: project) } - let!(:empty_milestone) do - create(:milestone, title: '2.0.0', project: project) - end - let!(:note) { create(:note_on_issue, author: user, project: project, noteable: issue) } - - let(:no_milestone_title) { URI.escape(Milestone::None.title) } - - before do - project.add_reporter(user) - project.add_guest(guest) - end - - describe "GET /issues" do - context "when unauthenticated" do - it "returns authentication error" do - get v3_api("/issues") - - expect(response).to have_gitlab_http_status(401) - end - end - - context "when authenticated" do - it "returns an array of issues" do - get v3_api("/issues", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.first['title']).to eq(issue.title) - expect(json_response.last).to have_key('web_url') - end - - it 'returns an array of closed issues' do - get v3_api('/issues?state=closed', user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(1) - expect(json_response.first['id']).to eq(closed_issue.id) - end - - it 'returns an array of opened issues' do - get v3_api('/issues?state=opened', user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(1) - expect(json_response.first['id']).to eq(issue.id) - end - - it 'returns an array of all issues' do - get v3_api('/issues?state=all', user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(2) - expect(json_response.first['id']).to eq(issue.id) - expect(json_response.second['id']).to eq(closed_issue.id) - end - - it 'returns an array of labeled issues' do - get v3_api("/issues?labels=#{label.title}", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(1) - expect(json_response.first['labels']).to eq([label.title]) - end - - it 'returns an array of labeled issues when at least one label matches' do - get v3_api("/issues?labels=#{label.title},foo,bar", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(1) - expect(json_response.first['labels']).to eq([label.title]) - end - - it 'returns an empty array if no issue matches labels' do - get v3_api('/issues?labels=foo,bar', user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(0) - end - - it 'returns an array of labeled issues matching given state' do - get v3_api("/issues?labels=#{label.title}&state=opened", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(1) - expect(json_response.first['labels']).to eq([label.title]) - expect(json_response.first['state']).to eq('opened') - end - - it 'returns an empty array if no issue matches labels and state filters' do - get v3_api("/issues?labels=#{label.title}&state=closed", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(0) - end - - it 'returns an empty array if no issue matches milestone' do - get v3_api("/issues?milestone=#{empty_milestone.title}", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(0) - end - - it 'returns an empty array if milestone does not exist' do - get v3_api("/issues?milestone=foo", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(0) - end - - it 'returns an array of issues in given milestone' do - get v3_api("/issues?milestone=#{milestone.title}", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(2) - expect(json_response.first['id']).to eq(issue.id) - expect(json_response.second['id']).to eq(closed_issue.id) - end - - it 'returns an array of issues matching state in milestone' do - get v3_api("/issues?milestone=#{milestone.title}", user), - '&state=closed' - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(1) - expect(json_response.first['id']).to eq(closed_issue.id) - end - - it 'returns an array of issues with no milestone' do - get v3_api("/issues?milestone=#{no_milestone_title}", author) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(1) - expect(json_response.first['id']).to eq(confidential_issue.id) - end - - it 'sorts by created_at descending by default' do - get v3_api('/issues', user) - - response_dates = json_response.map { |issue| issue['created_at'] } - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(response_dates).to eq(response_dates.sort.reverse) - end - - it 'sorts ascending when requested' do - get v3_api('/issues?sort=asc', user) - - response_dates = json_response.map { |issue| issue['created_at'] } - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(response_dates).to eq(response_dates.sort) - end - - it 'sorts by updated_at descending when requested' do - get v3_api('/issues?order_by=updated_at', user) - - response_dates = json_response.map { |issue| issue['updated_at'] } - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(response_dates).to eq(response_dates.sort.reverse) - end - - it 'sorts by updated_at ascending when requested' do - get v3_api('/issues?order_by=updated_at&sort=asc', user) - - response_dates = json_response.map { |issue| issue['updated_at'] } - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(response_dates).to eq(response_dates.sort) - end - - it 'matches V3 response schema' do - get v3_api('/issues', user) - - expect(response).to have_gitlab_http_status(200) - expect(response).to match_response_schema('public_api/v3/issues') - end - end - end - - describe "GET /groups/:id/issues" do - let!(:group) { create(:group) } - let!(:group_project) { create(:project, :public, creator_id: user.id, namespace: group) } - let!(:group_closed_issue) do - create :closed_issue, - author: user, - assignees: [user], - project: group_project, - state: :closed, - milestone: group_milestone, - updated_at: 3.hours.ago - end - let!(:group_confidential_issue) do - create :issue, - :confidential, - project: group_project, - author: author, - assignees: [assignee], - updated_at: 2.hours.ago - end - let!(:group_issue) do - create :issue, - author: user, - assignees: [user], - project: group_project, - milestone: group_milestone, - updated_at: 1.hour.ago - end - let!(:group_label) do - create(:label, title: 'group_lbl', color: '#FFAABB', project: group_project) - end - let!(:group_label_link) { create(:label_link, label: group_label, target: group_issue) } - let!(:group_milestone) { create(:milestone, title: '3.0.0', project: group_project) } - let!(:group_empty_milestone) do - create(:milestone, title: '4.0.0', project: group_project) - end - let!(:group_note) { create(:note_on_issue, author: user, project: group_project, noteable: group_issue) } - - before do - group_project.add_reporter(user) - end - let(:base_url) { "/groups/#{group.id}/issues" } - - it 'returns all group issues (including opened and closed)' do - get v3_api(base_url, admin) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(3) - end - - it 'returns group issues without confidential issues for non project members' do - get v3_api("#{base_url}?state=opened", non_member) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(1) - expect(json_response.first['title']).to eq(group_issue.title) - end - - it 'returns group confidential issues for author' do - get v3_api("#{base_url}?state=opened", author) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(2) - end - - it 'returns group confidential issues for assignee' do - get v3_api("#{base_url}?state=opened", assignee) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(2) - end - - it 'returns group issues with confidential issues for project members' do - get v3_api("#{base_url}?state=opened", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(2) - end - - it 'returns group confidential issues for admin' do - get v3_api("#{base_url}?state=opened", admin) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(2) - end - - it 'returns an array of labeled group issues' do - get v3_api("#{base_url}?labels=#{group_label.title}", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(1) - expect(json_response.first['labels']).to eq([group_label.title]) - end - - it 'returns an array of labeled group issues where all labels match' do - get v3_api("#{base_url}?labels=#{group_label.title},foo,bar", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(0) - end - - it 'returns an empty array if no group issue matches labels' do - get v3_api("#{base_url}?labels=foo,bar", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(0) - end - - it 'returns an empty array if no issue matches milestone' do - get v3_api("#{base_url}?milestone=#{group_empty_milestone.title}", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(0) - end - - it 'returns an empty array if milestone does not exist' do - get v3_api("#{base_url}?milestone=foo", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(0) - end - - it 'returns an array of issues in given milestone' do - get v3_api("#{base_url}?state=opened&milestone=#{group_milestone.title}", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(1) - expect(json_response.first['id']).to eq(group_issue.id) - end - - it 'returns an array of issues matching state in milestone' do - get v3_api("#{base_url}?milestone=#{group_milestone.title}", user), - '&state=closed' - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(1) - expect(json_response.first['id']).to eq(group_closed_issue.id) - end - - it 'returns an array of issues with no milestone' do - get v3_api("#{base_url}?milestone=#{no_milestone_title}", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(1) - expect(json_response.first['id']).to eq(group_confidential_issue.id) - end - - it 'sorts by created_at descending by default' do - get v3_api(base_url, user) - - response_dates = json_response.map { |issue| issue['created_at'] } - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(response_dates).to eq(response_dates.sort.reverse) - end - - it 'sorts ascending when requested' do - get v3_api("#{base_url}?sort=asc", user) - - response_dates = json_response.map { |issue| issue['created_at'] } - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(response_dates).to eq(response_dates.sort) - end - - it 'sorts by updated_at descending when requested' do - get v3_api("#{base_url}?order_by=updated_at", user) - - response_dates = json_response.map { |issue| issue['updated_at'] } - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(response_dates).to eq(response_dates.sort.reverse) - end - - it 'sorts by updated_at ascending when requested' do - get v3_api("#{base_url}?order_by=updated_at&sort=asc", user) - - response_dates = json_response.map { |issue| issue['updated_at'] } - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(response_dates).to eq(response_dates.sort) - end - end - - describe "GET /projects/:id/issues" do - let(:base_url) { "/projects/#{project.id}" } - - it 'returns 404 when project does not exist' do - get v3_api('/projects/1000/issues', non_member) - - expect(response).to have_gitlab_http_status(404) - end - - it "returns 404 on private projects for other users" do - private_project = create(:project, :private) - create(:issue, project: private_project) - - get v3_api("/projects/#{private_project.id}/issues", non_member) - - expect(response).to have_gitlab_http_status(404) - end - - it 'returns no issues when user has access to project but not issues' do - restricted_project = create(:project, :public, issues_access_level: ProjectFeature::PRIVATE) - create(:issue, project: restricted_project) - - get v3_api("/projects/#{restricted_project.id}/issues", non_member) - - expect(json_response).to eq([]) - end - - it 'returns project issues without confidential issues for non project members' do - get v3_api("#{base_url}/issues", non_member) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(2) - expect(json_response.first['title']).to eq(issue.title) - end - - it 'returns project issues without confidential issues for project members with guest role' do - get v3_api("#{base_url}/issues", guest) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(2) - expect(json_response.first['title']).to eq(issue.title) - end - - it 'returns project confidential issues for author' do - get v3_api("#{base_url}/issues", author) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(3) - expect(json_response.first['title']).to eq(issue.title) - end - - it 'returns project confidential issues for assignee' do - get v3_api("#{base_url}/issues", assignee) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(3) - expect(json_response.first['title']).to eq(issue.title) - end - - it 'returns project issues with confidential issues for project members' do - get v3_api("#{base_url}/issues", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(3) - expect(json_response.first['title']).to eq(issue.title) - end - - it 'returns project confidential issues for admin' do - get v3_api("#{base_url}/issues", admin) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(3) - expect(json_response.first['title']).to eq(issue.title) - end - - it 'returns an array of labeled project issues' do - get v3_api("#{base_url}/issues?labels=#{label.title}", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(1) - expect(json_response.first['labels']).to eq([label.title]) - end - - it 'returns an array of labeled project issues where all labels match' do - get v3_api("#{base_url}/issues?labels=#{label.title},foo,bar", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(1) - expect(json_response.first['labels']).to eq([label.title]) - end - - it 'returns an empty array if no project issue matches labels' do - get v3_api("#{base_url}/issues?labels=foo,bar", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(0) - end - - it 'returns an empty array if no issue matches milestone' do - get v3_api("#{base_url}/issues?milestone=#{empty_milestone.title}", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(0) - end - - it 'returns an empty array if milestone does not exist' do - get v3_api("#{base_url}/issues?milestone=foo", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(0) - end - - it 'returns an array of issues in given milestone' do - get v3_api("#{base_url}/issues?milestone=#{milestone.title}", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(2) - expect(json_response.first['id']).to eq(issue.id) - expect(json_response.second['id']).to eq(closed_issue.id) - end - - it 'returns an array of issues matching state in milestone' do - get v3_api("#{base_url}/issues?milestone=#{milestone.title}", user), - '&state=closed' - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(1) - expect(json_response.first['id']).to eq(closed_issue.id) - end - - it 'returns an array of issues with no milestone' do - get v3_api("#{base_url}/issues?milestone=#{no_milestone_title}", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(1) - expect(json_response.first['id']).to eq(confidential_issue.id) - end - - it 'sorts by created_at descending by default' do - get v3_api("#{base_url}/issues", user) - - response_dates = json_response.map { |issue| issue['created_at'] } - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(response_dates).to eq(response_dates.sort.reverse) - end - - it 'sorts ascending when requested' do - get v3_api("#{base_url}/issues?sort=asc", user) - - response_dates = json_response.map { |issue| issue['created_at'] } - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(response_dates).to eq(response_dates.sort) - end - - it 'sorts by updated_at descending when requested' do - get v3_api("#{base_url}/issues?order_by=updated_at", user) - - response_dates = json_response.map { |issue| issue['updated_at'] } - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(response_dates).to eq(response_dates.sort.reverse) - end - - it 'sorts by updated_at ascending when requested' do - get v3_api("#{base_url}/issues?order_by=updated_at&sort=asc", user) - - response_dates = json_response.map { |issue| issue['updated_at'] } - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(response_dates).to eq(response_dates.sort) - end - end - - describe "GET /projects/:id/issues/:issue_id" do - it 'exposes known attributes' do - get v3_api("/projects/#{project.id}/issues/#{issue.id}", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['id']).to eq(issue.id) - expect(json_response['iid']).to eq(issue.iid) - expect(json_response['project_id']).to eq(issue.project.id) - expect(json_response['title']).to eq(issue.title) - expect(json_response['description']).to eq(issue.description) - expect(json_response['state']).to eq(issue.state) - expect(json_response['created_at']).to be_present - expect(json_response['updated_at']).to be_present - expect(json_response['labels']).to eq(issue.label_names) - expect(json_response['milestone']).to be_a Hash - expect(json_response['assignee']).to be_a Hash - expect(json_response['author']).to be_a Hash - expect(json_response['confidential']).to be_falsy - end - - it "returns a project issue by id" do - get v3_api("/projects/#{project.id}/issues/#{issue.id}", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['title']).to eq(issue.title) - expect(json_response['iid']).to eq(issue.iid) - end - - it 'returns a project issue by iid' do - get v3_api("/projects/#{project.id}/issues?iid=#{issue.iid}", user) - - expect(response.status).to eq 200 - expect(json_response.length).to eq 1 - expect(json_response.first['title']).to eq issue.title - expect(json_response.first['id']).to eq issue.id - expect(json_response.first['iid']).to eq issue.iid - end - - it 'returns an empty array for an unknown project issue iid' do - get v3_api("/projects/#{project.id}/issues?iid=#{issue.iid + 10}", user) - - expect(response.status).to eq 200 - expect(json_response.length).to eq 0 - end - - it "returns 404 if issue id not found" do - get v3_api("/projects/#{project.id}/issues/54321", user) - - expect(response).to have_gitlab_http_status(404) - end - - context 'confidential issues' do - it "returns 404 for non project members" do - get v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", non_member) - - expect(response).to have_gitlab_http_status(404) - end - - it "returns 404 for project members with guest role" do - get v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", guest) - - expect(response).to have_gitlab_http_status(404) - end - - it "returns confidential issue for project members" do - get v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['title']).to eq(confidential_issue.title) - expect(json_response['iid']).to eq(confidential_issue.iid) - end - - it "returns confidential issue for author" do - get v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", author) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['title']).to eq(confidential_issue.title) - expect(json_response['iid']).to eq(confidential_issue.iid) - end - - it "returns confidential issue for assignee" do - get v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", assignee) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['title']).to eq(confidential_issue.title) - expect(json_response['iid']).to eq(confidential_issue.iid) - end - - it "returns confidential issue for admin" do - get v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", admin) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['title']).to eq(confidential_issue.title) - expect(json_response['iid']).to eq(confidential_issue.iid) - end - end - end - - describe "POST /projects/:id/issues" do - it 'creates a new project issue' do - post v3_api("/projects/#{project.id}/issues", user), - title: 'new issue', labels: 'label, label2', assignee_id: assignee.id - - expect(response).to have_gitlab_http_status(201) - expect(json_response['title']).to eq('new issue') - expect(json_response['description']).to be_nil - expect(json_response['labels']).to eq(%w(label label2)) - expect(json_response['confidential']).to be_falsy - expect(json_response['assignee']['name']).to eq(assignee.name) - end - - it 'creates a new confidential project issue' do - post v3_api("/projects/#{project.id}/issues", user), - title: 'new issue', confidential: true - - expect(response).to have_gitlab_http_status(201) - expect(json_response['title']).to eq('new issue') - expect(json_response['confidential']).to be_truthy - end - - it 'creates a new confidential project issue with a different param' do - post v3_api("/projects/#{project.id}/issues", user), - title: 'new issue', confidential: 'y' - - expect(response).to have_gitlab_http_status(201) - expect(json_response['title']).to eq('new issue') - expect(json_response['confidential']).to be_truthy - end - - it 'creates a public issue when confidential param is false' do - post v3_api("/projects/#{project.id}/issues", user), - title: 'new issue', confidential: false - - expect(response).to have_gitlab_http_status(201) - expect(json_response['title']).to eq('new issue') - expect(json_response['confidential']).to be_falsy - end - - it 'creates a public issue when confidential param is invalid' do - post v3_api("/projects/#{project.id}/issues", user), - title: 'new issue', confidential: 'foo' - - expect(response).to have_gitlab_http_status(400) - expect(json_response['error']).to eq('confidential is invalid') - end - - it "returns a 400 bad request if title not given" do - post v3_api("/projects/#{project.id}/issues", user), labels: 'label, label2' - - expect(response).to have_gitlab_http_status(400) - end - - it 'allows special label names' do - post v3_api("/projects/#{project.id}/issues", user), - title: 'new issue', - labels: 'label, label?, label&foo, ?, &' - - expect(response.status).to eq(201) - expect(json_response['labels']).to include 'label' - expect(json_response['labels']).to include 'label?' - expect(json_response['labels']).to include 'label&foo' - expect(json_response['labels']).to include '?' - expect(json_response['labels']).to include '&' - end - - it 'returns 400 if title is too long' do - post v3_api("/projects/#{project.id}/issues", user), - title: 'g' * 256 - - expect(response).to have_gitlab_http_status(400) - expect(json_response['message']['title']).to eq([ - 'is too long (maximum is 255 characters)' - ]) - end - - context 'resolving issues in a merge request' do - set(:diff_note_on_merge_request) { create(:diff_note_on_merge_request) } - let(:discussion) { diff_note_on_merge_request.to_discussion } - let(:merge_request) { discussion.noteable } - let(:project) { merge_request.source_project } - before do - project.add_master(user) - post v3_api("/projects/#{project.id}/issues", user), - title: 'New Issue', - merge_request_for_resolving_discussions: merge_request.iid - end - - it 'creates a new project issue' do - expect(response).to have_gitlab_http_status(:created) - end - - it 'resolves the discussions in a merge request' do - discussion.first_note.reload - - expect(discussion.resolved?).to be(true) - end - - it 'assigns a description to the issue mentioning the merge request' do - expect(json_response['description']).to include(merge_request.to_reference) - end - end - - context 'with due date' do - it 'creates a new project issue' do - due_date = 2.weeks.from_now.strftime('%Y-%m-%d') - - post v3_api("/projects/#{project.id}/issues", user), - title: 'new issue', due_date: due_date - - expect(response).to have_gitlab_http_status(201) - expect(json_response['title']).to eq('new issue') - expect(json_response['description']).to be_nil - expect(json_response['due_date']).to eq(due_date) - end - end - - context 'when an admin or owner makes the request' do - it 'accepts the creation date to be set' do - creation_time = 2.weeks.ago - post v3_api("/projects/#{project.id}/issues", user), - title: 'new issue', labels: 'label, label2', created_at: creation_time - - expect(response).to have_gitlab_http_status(201) - expect(Time.parse(json_response['created_at'])).to be_like_time(creation_time) - end - end - - context 'the user can only read the issue' do - it 'cannot create new labels' do - expect do - post v3_api("/projects/#{project.id}/issues", non_member), title: 'new issue', labels: 'label, label2' - end.not_to change { project.labels.count } - end - end - end - - describe 'POST /projects/:id/issues with spam filtering' do - before do - allow_any_instance_of(SpamService).to receive(:check_for_spam?).and_return(true) - allow_any_instance_of(AkismetService).to receive_messages(spam?: true) - end - - let(:params) do - { - title: 'new issue', - description: 'content here', - labels: 'label, label2' - } - end - - it "does not create a new project issue" do - expect { post v3_api("/projects/#{project.id}/issues", user), params }.not_to change(Issue, :count) - - expect(response).to have_gitlab_http_status(400) - expect(json_response['message']).to eq({ "error" => "Spam detected" }) - - spam_logs = SpamLog.all - - expect(spam_logs.count).to eq(1) - expect(spam_logs[0].title).to eq('new issue') - expect(spam_logs[0].description).to eq('content here') - expect(spam_logs[0].user).to eq(user) - expect(spam_logs[0].noteable_type).to eq('Issue') - end - end - - describe "PUT /projects/:id/issues/:issue_id to update only title" do - it "updates a project issue" do - put v3_api("/projects/#{project.id}/issues/#{issue.id}", user), - title: 'updated title' - - expect(response).to have_gitlab_http_status(200) - expect(json_response['title']).to eq('updated title') - end - - it "returns 404 error if issue id not found" do - put v3_api("/projects/#{project.id}/issues/44444", user), - title: 'updated title' - - expect(response).to have_gitlab_http_status(404) - end - - it 'allows special label names' do - put v3_api("/projects/#{project.id}/issues/#{issue.id}", user), - title: 'updated title', - labels: 'label, label?, label&foo, ?, &' - - expect(response.status).to eq(200) - expect(json_response['labels']).to include 'label' - expect(json_response['labels']).to include 'label?' - expect(json_response['labels']).to include 'label&foo' - expect(json_response['labels']).to include '?' - expect(json_response['labels']).to include '&' - end - - context 'confidential issues' do - it "returns 403 for non project members" do - put v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", non_member), - title: 'updated title' - - expect(response).to have_gitlab_http_status(403) - end - - it "returns 403 for project members with guest role" do - put v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", guest), - title: 'updated title' - - expect(response).to have_gitlab_http_status(403) - end - - it "updates a confidential issue for project members" do - put v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", user), - title: 'updated title' - - expect(response).to have_gitlab_http_status(200) - expect(json_response['title']).to eq('updated title') - end - - it "updates a confidential issue for author" do - put v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", author), - title: 'updated title' - - expect(response).to have_gitlab_http_status(200) - expect(json_response['title']).to eq('updated title') - end - - it "updates a confidential issue for admin" do - put v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", admin), - title: 'updated title' - - expect(response).to have_gitlab_http_status(200) - expect(json_response['title']).to eq('updated title') - end - - it 'sets an issue to confidential' do - put v3_api("/projects/#{project.id}/issues/#{issue.id}", user), - confidential: true - - expect(response).to have_gitlab_http_status(200) - expect(json_response['confidential']).to be_truthy - end - - it 'makes a confidential issue public' do - put v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", user), - confidential: false - - expect(response).to have_gitlab_http_status(200) - expect(json_response['confidential']).to be_falsy - end - - it 'does not update a confidential issue with wrong confidential flag' do - put v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", user), - confidential: 'foo' - - expect(response).to have_gitlab_http_status(400) - expect(json_response['error']).to eq('confidential is invalid') - end - end - end - - describe 'PUT /projects/:id/issues/:issue_id with spam filtering' do - let(:params) do - { - title: 'updated title', - description: 'content here', - labels: 'label, label2' - } - end - - it "does not create a new project issue" do - allow_any_instance_of(SpamService).to receive_messages(check_for_spam?: true) - allow_any_instance_of(AkismetService).to receive_messages(spam?: true) - - put v3_api("/projects/#{project.id}/issues/#{issue.id}", user), params - - expect(response).to have_gitlab_http_status(400) - expect(json_response['message']).to eq({ "error" => "Spam detected" }) - - spam_logs = SpamLog.all - expect(spam_logs.count).to eq(1) - expect(spam_logs[0].title).to eq('updated title') - expect(spam_logs[0].description).to eq('content here') - expect(spam_logs[0].user).to eq(user) - expect(spam_logs[0].noteable_type).to eq('Issue') - end - end - - describe 'PUT /projects/:id/issues/:issue_id to update labels' do - let!(:label) { create(:label, title: 'dummy', project: project) } - let!(:label_link) { create(:label_link, label: label, target: issue) } - - it 'does not update labels if not present' do - put v3_api("/projects/#{project.id}/issues/#{issue.id}", user), - title: 'updated title' - - expect(response).to have_gitlab_http_status(200) - expect(json_response['labels']).to eq([label.title]) - end - - it 'removes all labels' do - put v3_api("/projects/#{project.id}/issues/#{issue.id}", user), labels: '' - - expect(response).to have_gitlab_http_status(200) - expect(json_response['labels']).to eq([]) - end - - it 'updates labels' do - put v3_api("/projects/#{project.id}/issues/#{issue.id}", user), - labels: 'foo,bar' - - expect(response).to have_gitlab_http_status(200) - expect(json_response['labels']).to include 'foo' - expect(json_response['labels']).to include 'bar' - end - - it 'allows special label names' do - put v3_api("/projects/#{project.id}/issues/#{issue.id}", user), - labels: 'label:foo, label-bar,label_bar,label/bar,label?bar,label&bar,?,&' - - expect(response.status).to eq(200) - expect(json_response['labels']).to include 'label:foo' - expect(json_response['labels']).to include 'label-bar' - expect(json_response['labels']).to include 'label_bar' - expect(json_response['labels']).to include 'label/bar' - expect(json_response['labels']).to include 'label?bar' - expect(json_response['labels']).to include 'label&bar' - expect(json_response['labels']).to include '?' - expect(json_response['labels']).to include '&' - end - - it 'returns 400 if title is too long' do - put v3_api("/projects/#{project.id}/issues/#{issue.id}", user), - title: 'g' * 256 - - expect(response).to have_gitlab_http_status(400) - expect(json_response['message']['title']).to eq([ - 'is too long (maximum is 255 characters)' - ]) - end - end - - describe "PUT /projects/:id/issues/:issue_id to update state and label" do - it "updates a project issue" do - put v3_api("/projects/#{project.id}/issues/#{issue.id}", user), - labels: 'label2', state_event: "close" - - expect(response).to have_gitlab_http_status(200) - expect(json_response['labels']).to include 'label2' - expect(json_response['state']).to eq "closed" - end - - it 'reopens a project isssue' do - put v3_api("/projects/#{project.id}/issues/#{closed_issue.id}", user), state_event: 'reopen' - - expect(response).to have_gitlab_http_status(200) - expect(json_response['state']).to eq 'opened' - end - - context 'when an admin or owner makes the request' do - it 'accepts the update date to be set' do - update_time = 2.weeks.ago - put v3_api("/projects/#{project.id}/issues/#{issue.id}", user), - labels: 'label3', state_event: 'close', updated_at: update_time - - expect(response).to have_gitlab_http_status(200) - expect(json_response['labels']).to include 'label3' - expect(Time.parse(json_response['updated_at'])).to be_like_time(update_time) - end - end - end - - describe 'PUT /projects/:id/issues/:issue_id to update due date' do - it 'creates a new project issue' do - due_date = 2.weeks.from_now.strftime('%Y-%m-%d') - - put v3_api("/projects/#{project.id}/issues/#{issue.id}", user), due_date: due_date - - expect(response).to have_gitlab_http_status(200) - expect(json_response['due_date']).to eq(due_date) - end - end - - describe 'PUT /projects/:id/issues/:issue_id to update assignee' do - it 'updates an issue with no assignee' do - put v3_api("/projects/#{project.id}/issues/#{issue.id}", user), assignee_id: 0 - - expect(response).to have_gitlab_http_status(200) - expect(json_response['assignee']).to eq(nil) - end - - it 'updates an issue with assignee' do - put v3_api("/projects/#{project.id}/issues/#{issue.id}", user), assignee_id: user2.id - - expect(response).to have_gitlab_http_status(200) - expect(json_response['assignee']['name']).to eq(user2.name) - end - end - - describe "DELETE /projects/:id/issues/:issue_id" do - it "rejects a non member from deleting an issue" do - delete v3_api("/projects/#{project.id}/issues/#{issue.id}", non_member) - - expect(response).to have_gitlab_http_status(403) - end - - it "rejects a developer from deleting an issue" do - delete v3_api("/projects/#{project.id}/issues/#{issue.id}", author) - - expect(response).to have_gitlab_http_status(403) - end - - context "when the user is project owner" do - set(:owner) { create(:user) } - let(:project) { create(:project, namespace: owner.namespace) } - - it "deletes the issue if an admin requests it" do - delete v3_api("/projects/#{project.id}/issues/#{issue.id}", owner) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['state']).to eq 'opened' - end - end - - context 'when issue does not exist' do - it 'returns 404 when trying to move an issue' do - delete v3_api("/projects/#{project.id}/issues/123", user) - - expect(response).to have_gitlab_http_status(404) - end - end - end - - describe '/projects/:id/issues/:issue_id/move' do - let!(:target_project) { create(:project, creator_id: user.id, namespace: user.namespace ) } - let!(:target_project2) { create(:project, creator_id: non_member.id, namespace: non_member.namespace ) } - - it 'moves an issue' do - post v3_api("/projects/#{project.id}/issues/#{issue.id}/move", user), - to_project_id: target_project.id - - expect(response).to have_gitlab_http_status(201) - expect(json_response['project_id']).to eq(target_project.id) - end - - context 'when source and target projects are the same' do - it 'returns 400 when trying to move an issue' do - post v3_api("/projects/#{project.id}/issues/#{issue.id}/move", user), - to_project_id: project.id - - expect(response).to have_gitlab_http_status(400) - expect(json_response['message']).to eq('Cannot move issue to project it originates from!') - end - end - - context 'when the user does not have the permission to move issues' do - it 'returns 400 when trying to move an issue' do - post v3_api("/projects/#{project.id}/issues/#{issue.id}/move", user), - to_project_id: target_project2.id - - expect(response).to have_gitlab_http_status(400) - expect(json_response['message']).to eq('Cannot move issue due to insufficient permissions!') - end - end - - it 'moves the issue to another namespace if I am admin' do - post v3_api("/projects/#{project.id}/issues/#{issue.id}/move", admin), - to_project_id: target_project2.id - - expect(response).to have_gitlab_http_status(201) - expect(json_response['project_id']).to eq(target_project2.id) - end - - context 'when issue does not exist' do - it 'returns 404 when trying to move an issue' do - post v3_api("/projects/#{project.id}/issues/123/move", user), - to_project_id: target_project.id - - expect(response).to have_gitlab_http_status(404) - expect(json_response['message']).to eq('404 Issue Not Found') - end - end - - context 'when source project does not exist' do - it 'returns 404 when trying to move an issue' do - post v3_api("/projects/0/issues/#{issue.id}/move", user), - to_project_id: target_project.id - - expect(response).to have_gitlab_http_status(404) - expect(json_response['message']).to eq('404 Project Not Found') - end - end - - context 'when target project does not exist' do - it 'returns 404 when trying to move an issue' do - post v3_api("/projects/#{project.id}/issues/#{issue.id}/move", user), - to_project_id: 0 - - expect(response).to have_gitlab_http_status(404) - end - end - end - - describe 'POST :id/issues/:issue_id/subscription' do - it 'subscribes to an issue' do - post v3_api("/projects/#{project.id}/issues/#{issue.id}/subscription", user2) - - expect(response).to have_gitlab_http_status(201) - expect(json_response['subscribed']).to eq(true) - end - - it 'returns 304 if already subscribed' do - post v3_api("/projects/#{project.id}/issues/#{issue.id}/subscription", user) - - expect(response).to have_gitlab_http_status(304) - end - - it 'returns 404 if the issue is not found' do - post v3_api("/projects/#{project.id}/issues/123/subscription", user) - - expect(response).to have_gitlab_http_status(404) - end - - it 'returns 404 if the issue is confidential' do - post v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}/subscription", non_member) - - expect(response).to have_gitlab_http_status(404) - end - end - - describe 'DELETE :id/issues/:issue_id/subscription' do - it 'unsubscribes from an issue' do - delete v3_api("/projects/#{project.id}/issues/#{issue.id}/subscription", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['subscribed']).to eq(false) - end - - it 'returns 304 if not subscribed' do - delete v3_api("/projects/#{project.id}/issues/#{issue.id}/subscription", user2) - - expect(response).to have_gitlab_http_status(304) - end - - it 'returns 404 if the issue is not found' do - delete v3_api("/projects/#{project.id}/issues/123/subscription", user) - - expect(response).to have_gitlab_http_status(404) - end - - it 'returns 404 if the issue is confidential' do - delete v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}/subscription", non_member) - - expect(response).to have_gitlab_http_status(404) - end - end - - describe 'time tracking endpoints' do - let(:issuable) { issue } - - include_examples 'V3 time tracking endpoints', 'issue' - end -end diff --git a/spec/requests/api/v3/labels_spec.rb b/spec/requests/api/v3/labels_spec.rb deleted file mode 100644 index cdab4d2bd73..00000000000 --- a/spec/requests/api/v3/labels_spec.rb +++ /dev/null @@ -1,169 +0,0 @@ -require 'spec_helper' - -describe API::V3::Labels do - let(:user) { create(:user) } - let(:project) { create(:project, creator_id: user.id, namespace: user.namespace) } - let!(:label1) { create(:label, title: 'label1', project: project) } - let!(:priority_label) { create(:label, title: 'bug', project: project, priority: 3) } - - before do - project.add_master(user) - end - - describe 'GET /projects/:id/labels' do - it 'returns all available labels to the project' do - group = create(:group) - group_label = create(:group_label, title: 'feature', group: group) - project.update(group: group) - create(:labeled_issue, project: project, labels: [group_label], author: user) - create(:labeled_issue, project: project, labels: [label1], author: user, state: :closed) - create(:labeled_merge_request, labels: [priority_label], author: user, source_project: project ) - - expected_keys = %w( - id name color description - open_issues_count closed_issues_count open_merge_requests_count - subscribed priority - ) - - get v3_api("/projects/#{project.id}/labels", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.size).to eq(3) - expect(json_response.first.keys).to match_array expected_keys - expect(json_response.map { |l| l['name'] }).to match_array([group_label.name, priority_label.name, label1.name]) - - label1_response = json_response.find { |l| l['name'] == label1.title } - group_label_response = json_response.find { |l| l['name'] == group_label.title } - priority_label_response = json_response.find { |l| l['name'] == priority_label.title } - - expect(label1_response['open_issues_count']).to eq(0) - expect(label1_response['closed_issues_count']).to eq(1) - expect(label1_response['open_merge_requests_count']).to eq(0) - expect(label1_response['name']).to eq(label1.name) - expect(label1_response['color']).to be_present - expect(label1_response['description']).to be_nil - expect(label1_response['priority']).to be_nil - expect(label1_response['subscribed']).to be_falsey - - expect(group_label_response['open_issues_count']).to eq(1) - expect(group_label_response['closed_issues_count']).to eq(0) - expect(group_label_response['open_merge_requests_count']).to eq(0) - expect(group_label_response['name']).to eq(group_label.name) - expect(group_label_response['color']).to be_present - expect(group_label_response['description']).to be_nil - expect(group_label_response['priority']).to be_nil - expect(group_label_response['subscribed']).to be_falsey - - expect(priority_label_response['open_issues_count']).to eq(0) - expect(priority_label_response['closed_issues_count']).to eq(0) - expect(priority_label_response['open_merge_requests_count']).to eq(1) - expect(priority_label_response['name']).to eq(priority_label.name) - expect(priority_label_response['color']).to be_present - expect(priority_label_response['description']).to be_nil - expect(priority_label_response['priority']).to eq(3) - expect(priority_label_response['subscribed']).to be_falsey - end - end - - describe "POST /projects/:id/labels/:label_id/subscription" do - context "when label_id is a label title" do - it "subscribes to the label" do - post v3_api("/projects/#{project.id}/labels/#{label1.title}/subscription", user) - - expect(response).to have_gitlab_http_status(201) - expect(json_response["name"]).to eq(label1.title) - expect(json_response["subscribed"]).to be_truthy - end - end - - context "when label_id is a label ID" do - it "subscribes to the label" do - post v3_api("/projects/#{project.id}/labels/#{label1.id}/subscription", user) - - expect(response).to have_gitlab_http_status(201) - expect(json_response["name"]).to eq(label1.title) - expect(json_response["subscribed"]).to be_truthy - end - end - - context "when user is already subscribed to label" do - before { label1.subscribe(user, project) } - - it "returns 304" do - post v3_api("/projects/#{project.id}/labels/#{label1.id}/subscription", user) - - expect(response).to have_gitlab_http_status(304) - end - end - - context "when label ID is not found" do - it "returns 404 error" do - post v3_api("/projects/#{project.id}/labels/1234/subscription", user) - - expect(response).to have_gitlab_http_status(404) - end - end - end - - describe "DELETE /projects/:id/labels/:label_id/subscription" do - before { label1.subscribe(user, project) } - - context "when label_id is a label title" do - it "unsubscribes from the label" do - delete v3_api("/projects/#{project.id}/labels/#{label1.title}/subscription", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response["name"]).to eq(label1.title) - expect(json_response["subscribed"]).to be_falsey - end - end - - context "when label_id is a label ID" do - it "unsubscribes from the label" do - delete v3_api("/projects/#{project.id}/labels/#{label1.id}/subscription", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response["name"]).to eq(label1.title) - expect(json_response["subscribed"]).to be_falsey - end - end - - context "when user is already unsubscribed from label" do - before { label1.unsubscribe(user, project) } - - it "returns 304" do - delete v3_api("/projects/#{project.id}/labels/#{label1.id}/subscription", user) - - expect(response).to have_gitlab_http_status(304) - end - end - - context "when label ID is not found" do - it "returns 404 error" do - delete v3_api("/projects/#{project.id}/labels/1234/subscription", user) - - expect(response).to have_gitlab_http_status(404) - end - end - end - - describe 'DELETE /projects/:id/labels' do - it 'returns 200 for existing label' do - delete v3_api("/projects/#{project.id}/labels", user), name: 'label1' - - expect(response).to have_gitlab_http_status(200) - end - - it 'returns 404 for non existing label' do - delete v3_api("/projects/#{project.id}/labels", user), name: 'label2' - expect(response).to have_gitlab_http_status(404) - expect(json_response['message']).to eq('404 Label Not Found') - end - - it 'returns 400 for wrong parameters' do - delete v3_api("/projects/#{project.id}/labels", user) - expect(response).to have_gitlab_http_status(400) - end - end -end diff --git a/spec/requests/api/v3/members_spec.rb b/spec/requests/api/v3/members_spec.rb deleted file mode 100644 index de4339ecb8b..00000000000 --- a/spec/requests/api/v3/members_spec.rb +++ /dev/null @@ -1,350 +0,0 @@ -require 'spec_helper' - -describe API::V3::Members do - let(:master) { create(:user, username: 'master_user') } - let(:developer) { create(:user) } - let(:access_requester) { create(:user) } - let(:stranger) { create(:user) } - - let(:project) do - create(:project, :public, :access_requestable, creator_id: master.id, namespace: master.namespace) do |project| - project.add_developer(developer) - project.add_master(master) - project.request_access(access_requester) - end - end - - let!(:group) do - create(:group, :public, :access_requestable) do |group| - group.add_developer(developer) - group.add_owner(master) - group.request_access(access_requester) - end - end - - shared_examples 'GET /:sources/:id/members' do |source_type| - context "with :sources == #{source_type.pluralize}" do - it_behaves_like 'a 404 response when source is private' do - let(:route) { get v3_api("/#{source_type.pluralize}/#{source.id}/members", stranger) } - end - - %i[master developer access_requester stranger].each do |type| - context "when authenticated as a #{type}" do - it 'returns 200' do - user = public_send(type) - get v3_api("/#{source_type.pluralize}/#{source.id}/members", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response.size).to eq(2) - expect(json_response.map { |u| u['id'] }).to match_array [master.id, developer.id] - end - end - end - - it 'does not return invitees' do - create(:"#{source_type}_member", invite_token: '123', invite_email: 'test@abc.com', source: source, user: nil) - - get v3_api("/#{source_type.pluralize}/#{source.id}/members", developer) - - expect(response).to have_gitlab_http_status(200) - expect(json_response.size).to eq(2) - expect(json_response.map { |u| u['id'] }).to match_array [master.id, developer.id] - end - - it 'finds members with query string' do - get v3_api("/#{source_type.pluralize}/#{source.id}/members", developer), query: master.username - - expect(response).to have_gitlab_http_status(200) - expect(json_response.count).to eq(1) - expect(json_response.first['username']).to eq(master.username) - end - - it 'finds all members with no query specified' do - get v3_api("/#{source_type.pluralize}/#{source.id}/members", developer), query: '' - - expect(response).to have_gitlab_http_status(200) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array - expect(json_response.count).to eq(2) - expect(json_response.map { |u| u['id'] }).to match_array [master.id, developer.id] - end - end - end - - shared_examples 'GET /:sources/:id/members/:user_id' do |source_type| - context "with :sources == #{source_type.pluralize}" do - it_behaves_like 'a 404 response when source is private' do - let(:route) { get v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", stranger) } - end - - context 'when authenticated as a non-member' do - %i[access_requester stranger].each do |type| - context "as a #{type}" do - it 'returns 200' do - user = public_send(type) - get v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", user) - - expect(response).to have_gitlab_http_status(200) - # User attributes - expect(json_response['id']).to eq(developer.id) - expect(json_response['name']).to eq(developer.name) - expect(json_response['username']).to eq(developer.username) - expect(json_response['state']).to eq(developer.state) - expect(json_response['avatar_url']).to eq(developer.avatar_url) - expect(json_response['web_url']).to eq(Gitlab::Routing.url_helpers.user_url(developer)) - - # Member attributes - expect(json_response['access_level']).to eq(Member::DEVELOPER) - end - end - end - end - end - end - - shared_examples 'POST /:sources/:id/members' do |source_type| - context "with :sources == #{source_type.pluralize}" do - it_behaves_like 'a 404 response when source is private' do - let(:route) do - post v3_api("/#{source_type.pluralize}/#{source.id}/members", stranger), - user_id: access_requester.id, access_level: Member::MASTER - end - end - - context 'when authenticated as a non-member or member with insufficient rights' do - %i[access_requester stranger developer].each do |type| - context "as a #{type}" do - it 'returns 403' do - user = public_send(type) - post v3_api("/#{source_type.pluralize}/#{source.id}/members", user), - user_id: access_requester.id, access_level: Member::MASTER - - expect(response).to have_gitlab_http_status(403) - end - end - end - end - - context 'when authenticated as a master/owner' do - context 'and new member is already a requester' do - it 'transforms the requester into a proper member' do - expect do - post v3_api("/#{source_type.pluralize}/#{source.id}/members", master), - user_id: access_requester.id, access_level: Member::MASTER - - expect(response).to have_gitlab_http_status(201) - end.to change { source.members.count }.by(1) - expect(source.requesters.count).to eq(0) - expect(json_response['id']).to eq(access_requester.id) - expect(json_response['access_level']).to eq(Member::MASTER) - end - end - - it 'creates a new member' do - expect do - post v3_api("/#{source_type.pluralize}/#{source.id}/members", master), - user_id: stranger.id, access_level: Member::DEVELOPER, expires_at: '2016-08-05' - - expect(response).to have_gitlab_http_status(201) - end.to change { source.members.count }.by(1) - expect(json_response['id']).to eq(stranger.id) - expect(json_response['access_level']).to eq(Member::DEVELOPER) - expect(json_response['expires_at']).to eq('2016-08-05') - end - end - - it "returns #{source_type == 'project' ? 201 : 409} if member already exists" do - post v3_api("/#{source_type.pluralize}/#{source.id}/members", master), - user_id: master.id, access_level: Member::MASTER - - expect(response).to have_gitlab_http_status(source_type == 'project' ? 201 : 409) - end - - it 'returns 400 when user_id is not given' do - post v3_api("/#{source_type.pluralize}/#{source.id}/members", master), - access_level: Member::MASTER - - expect(response).to have_gitlab_http_status(400) - end - - it 'returns 400 when access_level is not given' do - post v3_api("/#{source_type.pluralize}/#{source.id}/members", master), - user_id: stranger.id - - expect(response).to have_gitlab_http_status(400) - end - - it 'returns 422 when access_level is not valid' do - post v3_api("/#{source_type.pluralize}/#{source.id}/members", master), - user_id: stranger.id, access_level: 1234 - - expect(response).to have_gitlab_http_status(422) - end - end - end - - shared_examples 'PUT /:sources/:id/members/:user_id' do |source_type| - context "with :sources == #{source_type.pluralize}" do - it_behaves_like 'a 404 response when source is private' do - let(:route) do - put v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", stranger), - access_level: Member::MASTER - end - end - - context 'when authenticated as a non-member or member with insufficient rights' do - %i[access_requester stranger developer].each do |type| - context "as a #{type}" do - it 'returns 403' do - user = public_send(type) - put v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", user), - access_level: Member::MASTER - - expect(response).to have_gitlab_http_status(403) - end - end - end - end - - context 'when authenticated as a master/owner' do - it 'updates the member' do - put v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", master), - access_level: Member::MASTER, expires_at: '2016-08-05' - - expect(response).to have_gitlab_http_status(200) - expect(json_response['id']).to eq(developer.id) - expect(json_response['access_level']).to eq(Member::MASTER) - expect(json_response['expires_at']).to eq('2016-08-05') - end - end - - it 'returns 409 if member does not exist' do - put v3_api("/#{source_type.pluralize}/#{source.id}/members/123", master), - access_level: Member::MASTER - - expect(response).to have_gitlab_http_status(404) - end - - it 'returns 400 when access_level is not given' do - put v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", master) - - expect(response).to have_gitlab_http_status(400) - end - - it 'returns 422 when access level is not valid' do - put v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", master), - access_level: 1234 - - expect(response).to have_gitlab_http_status(422) - end - end - end - - shared_examples 'DELETE /:sources/:id/members/:user_id' do |source_type| - context "with :sources == #{source_type.pluralize}" do - it_behaves_like 'a 404 response when source is private' do - let(:route) { delete v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", stranger) } - end - - context 'when authenticated as a non-member or member with insufficient rights' do - %i[access_requester stranger].each do |type| - context "as a #{type}" do - it 'returns 403' do - user = public_send(type) - delete v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", user) - - expect(response).to have_gitlab_http_status(403) - end - end - end - end - - context 'when authenticated as a member and deleting themself' do - it 'deletes the member' do - expect do - delete v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", developer) - - expect(response).to have_gitlab_http_status(200) - end.to change { source.members.count }.by(-1) - end - end - - context 'when authenticated as a master/owner' do - context 'and member is a requester' do - it "returns #{source_type == 'project' ? 200 : 404}" do - expect do - delete v3_api("/#{source_type.pluralize}/#{source.id}/members/#{access_requester.id}", master) - - expect(response).to have_gitlab_http_status(source_type == 'project' ? 200 : 404) - end.not_to change { source.requesters.count } - end - end - - it 'deletes the member' do - expect do - delete v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", master) - - expect(response).to have_gitlab_http_status(200) - end.to change { source.members.count }.by(-1) - end - end - - it "returns #{source_type == 'project' ? 200 : 404} if member does not exist" do - delete v3_api("/#{source_type.pluralize}/#{source.id}/members/123", master) - - expect(response).to have_gitlab_http_status(source_type == 'project' ? 200 : 404) - end - end - end - - it_behaves_like 'GET /:sources/:id/members', 'project' do - let(:source) { project } - end - - it_behaves_like 'GET /:sources/:id/members', 'group' do - let(:source) { group } - end - - it_behaves_like 'GET /:sources/:id/members/:user_id', 'project' do - let(:source) { project } - end - - it_behaves_like 'GET /:sources/:id/members/:user_id', 'group' do - let(:source) { group } - end - - it_behaves_like 'POST /:sources/:id/members', 'project' do - let(:source) { project } - end - - it_behaves_like 'POST /:sources/:id/members', 'group' do - let(:source) { group } - end - - it_behaves_like 'PUT /:sources/:id/members/:user_id', 'project' do - let(:source) { project } - end - - it_behaves_like 'PUT /:sources/:id/members/:user_id', 'group' do - let(:source) { group } - end - - it_behaves_like 'DELETE /:sources/:id/members/:user_id', 'project' do - let(:source) { project } - end - - it_behaves_like 'DELETE /:sources/:id/members/:user_id', 'group' do - let(:source) { group } - end - - context 'Adding owner to project' do - it 'returns 403' do - expect do - post v3_api("/projects/#{project.id}/members", master), - user_id: stranger.id, access_level: Member::OWNER - - expect(response).to have_gitlab_http_status(422) - end.to change { project.members.count }.by(0) - end - end -end diff --git a/spec/requests/api/v3/merge_request_diffs_spec.rb b/spec/requests/api/v3/merge_request_diffs_spec.rb deleted file mode 100644 index 547c066fadc..00000000000 --- a/spec/requests/api/v3/merge_request_diffs_spec.rb +++ /dev/null @@ -1,48 +0,0 @@ -require "spec_helper" - -describe API::V3::MergeRequestDiffs, 'MergeRequestDiffs' do - let!(:user) { create(:user) } - let!(:merge_request) { create(:merge_request, importing: true) } - let!(:project) { merge_request.target_project } - - before do - merge_request.merge_request_diffs.create(head_commit_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') - merge_request.merge_request_diffs.create(head_commit_sha: '5937ac0a7beb003549fc5fd26fc247adbce4a52e') - project.add_master(user) - end - - describe 'GET /projects/:id/merge_requests/:merge_request_id/versions' do - it 'returns 200 for a valid merge request' do - get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions", user) - merge_request_diff = merge_request.merge_request_diffs.last - - expect(response.status).to eq 200 - expect(json_response.size).to eq(merge_request.merge_request_diffs.size) - expect(json_response.first['id']).to eq(merge_request_diff.id) - expect(json_response.first['head_commit_sha']).to eq(merge_request_diff.head_commit_sha) - end - - it 'returns a 404 when merge_request_id not found' do - get v3_api("/projects/#{project.id}/merge_requests/999/versions", user) - expect(response).to have_gitlab_http_status(404) - end - end - - describe 'GET /projects/:id/merge_requests/:merge_request_id/versions/:version_id' do - it 'returns a 200 for a valid merge request' do - merge_request_diff = merge_request.merge_request_diffs.first - get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions/#{merge_request_diff.id}", user) - - expect(response.status).to eq 200 - expect(json_response['id']).to eq(merge_request_diff.id) - expect(json_response['head_commit_sha']).to eq(merge_request_diff.head_commit_sha) - expect(json_response['diffs'].size).to eq(merge_request_diff.diffs.size) - end - - it 'returns a 404 when merge_request_id not found' do - get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions/999", user) - - expect(response).to have_gitlab_http_status(404) - end - end -end diff --git a/spec/requests/api/v3/merge_requests_spec.rb b/spec/requests/api/v3/merge_requests_spec.rb deleted file mode 100644 index 79a16fbd1b0..00000000000 --- a/spec/requests/api/v3/merge_requests_spec.rb +++ /dev/null @@ -1,767 +0,0 @@ -require "spec_helper" - -describe API::MergeRequests do - include ProjectForksHelper - - let(:base_time) { Time.now } - let(:user) { create(:user) } - let(:admin) { create(:user, :admin) } - let(:non_member) { create(:user) } - let!(:project) { create(:project, :public, :repository, creator: user, namespace: user.namespace) } - let!(:merge_request) { create(:merge_request, :simple, author: user, assignee: user, source_project: project, title: "Test", created_at: base_time) } - let!(:merge_request_closed) { create(:merge_request, state: "closed", author: user, assignee: user, source_project: project, title: "Closed test", created_at: base_time + 1.second) } - let!(:merge_request_merged) { create(:merge_request, state: "merged", author: user, assignee: user, source_project: project, title: "Merged test", created_at: base_time + 2.seconds, merge_commit_sha: '9999999999999999999999999999999999999999') } - let(:milestone) { create(:milestone, title: '1.0.0', project: project) } - - before do - project.add_reporter(user) - end - - describe "GET /projects/:id/merge_requests" do - context "when unauthenticated" do - it "returns authentication error" do - get v3_api("/projects/#{project.id}/merge_requests") - expect(response).to have_gitlab_http_status(401) - end - end - - context "when authenticated" do - it "returns an array of all merge_requests" do - get v3_api("/projects/#{project.id}/merge_requests", user) - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(3) - expect(json_response.last['title']).to eq(merge_request.title) - expect(json_response.last).to have_key('web_url') - expect(json_response.last['sha']).to eq(merge_request.diff_head_sha) - expect(json_response.last['merge_commit_sha']).to be_nil - expect(json_response.last['merge_commit_sha']).to eq(merge_request.merge_commit_sha) - expect(json_response.first['title']).to eq(merge_request_merged.title) - expect(json_response.first['sha']).to eq(merge_request_merged.diff_head_sha) - expect(json_response.first['merge_commit_sha']).not_to be_nil - expect(json_response.first['merge_commit_sha']).to eq(merge_request_merged.merge_commit_sha) - expect(json_response.first['squash']).to eq(merge_request_merged.squash) - end - - it "returns an array of all merge_requests" do - get v3_api("/projects/#{project.id}/merge_requests?state", user) - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(3) - expect(json_response.last['title']).to eq(merge_request.title) - end - - it "returns an array of open merge_requests" do - get v3_api("/projects/#{project.id}/merge_requests?state=opened", user) - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(1) - expect(json_response.last['title']).to eq(merge_request.title) - end - - it "returns an array of closed merge_requests" do - get v3_api("/projects/#{project.id}/merge_requests?state=closed", user) - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(1) - expect(json_response.first['title']).to eq(merge_request_closed.title) - end - - it "returns an array of merged merge_requests" do - get v3_api("/projects/#{project.id}/merge_requests?state=merged", user) - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(1) - expect(json_response.first['title']).to eq(merge_request_merged.title) - end - - it 'matches V3 response schema' do - get v3_api("/projects/#{project.id}/merge_requests", user) - - expect(response).to have_gitlab_http_status(200) - expect(response).to match_response_schema('public_api/v3/merge_requests') - end - - context "with ordering" do - before do - @mr_later = mr_with_later_created_and_updated_at_time - @mr_earlier = mr_with_earlier_created_and_updated_at_time - end - - it "returns an array of merge_requests in ascending order" do - get v3_api("/projects/#{project.id}/merge_requests?sort=asc", user) - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(3) - response_dates = json_response.map { |merge_request| merge_request['created_at'] } - expect(response_dates).to eq(response_dates.sort) - end - - it "returns an array of merge_requests in descending order" do - get v3_api("/projects/#{project.id}/merge_requests?sort=desc", user) - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(3) - response_dates = json_response.map { |merge_request| merge_request['created_at'] } - expect(response_dates).to eq(response_dates.sort.reverse) - end - - it "returns an array of merge_requests ordered by updated_at" do - get v3_api("/projects/#{project.id}/merge_requests?order_by=updated_at", user) - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(3) - response_dates = json_response.map { |merge_request| merge_request['updated_at'] } - expect(response_dates).to eq(response_dates.sort.reverse) - end - - it "returns an array of merge_requests ordered by created_at" do - get v3_api("/projects/#{project.id}/merge_requests?order_by=created_at&sort=asc", user) - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(3) - response_dates = json_response.map { |merge_request| merge_request['created_at'] } - expect(response_dates).to eq(response_dates.sort) - end - end - end - end - - describe "GET /projects/:id/merge_requests/:merge_request_id" do - it 'exposes known attributes' do - get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['id']).to eq(merge_request.id) - expect(json_response['iid']).to eq(merge_request.iid) - expect(json_response['project_id']).to eq(merge_request.project.id) - expect(json_response['title']).to eq(merge_request.title) - expect(json_response['description']).to eq(merge_request.description) - expect(json_response['state']).to eq(merge_request.state) - expect(json_response['created_at']).to be_present - expect(json_response['updated_at']).to be_present - expect(json_response['labels']).to eq(merge_request.label_names) - expect(json_response['milestone']).to be_nil - expect(json_response['assignee']).to be_a Hash - expect(json_response['author']).to be_a Hash - expect(json_response['target_branch']).to eq(merge_request.target_branch) - expect(json_response['source_branch']).to eq(merge_request.source_branch) - expect(json_response['upvotes']).to eq(0) - expect(json_response['downvotes']).to eq(0) - expect(json_response['source_project_id']).to eq(merge_request.source_project.id) - expect(json_response['target_project_id']).to eq(merge_request.target_project.id) - expect(json_response['work_in_progress']).to be_falsy - expect(json_response['merge_when_build_succeeds']).to be_falsy - expect(json_response['merge_status']).to eq('can_be_merged') - expect(json_response['should_close_merge_request']).to be_falsy - expect(json_response['force_close_merge_request']).to be_falsy - end - - it "returns merge_request" do - get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user) - expect(response).to have_gitlab_http_status(200) - expect(json_response['title']).to eq(merge_request.title) - expect(json_response['iid']).to eq(merge_request.iid) - expect(json_response['work_in_progress']).to eq(false) - expect(json_response['merge_status']).to eq('can_be_merged') - expect(json_response['should_close_merge_request']).to be_falsy - expect(json_response['force_close_merge_request']).to be_falsy - end - - it 'returns merge_request by iid' do - url = "/projects/#{project.id}/merge_requests?iid=#{merge_request.iid}" - get v3_api(url, user) - expect(response.status).to eq 200 - expect(json_response.first['title']).to eq merge_request.title - expect(json_response.first['id']).to eq merge_request.id - end - - it 'returns merge_request by iid array' do - get v3_api("/projects/#{project.id}/merge_requests", user), iid: [merge_request.iid, merge_request_closed.iid] - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(2) - expect(json_response.first['title']).to eq merge_request_closed.title - expect(json_response.first['id']).to eq merge_request_closed.id - end - - it "returns a 404 error if merge_request_id not found" do - get v3_api("/projects/#{project.id}/merge_requests/999", user) - expect(response).to have_gitlab_http_status(404) - end - - context 'Work in Progress' do - let!(:merge_request_wip) { create(:merge_request, author: user, assignee: user, source_project: project, target_project: project, title: "WIP: Test", created_at: base_time + 1.second) } - - it "returns merge_request" do - get v3_api("/projects/#{project.id}/merge_requests/#{merge_request_wip.id}", user) - expect(response).to have_gitlab_http_status(200) - expect(json_response['work_in_progress']).to eq(true) - end - end - end - - describe 'GET /projects/:id/merge_requests/:merge_request_id/commits' do - it 'returns a 200 when merge request is valid' do - get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/commits", user) - commit = merge_request.commits.first - - expect(response.status).to eq 200 - expect(json_response.size).to eq(merge_request.commits.size) - expect(json_response.first['id']).to eq(commit.id) - expect(json_response.first['title']).to eq(commit.title) - end - - it 'returns a 404 when merge_request_id not found' do - get v3_api("/projects/#{project.id}/merge_requests/999/commits", user) - expect(response).to have_gitlab_http_status(404) - end - end - - describe 'GET /projects/:id/merge_requests/:merge_request_id/changes' do - it 'returns the change information of the merge_request' do - get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/changes", user) - expect(response.status).to eq 200 - expect(json_response['changes'].size).to eq(merge_request.diffs.size) - end - - it 'returns a 404 when merge_request_id not found' do - get v3_api("/projects/#{project.id}/merge_requests/999/changes", user) - expect(response).to have_gitlab_http_status(404) - end - end - - describe "POST /projects/:id/merge_requests" do - context 'between branches projects' do - it "returns merge_request" do - post v3_api("/projects/#{project.id}/merge_requests", user), - title: 'Test merge_request', - source_branch: 'feature_conflict', - target_branch: 'master', - author: user, - labels: 'label, label2', - milestone_id: milestone.id, - remove_source_branch: true, - squash: true - - expect(response).to have_gitlab_http_status(201) - expect(json_response['title']).to eq('Test merge_request') - expect(json_response['labels']).to eq(%w(label label2)) - expect(json_response['milestone']['id']).to eq(milestone.id) - expect(json_response['force_remove_source_branch']).to be_truthy - expect(json_response['squash']).to be_truthy - end - - it "returns 422 when source_branch equals target_branch" do - post v3_api("/projects/#{project.id}/merge_requests", user), - title: "Test merge_request", source_branch: "master", target_branch: "master", author: user - expect(response).to have_gitlab_http_status(422) - end - - it "returns 400 when source_branch is missing" do - post v3_api("/projects/#{project.id}/merge_requests", user), - title: "Test merge_request", target_branch: "master", author: user - expect(response).to have_gitlab_http_status(400) - end - - it "returns 400 when target_branch is missing" do - post v3_api("/projects/#{project.id}/merge_requests", user), - title: "Test merge_request", source_branch: "markdown", author: user - expect(response).to have_gitlab_http_status(400) - end - - it "returns 400 when title is missing" do - post v3_api("/projects/#{project.id}/merge_requests", user), - target_branch: 'master', source_branch: 'markdown' - expect(response).to have_gitlab_http_status(400) - end - - it 'allows special label names' do - post v3_api("/projects/#{project.id}/merge_requests", user), - title: 'Test merge_request', - source_branch: 'markdown', - target_branch: 'master', - author: user, - labels: 'label, label?, label&foo, ?, &' - expect(response.status).to eq(201) - expect(json_response['labels']).to include 'label' - expect(json_response['labels']).to include 'label?' - expect(json_response['labels']).to include 'label&foo' - expect(json_response['labels']).to include '?' - expect(json_response['labels']).to include '&' - end - - context 'with existing MR' do - before do - post v3_api("/projects/#{project.id}/merge_requests", user), - title: 'Test merge_request', - source_branch: 'feature_conflict', - target_branch: 'master', - author: user - @mr = MergeRequest.all.last - end - - it 'returns 409 when MR already exists for source/target' do - expect do - post v3_api("/projects/#{project.id}/merge_requests", user), - title: 'New test merge_request', - source_branch: 'feature_conflict', - target_branch: 'master', - author: user - end.to change { MergeRequest.count }.by(0) - expect(response).to have_gitlab_http_status(409) - end - end - end - - context 'forked projects' do - let!(:user2) { create(:user) } - let!(:forked_project) { fork_project(project, user2, repository: true) } - let!(:unrelated_project) { create(:project, namespace: create(:user).namespace, creator_id: user2.id) } - - before do - forked_project.add_reporter(user2) - end - - it "returns merge_request" do - post v3_api("/projects/#{forked_project.id}/merge_requests", user2), - title: 'Test merge_request', source_branch: "feature_conflict", target_branch: "master", - author: user2, target_project_id: project.id, description: 'Test description for Test merge_request' - expect(response).to have_gitlab_http_status(201) - expect(json_response['title']).to eq('Test merge_request') - expect(json_response['description']).to eq('Test description for Test merge_request') - end - - it "does not return 422 when source_branch equals target_branch" do - expect(project.id).not_to eq(forked_project.id) - expect(forked_project.forked?).to be_truthy - expect(forked_project.forked_from_project).to eq(project) - post v3_api("/projects/#{forked_project.id}/merge_requests", user2), - title: 'Test merge_request', source_branch: "master", target_branch: "master", author: user2, target_project_id: project.id - expect(response).to have_gitlab_http_status(201) - expect(json_response['title']).to eq('Test merge_request') - end - - it "returns 403 when target project has disabled merge requests" do - project.project_feature.update(merge_requests_access_level: 0) - - post v3_api("/projects/#{forked_project.id}/merge_requests", user2), - title: 'Test', - target_branch: "master", - source_branch: 'markdown', - author: user2, - target_project_id: project.id - - expect(response).to have_gitlab_http_status(403) - end - - it "returns 400 when source_branch is missing" do - post v3_api("/projects/#{forked_project.id}/merge_requests", user2), - title: 'Test merge_request', target_branch: "master", author: user2, target_project_id: project.id - expect(response).to have_gitlab_http_status(400) - end - - it "returns 400 when target_branch is missing" do - post v3_api("/projects/#{forked_project.id}/merge_requests", user2), - title: 'Test merge_request', target_branch: "master", author: user2, target_project_id: project.id - expect(response).to have_gitlab_http_status(400) - end - - it "returns 400 when title is missing" do - post v3_api("/projects/#{forked_project.id}/merge_requests", user2), - target_branch: 'master', source_branch: 'markdown', author: user2, target_project_id: project.id - expect(response).to have_gitlab_http_status(400) - end - - context 'when target_branch and target_project_id is specified' do - let(:params) do - { title: 'Test merge_request', - target_branch: 'master', - source_branch: 'markdown', - author: user2, - target_project_id: unrelated_project.id } - end - - it 'returns 422 if targeting a different fork' do - unrelated_project.add_developer(user2) - - post v3_api("/projects/#{forked_project.id}/merge_requests", user2), params - - expect(response).to have_gitlab_http_status(422) - end - - it 'returns 403 if targeting a different fork which user can not access' do - post v3_api("/projects/#{forked_project.id}/merge_requests", user2), params - - expect(response).to have_gitlab_http_status(403) - end - end - - it "returns 201 when target_branch is specified and for the same project" do - post v3_api("/projects/#{forked_project.id}/merge_requests", user2), - title: 'Test merge_request', target_branch: 'master', source_branch: 'markdown', author: user2, target_project_id: forked_project.id - expect(response).to have_gitlab_http_status(201) - end - end - end - - describe "DELETE /projects/:id/merge_requests/:merge_request_id" do - context "when the user is developer" do - let(:developer) { create(:user) } - - before do - project.add_developer(developer) - end - - it "denies the deletion of the merge request" do - delete v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", developer) - expect(response).to have_gitlab_http_status(403) - end - end - - context "when the user is project owner" do - it "destroys the merge request owners can destroy" do - delete v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user) - - expect(response).to have_gitlab_http_status(200) - end - end - end - - describe "PUT /projects/:id/merge_requests/:merge_request_id/merge" do - let(:pipeline) { create(:ci_pipeline_without_jobs) } - - it "returns merge_request in case of success" do - put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user) - - expect(response).to have_gitlab_http_status(200) - end - - it "returns 406 if branch can't be merged" do - allow_any_instance_of(MergeRequest) - .to receive(:can_be_merged?).and_return(false) - - put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user) - - expect(response).to have_gitlab_http_status(406) - expect(json_response['message']).to eq('Branch cannot be merged') - end - - it "returns 405 if merge_request is not open" do - merge_request.close - put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user) - expect(response).to have_gitlab_http_status(405) - expect(json_response['message']).to eq('405 Method Not Allowed') - end - - it "returns 405 if merge_request is a work in progress" do - merge_request.update_attribute(:title, "WIP: #{merge_request.title}") - put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user) - expect(response).to have_gitlab_http_status(405) - expect(json_response['message']).to eq('405 Method Not Allowed') - end - - it 'returns 405 if the build failed for a merge request that requires success' do - allow_any_instance_of(MergeRequest).to receive(:mergeable_ci_state?).and_return(false) - - put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user) - - expect(response).to have_gitlab_http_status(405) - expect(json_response['message']).to eq('405 Method Not Allowed') - end - - it "returns 401 if user has no permissions to merge" do - user2 = create(:user) - project.add_reporter(user2) - put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user2) - expect(response).to have_gitlab_http_status(401) - expect(json_response['message']).to eq('401 Unauthorized') - end - - it "returns 409 if the SHA parameter doesn't match" do - put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user), sha: merge_request.diff_head_sha.reverse - - expect(response).to have_gitlab_http_status(409) - expect(json_response['message']).to start_with('SHA does not match HEAD of source branch') - end - - it "succeeds if the SHA parameter matches" do - put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user), sha: merge_request.diff_head_sha - - expect(response).to have_gitlab_http_status(200) - end - - it "updates the MR's squash attribute" do - expect do - put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user), squash: true - end.to change { merge_request.reload.squash } - - expect(response).to have_gitlab_http_status(200) - end - - it "enables merge when pipeline succeeds if the pipeline is active" do - allow_any_instance_of(MergeRequest).to receive(:head_pipeline).and_return(pipeline) - allow(pipeline).to receive(:active?).and_return(true) - - put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user), merge_when_build_succeeds: true - - expect(response).to have_gitlab_http_status(200) - expect(json_response['title']).to eq('Test') - expect(json_response['merge_when_build_succeeds']).to eq(true) - end - end - - describe "PUT /projects/:id/merge_requests/:merge_request_id" do - context "to close a MR" do - it "returns merge_request" do - put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), state_event: "close" - - expect(response).to have_gitlab_http_status(200) - expect(json_response['state']).to eq('closed') - end - end - - it "updates title and returns merge_request" do - put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), title: "New title" - expect(response).to have_gitlab_http_status(200) - expect(json_response['title']).to eq('New title') - end - - it "updates description and returns merge_request" do - put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), description: "New description" - expect(response).to have_gitlab_http_status(200) - expect(json_response['description']).to eq('New description') - end - - it "updates milestone_id and returns merge_request" do - put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), milestone_id: milestone.id - expect(response).to have_gitlab_http_status(200) - expect(json_response['milestone']['id']).to eq(milestone.id) - end - - it "updates squash and returns merge_request" do - put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), squash: true - - expect(response).to have_gitlab_http_status(200) - expect(json_response['squash']).to be_truthy - end - - it "returns merge_request with renamed target_branch" do - put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), target_branch: "wiki" - expect(response).to have_gitlab_http_status(200) - expect(json_response['target_branch']).to eq('wiki') - end - - it "returns merge_request that removes the source branch" do - put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), remove_source_branch: true - - expect(response).to have_gitlab_http_status(200) - expect(json_response['force_remove_source_branch']).to be_truthy - end - - it 'allows special label names' do - put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), - title: 'new issue', - labels: 'label, label?, label&foo, ?, &' - - expect(response.status).to eq(200) - expect(json_response['labels']).to include 'label' - expect(json_response['labels']).to include 'label?' - expect(json_response['labels']).to include 'label&foo' - expect(json_response['labels']).to include '?' - expect(json_response['labels']).to include '&' - end - - it 'does not update state when title is empty' do - put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), state_event: 'close', title: nil - - merge_request.reload - expect(response).to have_gitlab_http_status(400) - expect(merge_request.state).to eq('opened') - end - - it 'does not update state when target_branch is empty' do - put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), state_event: 'close', target_branch: nil - - merge_request.reload - expect(response).to have_gitlab_http_status(400) - expect(merge_request.state).to eq('opened') - end - end - - describe "POST /projects/:id/merge_requests/:merge_request_id/comments" do - it "returns comment" do - original_count = merge_request.notes.size - - post v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user), note: "My comment" - - expect(response).to have_gitlab_http_status(201) - expect(json_response['note']).to eq('My comment') - expect(json_response['author']['name']).to eq(user.name) - expect(json_response['author']['username']).to eq(user.username) - expect(merge_request.reload.notes.size).to eq(original_count + 1) - end - - it "returns 400 if note is missing" do - post v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user) - expect(response).to have_gitlab_http_status(400) - end - - it "returns 404 if note is attached to non existent merge request" do - post v3_api("/projects/#{project.id}/merge_requests/404/comments", user), - note: 'My comment' - expect(response).to have_gitlab_http_status(404) - end - end - - describe "GET :id/merge_requests/:merge_request_id/comments" do - let!(:note) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "a comment on a MR") } - let!(:note2) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "another comment on a MR") } - - it "returns merge_request comments ordered by created_at" do - get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user) - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(2) - expect(json_response.first['note']).to eq("a comment on a MR") - expect(json_response.first['author']['id']).to eq(user.id) - expect(json_response.last['note']).to eq("another comment on a MR") - end - - it "returns a 404 error if merge_request_id not found" do - get v3_api("/projects/#{project.id}/merge_requests/999/comments", user) - expect(response).to have_gitlab_http_status(404) - end - end - - describe 'GET :id/merge_requests/:merge_request_id/closes_issues' do - it 'returns the issue that will be closed on merge' do - issue = create(:issue, project: project) - mr = merge_request.tap do |mr| - mr.update_attribute(:description, "Closes #{issue.to_reference(mr.project)}") - end - - get v3_api("/projects/#{project.id}/merge_requests/#{mr.id}/closes_issues", user) - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(1) - expect(json_response.first['id']).to eq(issue.id) - end - - it 'returns an empty array when there are no issues to be closed' do - get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/closes_issues", user) - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(0) - end - - it 'handles external issues' do - jira_project = create(:jira_project, :public, :repository, name: 'JIR_EXT1') - issue = ExternalIssue.new("#{jira_project.name}-123", jira_project) - merge_request = create(:merge_request, :simple, author: user, assignee: user, source_project: jira_project) - merge_request.update_attribute(:description, "Closes #{issue.to_reference(jira_project)}") - - get v3_api("/projects/#{jira_project.id}/merge_requests/#{merge_request.id}/closes_issues", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(1) - expect(json_response.first['title']).to eq(issue.title) - expect(json_response.first['id']).to eq(issue.id) - end - - it 'returns 403 if the user has no access to the merge request' do - project = create(:project, :private, :repository) - merge_request = create(:merge_request, :simple, source_project: project) - guest = create(:user) - project.add_guest(guest) - - get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/closes_issues", guest) - - expect(response).to have_gitlab_http_status(403) - end - end - - describe 'POST :id/merge_requests/:merge_request_id/subscription' do - it 'subscribes to a merge request' do - post v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", admin) - - expect(response).to have_gitlab_http_status(201) - expect(json_response['subscribed']).to eq(true) - end - - it 'returns 304 if already subscribed' do - post v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", user) - - expect(response).to have_gitlab_http_status(304) - end - - it 'returns 404 if the merge request is not found' do - post v3_api("/projects/#{project.id}/merge_requests/123/subscription", user) - - expect(response).to have_gitlab_http_status(404) - end - - it 'returns 403 if user has no access to read code' do - guest = create(:user) - project.add_guest(guest) - - post v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", guest) - - expect(response).to have_gitlab_http_status(403) - end - end - - describe 'DELETE :id/merge_requests/:merge_request_id/subscription' do - it 'unsubscribes from a merge request' do - delete v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['subscribed']).to eq(false) - end - - it 'returns 304 if not subscribed' do - delete v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", admin) - - expect(response).to have_gitlab_http_status(304) - end - - it 'returns 404 if the merge request is not found' do - post v3_api("/projects/#{project.id}/merge_requests/123/subscription", user) - - expect(response).to have_gitlab_http_status(404) - end - - it 'returns 403 if user has no access to read code' do - guest = create(:user) - project.add_guest(guest) - - delete v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", guest) - - expect(response).to have_gitlab_http_status(403) - end - end - - describe 'Time tracking' do - let(:issuable) { merge_request } - - include_examples 'V3 time tracking endpoints', 'merge_request' - end - - def mr_with_later_created_and_updated_at_time - merge_request - merge_request.created_at += 1.hour - merge_request.updated_at += 30.minutes - merge_request.save - merge_request - end - - def mr_with_earlier_created_and_updated_at_time - merge_request_closed - merge_request_closed.created_at -= 1.hour - merge_request_closed.updated_at -= 30.minutes - merge_request_closed.save - merge_request_closed - end -end diff --git a/spec/requests/api/v3/milestones_spec.rb b/spec/requests/api/v3/milestones_spec.rb deleted file mode 100644 index 6021600e09c..00000000000 --- a/spec/requests/api/v3/milestones_spec.rb +++ /dev/null @@ -1,238 +0,0 @@ -require 'spec_helper' - -describe API::V3::Milestones do - let(:user) { create(:user) } - let!(:project) { create(:project, namespace: user.namespace ) } - let!(:closed_milestone) { create(:closed_milestone, project: project) } - let!(:milestone) { create(:milestone, project: project) } - - before { project.add_developer(user) } - - describe 'GET /projects/:id/milestones' do - it 'returns project milestones' do - get v3_api("/projects/#{project.id}/milestones", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.first['title']).to eq(milestone.title) - end - - it 'returns a 401 error if user not authenticated' do - get v3_api("/projects/#{project.id}/milestones") - - expect(response).to have_gitlab_http_status(401) - end - - it 'returns an array of active milestones' do - get v3_api("/projects/#{project.id}/milestones?state=active", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(1) - expect(json_response.first['id']).to eq(milestone.id) - end - - it 'returns an array of closed milestones' do - get v3_api("/projects/#{project.id}/milestones?state=closed", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(1) - expect(json_response.first['id']).to eq(closed_milestone.id) - end - end - - describe 'GET /projects/:id/milestones/:milestone_id' do - it 'returns a project milestone by id' do - get v3_api("/projects/#{project.id}/milestones/#{milestone.id}", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['title']).to eq(milestone.title) - expect(json_response['iid']).to eq(milestone.iid) - end - - it 'returns a project milestone by iid' do - get v3_api("/projects/#{project.id}/milestones?iid=#{closed_milestone.iid}", user) - - expect(response.status).to eq 200 - expect(json_response.size).to eq(1) - expect(json_response.first['title']).to eq closed_milestone.title - expect(json_response.first['id']).to eq closed_milestone.id - end - - it 'returns a project milestone by iid array' do - get v3_api("/projects/#{project.id}/milestones", user), iid: [milestone.iid, closed_milestone.iid] - - expect(response).to have_gitlab_http_status(200) - expect(json_response.size).to eq(2) - expect(json_response.first['title']).to eq milestone.title - expect(json_response.first['id']).to eq milestone.id - end - - it 'returns 401 error if user not authenticated' do - get v3_api("/projects/#{project.id}/milestones/#{milestone.id}") - - expect(response).to have_gitlab_http_status(401) - end - - it 'returns a 404 error if milestone id not found' do - get v3_api("/projects/#{project.id}/milestones/1234", user) - - expect(response).to have_gitlab_http_status(404) - end - end - - describe 'POST /projects/:id/milestones' do - it 'creates a new project milestone' do - post v3_api("/projects/#{project.id}/milestones", user), title: 'new milestone' - - expect(response).to have_gitlab_http_status(201) - expect(json_response['title']).to eq('new milestone') - expect(json_response['description']).to be_nil - end - - it 'creates a new project milestone with description and dates' do - post v3_api("/projects/#{project.id}/milestones", user), - title: 'new milestone', description: 'release', due_date: '2013-03-02', start_date: '2013-02-02' - - expect(response).to have_gitlab_http_status(201) - expect(json_response['description']).to eq('release') - expect(json_response['due_date']).to eq('2013-03-02') - expect(json_response['start_date']).to eq('2013-02-02') - end - - it 'returns a 400 error if title is missing' do - post v3_api("/projects/#{project.id}/milestones", user) - - expect(response).to have_gitlab_http_status(400) - end - - it 'returns a 400 error if params are invalid (duplicate title)' do - post v3_api("/projects/#{project.id}/milestones", user), - title: milestone.title, description: 'release', due_date: '2013-03-02' - - expect(response).to have_gitlab_http_status(400) - end - - it 'creates a new project with reserved html characters' do - post v3_api("/projects/#{project.id}/milestones", user), title: 'foo & bar 1.1 -> 2.2' - - expect(response).to have_gitlab_http_status(201) - expect(json_response['title']).to eq('foo & bar 1.1 -> 2.2') - expect(json_response['description']).to be_nil - end - end - - describe 'PUT /projects/:id/milestones/:milestone_id' do - it 'updates a project milestone' do - put v3_api("/projects/#{project.id}/milestones/#{milestone.id}", user), - title: 'updated title' - - expect(response).to have_gitlab_http_status(200) - expect(json_response['title']).to eq('updated title') - end - - it 'removes a due date if nil is passed' do - milestone.update!(due_date: "2016-08-05") - - put v3_api("/projects/#{project.id}/milestones/#{milestone.id}", user), due_date: nil - - expect(response).to have_gitlab_http_status(200) - expect(json_response['due_date']).to be_nil - end - - it 'returns a 404 error if milestone id not found' do - put v3_api("/projects/#{project.id}/milestones/1234", user), - title: 'updated title' - - expect(response).to have_gitlab_http_status(404) - end - end - - describe 'PUT /projects/:id/milestones/:milestone_id to close milestone' do - it 'updates a project milestone' do - put v3_api("/projects/#{project.id}/milestones/#{milestone.id}", user), - state_event: 'close' - expect(response).to have_gitlab_http_status(200) - - expect(json_response['state']).to eq('closed') - end - end - - describe 'PUT /projects/:id/milestones/:milestone_id to test observer on close' do - it 'creates an activity event when an milestone is closed' do - expect(Event).to receive(:create!) - - put v3_api("/projects/#{project.id}/milestones/#{milestone.id}", user), - state_event: 'close' - end - end - - describe 'GET /projects/:id/milestones/:milestone_id/issues' do - before do - milestone.issues << create(:issue, project: project) - end - it 'returns project issues for a particular milestone' do - get v3_api("/projects/#{project.id}/milestones/#{milestone.id}/issues", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.first['milestone']['title']).to eq(milestone.title) - end - - it 'matches V3 response schema for a list of issues' do - get v3_api("/projects/#{project.id}/milestones/#{milestone.id}/issues", user) - - expect(response).to have_gitlab_http_status(200) - expect(response).to match_response_schema('public_api/v3/issues') - end - - it 'returns a 401 error if user not authenticated' do - get v3_api("/projects/#{project.id}/milestones/#{milestone.id}/issues") - - expect(response).to have_gitlab_http_status(401) - end - - describe 'confidential issues' do - let(:public_project) { create(:project, :public) } - let(:milestone) { create(:milestone, project: public_project) } - let(:issue) { create(:issue, project: public_project) } - let(:confidential_issue) { create(:issue, confidential: true, project: public_project) } - - before do - public_project.add_developer(user) - milestone.issues << issue << confidential_issue - end - - it 'returns confidential issues to team members' do - get v3_api("/projects/#{public_project.id}/milestones/#{milestone.id}/issues", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.size).to eq(2) - expect(json_response.map { |issue| issue['id'] }).to include(issue.id, confidential_issue.id) - end - - it 'does not return confidential issues to team members with guest role' do - member = create(:user) - project.add_guest(member) - - get v3_api("/projects/#{public_project.id}/milestones/#{milestone.id}/issues", member) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.size).to eq(1) - expect(json_response.map { |issue| issue['id'] }).to include(issue.id) - end - - it 'does not return confidential issues to regular users' do - get v3_api("/projects/#{public_project.id}/milestones/#{milestone.id}/issues", create(:user)) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.size).to eq(1) - expect(json_response.map { |issue| issue['id'] }).to include(issue.id) - end - end - end -end diff --git a/spec/requests/api/v3/notes_spec.rb b/spec/requests/api/v3/notes_spec.rb deleted file mode 100644 index 5532795ab02..00000000000 --- a/spec/requests/api/v3/notes_spec.rb +++ /dev/null @@ -1,431 +0,0 @@ -require 'spec_helper' - -describe API::V3::Notes do - let(:user) { create(:user) } - let!(:project) { create(:project, :public, namespace: user.namespace) } - let!(:issue) { create(:issue, project: project, author: user) } - let!(:merge_request) { create(:merge_request, source_project: project, target_project: project, author: user) } - let!(:snippet) { create(:project_snippet, project: project, author: user) } - let!(:issue_note) { create(:note, noteable: issue, project: project, author: user) } - let!(:merge_request_note) { create(:note, noteable: merge_request, project: project, author: user) } - let!(:snippet_note) { create(:note, noteable: snippet, project: project, author: user) } - - # For testing the cross-reference of a private issue in a public issue - let(:private_user) { create(:user) } - let(:private_project) do - create(:project, namespace: private_user.namespace) - .tap { |p| p.add_master(private_user) } - end - let(:private_issue) { create(:issue, project: private_project) } - - let(:ext_proj) { create(:project, :public) } - let(:ext_issue) { create(:issue, project: ext_proj) } - - let!(:cross_reference_note) do - create :note, - noteable: ext_issue, project: ext_proj, - note: "mentioned in issue #{private_issue.to_reference(ext_proj)}", - system: true - end - - before { project.add_reporter(user) } - - describe "GET /projects/:id/noteable/:noteable_id/notes" do - context "when noteable is an Issue" do - it "returns an array of issue notes" do - get v3_api("/projects/#{project.id}/issues/#{issue.id}/notes", user) - - expect(response).to have_gitlab_http_status(200) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array - expect(json_response.first['body']).to eq(issue_note.note) - expect(json_response.first['upvote']).to be_falsey - expect(json_response.first['downvote']).to be_falsey - end - - it "returns a 404 error when issue id not found" do - get v3_api("/projects/#{project.id}/issues/12345/notes", user) - - expect(response).to have_gitlab_http_status(404) - end - - context "and current user cannot view the notes" do - it "returns an empty array" do - get v3_api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes", user) - - expect(response).to have_gitlab_http_status(200) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array - expect(json_response).to be_empty - end - - context "and issue is confidential" do - before { ext_issue.update_attributes(confidential: true) } - - it "returns 404" do - get v3_api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes", user) - - expect(response).to have_gitlab_http_status(404) - end - end - - context "and current user can view the note" do - it "returns an empty array" do - get v3_api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes", private_user) - - expect(response).to have_gitlab_http_status(200) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array - expect(json_response.first['body']).to eq(cross_reference_note.note) - end - end - end - end - - context "when noteable is a Snippet" do - it "returns an array of snippet notes" do - get v3_api("/projects/#{project.id}/snippets/#{snippet.id}/notes", user) - - expect(response).to have_gitlab_http_status(200) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array - expect(json_response.first['body']).to eq(snippet_note.note) - end - - it "returns a 404 error when snippet id not found" do - get v3_api("/projects/#{project.id}/snippets/42/notes", user) - - expect(response).to have_gitlab_http_status(404) - end - - it "returns 404 when not authorized" do - get v3_api("/projects/#{project.id}/snippets/#{snippet.id}/notes", private_user) - - expect(response).to have_gitlab_http_status(404) - end - end - - context "when noteable is a Merge Request" do - it "returns an array of merge_requests notes" do - get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/notes", user) - - expect(response).to have_gitlab_http_status(200) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array - expect(json_response.first['body']).to eq(merge_request_note.note) - end - - it "returns a 404 error if merge request id not found" do - get v3_api("/projects/#{project.id}/merge_requests/4444/notes", user) - - expect(response).to have_gitlab_http_status(404) - end - - it "returns 404 when not authorized" do - get v3_api("/projects/#{project.id}/merge_requests/4444/notes", private_user) - - expect(response).to have_gitlab_http_status(404) - end - end - end - - describe "GET /projects/:id/noteable/:noteable_id/notes/:note_id" do - context "when noteable is an Issue" do - it "returns an issue note by id" do - get v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/#{issue_note.id}", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['body']).to eq(issue_note.note) - end - - it "returns a 404 error if issue note not found" do - get v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/12345", user) - - expect(response).to have_gitlab_http_status(404) - end - - context "and current user cannot view the note" do - it "returns a 404 error" do - get v3_api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes/#{cross_reference_note.id}", user) - - expect(response).to have_gitlab_http_status(404) - end - - context "when issue is confidential" do - before { issue.update_attributes(confidential: true) } - - it "returns 404" do - get v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/#{issue_note.id}", private_user) - - expect(response).to have_gitlab_http_status(404) - end - end - - context "and current user can view the note" do - it "returns an issue note by id" do - get v3_api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes/#{cross_reference_note.id}", private_user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['body']).to eq(cross_reference_note.note) - end - end - end - end - - context "when noteable is a Snippet" do - it "returns a snippet note by id" do - get v3_api("/projects/#{project.id}/snippets/#{snippet.id}/notes/#{snippet_note.id}", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['body']).to eq(snippet_note.note) - end - - it "returns a 404 error if snippet note not found" do - get v3_api("/projects/#{project.id}/snippets/#{snippet.id}/notes/12345", user) - - expect(response).to have_gitlab_http_status(404) - end - end - end - - describe "POST /projects/:id/noteable/:noteable_id/notes" do - context "when noteable is an Issue" do - it "creates a new issue note" do - post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes", user), body: 'hi!' - - expect(response).to have_gitlab_http_status(201) - expect(json_response['body']).to eq('hi!') - expect(json_response['author']['username']).to eq(user.username) - end - - it "returns a 400 bad request error if body not given" do - post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes", user) - - expect(response).to have_gitlab_http_status(400) - end - - it "returns a 401 unauthorized error if user not authenticated" do - post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes"), body: 'hi!' - - expect(response).to have_gitlab_http_status(401) - end - - context 'when an admin or owner makes the request' do - it 'accepts the creation date to be set' do - creation_time = 2.weeks.ago - post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes", user), - body: 'hi!', created_at: creation_time - - expect(response).to have_gitlab_http_status(201) - expect(json_response['body']).to eq('hi!') - expect(json_response['author']['username']).to eq(user.username) - expect(Time.parse(json_response['created_at'])).to be_like_time(creation_time) - end - end - - context 'when the user is posting an award emoji on an issue created by someone else' do - let(:issue2) { create(:issue, project: project) } - - it 'creates a new issue note' do - post v3_api("/projects/#{project.id}/issues/#{issue2.id}/notes", user), body: ':+1:' - - expect(response).to have_gitlab_http_status(201) - expect(json_response['body']).to eq(':+1:') - end - end - - context 'when the user is posting an award emoji on his/her own issue' do - it 'creates a new issue note' do - post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes", user), body: ':+1:' - - expect(response).to have_gitlab_http_status(201) - expect(json_response['body']).to eq(':+1:') - end - end - end - - context "when noteable is a Snippet" do - it "creates a new snippet note" do - post v3_api("/projects/#{project.id}/snippets/#{snippet.id}/notes", user), body: 'hi!' - - expect(response).to have_gitlab_http_status(201) - expect(json_response['body']).to eq('hi!') - expect(json_response['author']['username']).to eq(user.username) - end - - it "returns a 400 bad request error if body not given" do - post v3_api("/projects/#{project.id}/snippets/#{snippet.id}/notes", user) - - expect(response).to have_gitlab_http_status(400) - end - - it "returns a 401 unauthorized error if user not authenticated" do - post v3_api("/projects/#{project.id}/snippets/#{snippet.id}/notes"), body: 'hi!' - - expect(response).to have_gitlab_http_status(401) - end - end - - context 'when user does not have access to read the noteable' do - it 'responds with 404' do - project = create(:project, :private) { |p| p.add_guest(user) } - issue = create(:issue, :confidential, project: project) - - post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes", user), - body: 'Foo' - - expect(response).to have_gitlab_http_status(404) - end - end - - context 'when user does not have access to create noteable' do - let(:private_issue) { create(:issue, project: create(:project, :private)) } - - ## - # We are posting to project user has access to, but we use issue id - # from a different project, see #15577 - # - before do - post v3_api("/projects/#{project.id}/issues/#{private_issue.id}/notes", user), - body: 'Hi!' - end - - it 'responds with resource not found error' do - expect(response.status).to eq 404 - end - - it 'does not create new note' do - expect(private_issue.notes.reload).to be_empty - end - end - end - - describe "POST /projects/:id/noteable/:noteable_id/notes to test observer on create" do - it "creates an activity event when an issue note is created" do - expect(Event).to receive(:create!) - - post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes", user), body: 'hi!' - end - end - - describe 'PUT /projects/:id/noteable/:noteable_id/notes/:note_id' do - context 'when noteable is an Issue' do - it 'returns modified note' do - put v3_api("/projects/#{project.id}/issues/#{issue.id}/"\ - "notes/#{issue_note.id}", user), body: 'Hello!' - - expect(response).to have_gitlab_http_status(200) - expect(json_response['body']).to eq('Hello!') - end - - it 'returns a 404 error when note id not found' do - put v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/12345", user), - body: 'Hello!' - - expect(response).to have_gitlab_http_status(404) - end - - it 'returns a 400 bad request error if body not given' do - put v3_api("/projects/#{project.id}/issues/#{issue.id}/"\ - "notes/#{issue_note.id}", user) - - expect(response).to have_gitlab_http_status(400) - end - end - - context 'when noteable is a Snippet' do - it 'returns modified note' do - put v3_api("/projects/#{project.id}/snippets/#{snippet.id}/"\ - "notes/#{snippet_note.id}", user), body: 'Hello!' - - expect(response).to have_gitlab_http_status(200) - expect(json_response['body']).to eq('Hello!') - end - - it 'returns a 404 error when note id not found' do - put v3_api("/projects/#{project.id}/snippets/#{snippet.id}/"\ - "notes/12345", user), body: "Hello!" - - expect(response).to have_gitlab_http_status(404) - end - end - - context 'when noteable is a Merge Request' do - it 'returns modified note' do - put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/"\ - "notes/#{merge_request_note.id}", user), body: 'Hello!' - - expect(response).to have_gitlab_http_status(200) - expect(json_response['body']).to eq('Hello!') - end - - it 'returns a 404 error when note id not found' do - put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/"\ - "notes/12345", user), body: "Hello!" - - expect(response).to have_gitlab_http_status(404) - end - end - end - - describe 'DELETE /projects/:id/noteable/:noteable_id/notes/:note_id' do - context 'when noteable is an Issue' do - it 'deletes a note' do - delete v3_api("/projects/#{project.id}/issues/#{issue.id}/"\ - "notes/#{issue_note.id}", user) - - expect(response).to have_gitlab_http_status(200) - # Check if note is really deleted - delete v3_api("/projects/#{project.id}/issues/#{issue.id}/"\ - "notes/#{issue_note.id}", user) - expect(response).to have_gitlab_http_status(404) - end - - it 'returns a 404 error when note id not found' do - delete v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/12345", user) - - expect(response).to have_gitlab_http_status(404) - end - end - - context 'when noteable is a Snippet' do - it 'deletes a note' do - delete v3_api("/projects/#{project.id}/snippets/#{snippet.id}/"\ - "notes/#{snippet_note.id}", user) - - expect(response).to have_gitlab_http_status(200) - # Check if note is really deleted - delete v3_api("/projects/#{project.id}/snippets/#{snippet.id}/"\ - "notes/#{snippet_note.id}", user) - expect(response).to have_gitlab_http_status(404) - end - - it 'returns a 404 error when note id not found' do - delete v3_api("/projects/#{project.id}/snippets/#{snippet.id}/"\ - "notes/12345", user) - - expect(response).to have_gitlab_http_status(404) - end - end - - context 'when noteable is a Merge Request' do - it 'deletes a note' do - delete v3_api("/projects/#{project.id}/merge_requests/"\ - "#{merge_request.id}/notes/#{merge_request_note.id}", user) - - expect(response).to have_gitlab_http_status(200) - # Check if note is really deleted - delete v3_api("/projects/#{project.id}/merge_requests/"\ - "#{merge_request.id}/notes/#{merge_request_note.id}", user) - expect(response).to have_gitlab_http_status(404) - end - - it 'returns a 404 error when note id not found' do - delete v3_api("/projects/#{project.id}/merge_requests/"\ - "#{merge_request.id}/notes/12345", user) - - expect(response).to have_gitlab_http_status(404) - end - end - end -end diff --git a/spec/requests/api/v3/pipelines_spec.rb b/spec/requests/api/v3/pipelines_spec.rb deleted file mode 100644 index ea943f22c41..00000000000 --- a/spec/requests/api/v3/pipelines_spec.rb +++ /dev/null @@ -1,201 +0,0 @@ -require 'spec_helper' - -describe API::V3::Pipelines do - let(:user) { create(:user) } - let(:non_member) { create(:user) } - let(:project) { create(:project, :repository, creator: user) } - - let!(:pipeline) do - create(:ci_empty_pipeline, project: project, sha: project.commit.id, - ref: project.default_branch) - end - - before { project.add_master(user) } - - shared_examples 'a paginated resources' do - before do - # Fires the request - request - end - - it 'has pagination headers' do - expect(response).to include_pagination_headers - end - end - - describe 'GET /projects/:id/pipelines ' do - it_behaves_like 'a paginated resources' do - let(:request) { get v3_api("/projects/#{project.id}/pipelines", user) } - end - - context 'authorized user' do - it 'returns project pipelines' do - get v3_api("/projects/#{project.id}/pipelines", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.first['sha']).to match(/\A\h{40}\z/) - expect(json_response.first['id']).to eq pipeline.id - expect(json_response.first.keys).to contain_exactly(*%w[id sha ref status before_sha tag yaml_errors user created_at updated_at started_at finished_at committed_at duration coverage]) - end - end - - context 'unauthorized user' do - it 'does not return project pipelines' do - get v3_api("/projects/#{project.id}/pipelines", non_member) - - expect(response).to have_gitlab_http_status(404) - expect(json_response['message']).to eq '404 Project Not Found' - expect(json_response).not_to be_an Array - end - end - end - - describe 'POST /projects/:id/pipeline ' do - context 'authorized user' do - context 'with gitlab-ci.yml' do - before { stub_ci_pipeline_to_return_yaml_file } - - it 'creates and returns a new pipeline' do - expect do - post v3_api("/projects/#{project.id}/pipeline", user), ref: project.default_branch - end.to change { Ci::Pipeline.count }.by(1) - - expect(response).to have_gitlab_http_status(201) - expect(json_response).to be_a Hash - expect(json_response['sha']).to eq project.commit.id - end - - it 'fails when using an invalid ref' do - post v3_api("/projects/#{project.id}/pipeline", user), ref: 'invalid_ref' - - expect(response).to have_gitlab_http_status(400) - expect(json_response['message']['base'].first).to eq 'Reference not found' - expect(json_response).not_to be_an Array - end - end - - context 'without gitlab-ci.yml' do - it 'fails to create pipeline' do - post v3_api("/projects/#{project.id}/pipeline", user), ref: project.default_branch - - expect(response).to have_gitlab_http_status(400) - expect(json_response['message']['base'].first).to eq 'Missing .gitlab-ci.yml file' - expect(json_response).not_to be_an Array - end - end - end - - context 'unauthorized user' do - it 'does not create pipeline' do - post v3_api("/projects/#{project.id}/pipeline", non_member), ref: project.default_branch - - expect(response).to have_gitlab_http_status(404) - expect(json_response['message']).to eq '404 Project Not Found' - expect(json_response).not_to be_an Array - end - end - end - - describe 'GET /projects/:id/pipelines/:pipeline_id' do - context 'authorized user' do - it 'returns project pipelines' do - get v3_api("/projects/#{project.id}/pipelines/#{pipeline.id}", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['sha']).to match /\A\h{40}\z/ - end - - it 'returns 404 when it does not exist' do - get v3_api("/projects/#{project.id}/pipelines/123456", user) - - expect(response).to have_gitlab_http_status(404) - expect(json_response['message']).to eq '404 Not found' - expect(json_response['id']).to be nil - end - - context 'with coverage' do - before do - create(:ci_build, coverage: 30, pipeline: pipeline) - end - - it 'exposes the coverage' do - get v3_api("/projects/#{project.id}/pipelines/#{pipeline.id}", user) - - expect(json_response["coverage"].to_i).to eq(30) - end - end - end - - context 'unauthorized user' do - it 'should not return a project pipeline' do - get v3_api("/projects/#{project.id}/pipelines/#{pipeline.id}", non_member) - - expect(response).to have_gitlab_http_status(404) - expect(json_response['message']).to eq '404 Project Not Found' - expect(json_response['id']).to be nil - end - end - end - - describe 'POST /projects/:id/pipelines/:pipeline_id/retry' do - context 'authorized user' do - let!(:pipeline) do - create(:ci_pipeline, project: project, sha: project.commit.id, - ref: project.default_branch) - end - - let!(:build) { create(:ci_build, :failed, pipeline: pipeline) } - - it 'retries failed builds' do - expect do - post v3_api("/projects/#{project.id}/pipelines/#{pipeline.id}/retry", user) - end.to change { pipeline.builds.count }.from(1).to(2) - - expect(response).to have_gitlab_http_status(201) - expect(build.reload.retried?).to be true - end - end - - context 'unauthorized user' do - it 'should not return a project pipeline' do - post v3_api("/projects/#{project.id}/pipelines/#{pipeline.id}/retry", non_member) - - expect(response).to have_gitlab_http_status(404) - expect(json_response['message']).to eq '404 Project Not Found' - expect(json_response['id']).to be nil - end - end - end - - describe 'POST /projects/:id/pipelines/:pipeline_id/cancel' do - let!(:pipeline) do - create(:ci_empty_pipeline, project: project, sha: project.commit.id, - ref: project.default_branch) - end - - let!(:build) { create(:ci_build, :running, pipeline: pipeline) } - - context 'authorized user' do - it 'retries failed builds' do - post v3_api("/projects/#{project.id}/pipelines/#{pipeline.id}/cancel", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['status']).to eq('canceled') - end - end - - context 'user without proper access rights' do - let!(:reporter) { create(:user) } - - before { project.add_reporter(reporter) } - - it 'rejects the action' do - post v3_api("/projects/#{project.id}/pipelines/#{pipeline.id}/cancel", reporter) - - expect(response).to have_gitlab_http_status(403) - expect(pipeline.reload.status).to eq('pending') - end - end - end -end diff --git a/spec/requests/api/v3/project_hooks_spec.rb b/spec/requests/api/v3/project_hooks_spec.rb deleted file mode 100644 index 8f6a2330d25..00000000000 --- a/spec/requests/api/v3/project_hooks_spec.rb +++ /dev/null @@ -1,219 +0,0 @@ -require 'spec_helper' - -describe API::ProjectHooks, 'ProjectHooks' do - let(:user) { create(:user) } - let(:user3) { create(:user) } - let!(:project) { create(:project, creator_id: user.id, namespace: user.namespace) } - let!(:hook) do - create(:project_hook, - :all_events_enabled, - project: project, - url: 'http://example.com', - enable_ssl_verification: true) - end - - before do - project.add_master(user) - project.add_developer(user3) - end - - describe "GET /projects/:id/hooks" do - context "authorized user" do - it "returns project hooks" do - get v3_api("/projects/#{project.id}/hooks", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.count).to eq(1) - expect(json_response.first['url']).to eq("http://example.com") - expect(json_response.first['issues_events']).to eq(true) - expect(json_response.first['confidential_issues_events']).to eq(true) - expect(json_response.first['push_events']).to eq(true) - expect(json_response.first['merge_requests_events']).to eq(true) - expect(json_response.first['tag_push_events']).to eq(true) - expect(json_response.first['note_events']).to eq(true) - expect(json_response.first['build_events']).to eq(true) - expect(json_response.first['pipeline_events']).to eq(true) - expect(json_response.first['wiki_page_events']).to eq(true) - expect(json_response.first['enable_ssl_verification']).to eq(true) - end - end - - context "unauthorized user" do - it "does not access project hooks" do - get v3_api("/projects/#{project.id}/hooks", user3) - - expect(response).to have_gitlab_http_status(403) - end - end - end - - describe "GET /projects/:id/hooks/:hook_id" do - context "authorized user" do - it "returns a project hook" do - get v3_api("/projects/#{project.id}/hooks/#{hook.id}", user) - expect(response).to have_gitlab_http_status(200) - expect(json_response['url']).to eq(hook.url) - expect(json_response['issues_events']).to eq(hook.issues_events) - expect(json_response['confidential_issues_events']).to eq(hook.confidential_issues_events) - expect(json_response['push_events']).to eq(hook.push_events) - expect(json_response['merge_requests_events']).to eq(hook.merge_requests_events) - expect(json_response['tag_push_events']).to eq(hook.tag_push_events) - expect(json_response['note_events']).to eq(hook.note_events) - expect(json_response['build_events']).to eq(hook.job_events) - expect(json_response['pipeline_events']).to eq(hook.pipeline_events) - expect(json_response['wiki_page_events']).to eq(hook.wiki_page_events) - expect(json_response['enable_ssl_verification']).to eq(hook.enable_ssl_verification) - end - - it "returns a 404 error if hook id is not available" do - get v3_api("/projects/#{project.id}/hooks/1234", user) - expect(response).to have_gitlab_http_status(404) - end - end - - context "unauthorized user" do - it "does not access an existing hook" do - get v3_api("/projects/#{project.id}/hooks/#{hook.id}", user3) - expect(response).to have_gitlab_http_status(403) - end - end - - it "returns a 404 error if hook id is not available" do - get v3_api("/projects/#{project.id}/hooks/1234", user) - expect(response).to have_gitlab_http_status(404) - end - end - - describe "POST /projects/:id/hooks" do - it "adds hook to project" do - expect do - post v3_api("/projects/#{project.id}/hooks", user), - url: "http://example.com", issues_events: true, confidential_issues_events: true, wiki_page_events: true, build_events: true - end.to change {project.hooks.count}.by(1) - - expect(response).to have_gitlab_http_status(201) - expect(json_response['url']).to eq('http://example.com') - expect(json_response['issues_events']).to eq(true) - expect(json_response['confidential_issues_events']).to eq(true) - expect(json_response['push_events']).to eq(true) - expect(json_response['merge_requests_events']).to eq(false) - expect(json_response['tag_push_events']).to eq(false) - expect(json_response['note_events']).to eq(false) - expect(json_response['build_events']).to eq(true) - expect(json_response['pipeline_events']).to eq(false) - expect(json_response['wiki_page_events']).to eq(true) - expect(json_response['enable_ssl_verification']).to eq(true) - expect(json_response).not_to include('token') - end - - it "adds the token without including it in the response" do - token = "secret token" - - expect do - post v3_api("/projects/#{project.id}/hooks", user), url: "http://example.com", token: token - end.to change {project.hooks.count}.by(1) - - expect(response).to have_gitlab_http_status(201) - expect(json_response["url"]).to eq("http://example.com") - expect(json_response).not_to include("token") - - hook = project.hooks.find(json_response["id"]) - - expect(hook.url).to eq("http://example.com") - expect(hook.token).to eq(token) - end - - it "returns a 400 error if url not given" do - post v3_api("/projects/#{project.id}/hooks", user) - expect(response).to have_gitlab_http_status(400) - end - - it "returns a 422 error if url not valid" do - post v3_api("/projects/#{project.id}/hooks", user), "url" => "ftp://example.com" - expect(response).to have_gitlab_http_status(422) - end - end - - describe "PUT /projects/:id/hooks/:hook_id" do - it "updates an existing project hook" do - put v3_api("/projects/#{project.id}/hooks/#{hook.id}", user), - url: 'http://example.org', push_events: false, build_events: true - expect(response).to have_gitlab_http_status(200) - expect(json_response['url']).to eq('http://example.org') - expect(json_response['issues_events']).to eq(hook.issues_events) - expect(json_response['confidential_issues_events']).to eq(hook.confidential_issues_events) - expect(json_response['push_events']).to eq(false) - expect(json_response['merge_requests_events']).to eq(hook.merge_requests_events) - expect(json_response['tag_push_events']).to eq(hook.tag_push_events) - expect(json_response['note_events']).to eq(hook.note_events) - expect(json_response['build_events']).to eq(hook.job_events) - expect(json_response['pipeline_events']).to eq(hook.pipeline_events) - expect(json_response['wiki_page_events']).to eq(hook.wiki_page_events) - expect(json_response['enable_ssl_verification']).to eq(hook.enable_ssl_verification) - end - - it "adds the token without including it in the response" do - token = "secret token" - - put v3_api("/projects/#{project.id}/hooks/#{hook.id}", user), url: "http://example.org", token: token - - expect(response).to have_gitlab_http_status(200) - expect(json_response["url"]).to eq("http://example.org") - expect(json_response).not_to include("token") - - expect(hook.reload.url).to eq("http://example.org") - expect(hook.reload.token).to eq(token) - end - - it "returns 404 error if hook id not found" do - put v3_api("/projects/#{project.id}/hooks/1234", user), url: 'http://example.org' - expect(response).to have_gitlab_http_status(404) - end - - it "returns 400 error if url is not given" do - put v3_api("/projects/#{project.id}/hooks/#{hook.id}", user) - expect(response).to have_gitlab_http_status(400) - end - - it "returns a 422 error if url is not valid" do - put v3_api("/projects/#{project.id}/hooks/#{hook.id}", user), url: 'ftp://example.com' - expect(response).to have_gitlab_http_status(422) - end - end - - describe "DELETE /projects/:id/hooks/:hook_id" do - it "deletes hook from project" do - expect do - delete v3_api("/projects/#{project.id}/hooks/#{hook.id}", user) - end.to change {project.hooks.count}.by(-1) - expect(response).to have_gitlab_http_status(200) - end - - it "returns success when deleting hook" do - delete v3_api("/projects/#{project.id}/hooks/#{hook.id}", user) - expect(response).to have_gitlab_http_status(200) - end - - it "returns a 404 error when deleting non existent hook" do - delete v3_api("/projects/#{project.id}/hooks/42", user) - expect(response).to have_gitlab_http_status(404) - end - - it "returns a 404 error if hook id not given" do - delete v3_api("/projects/#{project.id}/hooks", user) - - expect(response).to have_gitlab_http_status(404) - end - - it "returns a 404 if a user attempts to delete project hooks he/she does not own" do - test_user = create(:user) - other_project = create(:project) - other_project.add_master(test_user) - - delete v3_api("/projects/#{other_project.id}/hooks/#{hook.id}", test_user) - expect(response).to have_gitlab_http_status(404) - expect(WebHook.exists?(hook.id)).to be_truthy - end - end -end diff --git a/spec/requests/api/v3/project_snippets_spec.rb b/spec/requests/api/v3/project_snippets_spec.rb deleted file mode 100644 index 2ed31b99516..00000000000 --- a/spec/requests/api/v3/project_snippets_spec.rb +++ /dev/null @@ -1,226 +0,0 @@ -require 'rails_helper' - -describe API::ProjectSnippets do - let(:project) { create(:project, :public) } - let(:user) { create(:user) } - let(:admin) { create(:admin) } - - describe 'GET /projects/:project_id/snippets/:id' do - # TODO (rspeicher): Deprecated; remove in 9.0 - it 'always exposes expires_at as nil' do - snippet = create(:project_snippet, author: admin) - - get v3_api("/projects/#{snippet.project.id}/snippets/#{snippet.id}", admin) - - expect(json_response).to have_key('expires_at') - expect(json_response['expires_at']).to be_nil - end - end - - describe 'GET /projects/:project_id/snippets/' do - let(:user) { create(:user) } - - it 'returns all snippets available to team member' do - project.add_developer(user) - public_snippet = create(:project_snippet, :public, project: project) - internal_snippet = create(:project_snippet, :internal, project: project) - private_snippet = create(:project_snippet, :private, project: project) - - get v3_api("/projects/#{project.id}/snippets/", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response.size).to eq(3) - expect(json_response.map { |snippet| snippet['id']} ).to include(public_snippet.id, internal_snippet.id, private_snippet.id) - expect(json_response.last).to have_key('web_url') - end - - it 'hides private snippets from regular user' do - create(:project_snippet, :private, project: project) - - get v3_api("/projects/#{project.id}/snippets/", user) - expect(response).to have_gitlab_http_status(200) - expect(json_response.size).to eq(0) - end - end - - describe 'POST /projects/:project_id/snippets/' do - let(:params) do - { - title: 'Test Title', - file_name: 'test.rb', - code: 'puts "hello world"', - visibility_level: Snippet::PUBLIC - } - end - - it 'creates a new snippet' do - post v3_api("/projects/#{project.id}/snippets/", admin), params - - expect(response).to have_gitlab_http_status(201) - snippet = ProjectSnippet.find(json_response['id']) - expect(snippet.content).to eq(params[:code]) - expect(snippet.title).to eq(params[:title]) - expect(snippet.file_name).to eq(params[:file_name]) - expect(snippet.visibility_level).to eq(params[:visibility_level]) - end - - it 'returns 400 for missing parameters' do - params.delete(:title) - - post v3_api("/projects/#{project.id}/snippets/", admin), params - - expect(response).to have_gitlab_http_status(400) - end - - context 'when the snippet is spam' do - def create_snippet(project, snippet_params = {}) - project.add_developer(user) - - post v3_api("/projects/#{project.id}/snippets", user), params.merge(snippet_params) - end - - before do - allow_any_instance_of(AkismetService).to receive(:spam?).and_return(true) - end - - context 'when the snippet is private' do - it 'creates the snippet' do - expect { create_snippet(project, visibility_level: Snippet::PRIVATE) } - .to change { Snippet.count }.by(1) - end - end - - context 'when the snippet is public' do - it 'rejects the shippet' do - expect { create_snippet(project, visibility_level: Snippet::PUBLIC) } - .not_to change { Snippet.count } - - expect(response).to have_gitlab_http_status(400) - expect(json_response['message']).to eq({ "error" => "Spam detected" }) - end - - it 'creates a spam log' do - expect { create_snippet(project, visibility_level: Snippet::PUBLIC) } - .to change { SpamLog.count }.by(1) - end - end - end - end - - describe 'PUT /projects/:project_id/snippets/:id/' do - let(:visibility_level) { Snippet::PUBLIC } - let(:snippet) { create(:project_snippet, author: admin, visibility_level: visibility_level) } - - it 'updates snippet' do - new_content = 'New content' - - put v3_api("/projects/#{snippet.project.id}/snippets/#{snippet.id}/", admin), code: new_content - - expect(response).to have_gitlab_http_status(200) - snippet.reload - expect(snippet.content).to eq(new_content) - end - - it 'returns 404 for invalid snippet id' do - put v3_api("/projects/#{snippet.project.id}/snippets/1234", admin), title: 'foo' - - expect(response).to have_gitlab_http_status(404) - expect(json_response['message']).to eq('404 Snippet Not Found') - end - - it 'returns 400 for missing parameters' do - put v3_api("/projects/#{project.id}/snippets/1234", admin) - - expect(response).to have_gitlab_http_status(400) - end - - context 'when the snippet is spam' do - def update_snippet(snippet_params = {}) - put v3_api("/projects/#{snippet.project.id}/snippets/#{snippet.id}", admin), snippet_params - end - - before do - allow_any_instance_of(AkismetService).to receive(:spam?).and_return(true) - end - - context 'when the snippet is private' do - let(:visibility_level) { Snippet::PRIVATE } - - it 'creates the snippet' do - expect { update_snippet(title: 'Foo') } - .to change { snippet.reload.title }.to('Foo') - end - end - - context 'when the snippet is public' do - let(:visibility_level) { Snippet::PUBLIC } - - it 'rejects the snippet' do - expect { update_snippet(title: 'Foo') } - .not_to change { snippet.reload.title } - end - - it 'creates a spam log' do - expect { update_snippet(title: 'Foo') } - .to change { SpamLog.count }.by(1) - end - end - - context 'when the private snippet is made public' do - let(:visibility_level) { Snippet::PRIVATE } - - it 'rejects the snippet' do - expect { update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC) } - .not_to change { snippet.reload.title } - - expect(response).to have_gitlab_http_status(400) - expect(json_response['message']).to eq({ "error" => "Spam detected" }) - end - - it 'creates a spam log' do - expect { update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC) } - .to change { SpamLog.count }.by(1) - end - end - end - end - - describe 'DELETE /projects/:project_id/snippets/:id/' do - let(:snippet) { create(:project_snippet, author: admin) } - - it 'deletes snippet' do - admin = create(:admin) - snippet = create(:project_snippet, author: admin) - - delete v3_api("/projects/#{snippet.project.id}/snippets/#{snippet.id}/", admin) - - expect(response).to have_gitlab_http_status(200) - end - - it 'returns 404 for invalid snippet id' do - delete v3_api("/projects/#{snippet.project.id}/snippets/1234", admin) - - expect(response).to have_gitlab_http_status(404) - expect(json_response['message']).to eq('404 Snippet Not Found') - end - end - - describe 'GET /projects/:project_id/snippets/:id/raw' do - let(:snippet) { create(:project_snippet, author: admin) } - - it 'returns raw text' do - get v3_api("/projects/#{snippet.project.id}/snippets/#{snippet.id}/raw", admin) - - expect(response).to have_gitlab_http_status(200) - expect(response.content_type).to eq 'text/plain' - expect(response.body).to eq(snippet.content) - end - - it 'returns 404 for invalid snippet id' do - delete v3_api("/projects/#{snippet.project.id}/snippets/1234", admin) - - expect(response).to have_gitlab_http_status(404) - expect(json_response['message']).to eq('404 Snippet Not Found') - end - end -end diff --git a/spec/requests/api/v3/projects_spec.rb b/spec/requests/api/v3/projects_spec.rb deleted file mode 100644 index 158ddf171bc..00000000000 --- a/spec/requests/api/v3/projects_spec.rb +++ /dev/null @@ -1,1495 +0,0 @@ -require 'spec_helper' - -describe API::V3::Projects do - let(:user) { create(:user) } - let(:user2) { create(:user) } - let(:user3) { create(:user) } - let(:admin) { create(:admin) } - let(:project) { create(:project, creator_id: user.id, namespace: user.namespace) } - let(:project2) { create(:project, creator_id: user.id, namespace: user.namespace) } - let(:snippet) { create(:project_snippet, :public, author: user, project: project, title: 'example') } - let(:project_member) { create(:project_member, :developer, user: user3, project: project) } - let(:user4) { create(:user) } - let(:project3) do - create(:project, - :private, - :repository, - name: 'second_project', - path: 'second_project', - creator_id: user.id, - namespace: user.namespace, - merge_requests_enabled: false, - issues_enabled: false, wiki_enabled: false, - snippets_enabled: false) - end - let(:project_member2) do - create(:project_member, - user: user4, - project: project3, - access_level: ProjectMember::MASTER) - end - let(:project4) do - create(:project, - name: 'third_project', - path: 'third_project', - creator_id: user4.id, - namespace: user4.namespace) - end - - describe 'GET /projects' do - before { project } - - context 'when unauthenticated' do - it 'returns authentication error' do - get v3_api('/projects') - expect(response).to have_gitlab_http_status(401) - end - end - - context 'when authenticated as regular user' do - it 'returns an array of projects' do - get v3_api('/projects', user) - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.first['name']).to eq(project.name) - expect(json_response.first['owner']['username']).to eq(user.username) - end - - it 'includes the project labels as the tag_list' do - get v3_api('/projects', user) - expect(response.status).to eq 200 - expect(json_response).to be_an Array - expect(json_response.first.keys).to include('tag_list') - end - - it 'includes open_issues_count' do - get v3_api('/projects', user) - expect(response.status).to eq 200 - expect(json_response).to be_an Array - expect(json_response.first.keys).to include('open_issues_count') - end - - it 'does not include open_issues_count' do - project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED) - - get v3_api('/projects', user) - expect(response.status).to eq 200 - expect(json_response).to be_an Array - expect(json_response.first.keys).not_to include('open_issues_count') - end - - context 'GET /projects?simple=true' do - it 'returns a simplified version of all the projects' do - expected_keys = %w( - id description default_branch tag_list - ssh_url_to_repo http_url_to_repo web_url readme_url - name name_with_namespace - path path_with_namespace - star_count forks_count - created_at last_activity_at - avatar_url - ) - - get v3_api('/projects?simple=true', user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.first.keys).to match_array expected_keys - end - end - - context 'and using search' do - it 'returns searched project' do - get v3_api('/projects', user), { search: project.name } - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(1) - end - end - - context 'and using the visibility filter' do - it 'filters based on private visibility param' do - get v3_api('/projects', user), { visibility: 'private' } - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(user.namespace.projects.where(visibility_level: Gitlab::VisibilityLevel::PRIVATE).count) - end - - it 'filters based on internal visibility param' do - get v3_api('/projects', user), { visibility: 'internal' } - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(user.namespace.projects.where(visibility_level: Gitlab::VisibilityLevel::INTERNAL).count) - end - - it 'filters based on public visibility param' do - get v3_api('/projects', user), { visibility: 'public' } - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(user.namespace.projects.where(visibility_level: Gitlab::VisibilityLevel::PUBLIC).count) - end - end - - context 'and using archived' do - let!(:archived_project) { create(:project, creator_id: user.id, namespace: user.namespace, archived: true) } - - it 'returns archived project' do - get v3_api('/projects?archived=true', user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(1) - expect(json_response.first['id']).to eq(archived_project.id) - end - - it 'returns non-archived project' do - get v3_api('/projects?archived=false', user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(1) - expect(json_response.first['id']).to eq(project.id) - end - - it 'returns all project' do - get v3_api('/projects', user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(2) - end - end - - context 'and using sorting' do - before do - project2 - project3 - end - - it 'returns the correct order when sorted by id' do - get v3_api('/projects', user), { order_by: 'id', sort: 'desc' } - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.first['id']).to eq(project3.id) - end - end - end - end - - describe 'GET /projects/all' do - before { project } - - context 'when unauthenticated' do - it 'returns authentication error' do - get v3_api('/projects/all') - expect(response).to have_gitlab_http_status(401) - end - end - - context 'when authenticated as regular user' do - it 'returns authentication error' do - get v3_api('/projects/all', user) - expect(response).to have_gitlab_http_status(403) - end - end - - context 'when authenticated as admin' do - it 'returns an array of all projects' do - get v3_api('/projects/all', admin) - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - - expect(json_response).to satisfy do |response| - response.one? do |entry| - entry.key?('permissions') && - entry['name'] == project.name && - entry['owner']['username'] == user.username - end - end - end - - it "does not include statistics by default" do - get v3_api('/projects/all', admin) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.first).not_to include('statistics') - end - - it "includes statistics if requested" do - get v3_api('/projects/all', admin), statistics: true - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.first).to include 'statistics' - end - end - end - - describe 'GET /projects/owned' do - before do - project3 - project4 - end - - context 'when unauthenticated' do - it 'returns authentication error' do - get v3_api('/projects/owned') - expect(response).to have_gitlab_http_status(401) - end - end - - context 'when authenticated as project owner' do - it 'returns an array of projects the user owns' do - get v3_api('/projects/owned', user4) - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.first['name']).to eq(project4.name) - expect(json_response.first['owner']['username']).to eq(user4.username) - end - - it "does not include statistics by default" do - get v3_api('/projects/owned', user4) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.first).not_to include('statistics') - end - - it "includes statistics if requested" do - attributes = { - commit_count: 23, - storage_size: 702, - repository_size: 123, - lfs_objects_size: 234, - build_artifacts_size: 345 - } - - project4.statistics.update!(attributes) - - get v3_api('/projects/owned', user4), statistics: true - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.first['statistics']).to eq attributes.stringify_keys - end - end - end - - describe 'GET /projects/visible' do - shared_examples_for 'visible projects response' do - it 'returns the visible projects' do - get v3_api('/projects/visible', current_user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.map { |p| p['id'] }).to contain_exactly(*projects.map(&:id)) - end - end - - let!(:public_project) { create(:project, :public) } - before do - project - project2 - project3 - project4 - end - - context 'when unauthenticated' do - it_behaves_like 'visible projects response' do - let(:current_user) { nil } - let(:projects) { [public_project] } - end - end - - context 'when authenticated' do - it_behaves_like 'visible projects response' do - let(:current_user) { user } - let(:projects) { [public_project, project, project2, project3] } - end - end - - context 'when authenticated as a different user' do - it_behaves_like 'visible projects response' do - let(:current_user) { user2 } - let(:projects) { [public_project] } - end - end - end - - describe 'GET /projects/starred' do - let(:public_project) { create(:project, :public) } - - before do - project_member - user3.update_attributes(starred_projects: [project, project2, project3, public_project]) - end - - it 'returns the starred projects viewable by the user' do - get v3_api('/projects/starred', user3) - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.map { |project| project['id'] }).to contain_exactly(project.id, public_project.id) - end - end - - describe 'POST /projects' do - context 'maximum number of projects reached' do - it 'does not create new project and respond with 403' do - allow_any_instance_of(User).to receive(:projects_limit_left).and_return(0) - expect { post v3_api('/projects', user2), name: 'foo' } - .to change {Project.count}.by(0) - expect(response).to have_gitlab_http_status(403) - end - end - - it 'creates new project without path but with name and returns 201' do - expect { post v3_api('/projects', user), name: 'Foo Project' } - .to change { Project.count }.by(1) - expect(response).to have_gitlab_http_status(201) - - project = Project.first - - expect(project.name).to eq('Foo Project') - expect(project.path).to eq('foo-project') - end - - it 'creates new project without name but with path and returns 201' do - expect { post v3_api('/projects', user), path: 'foo_project' } - .to change { Project.count }.by(1) - expect(response).to have_gitlab_http_status(201) - - project = Project.first - - expect(project.name).to eq('foo_project') - expect(project.path).to eq('foo_project') - end - - it 'creates new project name and path and returns 201' do - expect { post v3_api('/projects', user), path: 'foo-Project', name: 'Foo Project' } - .to change { Project.count }.by(1) - expect(response).to have_gitlab_http_status(201) - - project = Project.first - - expect(project.name).to eq('Foo Project') - expect(project.path).to eq('foo-Project') - end - - it 'creates last project before reaching project limit' do - allow_any_instance_of(User).to receive(:projects_limit_left).and_return(1) - post v3_api('/projects', user2), name: 'foo' - expect(response).to have_gitlab_http_status(201) - end - - it 'does not create new project without name or path and return 400' do - expect { post v3_api('/projects', user) }.not_to change { Project.count } - expect(response).to have_gitlab_http_status(400) - end - - it "assigns attributes to project" do - project = attributes_for(:project, { - path: 'camelCasePath', - issues_enabled: false, - merge_requests_enabled: false, - wiki_enabled: false, - only_allow_merge_if_build_succeeds: false, - request_access_enabled: true, - only_allow_merge_if_all_discussions_are_resolved: false - }) - - post v3_api('/projects', user), project - - project.each_pair do |k, v| - next if %i[storage_version has_external_issue_tracker issues_enabled merge_requests_enabled wiki_enabled].include?(k) - - expect(json_response[k.to_s]).to eq(v) - end - - # Check feature permissions attributes - project = Project.find_by_path(project[:path]) - expect(project.project_feature.issues_access_level).to eq(ProjectFeature::DISABLED) - expect(project.project_feature.merge_requests_access_level).to eq(ProjectFeature::DISABLED) - expect(project.project_feature.wiki_access_level).to eq(ProjectFeature::DISABLED) - end - - it 'sets a project as public' do - project = attributes_for(:project, :public) - post v3_api('/projects', user), project - expect(json_response['public']).to be_truthy - expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PUBLIC) - end - - it 'sets a project as public using :public' do - project = attributes_for(:project, { public: true }) - post v3_api('/projects', user), project - expect(json_response['public']).to be_truthy - expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PUBLIC) - end - - it 'sets a project as internal' do - project = attributes_for(:project, :internal) - post v3_api('/projects', user), project - expect(json_response['public']).to be_falsey - expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::INTERNAL) - end - - it 'sets a project as internal overriding :public' do - project = attributes_for(:project, :internal, { public: true }) - post v3_api('/projects', user), project - expect(json_response['public']).to be_falsey - expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::INTERNAL) - end - - it 'sets a project as private' do - project = attributes_for(:project, :private) - post v3_api('/projects', user), project - expect(json_response['public']).to be_falsey - expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE) - end - - it 'sets a project as private using :public' do - project = attributes_for(:project, { public: false }) - post v3_api('/projects', user), project - expect(json_response['public']).to be_falsey - expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE) - end - - it 'sets a project as allowing merge even if build fails' do - project = attributes_for(:project, { only_allow_merge_if_build_succeeds: false }) - post v3_api('/projects', user), project - expect(json_response['only_allow_merge_if_build_succeeds']).to be_falsey - end - - it 'sets a project as allowing merge only if merge_when_pipeline_succeeds' do - project = attributes_for(:project, { only_allow_merge_if_build_succeeds: true }) - post v3_api('/projects', user), project - expect(json_response['only_allow_merge_if_build_succeeds']).to be_truthy - end - - it 'sets a project as allowing merge even if discussions are unresolved' do - project = attributes_for(:project, { only_allow_merge_if_all_discussions_are_resolved: false }) - - post v3_api('/projects', user), project - - expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to be_falsey - end - - it 'sets a project as allowing merge if only_allow_merge_if_all_discussions_are_resolved is nil' do - project = attributes_for(:project, only_allow_merge_if_all_discussions_are_resolved: nil) - - post v3_api('/projects', user), project - - expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to be_falsey - end - - it 'sets a project as allowing merge only if all discussions are resolved' do - project = attributes_for(:project, { only_allow_merge_if_all_discussions_are_resolved: true }) - - post v3_api('/projects', user), project - - expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to be_truthy - end - - context 'when a visibility level is restricted' do - before do - @project = attributes_for(:project, { public: true }) - stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC]) - end - - it 'does not allow a non-admin to use a restricted visibility level' do - post v3_api('/projects', user), @project - - expect(response).to have_gitlab_http_status(400) - expect(json_response['message']['visibility_level'].first).to( - match('restricted by your GitLab administrator') - ) - end - - it 'allows an admin to override restricted visibility settings' do - post v3_api('/projects', admin), @project - expect(json_response['public']).to be_truthy - expect(json_response['visibility_level']).to( - eq(Gitlab::VisibilityLevel::PUBLIC) - ) - end - end - end - - describe 'POST /projects/user/:id' do - before { project } - before { admin } - - it 'should create new project without path and return 201' do - expect { post v3_api("/projects/user/#{user.id}", admin), name: 'foo' }.to change {Project.count}.by(1) - expect(response).to have_gitlab_http_status(201) - end - - it 'responds with 400 on failure and not project' do - expect { post v3_api("/projects/user/#{user.id}", admin) } - .not_to change { Project.count } - - expect(response).to have_gitlab_http_status(400) - expect(json_response['error']).to eq('name is missing') - end - - it 'assigns attributes to project' do - project = attributes_for(:project, { - issues_enabled: false, - merge_requests_enabled: false, - wiki_enabled: false, - request_access_enabled: true - }) - - post v3_api("/projects/user/#{user.id}", admin), project - - expect(response).to have_gitlab_http_status(201) - project.each_pair do |k, v| - next if %i[storage_version has_external_issue_tracker path].include?(k) - - expect(json_response[k.to_s]).to eq(v) - end - end - - it 'sets a project as public' do - project = attributes_for(:project, :public) - post v3_api("/projects/user/#{user.id}", admin), project - - expect(response).to have_gitlab_http_status(201) - expect(json_response['public']).to be_truthy - expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PUBLIC) - end - - it 'sets a project as public using :public' do - project = attributes_for(:project, { public: true }) - post v3_api("/projects/user/#{user.id}", admin), project - - expect(response).to have_gitlab_http_status(201) - expect(json_response['public']).to be_truthy - expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PUBLIC) - end - - it 'sets a project as internal' do - project = attributes_for(:project, :internal) - post v3_api("/projects/user/#{user.id}", admin), project - - expect(response).to have_gitlab_http_status(201) - expect(json_response['public']).to be_falsey - expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::INTERNAL) - end - - it 'sets a project as internal overriding :public' do - project = attributes_for(:project, :internal, { public: true }) - post v3_api("/projects/user/#{user.id}", admin), project - expect(response).to have_gitlab_http_status(201) - expect(json_response['public']).to be_falsey - expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::INTERNAL) - end - - it 'sets a project as private' do - project = attributes_for(:project, :private) - post v3_api("/projects/user/#{user.id}", admin), project - expect(json_response['public']).to be_falsey - expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE) - end - - it 'sets a project as private using :public' do - project = attributes_for(:project, { public: false }) - post v3_api("/projects/user/#{user.id}", admin), project - expect(json_response['public']).to be_falsey - expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE) - end - - it 'sets a project as allowing merge even if build fails' do - project = attributes_for(:project, { only_allow_merge_if_build_succeeds: false }) - post v3_api("/projects/user/#{user.id}", admin), project - expect(json_response['only_allow_merge_if_build_succeeds']).to be_falsey - end - - it 'sets a project as allowing merge only if merge_when_pipeline_succeeds' do - project = attributes_for(:project, { only_allow_merge_if_build_succeeds: true }) - post v3_api("/projects/user/#{user.id}", admin), project - expect(json_response['only_allow_merge_if_build_succeeds']).to be_truthy - end - - it 'sets a project as allowing merge even if discussions are unresolved' do - project = attributes_for(:project, { only_allow_merge_if_all_discussions_are_resolved: false }) - - post v3_api("/projects/user/#{user.id}", admin), project - - expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to be_falsey - end - - it 'sets a project as allowing merge only if all discussions are resolved' do - project = attributes_for(:project, { only_allow_merge_if_all_discussions_are_resolved: true }) - - post v3_api("/projects/user/#{user.id}", admin), project - - expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to be_truthy - end - end - - describe "POST /projects/:id/uploads" do - before { project } - - it "uploads the file and returns its info" do - post v3_api("/projects/#{project.id}/uploads", user), file: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png") - - expect(response).to have_gitlab_http_status(201) - expect(json_response['alt']).to eq("dk") - expect(json_response['url']).to start_with("/uploads/") - expect(json_response['url']).to end_with("/dk.png") - end - end - - describe 'GET /projects/:id' do - context 'when unauthenticated' do - it 'returns the public projects' do - public_project = create(:project, :public) - - get v3_api("/projects/#{public_project.id}") - - expect(response).to have_gitlab_http_status(200) - expect(json_response['id']).to eq(public_project.id) - expect(json_response['description']).to eq(public_project.description) - expect(json_response['default_branch']).to eq(public_project.default_branch) - expect(json_response.keys).not_to include('permissions') - end - end - - context 'when authenticated' do - before do - project - end - - it 'returns a project by id' do - group = create(:group) - link = create(:project_group_link, project: project, group: group) - - get v3_api("/projects/#{project.id}", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['id']).to eq(project.id) - expect(json_response['description']).to eq(project.description) - expect(json_response['default_branch']).to eq(project.default_branch) - expect(json_response['tag_list']).to be_an Array - expect(json_response['public']).to be_falsey - expect(json_response['archived']).to be_falsey - expect(json_response['visibility_level']).to be_present - expect(json_response['ssh_url_to_repo']).to be_present - expect(json_response['http_url_to_repo']).to be_present - expect(json_response['web_url']).to be_present - expect(json_response['owner']).to be_a Hash - expect(json_response['owner']).to be_a Hash - expect(json_response['name']).to eq(project.name) - expect(json_response['path']).to be_present - expect(json_response['issues_enabled']).to be_present - expect(json_response['merge_requests_enabled']).to be_present - expect(json_response['wiki_enabled']).to be_present - expect(json_response['builds_enabled']).to be_present - expect(json_response['snippets_enabled']).to be_present - expect(json_response['resolve_outdated_diff_discussions']).to eq(project.resolve_outdated_diff_discussions) - expect(json_response['container_registry_enabled']).to be_present - expect(json_response['created_at']).to be_present - expect(json_response['last_activity_at']).to be_present - expect(json_response['shared_runners_enabled']).to be_present - expect(json_response['creator_id']).to be_present - expect(json_response['namespace']).to be_present - expect(json_response['avatar_url']).to be_nil - expect(json_response['star_count']).to be_present - expect(json_response['forks_count']).to be_present - expect(json_response['public_builds']).to be_present - expect(json_response['shared_with_groups']).to be_an Array - expect(json_response['shared_with_groups'].length).to eq(1) - expect(json_response['shared_with_groups'][0]['group_id']).to eq(group.id) - expect(json_response['shared_with_groups'][0]['group_name']).to eq(group.name) - expect(json_response['shared_with_groups'][0]['group_access_level']).to eq(link.group_access) - expect(json_response['only_allow_merge_if_build_succeeds']).to eq(project.only_allow_merge_if_pipeline_succeeds) - expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to eq(project.only_allow_merge_if_all_discussions_are_resolved) - end - - it 'returns a project by path name' do - get v3_api("/projects/#{project.id}", user) - expect(response).to have_gitlab_http_status(200) - expect(json_response['name']).to eq(project.name) - end - - it 'returns a 404 error if not found' do - get v3_api('/projects/42', user) - expect(response).to have_gitlab_http_status(404) - expect(json_response['message']).to eq('404 Project Not Found') - end - - it 'returns a 404 error if user is not a member' do - other_user = create(:user) - get v3_api("/projects/#{project.id}", other_user) - expect(response).to have_gitlab_http_status(404) - end - - it 'handles users with dots' do - dot_user = create(:user, username: 'dot.user') - project = create(:project, creator_id: dot_user.id, namespace: dot_user.namespace) - - get v3_api("/projects/#{CGI.escape(project.full_path)}", dot_user) - expect(response).to have_gitlab_http_status(200) - expect(json_response['name']).to eq(project.name) - end - - it 'exposes namespace fields' do - get v3_api("/projects/#{project.id}", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['namespace']).to eq({ - 'id' => user.namespace.id, - 'name' => user.namespace.name, - 'path' => user.namespace.path, - 'kind' => user.namespace.kind, - 'full_path' => user.namespace.full_path, - 'parent_id' => nil - }) - end - - describe 'permissions' do - context 'all projects' do - before { project.add_master(user) } - - it 'contains permission information' do - get v3_api("/projects", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response.first['permissions']['project_access']['access_level']) - .to eq(Gitlab::Access::MASTER) - expect(json_response.first['permissions']['group_access']).to be_nil - end - end - - context 'personal project' do - it 'sets project access and returns 200' do - project.add_master(user) - get v3_api("/projects/#{project.id}", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['permissions']['project_access']['access_level']) - .to eq(Gitlab::Access::MASTER) - expect(json_response['permissions']['group_access']).to be_nil - end - end - - context 'group project' do - let(:project2) { create(:project, group: create(:group)) } - - before { project2.group.add_owner(user) } - - it 'sets the owner and return 200' do - get v3_api("/projects/#{project2.id}", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['permissions']['project_access']).to be_nil - expect(json_response['permissions']['group_access']['access_level']) - .to eq(Gitlab::Access::OWNER) - end - end - end - end - end - - describe 'GET /projects/:id/events' do - shared_examples_for 'project events response' do - it 'returns the project events' do - member = create(:user) - create(:project_member, :developer, user: member, project: project) - note = create(:note_on_issue, note: 'What an awesome day!', project: project) - EventCreateService.new.leave_note(note, note.author) - - get v3_api("/projects/#{project.id}/events", current_user) - - expect(response).to have_gitlab_http_status(200) - - first_event = json_response.first - - expect(first_event['action_name']).to eq('commented on') - expect(first_event['note']['body']).to eq('What an awesome day!') - - last_event = json_response.last - - expect(last_event['action_name']).to eq('joined') - expect(last_event['project_id'].to_i).to eq(project.id) - expect(last_event['author_username']).to eq(member.username) - expect(last_event['author']['name']).to eq(member.name) - end - end - - context 'when unauthenticated' do - it_behaves_like 'project events response' do - let(:project) { create(:project, :public) } - let(:current_user) { nil } - end - end - - context 'when authenticated' do - context 'valid request' do - it_behaves_like 'project events response' do - let(:current_user) { user } - end - end - - it 'returns a 404 error if not found' do - get v3_api('/projects/42/events', user) - - expect(response).to have_gitlab_http_status(404) - expect(json_response['message']).to eq('404 Project Not Found') - end - - it 'returns a 404 error if user is not a member' do - other_user = create(:user) - - get v3_api("/projects/#{project.id}/events", other_user) - - expect(response).to have_gitlab_http_status(404) - end - end - end - - describe 'GET /projects/:id/users' do - shared_examples_for 'project users response' do - it 'returns the project users' do - member = project.owner - - get v3_api("/projects/#{project.id}/users", current_user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.size).to eq(1) - - first_user = json_response.first - - expect(first_user['username']).to eq(member.username) - expect(first_user['name']).to eq(member.name) - expect(first_user.keys).to contain_exactly(*%w[name username id state avatar_url web_url]) - end - end - - context 'when unauthenticated' do - it_behaves_like 'project users response' do - let(:project) { create(:project, :public) } - let(:current_user) { nil } - end - end - - context 'when authenticated' do - context 'valid request' do - it_behaves_like 'project users response' do - let(:current_user) { user } - end - end - - it 'returns a 404 error if not found' do - get v3_api('/projects/42/users', user) - - expect(response).to have_gitlab_http_status(404) - expect(json_response['message']).to eq('404 Project Not Found') - end - - it 'returns a 404 error if user is not a member' do - other_user = create(:user) - - get v3_api("/projects/#{project.id}/users", other_user) - - expect(response).to have_gitlab_http_status(404) - end - end - end - - describe 'GET /projects/:id/snippets' do - before { snippet } - - it 'returns an array of project snippets' do - get v3_api("/projects/#{project.id}/snippets", user) - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.first['title']).to eq(snippet.title) - end - end - - describe 'GET /projects/:id/snippets/:snippet_id' do - it 'returns a project snippet' do - get v3_api("/projects/#{project.id}/snippets/#{snippet.id}", user) - expect(response).to have_gitlab_http_status(200) - expect(json_response['title']).to eq(snippet.title) - end - - it 'returns a 404 error if snippet id not found' do - get v3_api("/projects/#{project.id}/snippets/1234", user) - expect(response).to have_gitlab_http_status(404) - end - end - - describe 'POST /projects/:id/snippets' do - it 'creates a new project snippet' do - post v3_api("/projects/#{project.id}/snippets", user), - title: 'v3_api test', file_name: 'sample.rb', code: 'test', - visibility_level: '0' - expect(response).to have_gitlab_http_status(201) - expect(json_response['title']).to eq('v3_api test') - end - - it 'returns a 400 error if invalid snippet is given' do - post v3_api("/projects/#{project.id}/snippets", user) - expect(status).to eq(400) - end - end - - describe 'PUT /projects/:id/snippets/:snippet_id' do - it 'updates an existing project snippet' do - put v3_api("/projects/#{project.id}/snippets/#{snippet.id}", user), - code: 'updated code' - expect(response).to have_gitlab_http_status(200) - expect(json_response['title']).to eq('example') - expect(snippet.reload.content).to eq('updated code') - end - - it 'updates an existing project snippet with new title' do - put v3_api("/projects/#{project.id}/snippets/#{snippet.id}", user), - title: 'other v3_api test' - expect(response).to have_gitlab_http_status(200) - expect(json_response['title']).to eq('other v3_api test') - end - end - - describe 'DELETE /projects/:id/snippets/:snippet_id' do - before { snippet } - - it 'deletes existing project snippet' do - expect do - delete v3_api("/projects/#{project.id}/snippets/#{snippet.id}", user) - end.to change { Snippet.count }.by(-1) - expect(response).to have_gitlab_http_status(200) - end - - it 'returns 404 when deleting unknown snippet id' do - delete v3_api("/projects/#{project.id}/snippets/1234", user) - expect(response).to have_gitlab_http_status(404) - end - end - - describe 'GET /projects/:id/snippets/:snippet_id/raw' do - it 'gets a raw project snippet' do - get v3_api("/projects/#{project.id}/snippets/#{snippet.id}/raw", user) - expect(response).to have_gitlab_http_status(200) - end - - it 'returns a 404 error if raw project snippet not found' do - get v3_api("/projects/#{project.id}/snippets/5555/raw", user) - expect(response).to have_gitlab_http_status(404) - end - end - - describe 'fork management' do - let(:project_fork_target) { create(:project) } - let(:project_fork_source) { create(:project, :public) } - - describe 'POST /projects/:id/fork/:forked_from_id' do - let(:new_project_fork_source) { create(:project, :public) } - - it "is not available for non admin users" do - post v3_api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", user) - expect(response).to have_gitlab_http_status(403) - end - - it 'allows project to be forked from an existing project' do - expect(project_fork_target.forked?).not_to be_truthy - post v3_api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", admin) - expect(response).to have_gitlab_http_status(201) - project_fork_target.reload - expect(project_fork_target.forked_from_project.id).to eq(project_fork_source.id) - expect(project_fork_target.forked_project_link).not_to be_nil - expect(project_fork_target.forked?).to be_truthy - end - - it 'refreshes the forks count cachce' do - expect(project_fork_source.forks_count).to be_zero - - post v3_api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", admin) - - expect(project_fork_source.forks_count).to eq(1) - end - - it 'fails if forked_from project which does not exist' do - post v3_api("/projects/#{project_fork_target.id}/fork/9999", admin) - expect(response).to have_gitlab_http_status(404) - end - - it 'fails with 409 if already forked' do - post v3_api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", admin) - project_fork_target.reload - expect(project_fork_target.forked_from_project.id).to eq(project_fork_source.id) - post v3_api("/projects/#{project_fork_target.id}/fork/#{new_project_fork_source.id}", admin) - expect(response).to have_gitlab_http_status(409) - project_fork_target.reload - expect(project_fork_target.forked_from_project.id).to eq(project_fork_source.id) - expect(project_fork_target.forked?).to be_truthy - end - end - - describe 'DELETE /projects/:id/fork' do - it "is not visible to users outside group" do - delete v3_api("/projects/#{project_fork_target.id}/fork", user) - expect(response).to have_gitlab_http_status(404) - end - - context 'when users belong to project group' do - let(:project_fork_target) { create(:project, group: create(:group)) } - - before do - project_fork_target.group.add_owner user - project_fork_target.group.add_developer user2 - end - - it 'is forbidden to non-owner users' do - delete v3_api("/projects/#{project_fork_target.id}/fork", user2) - expect(response).to have_gitlab_http_status(403) - end - - it 'makes forked project unforked' do - post v3_api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", admin) - project_fork_target.reload - expect(project_fork_target.forked_from_project).not_to be_nil - expect(project_fork_target.forked?).to be_truthy - delete v3_api("/projects/#{project_fork_target.id}/fork", admin) - expect(response).to have_gitlab_http_status(200) - project_fork_target.reload - expect(project_fork_target.forked_from_project).to be_nil - expect(project_fork_target.forked?).not_to be_truthy - end - - it 'is idempotent if not forked' do - expect(project_fork_target.forked_from_project).to be_nil - delete v3_api("/projects/#{project_fork_target.id}/fork", admin) - expect(response).to have_gitlab_http_status(304) - expect(project_fork_target.reload.forked_from_project).to be_nil - end - end - end - end - - describe "POST /projects/:id/share" do - let(:group) { create(:group) } - - it "shares project with group" do - expires_at = 10.days.from_now.to_date - - expect do - post v3_api("/projects/#{project.id}/share", user), group_id: group.id, group_access: Gitlab::Access::DEVELOPER, expires_at: expires_at - end.to change { ProjectGroupLink.count }.by(1) - - expect(response).to have_gitlab_http_status(201) - expect(json_response['group_id']).to eq(group.id) - expect(json_response['group_access']).to eq(Gitlab::Access::DEVELOPER) - expect(json_response['expires_at']).to eq(expires_at.to_s) - end - - it "returns a 400 error when group id is not given" do - post v3_api("/projects/#{project.id}/share", user), group_access: Gitlab::Access::DEVELOPER - expect(response).to have_gitlab_http_status(400) - end - - it "returns a 400 error when access level is not given" do - post v3_api("/projects/#{project.id}/share", user), group_id: group.id - expect(response).to have_gitlab_http_status(400) - end - - it "returns a 400 error when sharing is disabled" do - project.namespace.update(share_with_group_lock: true) - post v3_api("/projects/#{project.id}/share", user), group_id: group.id, group_access: Gitlab::Access::DEVELOPER - expect(response).to have_gitlab_http_status(400) - end - - it 'returns a 404 error when user cannot read group' do - private_group = create(:group, :private) - - post v3_api("/projects/#{project.id}/share", user), group_id: private_group.id, group_access: Gitlab::Access::DEVELOPER - - expect(response).to have_gitlab_http_status(404) - end - - it 'returns a 404 error when group does not exist' do - post v3_api("/projects/#{project.id}/share", user), group_id: 1234, group_access: Gitlab::Access::DEVELOPER - - expect(response).to have_gitlab_http_status(404) - end - - it "returns a 400 error when wrong params passed" do - post v3_api("/projects/#{project.id}/share", user), group_id: group.id, group_access: 1234 - - expect(response).to have_gitlab_http_status(400) - expect(json_response['error']).to eq 'group_access does not have a valid value' - end - end - - describe 'DELETE /projects/:id/share/:group_id' do - it 'returns 204 when deleting a group share' do - group = create(:group, :public) - create(:project_group_link, group: group, project: project) - - delete v3_api("/projects/#{project.id}/share/#{group.id}", user) - - expect(response).to have_gitlab_http_status(204) - expect(project.project_group_links).to be_empty - end - - it 'returns a 400 when group id is not an integer' do - delete v3_api("/projects/#{project.id}/share/foo", user) - - expect(response).to have_gitlab_http_status(400) - end - - it 'returns a 404 error when group link does not exist' do - delete v3_api("/projects/#{project.id}/share/1234", user) - - expect(response).to have_gitlab_http_status(404) - end - - it 'returns a 404 error when project does not exist' do - delete v3_api("/projects/123/share/1234", user) - - expect(response).to have_gitlab_http_status(404) - end - end - - describe 'GET /projects/search/:query' do - let!(:query) { 'query'} - let!(:search) { create(:project, name: query, creator_id: user.id, namespace: user.namespace) } - let!(:pre) { create(:project, name: "pre_#{query}", creator_id: user.id, namespace: user.namespace) } - let!(:post) { create(:project, name: "#{query}_post", creator_id: user.id, namespace: user.namespace) } - let!(:pre_post) { create(:project, name: "pre_#{query}_post", creator_id: user.id, namespace: user.namespace) } - let!(:unfound) { create(:project, name: 'unfound', creator_id: user.id, namespace: user.namespace) } - let!(:internal) { create(:project, :internal, name: "internal #{query}") } - let!(:unfound_internal) { create(:project, :internal, name: 'unfound internal') } - let!(:public) { create(:project, :public, name: "public #{query}") } - let!(:unfound_public) { create(:project, :public, name: 'unfound public') } - let!(:one_dot_two) { create(:project, :public, name: "one.dot.two") } - - shared_examples_for 'project search response' do |args = {}| - it 'returns project search responses' do - get v3_api("/projects/search/#{args[:query]}", current_user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.size).to eq(args[:results]) - json_response.each { |project| expect(project['name']).to match(args[:match_regex] || /.*#{args[:query]}.*/) } - end - end - - context 'when unauthenticated' do - it_behaves_like 'project search response', query: 'query', results: 1 do - let(:current_user) { nil } - end - end - - context 'when authenticated' do - it_behaves_like 'project search response', query: 'query', results: 6 do - let(:current_user) { user } - end - it_behaves_like 'project search response', query: 'one.dot.two', results: 1 do - let(:current_user) { user } - end - end - - context 'when authenticated as a different user' do - it_behaves_like 'project search response', query: 'query', results: 2, match_regex: /(internal|public) query/ do - let(:current_user) { user2 } - end - end - end - - describe 'PUT /projects/:id' do - before { project } - before { user } - before { user3 } - before { user4 } - before { project3 } - before { project4 } - before { project_member2 } - before { project_member } - - context 'when unauthenticated' do - it 'returns authentication error' do - project_param = { name: 'bar' } - put v3_api("/projects/#{project.id}"), project_param - expect(response).to have_gitlab_http_status(401) - end - end - - context 'when authenticated as project owner' do - it 'updates name' do - project_param = { name: 'bar' } - put v3_api("/projects/#{project.id}", user), project_param - expect(response).to have_gitlab_http_status(200) - project_param.each_pair do |k, v| - expect(json_response[k.to_s]).to eq(v) - end - end - - it 'updates visibility_level' do - project_param = { visibility_level: 20 } - put v3_api("/projects/#{project3.id}", user), project_param - expect(response).to have_gitlab_http_status(200) - project_param.each_pair do |k, v| - expect(json_response[k.to_s]).to eq(v) - end - end - - it 'updates visibility_level from public to private' do - project3.update_attributes({ visibility_level: Gitlab::VisibilityLevel::PUBLIC }) - project_param = { public: false } - put v3_api("/projects/#{project3.id}", user), project_param - expect(response).to have_gitlab_http_status(200) - project_param.each_pair do |k, v| - expect(json_response[k.to_s]).to eq(v) - end - expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE) - end - - it 'does not update name to existing name' do - project_param = { name: project3.name } - put v3_api("/projects/#{project.id}", user), project_param - expect(response).to have_gitlab_http_status(400) - expect(json_response['message']['name']).to eq(['has already been taken']) - end - - it 'updates request_access_enabled' do - project_param = { request_access_enabled: false } - - put v3_api("/projects/#{project.id}", user), project_param - - expect(response).to have_gitlab_http_status(200) - expect(json_response['request_access_enabled']).to eq(false) - end - - it 'updates path & name to existing path & name in different namespace' do - project_param = { path: project4.path, name: project4.name } - put v3_api("/projects/#{project3.id}", user), project_param - expect(response).to have_gitlab_http_status(200) - project_param.each_pair do |k, v| - expect(json_response[k.to_s]).to eq(v) - end - end - end - - context 'when authenticated as project master' do - it 'updates path' do - project_param = { path: 'bar' } - put v3_api("/projects/#{project3.id}", user4), project_param - expect(response).to have_gitlab_http_status(200) - project_param.each_pair do |k, v| - expect(json_response[k.to_s]).to eq(v) - end - end - - it 'updates other attributes' do - project_param = { issues_enabled: true, - wiki_enabled: true, - snippets_enabled: true, - merge_requests_enabled: true, - description: 'new description' } - - put v3_api("/projects/#{project3.id}", user4), project_param - expect(response).to have_gitlab_http_status(200) - project_param.each_pair do |k, v| - expect(json_response[k.to_s]).to eq(v) - end - end - - it 'does not update path to existing path' do - project_param = { path: project.path } - put v3_api("/projects/#{project3.id}", user4), project_param - expect(response).to have_gitlab_http_status(400) - expect(json_response['message']['path']).to eq(['has already been taken']) - end - - it 'does not update name' do - project_param = { name: 'bar' } - put v3_api("/projects/#{project3.id}", user4), project_param - expect(response).to have_gitlab_http_status(403) - end - - it 'does not update visibility_level' do - project_param = { visibility_level: 20 } - put v3_api("/projects/#{project3.id}", user4), project_param - expect(response).to have_gitlab_http_status(403) - end - end - - context 'when authenticated as project developer' do - it 'does not update other attributes' do - project_param = { path: 'bar', - issues_enabled: true, - wiki_enabled: true, - snippets_enabled: true, - merge_requests_enabled: true, - description: 'new description', - request_access_enabled: true } - put v3_api("/projects/#{project.id}", user3), project_param - expect(response).to have_gitlab_http_status(403) - end - end - end - - describe 'POST /projects/:id/archive' do - context 'on an unarchived project' do - it 'archives the project' do - post v3_api("/projects/#{project.id}/archive", user) - - expect(response).to have_gitlab_http_status(201) - expect(json_response['archived']).to be_truthy - end - end - - context 'on an archived project' do - before do - project.archive! - end - - it 'remains archived' do - post v3_api("/projects/#{project.id}/archive", user) - - expect(response).to have_gitlab_http_status(201) - expect(json_response['archived']).to be_truthy - end - end - - context 'user without archiving rights to the project' do - before do - project.add_developer(user3) - end - - it 'rejects the action' do - post v3_api("/projects/#{project.id}/archive", user3) - - expect(response).to have_gitlab_http_status(403) - end - end - end - - describe 'POST /projects/:id/unarchive' do - context 'on an unarchived project' do - it 'remains unarchived' do - post v3_api("/projects/#{project.id}/unarchive", user) - - expect(response).to have_gitlab_http_status(201) - expect(json_response['archived']).to be_falsey - end - end - - context 'on an archived project' do - before do - project.archive! - end - - it 'unarchives the project' do - post v3_api("/projects/#{project.id}/unarchive", user) - - expect(response).to have_gitlab_http_status(201) - expect(json_response['archived']).to be_falsey - end - end - - context 'user without archiving rights to the project' do - before do - project.add_developer(user3) - end - - it 'rejects the action' do - post v3_api("/projects/#{project.id}/unarchive", user3) - - expect(response).to have_gitlab_http_status(403) - end - end - end - - describe 'POST /projects/:id/star' do - context 'on an unstarred project' do - it 'stars the project' do - expect { post v3_api("/projects/#{project.id}/star", user) }.to change { project.reload.star_count }.by(1) - - expect(response).to have_gitlab_http_status(201) - expect(json_response['star_count']).to eq(1) - end - end - - context 'on a starred project' do - before do - user.toggle_star(project) - project.reload - end - - it 'does not modify the star count' do - expect { post v3_api("/projects/#{project.id}/star", user) }.not_to change { project.reload.star_count } - - expect(response).to have_gitlab_http_status(304) - end - end - end - - describe 'DELETE /projects/:id/star' do - context 'on a starred project' do - before do - user.toggle_star(project) - project.reload - end - - it 'unstars the project' do - expect { delete v3_api("/projects/#{project.id}/star", user) }.to change { project.reload.star_count }.by(-1) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['star_count']).to eq(0) - end - end - - context 'on an unstarred project' do - it 'does not modify the star count' do - expect { delete v3_api("/projects/#{project.id}/star", user) }.not_to change { project.reload.star_count } - - expect(response).to have_gitlab_http_status(304) - end - end - end - - describe 'DELETE /projects/:id' do - context 'when authenticated as user' do - it 'removes project' do - delete v3_api("/projects/#{project.id}", user) - expect(response).to have_gitlab_http_status(200) - end - - it 'does not remove a project if not an owner' do - user3 = create(:user) - project.add_developer(user3) - delete v3_api("/projects/#{project.id}", user3) - expect(response).to have_gitlab_http_status(403) - end - - it 'does not remove a non existing project' do - delete v3_api('/projects/1328', user) - expect(response).to have_gitlab_http_status(404) - end - - it 'does not remove a project not attached to user' do - delete v3_api("/projects/#{project.id}", user2) - expect(response).to have_gitlab_http_status(404) - end - end - - context 'when authenticated as admin' do - it 'removes any existing project' do - delete v3_api("/projects/#{project.id}", admin) - expect(response).to have_gitlab_http_status(200) - end - - it 'does not remove a non existing project' do - delete v3_api('/projects/1328', admin) - expect(response).to have_gitlab_http_status(404) - end - end - end -end diff --git a/spec/requests/api/v3/repositories_spec.rb b/spec/requests/api/v3/repositories_spec.rb deleted file mode 100644 index 0167eb2c4f6..00000000000 --- a/spec/requests/api/v3/repositories_spec.rb +++ /dev/null @@ -1,366 +0,0 @@ -require 'spec_helper' -require 'mime/types' - -describe API::V3::Repositories do - include RepoHelpers - include WorkhorseHelpers - - let(:user) { create(:user) } - let(:guest) { create(:user).tap { |u| create(:project_member, :guest, user: u, project: project) } } - let!(:project) { create(:project, :repository, creator: user) } - let!(:master) { create(:project_member, :master, user: user, project: project) } - - describe "GET /projects/:id/repository/tree" do - let(:route) { "/projects/#{project.id}/repository/tree" } - - shared_examples_for 'repository tree' do - it 'returns the repository tree' do - get v3_api(route, current_user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - - first_commit = json_response.first - expect(first_commit['name']).to eq('bar') - expect(first_commit['type']).to eq('tree') - expect(first_commit['mode']).to eq('040000') - end - - context 'when ref does not exist' do - it_behaves_like '404 response' do - let(:request) { get v3_api("#{route}?ref_name=foo", current_user) } - let(:message) { '404 Tree Not Found' } - end - end - - context 'when repository is disabled' do - include_context 'disabled repository' - - it_behaves_like '403 response' do - let(:request) { get v3_api(route, current_user) } - end - end - - context 'with recursive=1' do - it 'returns recursive project paths tree' do - get v3_api("#{route}?recursive=1", current_user) - - expect(response.status).to eq(200) - expect(json_response).to be_an Array - expect(json_response[4]['name']).to eq('html') - expect(json_response[4]['path']).to eq('files/html') - expect(json_response[4]['type']).to eq('tree') - expect(json_response[4]['mode']).to eq('040000') - end - - context 'when repository is disabled' do - include_context 'disabled repository' - - it_behaves_like '403 response' do - let(:request) { get v3_api(route, current_user) } - end - end - - context 'when ref does not exist' do - it_behaves_like '404 response' do - let(:request) { get v3_api("#{route}?recursive=1&ref_name=foo", current_user) } - let(:message) { '404 Tree Not Found' } - end - end - end - end - - context 'when unauthenticated', 'and project is public' do - it_behaves_like 'repository tree' do - let(:project) { create(:project, :public, :repository) } - let(:current_user) { nil } - end - end - - context 'when unauthenticated', 'and project is private' do - it_behaves_like '404 response' do - let(:request) { get v3_api(route) } - let(:message) { '404 Project Not Found' } - end - end - - context 'when authenticated', 'as a developer' do - it_behaves_like 'repository tree' do - let(:current_user) { user } - end - end - - context 'when authenticated', 'as a guest' do - it_behaves_like '403 response' do - let(:request) { get v3_api(route, guest) } - end - end - end - - [ - ['blobs/:sha', 'blobs/master'], - ['blobs/:sha', 'blobs/v1.1.0'], - ['commits/:sha/blob', 'commits/master/blob'] - ].each do |desc_path, example_path| - describe "GET /projects/:id/repository/#{desc_path}" do - let(:route) { "/projects/#{project.id}/repository/#{example_path}?filepath=README.md" } - shared_examples_for 'repository blob' do - it 'returns the repository blob' do - get v3_api(route, current_user) - expect(response).to have_gitlab_http_status(200) - end - context 'when sha does not exist' do - it_behaves_like '404 response' do - let(:request) { get v3_api("/projects/#{project.id}/repository/#{desc_path.sub(':sha', 'invalid_branch_name')}?filepath=README.md", current_user) } - let(:message) { '404 Commit Not Found' } - end - end - context 'when filepath does not exist' do - it_behaves_like '404 response' do - let(:request) { get v3_api(route.sub('README.md', 'README.invalid'), current_user) } - let(:message) { '404 File Not Found' } - end - end - context 'when no filepath is given' do - it_behaves_like '400 response' do - let(:request) { get v3_api(route.sub('?filepath=README.md', ''), current_user) } - end - end - context 'when repository is disabled' do - include_context 'disabled repository' - it_behaves_like '403 response' do - let(:request) { get v3_api(route, current_user) } - end - end - end - context 'when unauthenticated', 'and project is public' do - it_behaves_like 'repository blob' do - let(:project) { create(:project, :public, :repository) } - let(:current_user) { nil } - end - end - context 'when unauthenticated', 'and project is private' do - it_behaves_like '404 response' do - let(:request) { get v3_api(route) } - let(:message) { '404 Project Not Found' } - end - end - context 'when authenticated', 'as a developer' do - it_behaves_like 'repository blob' do - let(:current_user) { user } - end - end - context 'when authenticated', 'as a guest' do - it_behaves_like '403 response' do - let(:request) { get v3_api(route, guest) } - end - end - end - end - describe "GET /projects/:id/repository/raw_blobs/:sha" do - let(:route) { "/projects/#{project.id}/repository/raw_blobs/#{sample_blob.oid}" } - shared_examples_for 'repository raw blob' do - it 'returns the repository raw blob' do - get v3_api(route, current_user) - expect(response).to have_gitlab_http_status(200) - end - context 'when sha does not exist' do - it_behaves_like '404 response' do - let(:request) { get v3_api(route.sub(sample_blob.oid, '123456'), current_user) } - let(:message) { '404 Blob Not Found' } - end - end - context 'when repository is disabled' do - include_context 'disabled repository' - it_behaves_like '403 response' do - let(:request) { get v3_api(route, current_user) } - end - end - end - context 'when unauthenticated', 'and project is public' do - it_behaves_like 'repository raw blob' do - let(:project) { create(:project, :public, :repository) } - let(:current_user) { nil } - end - end - context 'when unauthenticated', 'and project is private' do - it_behaves_like '404 response' do - let(:request) { get v3_api(route) } - let(:message) { '404 Project Not Found' } - end - end - context 'when authenticated', 'as a developer' do - it_behaves_like 'repository raw blob' do - let(:current_user) { user } - end - end - context 'when authenticated', 'as a guest' do - it_behaves_like '403 response' do - let(:request) { get v3_api(route, guest) } - end - end - end - describe "GET /projects/:id/repository/archive(.:format)?:sha" do - let(:route) { "/projects/#{project.id}/repository/archive" } - shared_examples_for 'repository archive' do - it 'returns the repository archive' do - get v3_api(route, current_user) - expect(response).to have_gitlab_http_status(200) - repo_name = project.repository.name.gsub("\.git", "") - type, params = workhorse_send_data - expect(type).to eq('git-archive') - expect(params['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.tar.gz/) - end - it 'returns the repository archive archive.zip' do - get v3_api("/projects/#{project.id}/repository/archive.zip", user) - expect(response).to have_gitlab_http_status(200) - repo_name = project.repository.name.gsub("\.git", "") - type, params = workhorse_send_data - expect(type).to eq('git-archive') - expect(params['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.zip/) - end - it 'returns the repository archive archive.tar.bz2' do - get v3_api("/projects/#{project.id}/repository/archive.tar.bz2", user) - expect(response).to have_gitlab_http_status(200) - repo_name = project.repository.name.gsub("\.git", "") - type, params = workhorse_send_data - expect(type).to eq('git-archive') - expect(params['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.tar.bz2/) - end - context 'when sha does not exist' do - it_behaves_like '404 response' do - let(:request) { get v3_api("#{route}?sha=xxx", current_user) } - let(:message) { '404 File Not Found' } - end - end - end - context 'when unauthenticated', 'and project is public' do - it_behaves_like 'repository archive' do - let(:project) { create(:project, :public, :repository) } - let(:current_user) { nil } - end - end - context 'when unauthenticated', 'and project is private' do - it_behaves_like '404 response' do - let(:request) { get v3_api(route) } - let(:message) { '404 Project Not Found' } - end - end - context 'when authenticated', 'as a developer' do - it_behaves_like 'repository archive' do - let(:current_user) { user } - end - end - context 'when authenticated', 'as a guest' do - it_behaves_like '403 response' do - let(:request) { get v3_api(route, guest) } - end - end - end - - describe 'GET /projects/:id/repository/compare' do - let(:route) { "/projects/#{project.id}/repository/compare" } - shared_examples_for 'repository compare' do - it "compares branches" do - get v3_api(route, current_user), from: 'master', to: 'feature' - expect(response).to have_gitlab_http_status(200) - expect(json_response['commits']).to be_present - expect(json_response['diffs']).to be_present - end - it "compares tags" do - get v3_api(route, current_user), from: 'v1.0.0', to: 'v1.1.0' - expect(response).to have_gitlab_http_status(200) - expect(json_response['commits']).to be_present - expect(json_response['diffs']).to be_present - end - it "compares commits" do - get v3_api(route, current_user), from: sample_commit.id, to: sample_commit.parent_id - expect(response).to have_gitlab_http_status(200) - expect(json_response['commits']).to be_empty - expect(json_response['diffs']).to be_empty - expect(json_response['compare_same_ref']).to be_falsey - end - it "compares commits in reverse order" do - get v3_api(route, current_user), from: sample_commit.parent_id, to: sample_commit.id - expect(response).to have_gitlab_http_status(200) - expect(json_response['commits']).to be_present - expect(json_response['diffs']).to be_present - end - it "compares same refs" do - get v3_api(route, current_user), from: 'master', to: 'master' - expect(response).to have_gitlab_http_status(200) - expect(json_response['commits']).to be_empty - expect(json_response['diffs']).to be_empty - expect(json_response['compare_same_ref']).to be_truthy - end - end - context 'when unauthenticated', 'and project is public' do - it_behaves_like 'repository compare' do - let(:project) { create(:project, :public, :repository) } - let(:current_user) { nil } - end - end - context 'when unauthenticated', 'and project is private' do - it_behaves_like '404 response' do - let(:request) { get v3_api(route) } - let(:message) { '404 Project Not Found' } - end - end - context 'when authenticated', 'as a developer' do - it_behaves_like 'repository compare' do - let(:current_user) { user } - end - end - context 'when authenticated', 'as a guest' do - it_behaves_like '403 response' do - let(:request) { get v3_api(route, guest) } - end - end - end - - describe 'GET /projects/:id/repository/contributors' do - let(:route) { "/projects/#{project.id}/repository/contributors" } - - shared_examples_for 'repository contributors' do - it 'returns valid data' do - get v3_api(route, current_user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - - first_contributor = json_response.first - expect(first_contributor['email']).to eq('tiagonbotelho@hotmail.com') - expect(first_contributor['name']).to eq('tiagonbotelho') - expect(first_contributor['commits']).to eq(1) - expect(first_contributor['additions']).to eq(0) - expect(first_contributor['deletions']).to eq(0) - end - end - - context 'when unauthenticated', 'and project is public' do - it_behaves_like 'repository contributors' do - let(:project) { create(:project, :public, :repository) } - let(:current_user) { nil } - end - end - - context 'when unauthenticated', 'and project is private' do - it_behaves_like '404 response' do - let(:request) { get v3_api(route) } - let(:message) { '404 Project Not Found' } - end - end - - context 'when authenticated', 'as a developer' do - it_behaves_like 'repository contributors' do - let(:current_user) { user } - end - end - - context 'when authenticated', 'as a guest' do - it_behaves_like '403 response' do - let(:request) { get v3_api(route, guest) } - end - end - end -end diff --git a/spec/requests/api/v3/runners_spec.rb b/spec/requests/api/v3/runners_spec.rb deleted file mode 100644 index c91b097a3c7..00000000000 --- a/spec/requests/api/v3/runners_spec.rb +++ /dev/null @@ -1,152 +0,0 @@ -require 'spec_helper' - -describe API::V3::Runners do - let(:admin) { create(:user, :admin) } - let(:user) { create(:user) } - let(:user2) { create(:user) } - - let(:project) { create(:project, creator_id: user.id) } - let(:project2) { create(:project, creator_id: user.id) } - - let!(:shared_runner) { create(:ci_runner, :shared) } - let!(:unused_specific_runner) { create(:ci_runner) } - - let!(:specific_runner) do - create(:ci_runner).tap do |runner| - create(:ci_runner_project, runner: runner, project: project) - end - end - - let!(:two_projects_runner) do - create(:ci_runner).tap do |runner| - create(:ci_runner_project, runner: runner, project: project) - create(:ci_runner_project, runner: runner, project: project2) - end - end - - before do - # Set project access for users - create(:project_member, :master, user: user, project: project) - create(:project_member, :reporter, user: user2, project: project) - end - - describe 'DELETE /runners/:id' do - context 'admin user' do - context 'when runner is shared' do - it 'deletes runner' do - expect do - delete v3_api("/runners/#{shared_runner.id}", admin) - - expect(response).to have_gitlab_http_status(200) - end.to change { Ci::Runner.shared.count }.by(-1) - end - end - - context 'when runner is not shared' do - it 'deletes unused runner' do - expect do - delete v3_api("/runners/#{unused_specific_runner.id}", admin) - - expect(response).to have_gitlab_http_status(200) - end.to change { Ci::Runner.specific.count }.by(-1) - end - - it 'deletes used runner' do - expect do - delete v3_api("/runners/#{specific_runner.id}", admin) - - expect(response).to have_gitlab_http_status(200) - end.to change { Ci::Runner.specific.count }.by(-1) - end - end - - it 'returns 404 if runner does not exists' do - delete v3_api('/runners/9999', admin) - - expect(response).to have_gitlab_http_status(404) - end - end - - context 'authorized user' do - context 'when runner is shared' do - it 'does not delete runner' do - delete v3_api("/runners/#{shared_runner.id}", user) - expect(response).to have_gitlab_http_status(403) - end - end - - context 'when runner is not shared' do - it 'does not delete runner without access to it' do - delete v3_api("/runners/#{specific_runner.id}", user2) - expect(response).to have_gitlab_http_status(403) - end - - it 'does not delete runner with more than one associated project' do - delete v3_api("/runners/#{two_projects_runner.id}", user) - expect(response).to have_gitlab_http_status(403) - end - - it 'deletes runner for one owned project' do - expect do - delete v3_api("/runners/#{specific_runner.id}", user) - - expect(response).to have_gitlab_http_status(200) - end.to change { Ci::Runner.specific.count }.by(-1) - end - end - end - - context 'unauthorized user' do - it 'does not delete runner' do - delete v3_api("/runners/#{specific_runner.id}") - - expect(response).to have_gitlab_http_status(401) - end - end - end - - describe 'DELETE /projects/:id/runners/:runner_id' do - context 'authorized user' do - context 'when runner have more than one associated projects' do - it "disables project's runner" do - expect do - delete v3_api("/projects/#{project.id}/runners/#{two_projects_runner.id}", user) - - expect(response).to have_gitlab_http_status(200) - end.to change { project.runners.count }.by(-1) - end - end - - context 'when runner have one associated projects' do - it "does not disable project's runner" do - expect do - delete v3_api("/projects/#{project.id}/runners/#{specific_runner.id}", user) - end.to change { project.runners.count }.by(0) - expect(response).to have_gitlab_http_status(403) - end - end - - it 'returns 404 is runner is not found' do - delete v3_api("/projects/#{project.id}/runners/9999", user) - - expect(response).to have_gitlab_http_status(404) - end - end - - context 'authorized user without permissions' do - it "does not disable project's runner" do - delete v3_api("/projects/#{project.id}/runners/#{specific_runner.id}", user2) - - expect(response).to have_gitlab_http_status(403) - end - end - - context 'unauthorized user' do - it "does not disable project's runner" do - delete v3_api("/projects/#{project.id}/runners/#{specific_runner.id}") - - expect(response).to have_gitlab_http_status(401) - end - end - end -end diff --git a/spec/requests/api/v3/services_spec.rb b/spec/requests/api/v3/services_spec.rb deleted file mode 100644 index c69a7d58ca6..00000000000 --- a/spec/requests/api/v3/services_spec.rb +++ /dev/null @@ -1,26 +0,0 @@ -require "spec_helper" - -describe API::V3::Services do - let(:user) { create(:user) } - let(:project) { create(:project, creator_id: user.id, namespace: user.namespace) } - - available_services = Service.available_services_names - available_services.delete('prometheus') - available_services.each do |service| - describe "DELETE /projects/:id/services/#{service.dasherize}" do - include_context service - - before do - initialize_service(service) - end - - it "deletes #{service}" do - delete v3_api("/projects/#{project.id}/services/#{dashed_service}", user) - - expect(response).to have_gitlab_http_status(200) - project.send(service_method).reload - expect(project.send(service_method).activated?).to be_falsey - end - end - end -end diff --git a/spec/requests/api/v3/settings_spec.rb b/spec/requests/api/v3/settings_spec.rb deleted file mode 100644 index 985bfbfa09c..00000000000 --- a/spec/requests/api/v3/settings_spec.rb +++ /dev/null @@ -1,63 +0,0 @@ -require 'spec_helper' - -describe API::V3::Settings, 'Settings' do - let(:user) { create(:user) } - let(:admin) { create(:admin) } - - describe "GET /application/settings" do - it "returns application settings" do - get v3_api("/application/settings", admin) - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Hash - expect(json_response['default_projects_limit']).to eq(42) - expect(json_response['password_authentication_enabled']).to be_truthy - expect(json_response['repository_storage']).to eq('default') - expect(json_response['koding_enabled']).to be_falsey - expect(json_response['koding_url']).to be_nil - expect(json_response['plantuml_enabled']).to be_falsey - expect(json_response['plantuml_url']).to be_nil - end - end - - describe "PUT /application/settings" do - context "custom repository storage type set in the config" do - before do - storages = { 'custom' => 'tmp/tests/custom_repositories' } - allow(Gitlab.config.repositories).to receive(:storages).and_return(storages) - end - - it "updates application settings" do - put v3_api("/application/settings", admin), - default_projects_limit: 3, password_authentication_enabled_for_web: false, repository_storage: 'custom', koding_enabled: true, koding_url: 'http://koding.example.com', - plantuml_enabled: true, plantuml_url: 'http://plantuml.example.com' - expect(response).to have_gitlab_http_status(200) - expect(json_response['default_projects_limit']).to eq(3) - expect(json_response['password_authentication_enabled_for_web']).to be_falsey - expect(json_response['repository_storage']).to eq('custom') - expect(json_response['repository_storages']).to eq(['custom']) - expect(json_response['koding_enabled']).to be_truthy - expect(json_response['koding_url']).to eq('http://koding.example.com') - expect(json_response['plantuml_enabled']).to be_truthy - expect(json_response['plantuml_url']).to eq('http://plantuml.example.com') - end - end - - context "missing koding_url value when koding_enabled is true" do - it "returns a blank parameter error message" do - put v3_api("/application/settings", admin), koding_enabled: true - - expect(response).to have_gitlab_http_status(400) - expect(json_response['error']).to eq('koding_url is missing') - end - end - - context "missing plantuml_url value when plantuml_enabled is true" do - it "returns a blank parameter error message" do - put v3_api("/application/settings", admin), plantuml_enabled: true - - expect(response).to have_gitlab_http_status(400) - expect(json_response['error']).to eq('plantuml_url is missing') - end - end - end -end diff --git a/spec/requests/api/v3/snippets_spec.rb b/spec/requests/api/v3/snippets_spec.rb deleted file mode 100644 index e8913039194..00000000000 --- a/spec/requests/api/v3/snippets_spec.rb +++ /dev/null @@ -1,186 +0,0 @@ -require 'rails_helper' - -describe API::V3::Snippets do - let!(:user) { create(:user) } - - describe 'GET /snippets/' do - it 'returns snippets available' do - public_snippet = create(:personal_snippet, :public, author: user) - private_snippet = create(:personal_snippet, :private, author: user) - internal_snippet = create(:personal_snippet, :internal, author: user) - - get v3_api("/snippets/", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response.map { |snippet| snippet['id']} ).to contain_exactly( - public_snippet.id, - internal_snippet.id, - private_snippet.id) - expect(json_response.last).to have_key('web_url') - expect(json_response.last).to have_key('raw_url') - end - - it 'hides private snippets from regular user' do - create(:personal_snippet, :private) - - get v3_api("/snippets/", user) - expect(response).to have_gitlab_http_status(200) - expect(json_response.size).to eq(0) - end - end - - describe 'GET /snippets/public' do - let!(:other_user) { create(:user) } - let!(:public_snippet) { create(:personal_snippet, :public, author: user) } - let!(:private_snippet) { create(:personal_snippet, :private, author: user) } - let!(:internal_snippet) { create(:personal_snippet, :internal, author: user) } - let!(:public_snippet_other) { create(:personal_snippet, :public, author: other_user) } - let!(:private_snippet_other) { create(:personal_snippet, :private, author: other_user) } - let!(:internal_snippet_other) { create(:personal_snippet, :internal, author: other_user) } - - it 'returns all snippets with public visibility from all users' do - get v3_api("/snippets/public", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response.map { |snippet| snippet['id']} ).to contain_exactly( - public_snippet.id, - public_snippet_other.id) - expect(json_response.map { |snippet| snippet['web_url']} ).to include( - "http://localhost/snippets/#{public_snippet.id}", - "http://localhost/snippets/#{public_snippet_other.id}") - expect(json_response.map { |snippet| snippet['raw_url']} ).to include( - "http://localhost/snippets/#{public_snippet.id}/raw", - "http://localhost/snippets/#{public_snippet_other.id}/raw") - end - end - - describe 'GET /snippets/:id/raw' do - let(:snippet) { create(:personal_snippet, author: user) } - - it 'returns raw text' do - get v3_api("/snippets/#{snippet.id}/raw", user) - - expect(response).to have_gitlab_http_status(200) - expect(response.content_type).to eq 'text/plain' - expect(response.body).to eq(snippet.content) - end - - it 'returns 404 for invalid snippet id' do - delete v3_api("/snippets/1234", user) - - expect(response).to have_gitlab_http_status(404) - expect(json_response['message']).to eq('404 Snippet Not Found') - end - end - - describe 'POST /snippets/' do - let(:params) do - { - title: 'Test Title', - file_name: 'test.rb', - content: 'puts "hello world"', - visibility_level: Snippet::PUBLIC - } - end - - it 'creates a new snippet' do - expect do - post v3_api("/snippets/", user), params - end.to change { PersonalSnippet.count }.by(1) - - expect(response).to have_gitlab_http_status(201) - expect(json_response['title']).to eq(params[:title]) - expect(json_response['file_name']).to eq(params[:file_name]) - end - - it 'returns 400 for missing parameters' do - params.delete(:title) - - post v3_api("/snippets/", user), params - - expect(response).to have_gitlab_http_status(400) - end - - context 'when the snippet is spam' do - def create_snippet(snippet_params = {}) - post v3_api('/snippets', user), params.merge(snippet_params) - end - - before do - allow_any_instance_of(AkismetService).to receive(:spam?).and_return(true) - end - - context 'when the snippet is private' do - it 'creates the snippet' do - expect { create_snippet(visibility_level: Snippet::PRIVATE) } - .to change { Snippet.count }.by(1) - end - end - - context 'when the snippet is public' do - it 'rejects the shippet' do - expect { create_snippet(visibility_level: Snippet::PUBLIC) } - .not_to change { Snippet.count } - expect(response).to have_gitlab_http_status(400) - end - - it 'creates a spam log' do - expect { create_snippet(visibility_level: Snippet::PUBLIC) } - .to change { SpamLog.count }.by(1) - end - end - end - end - - describe 'PUT /snippets/:id' do - let(:other_user) { create(:user) } - let(:public_snippet) { create(:personal_snippet, :public, author: user) } - it 'updates snippet' do - new_content = 'New content' - - put v3_api("/snippets/#{public_snippet.id}", user), content: new_content - - expect(response).to have_gitlab_http_status(200) - public_snippet.reload - expect(public_snippet.content).to eq(new_content) - end - - it 'returns 404 for invalid snippet id' do - put v3_api("/snippets/1234", user), title: 'foo' - - expect(response).to have_gitlab_http_status(404) - expect(json_response['message']).to eq('404 Snippet Not Found') - end - - it "returns 404 for another user's snippet" do - put v3_api("/snippets/#{public_snippet.id}", other_user), title: 'fubar' - - expect(response).to have_gitlab_http_status(404) - expect(json_response['message']).to eq('404 Snippet Not Found') - end - - it 'returns 400 for missing parameters' do - put v3_api("/snippets/1234", user) - - expect(response).to have_gitlab_http_status(400) - end - end - - describe 'DELETE /snippets/:id' do - let!(:public_snippet) { create(:personal_snippet, :public, author: user) } - it 'deletes snippet' do - expect do - delete v3_api("/snippets/#{public_snippet.id}", user) - - expect(response).to have_gitlab_http_status(204) - end.to change { PersonalSnippet.count }.by(-1) - end - - it 'returns 404 for invalid snippet id' do - delete v3_api("/snippets/1234", user) - - expect(response).to have_gitlab_http_status(404) - expect(json_response['message']).to eq('404 Snippet Not Found') - end - end -end diff --git a/spec/requests/api/v3/system_hooks_spec.rb b/spec/requests/api/v3/system_hooks_spec.rb deleted file mode 100644 index 30711c60faa..00000000000 --- a/spec/requests/api/v3/system_hooks_spec.rb +++ /dev/null @@ -1,56 +0,0 @@ -require 'spec_helper' - -describe API::V3::SystemHooks do - let(:user) { create(:user) } - let(:admin) { create(:admin) } - let!(:hook) { create(:system_hook, url: "http://example.com") } - - before { stub_request(:post, hook.url) } - - describe "GET /hooks" do - context "when no user" do - it "returns authentication error" do - get v3_api("/hooks") - - expect(response).to have_gitlab_http_status(401) - end - end - - context "when not an admin" do - it "returns forbidden error" do - get v3_api("/hooks", user) - - expect(response).to have_gitlab_http_status(403) - end - end - - context "when authenticated as admin" do - it "returns an array of hooks" do - get v3_api("/hooks", admin) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.first['url']).to eq(hook.url) - expect(json_response.first['push_events']).to be false - expect(json_response.first['tag_push_events']).to be false - expect(json_response.first['repository_update_events']).to be true - end - end - end - - describe "DELETE /hooks/:id" do - it "deletes a hook" do - expect do - delete v3_api("/hooks/#{hook.id}", admin) - - expect(response).to have_gitlab_http_status(200) - end.to change { SystemHook.count }.by(-1) - end - - it 'returns 404 if the system hook does not exist' do - delete v3_api('/hooks/12345', admin) - - expect(response).to have_gitlab_http_status(404) - end - end -end diff --git a/spec/requests/api/v3/tags_spec.rb b/spec/requests/api/v3/tags_spec.rb deleted file mode 100644 index e6ad005fa87..00000000000 --- a/spec/requests/api/v3/tags_spec.rb +++ /dev/null @@ -1,88 +0,0 @@ -require 'spec_helper' -require 'mime/types' - -describe API::V3::Tags do - include RepoHelpers - - let(:user) { create(:user) } - let(:user2) { create(:user) } - let!(:project) { create(:project, :repository, creator: user) } - let!(:master) { create(:project_member, :master, user: user, project: project) } - - describe "GET /projects/:id/repository/tags" do - let(:tag_name) { project.repository.tag_names.sort.reverse.first } - let(:description) { 'Awesome release!' } - - shared_examples_for 'repository tags' do - it 'returns the repository tags' do - get v3_api("/projects/#{project.id}/repository/tags", current_user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.first['name']).to eq(tag_name) - end - end - - context 'when unauthenticated' do - it_behaves_like 'repository tags' do - let(:project) { create(:project, :public, :repository) } - let(:current_user) { nil } - end - end - - context 'when authenticated' do - it_behaves_like 'repository tags' do - let(:current_user) { user } - end - end - - context 'without releases' do - it "returns an array of project tags" do - get v3_api("/projects/#{project.id}/repository/tags", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.first['name']).to eq(tag_name) - end - end - - context 'with releases' do - before do - release = project.releases.find_or_initialize_by(tag: tag_name) - release.update_attributes(description: description) - end - - it "returns an array of project tags with release info" do - get v3_api("/projects/#{project.id}/repository/tags", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.first['name']).to eq(tag_name) - expect(json_response.first['message']).to eq('Version 1.1.0') - expect(json_response.first['release']['description']).to eq(description) - end - end - end - - describe 'DELETE /projects/:id/repository/tags/:tag_name' do - let(:tag_name) { project.repository.tag_names.sort.reverse.first } - - before do - allow_any_instance_of(Repository).to receive(:rm_tag).and_return(true) - end - - context 'delete tag' do - it 'deletes an existing tag' do - delete v3_api("/projects/#{project.id}/repository/tags/#{tag_name}", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['tag_name']).to eq(tag_name) - end - - it 'raises 404 if the tag does not exist' do - delete v3_api("/projects/#{project.id}/repository/tags/foobar", user) - expect(response).to have_gitlab_http_status(404) - end - end - end -end diff --git a/spec/requests/api/v3/templates_spec.rb b/spec/requests/api/v3/templates_spec.rb deleted file mode 100644 index 1a637f3cf96..00000000000 --- a/spec/requests/api/v3/templates_spec.rb +++ /dev/null @@ -1,201 +0,0 @@ -require 'spec_helper' - -describe API::V3::Templates do - shared_examples_for 'the Template Entity' do |path| - before { get v3_api(path) } - - it { expect(json_response['name']).to eq('Ruby') } - it { expect(json_response['content']).to include('*.gem') } - end - - shared_examples_for 'the TemplateList Entity' do |path| - before { get v3_api(path) } - - it { expect(json_response.first['name']).not_to be_nil } - it { expect(json_response.first['content']).to be_nil } - end - - shared_examples_for 'requesting gitignores' do |path| - it 'returns a list of available gitignore templates' do - get v3_api(path) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.size).to be > 15 - end - end - - shared_examples_for 'requesting gitlab-ci-ymls' do |path| - it 'returns a list of available gitlab_ci_ymls' do - get v3_api(path) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.first['name']).not_to be_nil - end - end - - shared_examples_for 'requesting gitlab-ci-yml for Ruby' do |path| - it 'adds a disclaimer on the top' do - get v3_api(path) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['content']).to start_with("# This file is a template,") - end - end - - shared_examples_for 'the License Template Entity' do |path| - before { get v3_api(path) } - - it 'returns a license template' do - expect(json_response['key']).to eq('mit') - expect(json_response['name']).to eq('MIT License') - expect(json_response['nickname']).to be_nil - expect(json_response['popular']).to be true - expect(json_response['html_url']).to eq('http://choosealicense.com/licenses/mit/') - expect(json_response['source_url']).to eq('https://opensource.org/licenses/MIT') - expect(json_response['description']).to include('A short and simple permissive license with conditions') - expect(json_response['conditions']).to eq(%w[include-copyright]) - expect(json_response['permissions']).to eq(%w[commercial-use modifications distribution private-use]) - expect(json_response['limitations']).to eq(%w[liability warranty]) - expect(json_response['content']).to include('MIT License') - end - end - - shared_examples_for 'GET licenses' do |path| - it 'returns a list of available license templates' do - get v3_api(path) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.size).to eq(12) - expect(json_response.map { |l| l['key'] }).to include('agpl-3.0') - end - - describe 'the popular parameter' do - context 'with popular=1' do - it 'returns a list of available popular license templates' do - get v3_api("#{path}?popular=1") - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.size).to eq(3) - expect(json_response.map { |l| l['key'] }).to include('apache-2.0') - end - end - end - end - - shared_examples_for 'GET licenses/:name' do |path| - context 'with :project and :fullname given' do - before do - get v3_api("#{path}/#{license_type}?project=My+Awesome+Project&fullname=Anton+#{license_type.upcase}") - end - - context 'for the mit license' do - let(:license_type) { 'mit' } - - it 'returns the license text' do - expect(json_response['content']).to include('MIT License') - end - - it 'replaces placeholder values' do - expect(json_response['content']).to include("Copyright (c) #{Time.now.year} Anton") - end - end - - context 'for the agpl-3.0 license' do - let(:license_type) { 'agpl-3.0' } - - it 'returns the license text' do - expect(json_response['content']).to include('GNU AFFERO GENERAL PUBLIC LICENSE') - end - - it 'replaces placeholder values' do - expect(json_response['content']).to include('My Awesome Project') - expect(json_response['content']).to include("Copyright (C) #{Time.now.year} Anton") - end - end - - context 'for the gpl-3.0 license' do - let(:license_type) { 'gpl-3.0' } - - it 'returns the license text' do - expect(json_response['content']).to include('GNU GENERAL PUBLIC LICENSE') - end - - it 'replaces placeholder values' do - expect(json_response['content']).to include('My Awesome Project') - expect(json_response['content']).to include("Copyright (C) #{Time.now.year} Anton") - end - end - - context 'for the gpl-2.0 license' do - let(:license_type) { 'gpl-2.0' } - - it 'returns the license text' do - expect(json_response['content']).to include('GNU GENERAL PUBLIC LICENSE') - end - - it 'replaces placeholder values' do - expect(json_response['content']).to include('My Awesome Project') - expect(json_response['content']).to include("Copyright (C) #{Time.now.year} Anton") - end - end - - context 'for the apache-2.0 license' do - let(:license_type) { 'apache-2.0' } - - it 'returns the license text' do - expect(json_response['content']).to include('Apache License') - end - - it 'replaces placeholder values' do - expect(json_response['content']).to include("Copyright #{Time.now.year} Anton") - end - end - - context 'for an uknown license' do - let(:license_type) { 'muth-over9000' } - - it 'returns a 404' do - expect(response).to have_gitlab_http_status(404) - end - end - end - - context 'with no :fullname given' do - context 'with an authenticated user' do - let(:user) { create(:user) } - - it 'replaces the copyright owner placeholder with the name of the current user' do - get v3_api('/templates/licenses/mit', user) - - expect(json_response['content']).to include("Copyright (c) #{Time.now.year} #{user.name}") - end - end - end - end - - describe 'with /templates namespace' do - it_behaves_like 'the Template Entity', '/templates/gitignores/Ruby' - it_behaves_like 'the TemplateList Entity', '/templates/gitignores' - it_behaves_like 'requesting gitignores', '/templates/gitignores' - it_behaves_like 'requesting gitlab-ci-ymls', '/templates/gitlab_ci_ymls' - it_behaves_like 'requesting gitlab-ci-yml for Ruby', '/templates/gitlab_ci_ymls/Ruby' - it_behaves_like 'the License Template Entity', '/templates/licenses/mit' - it_behaves_like 'GET licenses', '/templates/licenses' - it_behaves_like 'GET licenses/:name', '/templates/licenses' - end - - describe 'without /templates namespace' do - it_behaves_like 'the Template Entity', '/gitignores/Ruby' - it_behaves_like 'the TemplateList Entity', '/gitignores' - it_behaves_like 'requesting gitignores', '/gitignores' - it_behaves_like 'requesting gitlab-ci-ymls', '/gitlab_ci_ymls' - it_behaves_like 'requesting gitlab-ci-yml for Ruby', '/gitlab_ci_ymls/Ruby' - it_behaves_like 'the License Template Entity', '/licenses/mit' - it_behaves_like 'GET licenses', '/licenses' - it_behaves_like 'GET licenses/:name', '/licenses' - end -end diff --git a/spec/requests/api/v3/todos_spec.rb b/spec/requests/api/v3/todos_spec.rb deleted file mode 100644 index ea648e3917f..00000000000 --- a/spec/requests/api/v3/todos_spec.rb +++ /dev/null @@ -1,77 +0,0 @@ -require 'spec_helper' - -describe API::V3::Todos do - let(:project_1) { create(:project) } - let(:project_2) { create(:project) } - let(:author_1) { create(:user) } - let(:author_2) { create(:user) } - let(:john_doe) { create(:user, username: 'john_doe') } - let!(:pending_1) { create(:todo, :mentioned, project: project_1, author: author_1, user: john_doe) } - let!(:pending_2) { create(:todo, project: project_2, author: author_2, user: john_doe) } - let!(:pending_3) { create(:todo, project: project_1, author: author_2, user: john_doe) } - let!(:done) { create(:todo, :done, project: project_1, author: author_1, user: john_doe) } - - before do - project_1.add_developer(john_doe) - project_2.add_developer(john_doe) - end - - describe 'DELETE /todos/:id' do - context 'when unauthenticated' do - it 'returns authentication error' do - delete v3_api("/todos/#{pending_1.id}") - - expect(response.status).to eq(401) - end - end - - context 'when authenticated' do - it 'marks a todo as done' do - delete v3_api("/todos/#{pending_1.id}", john_doe) - - expect(response.status).to eq(200) - expect(pending_1.reload).to be_done - end - - it 'updates todos cache' do - expect_any_instance_of(User).to receive(:update_todos_count_cache).and_call_original - - delete v3_api("/todos/#{pending_1.id}", john_doe) - end - - it 'returns 404 if the todo does not belong to the current user' do - delete v3_api("/todos/#{pending_1.id}", author_1) - - expect(response.status).to eq(404) - end - end - end - - describe 'DELETE /todos' do - context 'when unauthenticated' do - it 'returns authentication error' do - delete v3_api('/todos') - - expect(response.status).to eq(401) - end - end - - context 'when authenticated' do - it 'marks all todos as done' do - delete v3_api('/todos', john_doe) - - expect(response.status).to eq(200) - expect(response.body).to eq('3') - expect(pending_1.reload).to be_done - expect(pending_2.reload).to be_done - expect(pending_3.reload).to be_done - end - - it 'updates todos cache' do - expect_any_instance_of(User).to receive(:update_todos_count_cache).and_call_original - - delete v3_api("/todos", john_doe) - end - end - end -end diff --git a/spec/requests/api/v3/triggers_spec.rb b/spec/requests/api/v3/triggers_spec.rb deleted file mode 100644 index e8e2f49d7a0..00000000000 --- a/spec/requests/api/v3/triggers_spec.rb +++ /dev/null @@ -1,235 +0,0 @@ -require 'spec_helper' - -describe API::V3::Triggers do - let(:user) { create(:user) } - let(:user2) { create(:user) } - let!(:trigger_token) { 'secure_token' } - let!(:project) { create(:project, :repository, creator: user) } - let!(:master) { create(:project_member, :master, user: user, project: project) } - let!(:developer) { create(:project_member, :developer, user: user2, project: project) } - - let!(:trigger) do - create(:ci_trigger, project: project, token: trigger_token, owner: user) - end - - describe 'POST /projects/:project_id/trigger' do - let!(:project2) { create(:project) } - let(:options) do - { - token: trigger_token - } - end - - before do - stub_ci_pipeline_to_return_yaml_file - end - - context 'Handles errors' do - it 'returns bad request if token is missing' do - post v3_api("/projects/#{project.id}/trigger/builds"), ref: 'master' - expect(response).to have_gitlab_http_status(400) - end - - it 'returns not found if project is not found' do - post v3_api('/projects/0/trigger/builds'), options.merge(ref: 'master') - expect(response).to have_gitlab_http_status(404) - end - - it 'returns unauthorized if token is for different project' do - post v3_api("/projects/#{project2.id}/trigger/builds"), options.merge(ref: 'master') - expect(response).to have_gitlab_http_status(404) - end - end - - context 'Have a commit' do - let(:pipeline) { project.pipelines.last } - - it 'creates builds' do - post v3_api("/projects/#{project.id}/trigger/builds"), options.merge(ref: 'master') - expect(response).to have_gitlab_http_status(201) - pipeline.builds.reload - expect(pipeline.builds.pending.size).to eq(2) - expect(pipeline.builds.size).to eq(5) - end - - it 'returns bad request with no builds created if there\'s no commit for that ref' do - post v3_api("/projects/#{project.id}/trigger/builds"), options.merge(ref: 'other-branch') - expect(response).to have_gitlab_http_status(400) - expect(json_response['message']['base']) - .to contain_exactly('Reference not found') - end - - context 'Validates variables' do - let(:variables) do - { 'TRIGGER_KEY' => 'TRIGGER_VALUE' } - end - - it 'validates variables to be a hash' do - post v3_api("/projects/#{project.id}/trigger/builds"), options.merge(variables: 'value', ref: 'master') - expect(response).to have_gitlab_http_status(400) - expect(json_response['error']).to eq('variables is invalid') - end - - it 'validates variables needs to be a map of key-valued strings' do - post v3_api("/projects/#{project.id}/trigger/builds"), options.merge(variables: { key: %w(1 2) }, ref: 'master') - expect(response).to have_gitlab_http_status(400) - expect(json_response['message']).to eq('variables needs to be a map of key-valued strings') - end - - it 'creates trigger request with variables' do - post v3_api("/projects/#{project.id}/trigger/builds"), options.merge(variables: variables, ref: 'master') - expect(response).to have_gitlab_http_status(201) - pipeline.builds.reload - expect(pipeline.variables.map { |v| { v.key => v.value } }.first).to eq(variables) - expect(json_response['variables']).to eq(variables) - end - end - end - - context 'when triggering a pipeline from a trigger token' do - it 'creates builds from the ref given in the URL, not in the body' do - expect do - post v3_api("/projects/#{project.id}/ref/master/trigger/builds?token=#{trigger_token}"), { ref: 'refs/heads/other-branch' } - end.to change(project.builds, :count).by(5) - expect(response).to have_gitlab_http_status(201) - end - - context 'when ref contains a dot' do - it 'creates builds from the ref given in the URL, not in the body' do - project.repository.create_file(user, '.gitlab/gitlabhq/new_feature.md', 'something valid', message: 'new_feature', branch_name: 'v.1-branch') - - expect do - post v3_api("/projects/#{project.id}/ref/v.1-branch/trigger/builds?token=#{trigger_token}"), { ref: 'refs/heads/other-branch' } - end.to change(project.builds, :count).by(4) - - expect(response).to have_gitlab_http_status(201) - end - end - end - end - - describe 'GET /projects/:id/triggers' do - context 'authenticated user with valid permissions' do - it 'returns list of triggers' do - get v3_api("/projects/#{project.id}/triggers", user) - - expect(response).to have_gitlab_http_status(200) - expect(response).to include_pagination_headers - expect(json_response).to be_a(Array) - expect(json_response[0]).to have_key('token') - end - end - - context 'authenticated user with invalid permissions' do - it 'does not return triggers list' do - get v3_api("/projects/#{project.id}/triggers", user2) - - expect(response).to have_gitlab_http_status(403) - end - end - - context 'unauthenticated user' do - it 'does not return triggers list' do - get v3_api("/projects/#{project.id}/triggers") - - expect(response).to have_gitlab_http_status(401) - end - end - end - - describe 'GET /projects/:id/triggers/:token' do - context 'authenticated user with valid permissions' do - it 'returns trigger details' do - get v3_api("/projects/#{project.id}/triggers/#{trigger.token}", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_a(Hash) - end - - it 'responds with 404 Not Found if requesting non-existing trigger' do - get v3_api("/projects/#{project.id}/triggers/abcdef012345", user) - - expect(response).to have_gitlab_http_status(404) - end - end - - context 'authenticated user with invalid permissions' do - it 'does not return triggers list' do - get v3_api("/projects/#{project.id}/triggers/#{trigger.token}", user2) - - expect(response).to have_gitlab_http_status(403) - end - end - - context 'unauthenticated user' do - it 'does not return triggers list' do - get v3_api("/projects/#{project.id}/triggers/#{trigger.token}") - - expect(response).to have_gitlab_http_status(401) - end - end - end - - describe 'POST /projects/:id/triggers' do - context 'authenticated user with valid permissions' do - it 'creates trigger' do - expect do - post v3_api("/projects/#{project.id}/triggers", user) - end.to change {project.triggers.count}.by(1) - - expect(response).to have_gitlab_http_status(201) - expect(json_response).to be_a(Hash) - end - end - - context 'authenticated user with invalid permissions' do - it 'does not create trigger' do - post v3_api("/projects/#{project.id}/triggers", user2) - - expect(response).to have_gitlab_http_status(403) - end - end - - context 'unauthenticated user' do - it 'does not create trigger' do - post v3_api("/projects/#{project.id}/triggers") - - expect(response).to have_gitlab_http_status(401) - end - end - end - - describe 'DELETE /projects/:id/triggers/:token' do - context 'authenticated user with valid permissions' do - it 'deletes trigger' do - expect do - delete v3_api("/projects/#{project.id}/triggers/#{trigger.token}", user) - - expect(response).to have_gitlab_http_status(200) - end.to change {project.triggers.count}.by(-1) - end - - it 'responds with 404 Not Found if requesting non-existing trigger' do - delete v3_api("/projects/#{project.id}/triggers/abcdef012345", user) - - expect(response).to have_gitlab_http_status(404) - end - end - - context 'authenticated user with invalid permissions' do - it 'does not delete trigger' do - delete v3_api("/projects/#{project.id}/triggers/#{trigger.token}", user2) - - expect(response).to have_gitlab_http_status(403) - end - end - - context 'unauthenticated user' do - it 'does not delete trigger' do - delete v3_api("/projects/#{project.id}/triggers/#{trigger.token}") - - expect(response).to have_gitlab_http_status(401) - end - end - end -end diff --git a/spec/requests/api/v3/users_spec.rb b/spec/requests/api/v3/users_spec.rb deleted file mode 100644 index bbd05f240d2..00000000000 --- a/spec/requests/api/v3/users_spec.rb +++ /dev/null @@ -1,362 +0,0 @@ -require 'spec_helper' - -describe API::V3::Users do - let(:user) { create(:user) } - let(:admin) { create(:admin) } - let(:key) { create(:key, user: user) } - let(:email) { create(:email, user: user) } - let(:ldap_blocked_user) { create(:omniauth_user, provider: 'ldapmain', state: 'ldap_blocked') } - - describe 'GET /users' do - context 'when authenticated' do - it 'returns an array of users' do - get v3_api('/users', user) - - expect(response).to have_gitlab_http_status(200) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array - username = user.username - expect(json_response.detect do |user| - user['username'] == username - end['username']).to eq(username) - end - end - - context 'when authenticated as user' do - it 'does not reveal the `is_admin` flag of the user' do - get v3_api('/users', user) - - expect(json_response.first.keys).not_to include 'is_admin' - end - end - - context 'when authenticated as admin' do - it 'reveals the `is_admin` flag of the user' do - get v3_api('/users', admin) - - expect(json_response.first.keys).to include 'is_admin' - end - end - end - - describe 'GET /user/:id/keys' do - before { admin } - - context 'when unauthenticated' do - it 'returns authentication error' do - get v3_api("/users/#{user.id}/keys") - expect(response).to have_gitlab_http_status(401) - end - end - - context 'when authenticated' do - it 'returns 404 for non-existing user' do - get v3_api('/users/999999/keys', admin) - expect(response).to have_gitlab_http_status(404) - expect(json_response['message']).to eq('404 User Not Found') - end - - it 'returns array of ssh keys' do - user.keys << key - user.save - - get v3_api("/users/#{user.id}/keys", admin) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.first['title']).to eq(key.title) - end - end - - context "scopes" do - let(:user) { admin } - let(:path) { "/users/#{user.id}/keys" } - let(:api_call) { method(:v3_api) } - - before do - user.keys << key - user.save - end - - include_examples 'allows the "read_user" scope' - end - end - - describe 'GET /user/:id/emails' do - before { admin } - - context 'when unauthenticated' do - it 'returns authentication error' do - get v3_api("/users/#{user.id}/emails") - expect(response).to have_gitlab_http_status(401) - end - end - - context 'when authenticated' do - it 'returns 404 for non-existing user' do - get v3_api('/users/999999/emails', admin) - expect(response).to have_gitlab_http_status(404) - expect(json_response['message']).to eq('404 User Not Found') - end - - it 'returns array of emails' do - user.emails << email - user.save - - get v3_api("/users/#{user.id}/emails", admin) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.first['email']).to eq(email.email) - end - - it "returns a 404 for invalid ID" do - put v3_api("/users/ASDF/emails", admin) - - expect(response).to have_gitlab_http_status(404) - end - end - end - - describe "GET /user/keys" do - context "when unauthenticated" do - it "returns authentication error" do - get v3_api("/user/keys") - expect(response).to have_gitlab_http_status(401) - end - end - - context "when authenticated" do - it "returns array of ssh keys" do - user.keys << key - user.save - - get v3_api("/user/keys", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.first["title"]).to eq(key.title) - end - end - end - - describe "GET /user/emails" do - context "when unauthenticated" do - it "returns authentication error" do - get v3_api("/user/emails") - expect(response).to have_gitlab_http_status(401) - end - end - - context "when authenticated" do - it "returns array of emails" do - user.emails << email - user.save - - get v3_api("/user/emails", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.first["email"]).to eq(email.email) - end - end - end - - describe 'PUT /users/:id/block' do - before { admin } - it 'blocks existing user' do - put v3_api("/users/#{user.id}/block", admin) - expect(response).to have_gitlab_http_status(200) - expect(user.reload.state).to eq('blocked') - end - - it 'does not re-block ldap blocked users' do - put v3_api("/users/#{ldap_blocked_user.id}/block", admin) - expect(response).to have_gitlab_http_status(403) - expect(ldap_blocked_user.reload.state).to eq('ldap_blocked') - end - - it 'does not be available for non admin users' do - put v3_api("/users/#{user.id}/block", user) - expect(response).to have_gitlab_http_status(403) - expect(user.reload.state).to eq('active') - end - - it 'returns a 404 error if user id not found' do - put v3_api('/users/9999/block', admin) - expect(response).to have_gitlab_http_status(404) - expect(json_response['message']).to eq('404 User Not Found') - end - end - - describe 'PUT /users/:id/unblock' do - let(:blocked_user) { create(:user, state: 'blocked') } - before { admin } - - it 'unblocks existing user' do - put v3_api("/users/#{user.id}/unblock", admin) - expect(response).to have_gitlab_http_status(200) - expect(user.reload.state).to eq('active') - end - - it 'unblocks a blocked user' do - put v3_api("/users/#{blocked_user.id}/unblock", admin) - expect(response).to have_gitlab_http_status(200) - expect(blocked_user.reload.state).to eq('active') - end - - it 'does not unblock ldap blocked users' do - put v3_api("/users/#{ldap_blocked_user.id}/unblock", admin) - expect(response).to have_gitlab_http_status(403) - expect(ldap_blocked_user.reload.state).to eq('ldap_blocked') - end - - it 'does not be available for non admin users' do - put v3_api("/users/#{user.id}/unblock", user) - expect(response).to have_gitlab_http_status(403) - expect(user.reload.state).to eq('active') - end - - it 'returns a 404 error if user id not found' do - put v3_api('/users/9999/block', admin) - expect(response).to have_gitlab_http_status(404) - expect(json_response['message']).to eq('404 User Not Found') - end - - it "returns a 404 for invalid ID" do - put v3_api("/users/ASDF/block", admin) - - expect(response).to have_gitlab_http_status(404) - end - end - - describe 'GET /users/:id/events' do - let(:user) { create(:user) } - let(:project) { create(:project) } - let(:note) { create(:note_on_issue, note: 'What an awesome day!', project: project) } - - before do - project.add_user(user, :developer) - EventCreateService.new.leave_note(note, user) - end - - context "as a user than cannot see the event's project" do - it 'returns no events' do - other_user = create(:user) - - get api("/users/#{user.id}/events", other_user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_empty - end - end - - context "as a user than can see the event's project" do - context 'when the list of events includes push events' do - let(:event) { create(:push_event, author: user, project: project) } - let!(:payload) { create(:push_event_payload, event: event) } - let(:payload_hash) { json_response[0]['push_data'] } - - before do - get api("/users/#{user.id}/events?action=pushed", user) - end - - it 'responds with HTTP 200 OK' do - expect(response).to have_gitlab_http_status(200) - end - - it 'includes the push payload as a Hash' do - expect(payload_hash).to be_an_instance_of(Hash) - end - - it 'includes the push payload details' do - expect(payload_hash['commit_count']).to eq(payload.commit_count) - expect(payload_hash['action']).to eq(payload.action) - expect(payload_hash['ref_type']).to eq(payload.ref_type) - expect(payload_hash['commit_to']).to eq(payload.commit_to) - end - end - - context 'joined event' do - it 'returns the "joined" event' do - get v3_api("/users/#{user.id}/events", user) - - expect(response).to have_gitlab_http_status(200) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array - - comment_event = json_response.find { |e| e['action_name'] == 'commented on' } - - expect(comment_event['project_id'].to_i).to eq(project.id) - expect(comment_event['author_username']).to eq(user.username) - expect(comment_event['note']['id']).to eq(note.id) - expect(comment_event['note']['body']).to eq('What an awesome day!') - - joined_event = json_response.find { |e| e['action_name'] == 'joined' } - - expect(joined_event['project_id'].to_i).to eq(project.id) - expect(joined_event['author_username']).to eq(user.username) - expect(joined_event['author']['name']).to eq(user.name) - end - end - - context 'when there are multiple events from different projects' do - let(:second_note) { create(:note_on_issue, project: create(:project)) } - let(:third_note) { create(:note_on_issue, project: project) } - - before do - second_note.project.add_user(user, :developer) - - [second_note, third_note].each do |note| - EventCreateService.new.leave_note(note, user) - end - end - - it 'returns events in the correct order (from newest to oldest)' do - get v3_api("/users/#{user.id}/events", user) - - comment_events = json_response.select { |e| e['action_name'] == 'commented on' } - - expect(comment_events[0]['target_id']).to eq(third_note.id) - expect(comment_events[1]['target_id']).to eq(second_note.id) - expect(comment_events[2]['target_id']).to eq(note.id) - end - end - end - - it 'returns a 404 error if not found' do - get v3_api('/users/420/events', user) - - expect(response).to have_gitlab_http_status(404) - expect(json_response['message']).to eq('404 User Not Found') - end - end - - describe 'POST /users' do - it 'creates confirmed user when confirm parameter is false' do - optional_attributes = { confirm: false } - attributes = attributes_for(:user).merge(optional_attributes) - - post v3_api('/users', admin), attributes - - user_id = json_response['id'] - new_user = User.find(user_id) - - expect(new_user).to be_confirmed - end - - it 'does not reveal the `is_admin` flag of the user' do - post v3_api('/users', admin), attributes_for(:user) - - expect(json_response['is_admin']).to be_nil - end - - context "scopes" do - let(:user) { admin } - let(:path) { '/users' } - let(:api_call) { method(:v3_api) } - - include_examples 'does not allow the "read_user" scope' - end - end -end diff --git a/spec/support/api/v3/time_tracking_shared_examples.rb b/spec/support/api/v3/time_tracking_shared_examples.rb deleted file mode 100644 index f27a2d06c83..00000000000 --- a/spec/support/api/v3/time_tracking_shared_examples.rb +++ /dev/null @@ -1,128 +0,0 @@ -shared_examples 'V3 time tracking endpoints' do |issuable_name| - issuable_collection_name = issuable_name.pluralize - - describe "POST /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/time_estimate" do - context 'with an unauthorized user' do - subject { post(v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_estimate", non_member), duration: '1w') } - - it_behaves_like 'an unauthorized API user' - end - - it "sets the time estimate for #{issuable_name}" do - post v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_estimate", user), duration: '1w' - - expect(response).to have_gitlab_http_status(200) - expect(json_response['human_time_estimate']).to eq('1w') - end - - describe 'updating the current estimate' do - before do - post v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_estimate", user), duration: '1w' - end - - context 'when duration has a bad format' do - it 'does not modify the original estimate' do - post v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_estimate", user), duration: 'foo' - - expect(response).to have_gitlab_http_status(400) - expect(issuable.reload.human_time_estimate).to eq('1w') - end - end - - context 'with a valid duration' do - it 'updates the estimate' do - post v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_estimate", user), duration: '3w1h' - - expect(response).to have_gitlab_http_status(200) - expect(issuable.reload.human_time_estimate).to eq('3w 1h') - end - end - end - end - - describe "POST /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/reset_time_estimate" do - context 'with an unauthorized user' do - subject { post(v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/reset_time_estimate", non_member)) } - - it_behaves_like 'an unauthorized API user' - end - - it "resets the time estimate for #{issuable_name}" do - post v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/reset_time_estimate", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['time_estimate']).to eq(0) - end - end - - describe "POST /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/add_spent_time" do - context 'with an unauthorized user' do - subject do - post v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/add_spent_time", non_member), - duration: '2h' - end - - it_behaves_like 'an unauthorized API user' - end - - it "add spent time for #{issuable_name}" do - post v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/add_spent_time", user), - duration: '2h' - - expect(response).to have_gitlab_http_status(201) - expect(json_response['human_total_time_spent']).to eq('2h') - end - - context 'when subtracting time' do - it 'subtracts time of the total spent time' do - issuable.update_attributes!(spend_time: { duration: 7200, user_id: user.id }) - - post v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/add_spent_time", user), - duration: '-1h' - - expect(response).to have_gitlab_http_status(201) - expect(json_response['total_time_spent']).to eq(3600) - end - end - - context 'when time to subtract is greater than the total spent time' do - it 'does not modify the total time spent' do - issuable.update_attributes!(spend_time: { duration: 7200, user_id: user.id }) - - post v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/add_spent_time", user), - duration: '-1w' - - expect(response).to have_gitlab_http_status(400) - expect(json_response['message']['time_spent'].first).to match(/exceeds the total time spent/) - end - end - end - - describe "POST /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/reset_spent_time" do - context 'with an unauthorized user' do - subject { post(v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/reset_spent_time", non_member)) } - - it_behaves_like 'an unauthorized API user' - end - - it "resets spent time for #{issuable_name}" do - post v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/reset_spent_time", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['total_time_spent']).to eq(0) - end - end - - describe "GET /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/time_stats" do - it "returns the time stats for #{issuable_name}" do - issuable.update_attributes!(spend_time: { duration: 1800, user_id: user.id }, - time_estimate: 3600) - - get v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_stats", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['total_time_spent']).to eq(1800) - expect(json_response['time_estimate']).to eq(3600) - end - end -end diff --git a/spec/support/gitlab_stubs/project_8.json b/spec/support/gitlab_stubs/project_8.json index f0a9fce859c..81b08ab8288 100644 --- a/spec/support/gitlab_stubs/project_8.json +++ b/spec/support/gitlab_stubs/project_8.json @@ -1,38 +1,38 @@ { - "id":8, - "description":"ssh access and repository management app for GitLab", - "default_branch":"master", - "public":false, - "visibility_level":0, - "ssh_url_to_repo":"git@demo.gitlab.com:gitlab/gitlab-shell.git", - "http_url_to_repo":"http://demo.gitlab.com/gitlab/gitlab-shell.git", - "web_url":"http://demo.gitlab.com/gitlab/gitlab-shell", - "owner": { - "id":4, - "name":"GitLab", - "created_at":"2012-12-21T13:03:05Z" - }, - "name":"gitlab-shell", - "name_with_namespace":"GitLab / gitlab-shell", - "path":"gitlab-shell", - "path_with_namespace":"gitlab/gitlab-shell", - "issues_enabled":true, - "merge_requests_enabled":true, - "wall_enabled":false, - "wiki_enabled":true, - "snippets_enabled":false, - "created_at":"2013-03-20T13:28:53Z", - "last_activity_at":"2013-11-30T00:11:17Z", - "namespace":{ - "created_at":"2012-12-21T13:03:05Z", - "description":"Self hosted Git management software", - "id":4, - "name":"GitLab", - "owner_id":1, - "path":"gitlab", - "updated_at":"2013-03-20T13:29:13Z" + "id": 8, + "description": "ssh access and repository management app for GitLab", + "default_branch": "master", + "visibility": "private", + "ssh_url_to_repo": "git@demo.gitlab.com:gitlab/gitlab-shell.git", + "http_url_to_repo": "http://demo.gitlab.com/gitlab/gitlab-shell.git", + "web_url": "http://demo.gitlab.com/gitlab/gitlab-shell", + "owner": { + "id": 4, + "name": "GitLab", + "username": "gitlab", + "state": "active", + "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61", + "web_url": "http://demo.gitlab.com/gitlab" }, - "permissions":{ + "name": "gitlab-shell", + "name_with_namespace": "GitLab / gitlab-shell", + "path": "gitlab-shell", + "path_with_namespace": "gitlab/gitlab-shell", + "issues_enabled": true, + "merge_requests_enabled": true, + "wiki_enabled": true, + "snippets_enabled": false, + "created_at": "2013-03-20T13:28:53Z", + "last_activity_at": "2013-11-30T00:11:17Z", + "namespace": { + "id": 4, + "name": "GitLab", + "path": "gitlab", + "kind": "group", + "full_path": "gitlab", + "parent_id": null + }, + "permissions": { "project_access": { "access_level": 10, "notification_level": 3 @@ -42,4 +42,4 @@ "notification_level": 3 } } -} \ No newline at end of file +} diff --git a/spec/support/gitlab_stubs/projects.json b/spec/support/gitlab_stubs/projects.json index ca42c14c5d8..67ce1acca2c 100644 --- a/spec/support/gitlab_stubs/projects.json +++ b/spec/support/gitlab_stubs/projects.json @@ -1 +1,282 @@ -[{"id":3,"description":"GitLab is open source software to collaborate on code. Create projects and repositories, manage access and do code reviews.","default_branch":"master","public":true,"visibility_level":20,"ssh_url_to_repo":"git@demo.gitlab.com:gitlab/gitlabhq.git","http_url_to_repo":"http://demo.gitlab.com/gitlab/gitlabhq.git","web_url":"http://demo.gitlab.com/gitlab/gitlabhq","owner":{"id":4,"name":"GitLab","created_at":"2012-12-21T13:03:05Z"},"name":"gitlabhq","name_with_namespace":"GitLab / gitlabhq","path":"gitlabhq","path_with_namespace":"gitlab/gitlabhq","issues_enabled":true,"merge_requests_enabled":true,"wall_enabled":true,"wiki_enabled":true,"snippets_enabled":true,"created_at":"2012-12-21T13:06:34Z","last_activity_at":"2013-12-02T19:10:10Z","namespace":{"created_at":"2012-12-21T13:03:05Z","description":"Self hosted Git management software","id":4,"name":"GitLab","owner_id":1,"path":"gitlab","updated_at":"2013-03-20T13:29:13Z"}},{"id":4,"description":"Component of GitLab CI. Web application","default_branch":"master","public":false,"visibility_level":0,"ssh_url_to_repo":"git@demo.gitlab.com:gitlab/gitlab-ci.git","http_url_to_repo":"http://demo.gitlab.com/gitlab/gitlab-ci.git","web_url":"http://demo.gitlab.com/gitlab/gitlab-ci","owner":{"id":4,"name":"GitLab","created_at":"2012-12-21T13:03:05Z"},"name":"gitlab-ci","name_with_namespace":"GitLab / gitlab-ci","path":"gitlab-ci","path_with_namespace":"gitlab/gitlab-ci","issues_enabled":true,"merge_requests_enabled":true,"wall_enabled":true,"wiki_enabled":true,"snippets_enabled":true,"created_at":"2012-12-21T13:06:50Z","last_activity_at":"2013-11-28T19:26:54Z","namespace":{"created_at":"2012-12-21T13:03:05Z","description":"Self hosted Git management software","id":4,"name":"GitLab","owner_id":1,"path":"gitlab","updated_at":"2013-03-20T13:29:13Z"}},{"id":5,"description":"","default_branch":"master","public":true,"visibility_level":20,"ssh_url_to_repo":"git@demo.gitlab.com:gitlab/gitlab-recipes.git","http_url_to_repo":"http://demo.gitlab.com/gitlab/gitlab-recipes.git","web_url":"http://demo.gitlab.com/gitlab/gitlab-recipes","owner":{"id":4,"name":"GitLab","created_at":"2012-12-21T13:03:05Z"},"name":"gitlab-recipes","name_with_namespace":"GitLab / gitlab-recipes","path":"gitlab-recipes","path_with_namespace":"gitlab/gitlab-recipes","issues_enabled":true,"merge_requests_enabled":true,"wall_enabled":true,"wiki_enabled":true,"snippets_enabled":true,"created_at":"2012-12-21T13:07:02Z","last_activity_at":"2013-12-02T13:54:10Z","namespace":{"created_at":"2012-12-21T13:03:05Z","description":"Self hosted Git management software","id":4,"name":"GitLab","owner_id":1,"path":"gitlab","updated_at":"2013-03-20T13:29:13Z"}},{"id":8,"description":"ssh access and repository management app for GitLab","default_branch":"master","public":false,"visibility_level":0,"ssh_url_to_repo":"git@demo.gitlab.com:gitlab/gitlab-shell.git","http_url_to_repo":"http://demo.gitlab.com/gitlab/gitlab-shell.git","web_url":"http://demo.gitlab.com/gitlab/gitlab-shell","owner":{"id":4,"name":"GitLab","created_at":"2012-12-21T13:03:05Z"},"name":"gitlab-shell","name_with_namespace":"GitLab / gitlab-shell","path":"gitlab-shell","path_with_namespace":"gitlab/gitlab-shell","issues_enabled":true,"merge_requests_enabled":true,"wall_enabled":false,"wiki_enabled":true,"snippets_enabled":false,"created_at":"2013-03-20T13:28:53Z","last_activity_at":"2013-11-30T00:11:17Z","namespace":{"created_at":"2012-12-21T13:03:05Z","description":"Self hosted Git management software","id":4,"name":"GitLab","owner_id":1,"path":"gitlab","updated_at":"2013-03-20T13:29:13Z"}},{"id":9,"description":null,"default_branch":"master","public":false,"visibility_level":0,"ssh_url_to_repo":"git@demo.gitlab.com:gitlab/gitlab_git.git","http_url_to_repo":"http://demo.gitlab.com/gitlab/gitlab_git.git","web_url":"http://demo.gitlab.com/gitlab/gitlab_git","owner":{"id":4,"name":"GitLab","created_at":"2012-12-21T13:03:05Z"},"name":"gitlab_git","name_with_namespace":"GitLab / gitlab_git","path":"gitlab_git","path_with_namespace":"gitlab/gitlab_git","issues_enabled":true,"merge_requests_enabled":true,"wall_enabled":false,"wiki_enabled":true,"snippets_enabled":false,"created_at":"2013-04-28T19:15:08Z","last_activity_at":"2013-12-02T13:07:13Z","namespace":{"created_at":"2012-12-21T13:03:05Z","description":"Self hosted Git management software","id":4,"name":"GitLab","owner_id":1,"path":"gitlab","updated_at":"2013-03-20T13:29:13Z"}},{"id":10,"description":"ultra lite authorization library http://randx.github.com/six/\\r\\n ","default_branch":"master","public":true,"visibility_level":20,"ssh_url_to_repo":"git@demo.gitlab.com:sandbox/six.git","http_url_to_repo":"http://demo.gitlab.com/sandbox/six.git","web_url":"http://demo.gitlab.com/sandbox/six","owner":{"id":8,"name":"Sandbox","created_at":"2013-08-01T16:44:17Z"},"name":"Six","name_with_namespace":"Sandbox / Six","path":"six","path_with_namespace":"sandbox/six","issues_enabled":true,"merge_requests_enabled":true,"wall_enabled":false,"wiki_enabled":true,"snippets_enabled":false,"created_at":"2013-08-01T16:45:02Z","last_activity_at":"2013-11-29T11:30:56Z","namespace":{"created_at":"2013-08-01T16:44:17Z","description":"","id":8,"name":"Sandbox","owner_id":1,"path":"sandbox","updated_at":"2013-08-01T16:44:17Z"}},{"id":11,"description":"Simple HTML5 Charts using the tag ","default_branch":"master","public":false,"visibility_level":0,"ssh_url_to_repo":"git@demo.gitlab.com:sandbox/charts-js.git","http_url_to_repo":"http://demo.gitlab.com/sandbox/charts-js.git","web_url":"http://demo.gitlab.com/sandbox/charts-js","owner":{"id":8,"name":"Sandbox","created_at":"2013-08-01T16:44:17Z"},"name":"Charts.js","name_with_namespace":"Sandbox / Charts.js","path":"charts-js","path_with_namespace":"sandbox/charts-js","issues_enabled":true,"merge_requests_enabled":true,"wall_enabled":false,"wiki_enabled":true,"snippets_enabled":false,"created_at":"2013-08-01T16:47:29Z","last_activity_at":"2013-12-02T15:18:11Z","namespace":{"created_at":"2013-08-01T16:44:17Z","description":"","id":8,"name":"Sandbox","owner_id":1,"path":"sandbox","updated_at":"2013-08-01T16:44:17Z"}},{"id":13,"description":"","default_branch":"master","public":false,"visibility_level":0,"ssh_url_to_repo":"git@demo.gitlab.com:sandbox/afro.git","http_url_to_repo":"http://demo.gitlab.com/sandbox/afro.git","web_url":"http://demo.gitlab.com/sandbox/afro","owner":{"id":8,"name":"Sandbox","created_at":"2013-08-01T16:44:17Z"},"name":"Afro","name_with_namespace":"Sandbox / Afro","path":"afro","path_with_namespace":"sandbox/afro","issues_enabled":true,"merge_requests_enabled":true,"wall_enabled":false,"wiki_enabled":true,"snippets_enabled":false,"created_at":"2013-11-14T17:45:19Z","last_activity_at":"2013-12-02T17:41:45Z","namespace":{"created_at":"2013-08-01T16:44:17Z","description":"","id":8,"name":"Sandbox","owner_id":1,"path":"sandbox","updated_at":"2013-08-01T16:44:17Z"}}] \ No newline at end of file +[ + { + "id": 3, + "description": "GitLab is open source software to collaborate on code. Create projects and repositories, manage access and do code reviews.", + "default_branch": "master", + "visibility": "public", + "ssh_url_to_repo": "git@demo.gitlab.com:gitlab/gitlabhq.git", + "http_url_to_repo": "http://demo.gitlab.com/gitlab/gitlabhq.git", + "web_url": "http://demo.gitlab.com/gitlab/gitlabhq", + "owner": { + "id": 4, + "name": "GitLab", + "username": "gitlab", + "state": "active", + "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61", + "web_url": "http://demo.gitlab.com/gitlab" + }, + "name": "gitlabhq", + "name_with_namespace": "GitLab / gitlabhq", + "path": "gitlabhq", + "path_with_namespace": "gitlab/gitlabhq", + "issues_enabled": true, + "merge_requests_enabled": true, + "wiki_enabled": true, + "snippets_enabled": true, + "created_at": "2012-12-21T13:06:34Z", + "last_activity_at": "2013-12-02T19:10:10Z", + "namespace": { + "id": 4, + "name": "GitLab", + "path": "gitlab", + "kind": "group", + "full_path": "gitlab", + "parent_id": null + } + }, + { + "id": 4, + "description": "Component of GitLab CI. Web application", + "default_branch": "master", + "visibility": "private", + "ssh_url_to_repo": "git@demo.gitlab.com:gitlab/gitlab-ci.git", + "http_url_to_repo": "http://demo.gitlab.com/gitlab/gitlab-ci.git", + "web_url": "http://demo.gitlab.com/gitlab/gitlab-ci", + "owner": { + "id": 4, + "name": "GitLab", + "username": "gitlab", + "state": "active", + "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61", + "web_url": "http://demo.gitlab.com/gitlab" + }, + "name": "gitlab-ci", + "name_with_namespace": "GitLab / gitlab-ci", + "path": "gitlab-ci", + "path_with_namespace": "gitlab/gitlab-ci", + "issues_enabled": true, + "merge_requests_enabled": true, + "wiki_enabled": true, + "snippets_enabled": true, + "created_at": "2012-12-21T13:06:50Z", + "last_activity_at": "2013-11-28T19:26:54Z", + "namespace": { + "id": 4, + "name": "GitLab", + "path": "gitlab", + "kind": "group", + "full_path": "gitlab", + "parent_id": null + } + }, + { + "id": 5, + "description": "", + "default_branch": "master", + "visibility": "public", + "ssh_url_to_repo": "git@demo.gitlab.com:gitlab/gitlab-recipes.git", + "http_url_to_repo": "http://demo.gitlab.com/gitlab/gitlab-recipes.git", + "web_url": "http://demo.gitlab.com/gitlab/gitlab-recipes", + "owner": { + "id": 4, + "name": "GitLab", + "username": "gitlab", + "state": "active", + "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61", + "web_url": "http://demo.gitlab.com/gitlab" + }, + "name": "gitlab-recipes", + "name_with_namespace": "GitLab / gitlab-recipes", + "path": "gitlab-recipes", + "path_with_namespace": "gitlab/gitlab-recipes", + "issues_enabled": true, + "merge_requests_enabled": true, + "wiki_enabled": true, + "snippets_enabled": true, + "created_at": "2012-12-21T13:07:02Z", + "last_activity_at": "2013-12-02T13:54:10Z", + "namespace": { + "id": 4, + "name": "GitLab", + "path": "gitlab", + "kind": "group", + "full_path": "gitlab", + "parent_id": null + } + }, + { + "id": 8, + "description": "ssh access and repository management app for GitLab", + "default_branch": "master", + "visibility": "private", + "ssh_url_to_repo": "git@demo.gitlab.com:gitlab/gitlab-shell.git", + "http_url_to_repo": "http://demo.gitlab.com/gitlab/gitlab-shell.git", + "web_url": "http://demo.gitlab.com/gitlab/gitlab-shell", + "owner": { + "id": 4, + "name": "GitLab", + "username": "gitlab", + "state": "active", + "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61", + "web_url": "http://demo.gitlab.com/gitlab" + }, + "name": "gitlab-shell", + "name_with_namespace": "GitLab / gitlab-shell", + "path": "gitlab-shell", + "path_with_namespace": "gitlab/gitlab-shell", + "issues_enabled": true, + "merge_requests_enabled": true, + "wiki_enabled": true, + "snippets_enabled": false, + "created_at": "2013-03-20T13:28:53Z", + "last_activity_at": "2013-11-30T00:11:17Z", + "namespace": { + "id": 4, + "name": "GitLab", + "path": "gitlab", + "kind": "group", + "full_path": "gitlab", + "parent_id": null + } + }, + { + "id": 9, + "description": null, + "default_branch": "master", + "visibility": "private", + "ssh_url_to_repo": "git@demo.gitlab.com:gitlab/gitlab_git.git", + "http_url_to_repo": "http://demo.gitlab.com/gitlab/gitlab_git.git", + "web_url": "http://demo.gitlab.com/gitlab/gitlab_git", + "owner": { + "id": 4, + "name": "GitLab", + "username": "gitlab", + "state": "active", + "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61", + "web_url": "http://demo.gitlab.com/gitlab" + }, + "name": "gitlab_git", + "name_with_namespace": "GitLab / gitlab_git", + "path": "gitlab_git", + "path_with_namespace": "gitlab/gitlab_git", + "issues_enabled": true, + "merge_requests_enabled": true, + "wiki_enabled": true, + "snippets_enabled": false, + "created_at": "2013-04-28T19:15:08Z", + "last_activity_at": "2013-12-02T13:07:13Z", + "namespace": { + "id": 4, + "name": "GitLab", + "path": "gitlab", + "kind": "group", + "full_path": "gitlab", + "parent_id": null + } + }, + { + "id": 10, + "description": "ultra lite authorization library http://randx.github.com/six/\\r\\n ", + "default_branch": "master", + "visibility": "public", + "ssh_url_to_repo": "git@demo.gitlab.com:sandbox/six.git", + "http_url_to_repo": "http://demo.gitlab.com/sandbox/six.git", + "web_url": "http://demo.gitlab.com/sandbox/six", + "owner": { + "id": 4, + "name": "GitLab", + "username": "gitlab", + "state": "active", + "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61", + "web_url": "http://demo.gitlab.com/gitlab" + }, + "name": "Six", + "name_with_namespace": "Sandbox / Six", + "path": "six", + "path_with_namespace": "sandbox/six", + "issues_enabled": true, + "merge_requests_enabled": true, + "wiki_enabled": true, + "snippets_enabled": false, + "created_at": "2013-08-01T16:45:02Z", + "last_activity_at": "2013-11-29T11:30:56Z", + "namespace": { + "id": 4, + "name": "GitLab", + "path": "gitlab", + "kind": "group", + "full_path": "gitlab", + "parent_id": null + } + }, + { + "id": 11, + "description": "Simple HTML5 Charts using the tag ", + "default_branch": "master", + "visibility": "private", + "ssh_url_to_repo": "git@demo.gitlab.com:sandbox/charts-js.git", + "http_url_to_repo": "http://demo.gitlab.com/sandbox/charts-js.git", + "web_url": "http://demo.gitlab.com/sandbox/charts-js", + "owner": { + "id": 4, + "name": "GitLab", + "username": "gitlab", + "state": "active", + "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61", + "web_url": "http://demo.gitlab.com/gitlab" + }, + "name": "Charts.js", + "name_with_namespace": "Sandbox / Charts.js", + "path": "charts-js", + "path_with_namespace": "sandbox/charts-js", + "issues_enabled": true, + "merge_requests_enabled": true, + "wiki_enabled": true, + "snippets_enabled": false, + "created_at": "2013-08-01T16:47:29Z", + "last_activity_at": "2013-12-02T15:18:11Z", + "namespace": { + "id": 4, + "name": "GitLab", + "path": "gitlab", + "kind": "group", + "full_path": "gitlab", + "parent_id": null + } + }, + { + "id": 13, + "description": "", + "default_branch": "master", + "visibility": "private", + "ssh_url_to_repo": "git@demo.gitlab.com:sandbox/afro.git", + "http_url_to_repo": "http://demo.gitlab.com/sandbox/afro.git", + "web_url": "http://demo.gitlab.com/sandbox/afro", + "owner": { + "id": 4, + "name": "GitLab", + "username": "gitlab", + "state": "active", + "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61", + "web_url": "http://demo.gitlab.com/gitlab" + }, + "name": "Afro", + "name_with_namespace": "Sandbox / Afro", + "path": "afro", + "path_with_namespace": "sandbox/afro", + "issues_enabled": true, + "merge_requests_enabled": true, + "wiki_enabled": true, + "snippets_enabled": false, + "created_at": "2013-11-14T17:45:19Z", + "last_activity_at": "2013-12-02T17:41:45Z", + "namespace": { + "id": 4, + "name": "GitLab", + "path": "gitlab", + "kind": "group", + "full_path": "gitlab", + "parent_id": null + } + } +] diff --git a/spec/support/gitlab_stubs/session.json b/spec/support/gitlab_stubs/session.json deleted file mode 100644 index 658ff5871b0..00000000000 --- a/spec/support/gitlab_stubs/session.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "id":2, - "username":"jsmith", - "email":"test@test.com", - "name":"John Smith", - "bio":"", - "skype":"aertert", - "linkedin":"", - "twitter":"", - "theme_id":2,"color_scheme_id":2, - "state":"active", - "created_at":"2012-12-21T13:02:20Z", - "extern_uid":null, - "provider":null, - "is_admin":false, - "can_create_group":false, - "can_create_project":false -} diff --git a/spec/support/helpers/api_helpers.rb b/spec/support/helpers/api_helpers.rb index ac0c7a9b493..a57a3b2cf34 100644 --- a/spec/support/helpers/api_helpers.rb +++ b/spec/support/helpers/api_helpers.rb @@ -36,15 +36,4 @@ module ApiHelpers full_path end - - # Temporary helper method for simplifying V3 exclusive API specs - def v3_api(path, user = nil, personal_access_token: nil, oauth_access_token: nil) - api( - path, - user, - version: 'v3', - personal_access_token: personal_access_token, - oauth_access_token: oauth_access_token - ) - end end diff --git a/spec/support/helpers/stub_gitlab_calls.rb b/spec/support/helpers/stub_gitlab_calls.rb index c1618f5086c..2933f2c78dc 100644 --- a/spec/support/helpers/stub_gitlab_calls.rb +++ b/spec/support/helpers/stub_gitlab_calls.rb @@ -1,6 +1,5 @@ module StubGitlabCalls def stub_gitlab_calls - stub_session stub_user stub_project_8 stub_project_8_hooks @@ -71,23 +70,14 @@ module StubGitlabCalls Gitlab.config.gitlab.url end - def stub_session - f = File.read(Rails.root.join('spec/support/gitlab_stubs/session.json')) - - stub_request(:post, "#{gitlab_url}api/v3/session.json") - .with(body: "{\"email\":\"test@test.com\",\"password\":\"123456\"}", - headers: { 'Content-Type' => 'application/json' }) - .to_return(status: 201, body: f, headers: { 'Content-Type' => 'application/json' }) - end - def stub_user f = File.read(Rails.root.join('spec/support/gitlab_stubs/user.json')) - stub_request(:get, "#{gitlab_url}api/v3/user?private_token=Wvjy2Krpb7y8xi93owUz") + stub_request(:get, "#{gitlab_url}api/v4/user?private_token=Wvjy2Krpb7y8xi93owUz") .with(headers: { 'Content-Type' => 'application/json' }) .to_return(status: 200, body: f, headers: { 'Content-Type' => 'application/json' }) - stub_request(:get, "#{gitlab_url}api/v3/user?access_token=some_token") + stub_request(:get, "#{gitlab_url}api/v4/user?access_token=some_token") .with(headers: { 'Content-Type' => 'application/json' }) .to_return(status: 200, body: f, headers: { 'Content-Type' => 'application/json' }) end @@ -105,19 +95,19 @@ module StubGitlabCalls def stub_projects f = File.read(Rails.root.join('spec/support/gitlab_stubs/projects.json')) - stub_request(:get, "#{gitlab_url}api/v3/projects.json?archived=false&ci_enabled_first=true&private_token=Wvjy2Krpb7y8xi93owUz") + stub_request(:get, "#{gitlab_url}api/v4/projects.json?archived=false&ci_enabled_first=true&private_token=Wvjy2Krpb7y8xi93owUz") .with(headers: { 'Content-Type' => 'application/json' }) .to_return(status: 200, body: f, headers: { 'Content-Type' => 'application/json' }) end def stub_projects_owned - stub_request(:get, "#{gitlab_url}api/v3/projects/owned.json?archived=false&ci_enabled_first=true&private_token=Wvjy2Krpb7y8xi93owUz") + stub_request(:get, "#{gitlab_url}api/v4/projects?owned=true&archived=false&ci_enabled_first=true&private_token=Wvjy2Krpb7y8xi93owUz") .with(headers: { 'Content-Type' => 'application/json' }) .to_return(status: 200, body: "", headers: {}) end def stub_ci_enable - stub_request(:put, "#{gitlab_url}api/v3/projects/2/services/gitlab-ci.json?private_token=Wvjy2Krpb7y8xi93owUz") + stub_request(:put, "#{gitlab_url}api/v4/projects/2/services/gitlab-ci.json?private_token=Wvjy2Krpb7y8xi93owUz") .with(headers: { 'Content-Type' => 'application/json' }) .to_return(status: 200, body: "", headers: {}) end From 6961ef1a9a939b0defa27919710e50e654939c3b Mon Sep 17 00:00:00 2001 From: Jan Provaznik Date: Mon, 28 May 2018 20:59:32 +0200 Subject: [PATCH 20/46] Fix cattr_accessor definition Use cattr_accessor directly inside the defined class, otherwise in Rails 5 this accessor returns nil. --- spec/workers/concerns/waitable_worker_spec.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/spec/workers/concerns/waitable_worker_spec.rb b/spec/workers/concerns/waitable_worker_spec.rb index 54ab07981a4..199825b5097 100644 --- a/spec/workers/concerns/waitable_worker_spec.rb +++ b/spec/workers/concerns/waitable_worker_spec.rb @@ -7,9 +7,7 @@ describe WaitableWorker do 'Gitlab::Foo::Bar::DummyWorker' end - class << self - cattr_accessor(:counter) { 0 } - end + cattr_accessor(:counter) { 0 } include ApplicationWorker prepend WaitableWorker From da9cc1b91fa34dc91e797fba38f23cfc53e50a09 Mon Sep 17 00:00:00 2001 From: Brett Walker Date: Wed, 30 May 2018 11:52:36 -0500 Subject: [PATCH 21/46] better handle a missing gitlab-shell version --- config/initializers/console_message.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/initializers/console_message.rb b/config/initializers/console_message.rb index 2c46a25f365..f7c26732e6d 100644 --- a/config/initializers/console_message.rb +++ b/config/initializers/console_message.rb @@ -3,8 +3,8 @@ if defined?(Rails::Console) # note that this will not print out when using `spring` justify = 15 puts "-------------------------------------------------------------------------------------" - puts " Gitlab:".ljust(justify) + "#{Gitlab::VERSION} (#{Gitlab.revision})" - puts " Gitlab Shell:".ljust(justify) + Gitlab::Shell.new.version + puts " GitLab:".ljust(justify) + "#{Gitlab::VERSION} (#{Gitlab.revision})" + puts " GitLab Shell:".ljust(justify) + "#{Gitlab::VersionInfo.parse(Gitlab::Shell.new.version)}" puts " #{Gitlab::Database.adapter_name}:".ljust(justify) + Gitlab::Database.version puts "-------------------------------------------------------------------------------------" end From 23e4ad528743290c31b8af22e97917c0539a6008 Mon Sep 17 00:00:00 2001 From: Oswaldo Ferreira Date: Wed, 30 May 2018 13:18:04 -0300 Subject: [PATCH 22/46] Adjust permitted params filtering on merge scheduling --- app/controllers/projects/merge_requests_controller.rb | 4 ++-- ...sw-fix-permitted-params-filtering-on-merge-scheduling.yml | 5 +++++ spec/controllers/projects/merge_requests_controller_spec.rb | 4 ++-- 3 files changed, 9 insertions(+), 4 deletions(-) create mode 100644 changelogs/unreleased/46903-osw-fix-permitted-params-filtering-on-merge-scheduling.yml diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 507a07c6e1b..ecea6e1b2bf 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -296,14 +296,14 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo elsif @merge_request.actual_head_pipeline.success? # This can be triggered when a user clicks the auto merge button while # the tests finish at about the same time - @merge_request.merge_async(current_user.id, params) + @merge_request.merge_async(current_user.id, merge_params) :success else :failed end else - @merge_request.merge_async(current_user.id, params) + @merge_request.merge_async(current_user.id, merge_params) :success end diff --git a/changelogs/unreleased/46903-osw-fix-permitted-params-filtering-on-merge-scheduling.yml b/changelogs/unreleased/46903-osw-fix-permitted-params-filtering-on-merge-scheduling.yml new file mode 100644 index 00000000000..b3c8c8e4045 --- /dev/null +++ b/changelogs/unreleased/46903-osw-fix-permitted-params-filtering-on-merge-scheduling.yml @@ -0,0 +1,5 @@ +--- +title: Adjust permitted params filtering on merge scheduling +merge_request: +author: +type: fixed diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index 5efaaf2bb3a..6e8de6db9c3 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -326,8 +326,8 @@ describe Projects::MergeRequestsController do expect(json_response).to eq('status' => 'success') end - it 'starts the merge immediately' do - expect(MergeWorker).to receive(:perform_async).with(merge_request.id, anything, anything) + it 'starts the merge immediately with permitted params' do + expect(MergeWorker).to receive(:perform_async).with(merge_request.id, anything, { 'squash' => false }) merge_with_sha end From 306cd78202fae58b3352d2a716f27a74c1f44aa4 Mon Sep 17 00:00:00 2001 From: Lukas Eipert Date: Thu, 31 May 2018 08:44:32 +0000 Subject: [PATCH 23/46] Cleanup code for General Project Settings --- ...ge_request_fast_forward_settings.html.haml | 13 ------- ...ge_request_merge_method_settings.html.haml | 36 +++++++++++++++++++ .../_merge_request_rebase_settings.html.haml | 13 ------- .../_merge_request_settings.html.haml | 15 +------- app/views/projects/edit.html.haml | 13 ++++++- qa/qa/page/project/settings/merge_request.rb | 2 +- 6 files changed, 50 insertions(+), 42 deletions(-) delete mode 100644 app/views/projects/_merge_request_fast_forward_settings.html.haml create mode 100644 app/views/projects/_merge_request_merge_method_settings.html.haml delete mode 100644 app/views/projects/_merge_request_rebase_settings.html.haml diff --git a/app/views/projects/_merge_request_fast_forward_settings.html.haml b/app/views/projects/_merge_request_fast_forward_settings.html.haml deleted file mode 100644 index 2f08a28e26e..00000000000 --- a/app/views/projects/_merge_request_fast_forward_settings.html.haml +++ /dev/null @@ -1,13 +0,0 @@ -- form = local_assigns.fetch(:form) -- project = local_assigns.fetch(:project) - -.form-check - = label_tag :project_merge_method_ff do - = form.radio_button :merge_method, :ff, class: "js-merge-method-radio qa-radio-button-merge-ff" - %strong Fast-forward merge - %br - %span.descr - No merge commits are created and all merges are fast-forwarded, which means that merging is only allowed if the branch could be fast-forwarded. - %br - %span.descr - When fast-forward merge is not possible, the user is given the option to rebase. diff --git a/app/views/projects/_merge_request_merge_method_settings.html.haml b/app/views/projects/_merge_request_merge_method_settings.html.haml new file mode 100644 index 00000000000..c75edbb9739 --- /dev/null +++ b/app/views/projects/_merge_request_merge_method_settings.html.haml @@ -0,0 +1,36 @@ +- form = local_assigns.fetch(:form) +- project = local_assigns.fetch(:project) + +.form-group + = label_tag :merge_method_merge, class: 'label-light' do + Merge method + .form-check + = label_tag :project_merge_method_merge do + = form.radio_button :merge_method, :merge, class: "js-merge-method-radio" + %strong Merge commit + %br + %span.descr + A merge commit is created for every merge, and merging is allowed as long as there are no conflicts. + + .form-check + = label_tag :project_merge_method_rebase_merge do + = form.radio_button :merge_method, :rebase_merge, class: "js-merge-method-radio" + %strong Merge commit with semi-linear history + %br + %span.descr + A merge commit is created for every merge, but merging is only allowed if fast-forward merge is possible. + This way you could make sure that if this merge request would build, after merging to target branch it would also build. + %br + %span.descr + When fast-forward merge is not possible, the user is given the option to rebase. + + .form-check + = label_tag :project_merge_method_ff do + = form.radio_button :merge_method, :ff, class: "js-merge-method-radio qa-radio-button-merge-ff" + %strong Fast-forward merge + %br + %span.descr + No merge commits are created and all merges are fast-forwarded, which means that merging is only allowed if the branch could be fast-forwarded. + %br + %span.descr + When fast-forward merge is not possible, the user is given the option to rebase. diff --git a/app/views/projects/_merge_request_rebase_settings.html.haml b/app/views/projects/_merge_request_rebase_settings.html.haml deleted file mode 100644 index 93895a55435..00000000000 --- a/app/views/projects/_merge_request_rebase_settings.html.haml +++ /dev/null @@ -1,13 +0,0 @@ -- form = local_assigns.fetch(:form) - -.form-check - = label_tag :project_merge_method_rebase_merge do - = form.radio_button :merge_method, :rebase_merge, class: "js-merge-method-radio" - %strong Merge commit with semi-linear history - %br - %span.descr - A merge commit is created for every merge, but merging is only allowed if fast-forward merge is possible. - This way you could make sure that if this merge request would build, after merging to target branch it would also build. - %br - %span.descr - When fast-forward merge is not possible, the user is given the option to rebase. diff --git a/app/views/projects/_merge_request_settings.html.haml b/app/views/projects/_merge_request_settings.html.haml index a9ddcd94865..c80e831dd33 100644 --- a/app/views/projects/_merge_request_settings.html.haml +++ b/app/views/projects/_merge_request_settings.html.haml @@ -1,18 +1,5 @@ - form = local_assigns.fetch(:form) -.form-group - = label_tag :merge_method_merge, class: 'label-light' do - Merge method - .form-check - = label_tag :project_merge_method_merge do - = form.radio_button :merge_method, :merge, class: "js-merge-method-radio" - %strong Merge commit - %br - %span.descr - A merge commit is created for every merge, and merging is allowed as long as there are no conflicts. - - = render 'merge_request_rebase_settings', form: form - - = render 'merge_request_fast_forward_settings', project: @project, form: form += render 'projects/merge_request_merge_method_settings', project: @project, form: form = render 'projects/merge_request_merge_settings', form: form diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 79b22766bc7..77ba422e87e 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -33,10 +33,15 @@ %span.light (optional) = f.text_area :description, class: "form-control", rows: 3, maxlength: 250 + = render_if_exists 'projects/classification_policy_settings', f: f + - unless @project.empty_repo? .form-group = f.label :default_branch, "Default Branch", class: 'label-light' = f.select(:default_branch, @project.repository.branch_names, {}, {class: 'select2 select-wide'}) + + = render_if_exists 'shared/repository_size_limit_setting', form: f, type: :project + .form-group = f.label :tag_list, "Tags", class: 'label-light' = f.text_field :tag_list, value: @project.tag_list.sort.join(', '), maxlength: 2000, class: "form-control" @@ -75,6 +80,8 @@ .js-project-permissions-form = f.submit 'Save changes', class: "btn btn-save" + = render_if_exists 'projects/issues_settings' + %section.settings.merge-requests-feature.no-animate{ class: [('expanded' if expanded), ('hidden' if @project.project_feature.send(:merge_requests_access_level) == 0)] } .settings-header %h4 @@ -84,10 +91,14 @@ %p Customize your merge request restrictions. .settings-content + = render_if_exists 'shared/promotions/promote_mr_features' + = form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "merge-request-settings-form" }, authenticity_token: true do |f| - = render 'merge_request_settings', form: f + = render 'projects/merge_request_settings', form: f = f.submit 'Save changes', class: "btn btn-save qa-save-merge-request-changes" + = render_if_exists 'projects/service_desk_settings' + = render 'export', project: @project %section.settings.advanced-settings.no-animate{ class: ('expanded' if expanded) } diff --git a/qa/qa/page/project/settings/merge_request.rb b/qa/qa/page/project/settings/merge_request.rb index b147c91b467..a88cd661016 100644 --- a/qa/qa/page/project/settings/merge_request.rb +++ b/qa/qa/page/project/settings/merge_request.rb @@ -5,7 +5,7 @@ module QA class MergeRequest < QA::Page::Base include Common - view 'app/views/projects/_merge_request_fast_forward_settings.html.haml' do + view 'app/views/projects/_merge_request_merge_method_settings.html.haml' do element :radio_button_merge_ff end From 10703fd5de6f228209eb8655f0b62df10c1ab8d2 Mon Sep 17 00:00:00 2001 From: Sam Beckham Date: Thu, 31 May 2018 09:08:34 +0000 Subject: [PATCH 24/46] Resolve "Remove links from Web IDE unexpectedly navigate you to a different page" --- .../ide/components/external_link.vue | 41 +++++++++ .../ide/components/ide_file_buttons.vue | 84 ------------------- .../ide/components/repo_editor.vue | 6 +- .../45520-remove-links-from-web-ide.yml | 5 ++ .../ide/components/external_link_spec.js | 35 ++++++++ .../ide/components/ide_file_buttons_spec.js | 61 -------------- 6 files changed, 84 insertions(+), 148 deletions(-) create mode 100644 app/assets/javascripts/ide/components/external_link.vue delete mode 100644 app/assets/javascripts/ide/components/ide_file_buttons.vue create mode 100644 changelogs/unreleased/45520-remove-links-from-web-ide.yml create mode 100644 spec/javascripts/ide/components/external_link_spec.js delete mode 100644 spec/javascripts/ide/components/ide_file_buttons_spec.js diff --git a/app/assets/javascripts/ide/components/external_link.vue b/app/assets/javascripts/ide/components/external_link.vue new file mode 100644 index 00000000000..cf3316a8179 --- /dev/null +++ b/app/assets/javascripts/ide/components/external_link.vue @@ -0,0 +1,41 @@ + + + diff --git a/app/assets/javascripts/ide/components/ide_file_buttons.vue b/app/assets/javascripts/ide/components/ide_file_buttons.vue deleted file mode 100644 index 30b00abf6ed..00000000000 --- a/app/assets/javascripts/ide/components/ide_file_buttons.vue +++ /dev/null @@ -1,84 +0,0 @@ - - - diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue index a281ecb95c8..93453989c08 100644 --- a/app/assets/javascripts/ide/components/repo_editor.vue +++ b/app/assets/javascripts/ide/components/repo_editor.vue @@ -6,12 +6,12 @@ import ContentViewer from '~/vue_shared/components/content_viewer/content_viewer import { activityBarViews, viewerTypes } from '../constants'; import monacoLoader from '../monaco_loader'; import Editor from '../lib/editor'; -import IdeFileButtons from './ide_file_buttons.vue'; +import ExternalLink from './external_link.vue'; export default { components: { ContentViewer, - IdeFileButtons, + ExternalLink, }, props: { file: { @@ -224,7 +224,7 @@ export default { - diff --git a/changelogs/unreleased/45520-remove-links-from-web-ide.yml b/changelogs/unreleased/45520-remove-links-from-web-ide.yml new file mode 100644 index 00000000000..81d5c26992f --- /dev/null +++ b/changelogs/unreleased/45520-remove-links-from-web-ide.yml @@ -0,0 +1,5 @@ +--- +title: Change the IDE file buttons for an "Open in file view" button +merge_request: 19129 +author: Sam Beckham +type: changed diff --git a/spec/javascripts/ide/components/external_link_spec.js b/spec/javascripts/ide/components/external_link_spec.js new file mode 100644 index 00000000000..b3d94c041fa --- /dev/null +++ b/spec/javascripts/ide/components/external_link_spec.js @@ -0,0 +1,35 @@ +import Vue from 'vue'; +import externalLink from '~/ide/components/external_link.vue'; +import createVueComponent from '../../helpers/vue_mount_component_helper'; +import { file } from '../helpers'; + +describe('ExternalLink', () => { + const activeFile = file(); + let vm; + + function createComponent() { + const ExternalLink = Vue.extend(externalLink); + + activeFile.permalink = 'test'; + + return createVueComponent(ExternalLink, { + file: activeFile, + }); + } + + afterEach(() => { + vm.$destroy(); + }); + + it('renders the external link with the correct href', done => { + activeFile.binary = true; + vm = createComponent(); + + vm.$nextTick(() => { + const openLink = vm.$el.querySelector('a'); + + expect(openLink.href).toMatch(`/${activeFile.permalink}`); + done(); + }); + }); +}); diff --git a/spec/javascripts/ide/components/ide_file_buttons_spec.js b/spec/javascripts/ide/components/ide_file_buttons_spec.js deleted file mode 100644 index 8ac8d1b2acf..00000000000 --- a/spec/javascripts/ide/components/ide_file_buttons_spec.js +++ /dev/null @@ -1,61 +0,0 @@ -import Vue from 'vue'; -import repoFileButtons from '~/ide/components/ide_file_buttons.vue'; -import createVueComponent from '../../helpers/vue_mount_component_helper'; -import { file } from '../helpers'; - -describe('RepoFileButtons', () => { - const activeFile = file(); - let vm; - - function createComponent() { - const RepoFileButtons = Vue.extend(repoFileButtons); - - activeFile.rawPath = 'test'; - activeFile.blamePath = 'test'; - activeFile.commitsPath = 'test'; - - return createVueComponent(RepoFileButtons, { - file: activeFile, - }); - } - - afterEach(() => { - vm.$destroy(); - }); - - it('renders Raw, Blame, History and Permalink', done => { - vm = createComponent(); - - vm.$nextTick(() => { - const raw = vm.$el.querySelector('.raw'); - const blame = vm.$el.querySelector('.blame'); - const history = vm.$el.querySelector('.history'); - - expect(raw.href).toMatch(`/${activeFile.rawPath}`); - expect(raw.getAttribute('data-original-title')).toEqual('Raw'); - expect(blame.href).toMatch(`/${activeFile.blamePath}`); - expect(blame.getAttribute('data-original-title')).toEqual('Blame'); - expect(history.href).toMatch(`/${activeFile.commitsPath}`); - expect(history.getAttribute('data-original-title')).toEqual('History'); - expect(vm.$el.querySelector('.permalink').getAttribute('data-original-title')).toEqual( - 'Permalink', - ); - - done(); - }); - }); - - it('renders Download', done => { - activeFile.binary = true; - vm = createComponent(); - - vm.$nextTick(() => { - const raw = vm.$el.querySelector('.raw'); - - expect(raw.href).toMatch(`/${activeFile.rawPath}`); - expect(raw.getAttribute('data-original-title')).toEqual('Download'); - - done(); - }); - }); -}); From 6a81567755eff39c99566ab5cc37ed150ae8b6de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kim=20=22BKC=22=20Carlb=C3=A4cker?= Date: Wed, 30 May 2018 12:48:25 +0200 Subject: [PATCH 25/46] Fix encoding error in Gitaly::Commit::TreeEntry --- lib/gitlab/gitaly_client/commit_service.rb | 2 +- spec/lib/gitlab/git/blob_spec.rb | 6 ++++++ spec/lib/gitlab/git/commit_spec.rb | 4 ++-- spec/lib/gitlab/git/repository_spec.rb | 4 ++-- spec/support/gitlab-git-test.git/packed-refs | 1 + spec/support/helpers/seed_repo.rb | 1 + 6 files changed, 13 insertions(+), 5 deletions(-) diff --git a/lib/gitlab/gitaly_client/commit_service.rb b/lib/gitlab/gitaly_client/commit_service.rb index 1f5f88bf792..a4cc64de80d 100644 --- a/lib/gitlab/gitaly_client/commit_service.rb +++ b/lib/gitlab/gitaly_client/commit_service.rb @@ -78,7 +78,7 @@ module Gitlab def tree_entry(ref, path, limit = nil) request = Gitaly::TreeEntryRequest.new( repository: @gitaly_repo, - revision: ref, + revision: encode_binary(ref), path: encode_binary(path), limit: limit.to_i ) diff --git a/spec/lib/gitlab/git/blob_spec.rb b/spec/lib/gitlab/git/blob_spec.rb index e2547ed0311..94eaf86ef80 100644 --- a/spec/lib/gitlab/git/blob_spec.rb +++ b/spec/lib/gitlab/git/blob_spec.rb @@ -22,6 +22,12 @@ describe Gitlab::Git::Blob, seed_helper: true do it { expect(blob).to eq(nil) } end + context 'utf-8 branch' do + let(:blob) { Gitlab::Git::Blob.find(repository, 'Ääh-test-utf-8', "files/ruby/popen.rb")} + + it { expect(blob.id).to eq(SeedRepo::RubyBlob::ID) } + end + context 'blank path' do let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, '') } diff --git a/spec/lib/gitlab/git/commit_spec.rb b/spec/lib/gitlab/git/commit_spec.rb index 08c6d1e55e9..89be8a1b7f2 100644 --- a/spec/lib/gitlab/git/commit_spec.rb +++ b/spec/lib/gitlab/git/commit_spec.rb @@ -603,8 +603,8 @@ describe Gitlab::Git::Commit, seed_helper: true do let(:commit) { described_class.find(repository, 'master') } subject { commit.ref_names(repository) } - it 'has 1 element' do - expect(subject.size).to eq(1) + it 'has 2 element' do + expect(subject.size).to eq(2) end it { is_expected.to include("master") } it { is_expected.not_to include("feature") } diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index af6a486ab20..dd5c498706d 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -1374,7 +1374,7 @@ describe Gitlab::Git::Repository, seed_helper: true do describe '#branch_count' do it 'returns the number of branches' do - expect(repository.branch_count).to eq(10) + expect(repository.branch_count).to eq(11) end context 'with local and remote branches' do @@ -2248,7 +2248,7 @@ describe Gitlab::Git::Repository, seed_helper: true do describe '#checksum' do it 'calculates the checksum for non-empty repo' do - expect(repository.checksum).to eq '54f21be4c32c02f6788d72207fa03ad3bce725e4' + expect(repository.checksum).to eq '4be7d24ce7e8d845502d599b72d567d23e6a40c0' end it 'returns 0000000000000000000000000000000000000000 for an empty repo' do diff --git a/spec/support/gitlab-git-test.git/packed-refs b/spec/support/gitlab-git-test.git/packed-refs index ea50e4ad3f6..6a61e5df267 100644 --- a/spec/support/gitlab-git-test.git/packed-refs +++ b/spec/support/gitlab-git-test.git/packed-refs @@ -9,6 +9,7 @@ 4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6 refs/heads/master 5937ac0a7beb003549fc5fd26fc247adbce4a52e refs/heads/merge-test 9596bc54a6f0c0c98248fe97077eb5ccf48a98d0 refs/heads/missing-gitmodules +4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6 refs/heads/Ääh-test-utf-8 f4e6814c3e4e7a0de82a9e7cd20c626cc963a2f8 refs/tags/v1.0.0 ^6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 8a2a6eb295bb170b34c24c76c49ed0e9b2eaf34b refs/tags/v1.1.0 diff --git a/spec/support/helpers/seed_repo.rb b/spec/support/helpers/seed_repo.rb index b4868e82cd7..71f1a86b0c1 100644 --- a/spec/support/helpers/seed_repo.rb +++ b/spec/support/helpers/seed_repo.rb @@ -98,6 +98,7 @@ module SeedRepo master merge-test missing-gitmodules + Ääh-test-utf-8 ].freeze TAGS = %w[ v1.0.0 From 8902a5559d522bee5b21d502f2e19cd3c1780bbe Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Wed, 30 May 2018 18:33:43 +0200 Subject: [PATCH 26/46] Use Gitaly 0.104.0 --- GITALY_SERVER_VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 89eba2c5b85..e49057b3302 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -0.103.0 +0.104.0 From 8175bf0dddcb42ae176b7f2ea7885858b2c7f10e Mon Sep 17 00:00:00 2001 From: Jan Provaznik Date: Sun, 27 May 2018 17:53:21 +0200 Subject: [PATCH 27/46] Replace `.exists` with `EXISTS ()` `.exists` should not be used because it's an internal ActiveRecord method, but we can easily generate the same sql query with `EXISTS`. --- app/services/boards/issues/list_service.rb | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/app/services/boards/issues/list_service.rb b/app/services/boards/issues/list_service.rb index ac70a99c2c5..5a961ac89e4 100644 --- a/app/services/boards/issues/list_service.rb +++ b/app/services/boards/issues/list_service.rb @@ -63,7 +63,7 @@ module Boards def without_board_labels(issues) return issues unless board_label_ids.any? - issues.where.not(issues_label_links.limit(1).arel.exists) + issues.where.not('EXISTS (?)', issues_label_links.limit(1)) end def issues_label_links @@ -71,10 +71,8 @@ module Boards end def with_list_label(issues) - issues.where( - LabelLink.where("label_links.target_type = 'Issue' AND label_links.target_id = issues.id") - .where("label_links.label_id = ?", list.label_id).limit(1).arel.exists - ) + issues.where('EXISTS (?)', LabelLink.where("label_links.target_type = 'Issue' AND label_links.target_id = issues.id") + .where("label_links.label_id = ?", list.label_id).limit(1)) end end end From a576f8612756cc971340b5a1c4e60b91fb3a9a04 Mon Sep 17 00:00:00 2001 From: Jan Provaznik Date: Tue, 29 May 2018 20:20:43 +0200 Subject: [PATCH 28/46] Replace .having with .where in calendar query the current syntax doesn't work properly in Rails 5, the resulting query looks like: HAVING "events"."project_id" IN (0) instead of: HAVING "events"."project_id" IN (SELECT "projects"."id" FROM... Also we should not use ActiveRecord internal methods. In this case we can filter projects in WHERE clause instead of doing this in HAVING clause. Usage of WHERE should be also more efficient because grouping is then done on much smaller subset of records. --- lib/gitlab/contributions_calendar.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/gitlab/contributions_calendar.rb b/lib/gitlab/contributions_calendar.rb index d7369060cc5..4c28489f45a 100644 --- a/lib/gitlab/contributions_calendar.rb +++ b/lib/gitlab/contributions_calendar.rb @@ -85,7 +85,7 @@ module Gitlab .select(t[:project_id], t[:target_type], t[:action], "date(created_at + #{date_interval}) AS date", 'count(id) as total_amount') .group(t[:project_id], t[:target_type], t[:action], "date(created_at + #{date_interval})") .where(conditions) - .having(t[:project_id].in(Arel::Nodes::SqlLiteral.new(authed_projects.to_sql))) + .where("events.project_id in (#{authed_projects.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection end end end From 8d8e8f771e3655fb20498659363b412c84db0d7c Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Wed, 30 May 2018 18:49:50 +0200 Subject: [PATCH 29/46] Add /vendor/gitaly-ruby to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index c7d1648615d..51b77d5ac9e 100644 --- a/.gitignore +++ b/.gitignore @@ -64,6 +64,7 @@ eslint-report.html /tags /tmp/* /vendor/bundle/* +/vendor/gitaly-ruby /builds* /shared/* /.gitlab_workhorse_secret From 7a5da502d0304705b85608b4fd84e32312798104 Mon Sep 17 00:00:00 2001 From: Jasper Maes Date: Wed, 30 May 2018 19:26:21 +0200 Subject: [PATCH 30/46] Use strings as properties key in kubernetes service spec. --- changelogs/unreleased/rails5-fix-46230.yml | 5 +++++ .../project_services/kubernetes_service_spec.rb | 12 ++++++------ 2 files changed, 11 insertions(+), 6 deletions(-) create mode 100644 changelogs/unreleased/rails5-fix-46230.yml diff --git a/changelogs/unreleased/rails5-fix-46230.yml b/changelogs/unreleased/rails5-fix-46230.yml new file mode 100644 index 00000000000..8ec28604483 --- /dev/null +++ b/changelogs/unreleased/rails5-fix-46230.yml @@ -0,0 +1,5 @@ +--- +title: Use strings as properties key in kubernetes service spec. +merge_request: 19265 +author: Jasper Maes +type: fixed diff --git a/spec/models/project_services/kubernetes_service_spec.rb b/spec/models/project_services/kubernetes_service_spec.rb index 3be023a48c1..68ab9fd08ec 100644 --- a/spec/models/project_services/kubernetes_service_spec.rb +++ b/spec/models/project_services/kubernetes_service_spec.rb @@ -65,7 +65,7 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do before do kubernetes_service.update_attribute(:active, false) - kubernetes_service.properties[:namespace] = "foo" + kubernetes_service.properties['namespace'] = "foo" end it 'should not update attributes' do @@ -82,7 +82,7 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do let(:kubernetes_service) { create(:kubernetes_service) } it 'should update attributes' do - kubernetes_service.properties[:namespace] = 'foo' + kubernetes_service.properties['namespace'] = 'foo' expect(kubernetes_service.save).to be_truthy end end @@ -92,7 +92,7 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do before do kubernetes_service.active = false - kubernetes_service.properties[:namespace] = 'foo' + kubernetes_service.properties['namespace'] = 'foo' kubernetes_service.save end @@ -105,7 +105,7 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do end it 'should update attributes' do - expect(kubernetes_service.properties[:namespace]).to eq("foo") + expect(kubernetes_service.properties['namespace']).to eq("foo") end end @@ -113,12 +113,12 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do let(:kubernetes_service) { create(:kubernetes_service, template: true, active: false) } before do - kubernetes_service.properties[:namespace] = 'foo' + kubernetes_service.properties['namespace'] = 'foo' end it 'should update attributes' do expect(kubernetes_service.save).to be_truthy - expect(kubernetes_service.properties[:namespace]).to eq('foo') + expect(kubernetes_service.properties['namespace']).to eq('foo') end end end From abad4ff00c34409ef7a35b11b74c0ed09c1c1666 Mon Sep 17 00:00:00 2001 From: Dennis Tang Date: Thu, 31 May 2018 14:37:24 +0200 Subject: [PATCH 31/46] remove focus on test --- .../projects/clusters_controller_spec.rb | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/spec/controllers/projects/clusters_controller_spec.rb b/spec/controllers/projects/clusters_controller_spec.rb index d634e2002de..314384a6543 100644 --- a/spec/controllers/projects/clusters_controller_spec.rb +++ b/spec/controllers/projects/clusters_controller_spec.rb @@ -176,10 +176,7 @@ describe Projects::ClustersController do cluster: { name: 'new-cluster', provider_gcp_attributes: { - gcp_project_id: '111', - zone: 'us-central1-a', - num_nodes: 3, - machine_type: 'n1-standard-1' + gcp_project_id: '111' } } } @@ -221,15 +218,15 @@ describe Projects::ClustersController do end end - describe 'security', :focus => true do + describe 'security' do it { expect { go }.to be_allowed_for(:admin) } it { expect { go }.to be_allowed_for(:owner).of(project) } it { expect { go }.to be_allowed_for(:master).of(project) } - # it { expect { go }.to be_denied_for(:developer).of(project) } - # it { expect { go }.to be_denied_for(:reporter).of(project) } - # it { expect { go }.to be_denied_for(:guest).of(project) } - # it { expect { go }.to be_denied_for(:user) } - # it { expect { go }.to be_denied_for(:external) } + it { expect { go }.to be_denied_for(:developer).of(project) } + it { expect { go }.to be_denied_for(:reporter).of(project) } + it { expect { go }.to be_denied_for(:guest).of(project) } + it { expect { go }.to be_denied_for(:user) } + it { expect { go }.to be_denied_for(:external) } end def go From fcb7b31ce0e5b1f38be08bb6468609d1d69734ea Mon Sep 17 00:00:00 2001 From: Dennis Tang Date: Fri, 1 Jun 2018 21:22:07 -0700 Subject: [PATCH 32/46] fix tests for new cluster POST --- .../projects/clusters_controller_spec.rb | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/spec/controllers/projects/clusters_controller_spec.rb b/spec/controllers/projects/clusters_controller_spec.rb index 314384a6543..c4f43fd6bb4 100644 --- a/spec/controllers/projects/clusters_controller_spec.rb +++ b/spec/controllers/projects/clusters_controller_spec.rb @@ -176,7 +176,7 @@ describe Projects::ClustersController do cluster: { name: 'new-cluster', provider_gcp_attributes: { - gcp_project_id: '111' + gcp_project_id: 'gcp-project-12345' } } } @@ -219,6 +219,22 @@ describe Projects::ClustersController do end describe 'security' do + before do + allow_any_instance_of(described_class) + .to receive(:token_in_session).and_return('token') + allow_any_instance_of(described_class) + .to receive(:expires_at_in_session).and_return(1.hour.since.to_i.to_s) + allow_any_instance_of(GoogleApi::CloudPlatform::Client) + .to receive(:projects_zones_clusters_create) do + OpenStruct.new( + self_link: 'projects/gcp-project-12345/zones/us-central1-a/operations/ope-123', + status: 'RUNNING' + ) + end + + allow(WaitForClusterCreationWorker).to receive(:perform_in).and_return(nil) + end + it { expect { go }.to be_allowed_for(:admin) } it { expect { go }.to be_allowed_for(:owner).of(project) } it { expect { go }.to be_allowed_for(:master).of(project) } From 8e4544ce8d03ecdec2202e2b20b2a598fa37cea4 Mon Sep 17 00:00:00 2001 From: Dennis Tang Date: Tue, 5 Jun 2018 15:37:21 -0700 Subject: [PATCH 33/46] fix blank space after google signin image --- app/views/projects/clusters/new.html.haml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/views/projects/clusters/new.html.haml b/app/views/projects/clusters/new.html.haml index ebbfe2d7fd5..945ac192d74 100644 --- a/app/views/projects/clusters/new.html.haml +++ b/app/views/projects/clusters/new.html.haml @@ -23,8 +23,7 @@ - if @valid_gcp_token = render 'projects/clusters/gcp/form' - elsif @authorize_url - = link_to @authorize_url do - = image_tag('auth_buttons/signin_with_google.png', width: '191px', class: 'signin-with-google') + = link_to(image_tag('auth_buttons/signin_with_google.png', width: '191px'), @authorize_url) = _('or') = link_to('create a new Google account', 'https://accounts.google.com/SignUpWithoutGmail?service=cloudconsole&continue=https%3A%2F%2Fconsole.cloud.google.com%2Ffreetrial%3Futm_campaign%3D2018_cpanel%26utm_source%3Dgitlab%26utm_medium%3Dreferral', target: '_blank', rel: 'noopener noreferrer') - else From 151b6b2b2b60debccdf09eda5dbe9be31cc395f6 Mon Sep 17 00:00:00 2001 From: Dennis Tang Date: Tue, 5 Jun 2018 17:06:54 -0700 Subject: [PATCH 34/46] restore .signin-with-google class for spec tests --- app/views/projects/clusters/new.html.haml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/views/projects/clusters/new.html.haml b/app/views/projects/clusters/new.html.haml index 945ac192d74..b322ad9a844 100644 --- a/app/views/projects/clusters/new.html.haml +++ b/app/views/projects/clusters/new.html.haml @@ -23,9 +23,10 @@ - if @valid_gcp_token = render 'projects/clusters/gcp/form' - elsif @authorize_url - = link_to(image_tag('auth_buttons/signin_with_google.png', width: '191px'), @authorize_url) - = _('or') - = link_to('create a new Google account', 'https://accounts.google.com/SignUpWithoutGmail?service=cloudconsole&continue=https%3A%2F%2Fconsole.cloud.google.com%2Ffreetrial%3Futm_campaign%3D2018_cpanel%26utm_source%3Dgitlab%26utm_medium%3Dreferral', target: '_blank', rel: 'noopener noreferrer') + .signin-with-google + = link_to(image_tag('auth_buttons/signin_with_google.png', width: '191px'), @authorize_url) + = _('or') + = link_to('create a new Google account', 'https://accounts.google.com/SignUpWithoutGmail?service=cloudconsole&continue=https%3A%2F%2Fconsole.cloud.google.com%2Ffreetrial%3Futm_campaign%3D2018_cpanel%26utm_source%3Dgitlab%26utm_medium%3Dreferral', target: '_blank', rel: 'noopener noreferrer') - else - link = link_to(s_('ClusterIntegration|properly configured'), help_page_path("integration/google"), target: '_blank', rel: 'noopener noreferrer') = s_('Google authentication is not %{link_to_documentation}. Ask your GitLab administrator if you want to use this service.').html_safe % { link_to_documentation: link } From edb89f555b25aa4d4aad7a3bdf5a0bd75bd076fc Mon Sep 17 00:00:00 2001 From: Dennis Tang Date: Tue, 5 Jun 2018 20:04:40 -0700 Subject: [PATCH 35/46] split into gcp and user controllers again --- .../pages/projects/clusters/gcp/new/index.js | 5 - .../javascripts/pages/projects/index.js | 6 +- .../projects/clusters/gcp_controller.rb | 41 +++++++ .../projects/clusters/user_controller.rb | 42 +++++++ .../projects/clusters_controller.rb | 116 +----------------- app/helpers/clusters_helper.rb | 42 +++++++ .../projects/clusters/gcp/_form.html.haml | 8 +- app/views/projects/clusters/new.html.haml | 18 +-- .../projects/clusters/user/_form.html.haml | 6 +- config/routes/project.rb | 9 +- 10 files changed, 160 insertions(+), 133 deletions(-) delete mode 100644 app/assets/javascripts/pages/projects/clusters/gcp/new/index.js create mode 100644 app/controllers/projects/clusters/gcp_controller.rb create mode 100644 app/controllers/projects/clusters/user_controller.rb diff --git a/app/assets/javascripts/pages/projects/clusters/gcp/new/index.js b/app/assets/javascripts/pages/projects/clusters/gcp/new/index.js deleted file mode 100644 index d4f34e32a48..00000000000 --- a/app/assets/javascripts/pages/projects/clusters/gcp/new/index.js +++ /dev/null @@ -1,5 +0,0 @@ -import initGkeDropdowns from '~/projects/gke_cluster_dropdowns'; - -document.addEventListener('DOMContentLoaded', () => { - initGkeDropdowns(); -}); diff --git a/app/assets/javascripts/pages/projects/index.js b/app/assets/javascripts/pages/projects/index.js index ba8bc5bc5fa..7bbba9acb38 100644 --- a/app/assets/javascripts/pages/projects/index.js +++ b/app/assets/javascripts/pages/projects/index.js @@ -5,7 +5,11 @@ import ShortcutsNavigation from '../../shortcuts_navigation'; document.addEventListener('DOMContentLoaded', () => { const page = document.body.dataset.page; - const newClusterViews = ['projects:clusters:new', 'projects:clusters:create']; + const newClusterViews = [ + 'projects:clusters:new', + 'projects:clusters:gcp:create', + 'projects:clusters:user:create', + ]; if (newClusterViews.indexOf(page) > -1) { gcpSignupOffer(); diff --git a/app/controllers/projects/clusters/gcp_controller.rb b/app/controllers/projects/clusters/gcp_controller.rb new file mode 100644 index 00000000000..ac9a906fe47 --- /dev/null +++ b/app/controllers/projects/clusters/gcp_controller.rb @@ -0,0 +1,41 @@ +class Projects::Clusters::GcpController < Projects::ApplicationController + include ClustersHelper + before_action :authorize_read_cluster! + before_action :authorize_create_cluster!, only: [:create] + helper_method :gcp_authorize_url + helper_method :token_in_session + helper_method :valid_gcp_token + + def create + @cluster = ::Clusters::CreateService + .new(project, current_user, create_params) + .execute(token_in_session) + + if @cluster.persisted? + redirect_to project_cluster_path(project, @cluster) + else + @gcp_cluster = @cluster + user_cluster + + render 'projects/clusters/new', locals: { active_tab: 'gcp' } + end + end + + private + + def create_params + params.require(:cluster).permit( + :enabled, + :name, + :environment_scope, + provider_gcp_attributes: [ + :gcp_project_id, + :zone, + :num_nodes, + :machine_type + ]).merge( + provider_type: :gcp, + platform_type: :kubernetes + ) + end +end diff --git a/app/controllers/projects/clusters/user_controller.rb b/app/controllers/projects/clusters/user_controller.rb new file mode 100644 index 00000000000..9da6a60e6b3 --- /dev/null +++ b/app/controllers/projects/clusters/user_controller.rb @@ -0,0 +1,42 @@ +class Projects::Clusters::UserController < Projects::ApplicationController + include ClustersHelper + before_action :authorize_read_cluster! + before_action :authorize_create_cluster!, only: [:create] + helper_method :gcp_authorize_url + helper_method :token_in_session + helper_method :valid_gcp_token + + def create + @cluster = ::Clusters::CreateService + .new(project, current_user, create_params) + .execute + + if @cluster.persisted? + redirect_to project_cluster_path(project, @cluster) + else + @user_cluster = @cluster + gcp_cluster + + render 'projects/clusters/new', locals: { active_tab: 'user' } + end + end + + private + + def create_params + params.require(:cluster).permit( + :enabled, + :name, + :environment_scope, + platform_kubernetes_attributes: [ + :namespace, + :api_url, + :token, + :ca_cert + ]).merge( + provider_type: :user, + platform_type: :kubernetes + ) + end +end + diff --git a/app/controllers/projects/clusters_controller.rb b/app/controllers/projects/clusters_controller.rb index 1e8f3ac1433..38140fe9f27 100644 --- a/app/controllers/projects/clusters_controller.rb +++ b/app/controllers/projects/clusters_controller.rb @@ -1,15 +1,16 @@ class Projects::ClustersController < Projects::ApplicationController - before_action :cluster, except: [:index, :new, :create] + include ClustersHelper + before_action :cluster, except: [:index, :new] before_action :authorize_read_cluster! - before_action :generate_gcp_authorize_url, only: [:new] - before_action :validate_gcp_token, only: [:new] - before_action :new_cluster, only: [:new] - before_action :existing_cluster, only: [:new] + before_action :gcp_cluster, only: [:new] + before_action :user_cluster, only: [:new] before_action :authorize_create_cluster!, only: [:new] before_action :authorize_update_cluster!, only: [:update] before_action :authorize_admin_cluster!, only: [:destroy] before_action :update_applications_status, only: [:status] + helper_method :gcp_authorize_url helper_method :token_in_session + helper_method :valid_gcp_token STATUS_POLLING_INTERVAL = 10_000 @@ -69,37 +70,6 @@ class Projects::ClustersController < Projects::ApplicationController end end - def create - case params[:type] - when 'new' - cluster_params = create_new_cluster_params - when 'existing' - cluster_params = create_existing_cluster_params - end - - @cluster = ::Clusters::CreateService - .new(project, current_user, cluster_params) - .execute(token_in_session) - - if @cluster.persisted? - redirect_to project_cluster_path(project, @cluster) - else - generate_gcp_authorize_url - validate_gcp_token - - case params[:type] - when 'new' - @new_cluster = @cluster - existing_cluster - when 'existing' - @existing_cluster = @cluster - new_cluster - end - - render :new, locals: { active_tab: params[:type] } - end - end - private def cluster @@ -131,80 +101,6 @@ class Projects::ClustersController < Projects::ApplicationController end end - def create_new_cluster_params - params.require(:cluster).permit( - :enabled, - :name, - :environment_scope, - provider_gcp_attributes: [ - :gcp_project_id, - :zone, - :num_nodes, - :machine_type - ]).merge( - provider_type: :gcp, - platform_type: :kubernetes - ) - end - - def create_existing_cluster_params - params.require(:cluster).permit( - :enabled, - :name, - :environment_scope, - platform_kubernetes_attributes: [ - :namespace, - :api_url, - :token, - :ca_cert - ]).merge( - provider_type: :user, - platform_type: :kubernetes - ) - end - - def generate_gcp_authorize_url - state = generate_session_key_redirect(new_project_cluster_path(@project).to_s) - - @authorize_url = GoogleApi::CloudPlatform::Client.new( - nil, callback_google_api_auth_url, - state: state).authorize_url - rescue GoogleApi::Auth::ConfigMissingError - # no-op - end - - def new_cluster - @new_cluster = ::Clusters::Cluster.new.tap do |cluster| - cluster.build_provider_gcp - end - end - - def existing_cluster - @existing_cluster = ::Clusters::Cluster.new.tap do |cluster| - cluster.build_platform_kubernetes - end - end - - def validate_gcp_token - @valid_gcp_token = GoogleApi::CloudPlatform::Client.new(token_in_session, nil) - .validate_token(expires_at_in_session) - end - - def token_in_session - session[GoogleApi::CloudPlatform::Client.session_key_for_token] - end - - def expires_at_in_session - @expires_at_in_session ||= - session[GoogleApi::CloudPlatform::Client.session_key_for_expires_at] - end - - def generate_session_key_redirect(uri) - GoogleApi::CloudPlatform::Client.new_session_key_for_redirect_uri do |key| - session[key] = uri - end - end - def authorize_update_cluster! access_denied! unless can?(current_user, :update_cluster, cluster) end diff --git a/app/helpers/clusters_helper.rb b/app/helpers/clusters_helper.rb index c24d340d184..e1be917f298 100644 --- a/app/helpers/clusters_helper.rb +++ b/app/helpers/clusters_helper.rb @@ -10,4 +10,46 @@ module ClustersHelper render 'projects/clusters/gcp_signup_offer_banner' end end + + def gcp_cluster + @gcp_cluster = ::Clusters::Cluster.new.tap do |cluster| + cluster.build_provider_gcp + end + end + + def user_cluster + @user_cluster = ::Clusters::Cluster.new.tap do |cluster| + cluster.build_platform_kubernetes + end + end + + def gcp_authorize_url + state = generate_session_key_redirect(new_project_cluster_path(@project).to_s) + + GoogleApi::CloudPlatform::Client.new( + nil, callback_google_api_auth_url, + state: state).authorize_url + rescue GoogleApi::Auth::ConfigMissingError + # no-op + end + + def generate_session_key_redirect(uri) + GoogleApi::CloudPlatform::Client.new_session_key_for_redirect_uri do |key| + session[key] = uri + end + end + + def token_in_session + session[GoogleApi::CloudPlatform::Client.session_key_for_token] + end + + def expires_at_in_session + @expires_at_in_session ||= + session[GoogleApi::CloudPlatform::Client.session_key_for_expires_at] + end + + def valid_gcp_token + GoogleApi::CloudPlatform::Client.new(token_in_session, nil) + .validate_token(expires_at_in_session) + end end diff --git a/app/views/projects/clusters/gcp/_form.html.haml b/app/views/projects/clusters/gcp/_form.html.haml index 4a73ac24072..a6589cca529 100644 --- a/app/views/projects/clusters/gcp/_form.html.haml +++ b/app/views/projects/clusters/gcp/_form.html.haml @@ -4,10 +4,10 @@ - link_to_help_page = link_to(s_('ClusterIntegration|help page'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer') = s_('ClusterIntegration|Read our %{link_to_help_page} on Kubernetes cluster integration.').html_safe % { link_to_help_page: link_to_help_page} -%p= link_to('Select a different Google account', @authorize_url) +%p= link_to('Select a different Google account', gcp_authorize_url) -= form_for @new_cluster, html: { class: 'js-gke-cluster-creation prepend-top-20', data: { token: token_in_session } }, url: namespace_project_clusters_path(@project.namespace, @project, { type: 'new' }), as: :cluster do |field| - = form_errors(@new_cluster) += form_for @gcp_cluster, html: { class: 'js-gke-cluster-creation prepend-top-20', data: { token: token_in_session } }, url: gcp_namespace_project_clusters_path(@project.namespace, @project), as: :cluster do |field| + = form_errors(@gcp_cluster) .form-group = field.label :name, s_('ClusterIntegration|Kubernetes cluster name') = field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Kubernetes cluster name') @@ -15,7 +15,7 @@ = field.label :environment_scope, s_('ClusterIntegration|Environment scope') = field.text_field :environment_scope, class: 'form-control', readonly: !has_multiple_clusters?(@project), placeholder: s_('ClusterIntegration|Environment scope') - = field.fields_for :provider_gcp, @new_cluster.provider_gcp do |provider_gcp_field| + = field.fields_for :provider_gcp, @gcp_cluster.provider_gcp do |provider_gcp_field| .form-group = provider_gcp_field.label :gcp_project_id, s_('ClusterIntegration|Google Cloud Platform project ID') .js-gcp-project-id-dropdown-entry-point{ data: { docsUrl: 'https://console.cloud.google.com/home/dashboard' } } diff --git a/app/views/projects/clusters/new.html.haml b/app/views/projects/clusters/new.html.haml index b322ad9a844..11b7c363553 100644 --- a/app/views/projects/clusters/new.html.haml +++ b/app/views/projects/clusters/new.html.haml @@ -1,36 +1,36 @@ - breadcrumb_title 'Kubernetes' - page_title _("Kubernetes Cluster") -- active_tab = local_assigns.fetch(:active_tab, 'new') +- active_tab = local_assigns.fetch(:active_tab, 'gcp') = javascript_include_tag 'https://apis.google.com/js/api.js' = render_gcp_signup_offer .row.prepend-top-default .col-md-3 - = render 'sidebar' + = render 'projects/clusters/sidebar' .col-md-9.js-toggle-container %ul.nav-links.nav-tabs.gitlab-tabs.nav{ role: 'tablist' } %li.nav-item{ role: 'presentation' } - %a.nav-link{ href: '#create-new-cluster-pane', id: 'create-new-cluster-tab', class: active_when(active_tab == 'new'), data: { toggle: 'tab' }, role: 'tab' } + %a.nav-link{ href: '#create-gcp-cluster-pane', id: 'create-gcp-cluster-tab', class: active_when(active_tab == 'gcp'), data: { toggle: 'tab' }, role: 'tab' } %span Create new Cluster on GKE %li.nav-item{ role: 'presentation' } - %a.nav-link{ href: '#add-existing-cluster-pane', id: 'add-existing-cluster-tab', class: active_when(active_tab == 'existing'), data: { toggle: 'tab' }, role: 'tab' } + %a.nav-link{ href: '#add-user-cluster-pane', id: 'add-user-cluster-tab', class: active_when(active_tab == 'user'), data: { toggle: 'tab' }, role: 'tab' } %span Add existing cluster .tab-content.gitlab-tab-content - .tab-pane{ id: 'create-new-cluster-pane', class: active_when(active_tab == 'new'), role: 'tabpanel' } + .tab-pane{ id: 'create-gcp-cluster-pane', class: active_when(active_tab == 'gcp'), role: 'tabpanel' } = render 'projects/clusters/gcp/header' - - if @valid_gcp_token + - if valid_gcp_token = render 'projects/clusters/gcp/form' - - elsif @authorize_url + - elsif gcp_authorize_url .signin-with-google - = link_to(image_tag('auth_buttons/signin_with_google.png', width: '191px'), @authorize_url) + = link_to(image_tag('auth_buttons/signin_with_google.png', width: '191px'), gcp_authorize_url) = _('or') = link_to('create a new Google account', 'https://accounts.google.com/SignUpWithoutGmail?service=cloudconsole&continue=https%3A%2F%2Fconsole.cloud.google.com%2Ffreetrial%3Futm_campaign%3D2018_cpanel%26utm_source%3Dgitlab%26utm_medium%3Dreferral', target: '_blank', rel: 'noopener noreferrer') - else - link = link_to(s_('ClusterIntegration|properly configured'), help_page_path("integration/google"), target: '_blank', rel: 'noopener noreferrer') = s_('Google authentication is not %{link_to_documentation}. Ask your GitLab administrator if you want to use this service.').html_safe % { link_to_documentation: link } - .tab-pane{ id: 'add-existing-cluster-pane', class: active_when(active_tab == 'existing'), role: 'tabpanel' } + .tab-pane{ id: 'add-user-cluster-pane', class: active_when(active_tab == 'user'), role: 'tabpanel' } = render 'projects/clusters/user/header' = render 'projects/clusters/user/form' diff --git a/app/views/projects/clusters/user/_form.html.haml b/app/views/projects/clusters/user/_form.html.haml index bf1fd1f8898..4167c8787ff 100644 --- a/app/views/projects/clusters/user/_form.html.haml +++ b/app/views/projects/clusters/user/_form.html.haml @@ -1,5 +1,5 @@ -= form_for @existing_cluster, url: namespace_project_clusters_path(@project.namespace, @project, { type: 'existing' }), as: :cluster do |field| - = form_errors(@existing_cluster) += form_for @user_cluster, url: user_namespace_project_clusters_path(@project.namespace, @project), as: :cluster do |field| + = form_errors(@user_cluster) .form-group = field.label :name, s_('ClusterIntegration|Kubernetes cluster name') = field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Kubernetes cluster name') @@ -7,7 +7,7 @@ = field.label :environment_scope, s_('ClusterIntegration|Environment scope') = field.text_field :environment_scope, class: 'form-control', readonly: !has_multiple_clusters?(@project), placeholder: s_('ClusterIntegration|Environment scope') - = field.fields_for :platform_kubernetes, @existing_cluster.platform_kubernetes do |platform_kubernetes_field| + = field.fields_for :platform_kubernetes, @user_cluster.platform_kubernetes do |platform_kubernetes_field| .form-group = platform_kubernetes_field.label :api_url, s_('ClusterIntegration|API URL') = platform_kubernetes_field.text_field :api_url, class: 'form-control', placeholder: s_('ClusterIntegration|API URL') diff --git a/config/routes/project.rb b/config/routes/project.rb index 7df2d4abb75..f672291a9f1 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -204,7 +204,14 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do end end - resources :clusters, except: [:edit] do + resources :clusters, except: [:edit, :create] do + collection do + scope :providers do + post '/user', to: 'clusters/user#create' + post '/gcp', to: 'clusters/gcp#create' + end + end + member do get :status, format: :json From 08d513a59e3cc2aa09b91217773267f3d3fd5653 Mon Sep 17 00:00:00 2001 From: Dennis Tang Date: Tue, 5 Jun 2018 21:47:09 -0700 Subject: [PATCH 36/46] redirect back to form if token expires --- .../projects/clusters/gcp_controller.rb | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/app/controllers/projects/clusters/gcp_controller.rb b/app/controllers/projects/clusters/gcp_controller.rb index ac9a906fe47..9741164377b 100644 --- a/app/controllers/projects/clusters/gcp_controller.rb +++ b/app/controllers/projects/clusters/gcp_controller.rb @@ -7,17 +7,21 @@ class Projects::Clusters::GcpController < Projects::ApplicationController helper_method :valid_gcp_token def create - @cluster = ::Clusters::CreateService - .new(project, current_user, create_params) - .execute(token_in_session) + if valid_gcp_token + @cluster = ::Clusters::CreateService + .new(project, current_user, create_params) + .execute(token_in_session) - if @cluster.persisted? - redirect_to project_cluster_path(project, @cluster) + if @cluster.persisted? + redirect_to project_cluster_path(project, @cluster) + else + @gcp_cluster = @cluster + user_cluster + + render 'projects/clusters/new', locals: { active_tab: 'gcp' } + end else - @gcp_cluster = @cluster - user_cluster - - render 'projects/clusters/new', locals: { active_tab: 'gcp' } + redirect_to new_project_cluster_path(@project) end end From 3ba9a372e726bbd8d2a74aa697536d90db80255d Mon Sep 17 00:00:00 2001 From: Dennis Tang Date: Tue, 5 Jun 2018 21:47:16 -0700 Subject: [PATCH 37/46] cleanup --- app/helpers/clusters_helper.rb | 3 +-- app/views/projects/clusters/_dropdown.html.haml | 12 ------------ 2 files changed, 1 insertion(+), 14 deletions(-) delete mode 100644 app/views/projects/clusters/_dropdown.html.haml diff --git a/app/helpers/clusters_helper.rb b/app/helpers/clusters_helper.rb index e1be917f298..0663ef187e8 100644 --- a/app/helpers/clusters_helper.rb +++ b/app/helpers/clusters_helper.rb @@ -44,8 +44,7 @@ module ClustersHelper end def expires_at_in_session - @expires_at_in_session ||= - session[GoogleApi::CloudPlatform::Client.session_key_for_expires_at] + session[GoogleApi::CloudPlatform::Client.session_key_for_expires_at] end def valid_gcp_token diff --git a/app/views/projects/clusters/_dropdown.html.haml b/app/views/projects/clusters/_dropdown.html.haml deleted file mode 100644 index d55a9c60b64..00000000000 --- a/app/views/projects/clusters/_dropdown.html.haml +++ /dev/null @@ -1,12 +0,0 @@ -%h4.prepend-top-0= s_('ClusterIntegration|Choose how to set up Kubernetes cluster integration') - -.dropdown.clusters-dropdown - %button.dropdown-menu-toggle.dropdown-menu-full-width{ type: 'button', data: { toggle: 'dropdown' }, 'aria-haspopup': true, 'aria-expanded': false } - %span.dropdown-toggle-text - = dropdown_text - = icon('chevron-down') - %ul.dropdown-menu.clusters-dropdown-menu.dropdown-menu-full-width - %li - = link_to(s_('ClusterIntegration|Create Kubernetes cluster on Google Kubernetes Engine'), gcp_new_namespace_project_clusters_path(@project.namespace, @project)) - %li - = link_to(s_('ClusterIntegration|Add an existing Kubernetes cluster'), user_new_namespace_project_clusters_path(@project.namespace, @project)) From 98cf3a0b54ef1a0b96d128b07d10af240dfa9138 Mon Sep 17 00:00:00 2001 From: Dennis Tang Date: Tue, 5 Jun 2018 21:47:22 -0700 Subject: [PATCH 38/46] update tests --- .../projects/clusters/gcp_controller_spec.rb | 95 +++++++++++ .../projects/clusters/user_controller_spec.rb | 57 +++++++ .../projects/clusters_controller_spec.rb | 154 ++---------------- 3 files changed, 164 insertions(+), 142 deletions(-) create mode 100644 spec/controllers/projects/clusters/gcp_controller_spec.rb create mode 100644 spec/controllers/projects/clusters/user_controller_spec.rb diff --git a/spec/controllers/projects/clusters/gcp_controller_spec.rb b/spec/controllers/projects/clusters/gcp_controller_spec.rb new file mode 100644 index 00000000000..acaf2a1b948 --- /dev/null +++ b/spec/controllers/projects/clusters/gcp_controller_spec.rb @@ -0,0 +1,95 @@ +require 'spec_helper' + +describe Projects::Clusters::GcpController do + include AccessMatchersForController + include GoogleApi::CloudPlatformHelpers + + set(:project) { create(:project) } + + describe 'POST create' do + let(:params) do + { + cluster: { + name: 'new-cluster', + provider_gcp_attributes: { + gcp_project_id: '111' + } + } + } + end + + before do + allow_any_instance_of(GoogleApi::CloudPlatform::Client) + .to receive(:projects_zones_clusters_create) do + OpenStruct.new( + self_link: 'projects/gcp-project-12345/zones/us-central1-a/operations/ope-123', + status: 'RUNNING' + ) + end + end + + describe 'functionality' do + let(:user) { create(:user) } + + before do + project.add_master(user) + sign_in(user) + end + + context 'when access token is valid' do + before do + stub_google_api_validate_token + end + + it 'creates a new cluster' do + expect(ClusterProvisionWorker).to receive(:perform_async) + expect { go }.to change { Clusters::Cluster.count } + .and change { Clusters::Providers::Gcp.count } + expect(response).to redirect_to(project_cluster_path(project, project.clusters.first)) + expect(project.clusters.first).to be_gcp + expect(project.clusters.first).to be_kubernetes + end + end + + context 'when access token is expired' do + before do + stub_google_api_expired_token + end + + it 'redirects to new clusters form' do + puts described_class + expect(go).to redirect_to(new_project_cluster_path(project)) + end + end + + context 'when access token is not stored in session' do + it 'redirects to new clusters form' do + expect(go).to redirect_to(new_project_cluster_path(project)) + end + end + end + + describe 'security' do + before do + allow_any_instance_of(described_class) + .to receive(:token_in_session).and_return('token') + allow_any_instance_of(described_class) + .to receive(:expires_at_in_session).and_return(1.hour.since.to_i.to_s) + allow(WaitForClusterCreationWorker).to receive(:perform_in).and_return(nil) + end + + it { expect { go }.to be_allowed_for(:admin) } + it { expect { go }.to be_allowed_for(:owner).of(project) } + it { expect { go }.to be_allowed_for(:master).of(project) } + it { expect { go }.to be_denied_for(:developer).of(project) } + it { expect { go }.to be_denied_for(:reporter).of(project) } + it { expect { go }.to be_denied_for(:guest).of(project) } + it { expect { go }.to be_denied_for(:user) } + it { expect { go }.to be_denied_for(:external) } + end + + def go + post :create, params.merge(namespace_id: project.namespace, project_id: project) + end + end +end diff --git a/spec/controllers/projects/clusters/user_controller_spec.rb b/spec/controllers/projects/clusters/user_controller_spec.rb new file mode 100644 index 00000000000..eba1c62a1f2 --- /dev/null +++ b/spec/controllers/projects/clusters/user_controller_spec.rb @@ -0,0 +1,57 @@ +require 'spec_helper' + +describe Projects::Clusters::UserController do + include AccessMatchersForController + + set(:project) { create(:project) } + + describe 'POST create' do + let(:params) do + { + cluster: { + name: 'new-cluster', + platform_kubernetes_attributes: { + api_url: 'http://my-url', + token: 'test', + namespace: 'aaa' + } + } + } + end + + describe 'functionality' do + let(:user) { create(:user) } + + before do + project.add_master(user) + sign_in(user) + end + + context 'when creates a cluster' do + it 'creates a new cluster' do + expect(ClusterProvisionWorker).to receive(:perform_async) + expect { go }.to change { Clusters::Cluster.count } + .and change { Clusters::Platforms::Kubernetes.count } + expect(response).to redirect_to(project_cluster_path(project, project.clusters.first)) + expect(project.clusters.first).to be_user + expect(project.clusters.first).to be_kubernetes + end + end + end + + describe 'security' do + it { expect { go }.to be_allowed_for(:admin) } + it { expect { go }.to be_allowed_for(:owner).of(project) } + it { expect { go }.to be_allowed_for(:master).of(project) } + it { expect { go }.to be_denied_for(:developer).of(project) } + it { expect { go }.to be_denied_for(:reporter).of(project) } + it { expect { go }.to be_denied_for(:guest).of(project) } + it { expect { go }.to be_denied_for(:user) } + it { expect { go }.to be_denied_for(:external) } + end + + def go + post :create, params.merge(namespace_id: project.namespace, project_id: project) + end + end +end diff --git a/spec/controllers/projects/clusters_controller_spec.rb b/spec/controllers/projects/clusters_controller_spec.rb index c4f43fd6bb4..a3aab70ab77 100644 --- a/spec/controllers/projects/clusters_controller_spec.rb +++ b/spec/controllers/projects/clusters_controller_spec.rb @@ -3,6 +3,7 @@ require 'spec_helper' describe Projects::ClustersController do include AccessMatchersForController include GoogleApi::CloudPlatformHelpers + include ClustersHelper set(:project) { create(:project) } @@ -75,12 +76,13 @@ describe Projects::ClustersController do end describe 'GET new' do - describe 'functionality for new cluster' do + describe 'functionality for gcp cluster' do let(:user) { create(:user) } before do project.add_master(user) sign_in(user) + @project = project end context 'when omniauth has been configured' do @@ -93,10 +95,10 @@ describe Projects::ClustersController do allow(SecureRandom).to receive(:hex).and_return(key) end - it 'has authorize_url' do + it 'has gcp_authorize_url' do go - expect(assigns(:authorize_url)).to include(key) + expect(gcp_authorize_url).to include(key) expect(session[session_key_for_redirect_uri]).to eq(new_project_cluster_path(project)) end end @@ -106,10 +108,10 @@ describe Projects::ClustersController do stub_omniauth_setting(providers: []) end - it 'does not have authorize_url' do + it 'does not have gcp_authorize_url' do go - expect(assigns(:authorize_url)).to be_nil + expect(gcp_authorize_url).to be_nil end end @@ -121,7 +123,7 @@ describe Projects::ClustersController do it 'has new object' do go - expect(assigns(:new_cluster)).to be_an_instance_of(Clusters::Cluster) + expect(assigns(:gcp_cluster)).to be_an_instance_of(Clusters::Cluster) end end @@ -130,15 +132,15 @@ describe Projects::ClustersController do stub_google_api_expired_token end - it { expect(@valid_gcp_token).to be_falsey } + it { expect(valid_gcp_token).to be_falsey } end context 'when access token is not stored in session' do - it { expect(@valid_gcp_token).to be_falsey } + it { expect(valid_gcp_token).to be_falsey } end end - describe 'functionality for existing cluster' do + describe 'functionality for user cluster' do let(:user) { create(:user) } before do @@ -149,7 +151,7 @@ describe Projects::ClustersController do it 'has new object' do go - expect(assigns(:existing_cluster)).to be_an_instance_of(Clusters::Cluster) + expect(assigns(:user_cluster)).to be_an_instance_of(Clusters::Cluster) end end @@ -169,138 +171,6 @@ describe Projects::ClustersController do end end - describe 'POST create for new cluster' do - let(:params) do - { - type: 'new', - cluster: { - name: 'new-cluster', - provider_gcp_attributes: { - gcp_project_id: 'gcp-project-12345' - } - } - } - end - - describe 'functionality' do - let(:user) { create(:user) } - - before do - project.add_master(user) - sign_in(user) - end - - context 'when access token is valid' do - before do - stub_google_api_validate_token - end - - it 'creates a new cluster' do - expect(ClusterProvisionWorker).to receive(:perform_async) - expect { go }.to change { Clusters::Cluster.count } - .and change { Clusters::Providers::Gcp.count } - expect(response).to redirect_to(project_cluster_path(project, project.clusters.first)) - expect(project.clusters.first).to be_gcp - expect(project.clusters.first).to be_kubernetes - end - end - - context 'when access token is expired' do - before do - stub_google_api_expired_token - end - - it { expect(@valid_gcp_token).to be_falsey } - end - - context 'when access token is not stored in session' do - it { expect(@valid_gcp_token).to be_falsey } - end - end - - describe 'security' do - before do - allow_any_instance_of(described_class) - .to receive(:token_in_session).and_return('token') - allow_any_instance_of(described_class) - .to receive(:expires_at_in_session).and_return(1.hour.since.to_i.to_s) - allow_any_instance_of(GoogleApi::CloudPlatform::Client) - .to receive(:projects_zones_clusters_create) do - OpenStruct.new( - self_link: 'projects/gcp-project-12345/zones/us-central1-a/operations/ope-123', - status: 'RUNNING' - ) - end - - allow(WaitForClusterCreationWorker).to receive(:perform_in).and_return(nil) - end - - it { expect { go }.to be_allowed_for(:admin) } - it { expect { go }.to be_allowed_for(:owner).of(project) } - it { expect { go }.to be_allowed_for(:master).of(project) } - it { expect { go }.to be_denied_for(:developer).of(project) } - it { expect { go }.to be_denied_for(:reporter).of(project) } - it { expect { go }.to be_denied_for(:guest).of(project) } - it { expect { go }.to be_denied_for(:user) } - it { expect { go }.to be_denied_for(:external) } - end - - def go - post :create, params.merge(namespace_id: project.namespace, project_id: project) - end - end - - describe 'POST create for existing cluster' do - let(:params) do - { - type: 'existing', - cluster: { - name: 'new-cluster', - platform_kubernetes_attributes: { - api_url: 'http://my-url', - token: 'test', - namespace: 'aaa' - } - } - } - end - - describe 'functionality' do - let(:user) { create(:user) } - - before do - project.add_master(user) - sign_in(user) - end - - context 'when creates a cluster' do - it 'creates a new cluster' do - expect(ClusterProvisionWorker).to receive(:perform_async) - expect { go }.to change { Clusters::Cluster.count } - .and change { Clusters::Platforms::Kubernetes.count } - expect(response).to redirect_to(project_cluster_path(project, project.clusters.first)) - expect(project.clusters.first).to be_user - expect(project.clusters.first).to be_kubernetes - end - end - end - - describe 'security' do - it { expect { go }.to be_allowed_for(:admin) } - it { expect { go }.to be_allowed_for(:owner).of(project) } - it { expect { go }.to be_allowed_for(:master).of(project) } - it { expect { go }.to be_denied_for(:developer).of(project) } - it { expect { go }.to be_denied_for(:reporter).of(project) } - it { expect { go }.to be_denied_for(:guest).of(project) } - it { expect { go }.to be_denied_for(:user) } - it { expect { go }.to be_denied_for(:external) } - end - - def go - post :create, params.merge(namespace_id: project.namespace, project_id: project) - end - end - describe 'GET status' do let(:cluster) { create(:cluster, :providing_by_gcp, projects: [project]) } From f00c1b7f47a1cd9492ea5170eeea96a9d654590e Mon Sep 17 00:00:00 2001 From: Dennis Tang Date: Wed, 6 Jun 2018 08:43:23 -0700 Subject: [PATCH 39/46] rubocop fix --- app/controllers/projects/clusters/user_controller.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/app/controllers/projects/clusters/user_controller.rb b/app/controllers/projects/clusters/user_controller.rb index 9da6a60e6b3..40bc3dc6f08 100644 --- a/app/controllers/projects/clusters/user_controller.rb +++ b/app/controllers/projects/clusters/user_controller.rb @@ -39,4 +39,3 @@ class Projects::Clusters::UserController < Projects::ApplicationController ) end end - From cc0305565704e8f713cd615f5a6767498bd03706 Mon Sep 17 00:00:00 2001 From: Dennis Tang Date: Wed, 6 Jun 2018 08:43:31 -0700 Subject: [PATCH 40/46] fix gcp_spec.rb --- spec/features/projects/clusters/gcp_spec.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/spec/features/projects/clusters/gcp_spec.rb b/spec/features/projects/clusters/gcp_spec.rb index 0ebe3459a65..31c0735f577 100644 --- a/spec/features/projects/clusters/gcp_spec.rb +++ b/spec/features/projects/clusters/gcp_spec.rb @@ -20,6 +20,10 @@ feature 'Gcp Cluster', :js do .to receive(:token_in_session).and_return('token') allow_any_instance_of(Projects::ClustersController) .to receive(:expires_at_in_session).and_return(1.hour.since.to_i.to_s) + allow_any_instance_of(Projects::Clusters::GcpController) + .to receive(:token_in_session).and_return('token') + allow_any_instance_of(Projects::Clusters::GcpController) + .to receive(:expires_at_in_session).and_return(1.hour.since.to_i.to_s) end context 'when user does not have a cluster and visits cluster index page' do From ce0ce1cb196e924b13631565114352ed89eba5af Mon Sep 17 00:00:00 2001 From: Dennis Tang Date: Mon, 11 Jun 2018 22:57:04 -0700 Subject: [PATCH 41/46] consolidate back into one controller --- .../pages/projects/clusters/gcp/new/index.js | 5 + .../javascripts/pages/projects/index.js | 6 +- .../projects/clusters/gcp_controller.rb | 45 ------ .../projects/clusters/user_controller.rb | 41 ----- .../projects/clusters_controller.rb | 113 ++++++++++++- app/helpers/clusters_helper.rb | 41 ----- .../projects/clusters/gcp/_form.html.haml | 4 +- app/views/projects/clusters/new.html.haml | 10 +- .../projects/clusters/user/_form.html.haml | 2 +- config/routes/project.rb | 6 +- .../projects/clusters/gcp_controller_spec.rb | 95 ----------- .../projects/clusters/user_controller_spec.rb | 57 ------- .../projects/clusters_controller_spec.rb | 148 ++++++++++++++++-- spec/features/projects/clusters/gcp_spec.rb | 4 - 14 files changed, 263 insertions(+), 314 deletions(-) create mode 100644 app/assets/javascripts/pages/projects/clusters/gcp/new/index.js delete mode 100644 app/controllers/projects/clusters/gcp_controller.rb delete mode 100644 app/controllers/projects/clusters/user_controller.rb delete mode 100644 spec/controllers/projects/clusters/gcp_controller_spec.rb delete mode 100644 spec/controllers/projects/clusters/user_controller_spec.rb diff --git a/app/assets/javascripts/pages/projects/clusters/gcp/new/index.js b/app/assets/javascripts/pages/projects/clusters/gcp/new/index.js new file mode 100644 index 00000000000..d4f34e32a48 --- /dev/null +++ b/app/assets/javascripts/pages/projects/clusters/gcp/new/index.js @@ -0,0 +1,5 @@ +import initGkeDropdowns from '~/projects/gke_cluster_dropdowns'; + +document.addEventListener('DOMContentLoaded', () => { + initGkeDropdowns(); +}); diff --git a/app/assets/javascripts/pages/projects/index.js b/app/assets/javascripts/pages/projects/index.js index 7bbba9acb38..ba8bc5bc5fa 100644 --- a/app/assets/javascripts/pages/projects/index.js +++ b/app/assets/javascripts/pages/projects/index.js @@ -5,11 +5,7 @@ import ShortcutsNavigation from '../../shortcuts_navigation'; document.addEventListener('DOMContentLoaded', () => { const page = document.body.dataset.page; - const newClusterViews = [ - 'projects:clusters:new', - 'projects:clusters:gcp:create', - 'projects:clusters:user:create', - ]; + const newClusterViews = ['projects:clusters:new', 'projects:clusters:create']; if (newClusterViews.indexOf(page) > -1) { gcpSignupOffer(); diff --git a/app/controllers/projects/clusters/gcp_controller.rb b/app/controllers/projects/clusters/gcp_controller.rb deleted file mode 100644 index 9741164377b..00000000000 --- a/app/controllers/projects/clusters/gcp_controller.rb +++ /dev/null @@ -1,45 +0,0 @@ -class Projects::Clusters::GcpController < Projects::ApplicationController - include ClustersHelper - before_action :authorize_read_cluster! - before_action :authorize_create_cluster!, only: [:create] - helper_method :gcp_authorize_url - helper_method :token_in_session - helper_method :valid_gcp_token - - def create - if valid_gcp_token - @cluster = ::Clusters::CreateService - .new(project, current_user, create_params) - .execute(token_in_session) - - if @cluster.persisted? - redirect_to project_cluster_path(project, @cluster) - else - @gcp_cluster = @cluster - user_cluster - - render 'projects/clusters/new', locals: { active_tab: 'gcp' } - end - else - redirect_to new_project_cluster_path(@project) - end - end - - private - - def create_params - params.require(:cluster).permit( - :enabled, - :name, - :environment_scope, - provider_gcp_attributes: [ - :gcp_project_id, - :zone, - :num_nodes, - :machine_type - ]).merge( - provider_type: :gcp, - platform_type: :kubernetes - ) - end -end diff --git a/app/controllers/projects/clusters/user_controller.rb b/app/controllers/projects/clusters/user_controller.rb deleted file mode 100644 index 40bc3dc6f08..00000000000 --- a/app/controllers/projects/clusters/user_controller.rb +++ /dev/null @@ -1,41 +0,0 @@ -class Projects::Clusters::UserController < Projects::ApplicationController - include ClustersHelper - before_action :authorize_read_cluster! - before_action :authorize_create_cluster!, only: [:create] - helper_method :gcp_authorize_url - helper_method :token_in_session - helper_method :valid_gcp_token - - def create - @cluster = ::Clusters::CreateService - .new(project, current_user, create_params) - .execute - - if @cluster.persisted? - redirect_to project_cluster_path(project, @cluster) - else - @user_cluster = @cluster - gcp_cluster - - render 'projects/clusters/new', locals: { active_tab: 'user' } - end - end - - private - - def create_params - params.require(:cluster).permit( - :enabled, - :name, - :environment_scope, - platform_kubernetes_attributes: [ - :namespace, - :api_url, - :token, - :ca_cert - ]).merge( - provider_type: :user, - platform_type: :kubernetes - ) - end -end diff --git a/app/controllers/projects/clusters_controller.rb b/app/controllers/projects/clusters_controller.rb index 38140fe9f27..62193257940 100644 --- a/app/controllers/projects/clusters_controller.rb +++ b/app/controllers/projects/clusters_controller.rb @@ -1,16 +1,15 @@ class Projects::ClustersController < Projects::ApplicationController - include ClustersHelper - before_action :cluster, except: [:index, :new] + before_action :cluster, except: [:index, :new, :create_gcp, :create_user] before_action :authorize_read_cluster! + before_action :generate_gcp_authorize_url, only: [:new] + before_action :validate_gcp_token, only: [:new] before_action :gcp_cluster, only: [:new] before_action :user_cluster, only: [:new] before_action :authorize_create_cluster!, only: [:new] before_action :authorize_update_cluster!, only: [:update] before_action :authorize_admin_cluster!, only: [:destroy] before_action :update_applications_status, only: [:status] - helper_method :gcp_authorize_url helper_method :token_in_session - helper_method :valid_gcp_token STATUS_POLLING_INTERVAL = 10_000 @@ -70,6 +69,38 @@ class Projects::ClustersController < Projects::ApplicationController end end + def create_gcp + @gcp_cluster = ::Clusters::CreateService + .new(project, current_user, create_gcp_cluster_params) + .execute(token_in_session) + + if @gcp_cluster.persisted? + redirect_to project_cluster_path(project, @gcp_cluster) + else + generate_gcp_authorize_url + validate_gcp_token + user_cluster + + render :new, locals: { active_tab: 'gcp' } + end + end + + def create_user + @user_cluster = ::Clusters::CreateService + .new(project, current_user, create_user_cluster_params) + .execute(token_in_session) + + if @user_cluster.persisted? + redirect_to project_cluster_path(project, @user_cluster) + else + generate_gcp_authorize_url + validate_gcp_token + gcp_cluster + + render :new, locals: { active_tab: 'user' } + end + end + private def cluster @@ -101,6 +132,80 @@ class Projects::ClustersController < Projects::ApplicationController end end + def create_gcp_cluster_params + params.require(:cluster).permit( + :enabled, + :name, + :environment_scope, + provider_gcp_attributes: [ + :gcp_project_id, + :zone, + :num_nodes, + :machine_type + ]).merge( + provider_type: :gcp, + platform_type: :kubernetes + ) + end + + def create_user_cluster_params + params.require(:cluster).permit( + :enabled, + :name, + :environment_scope, + platform_kubernetes_attributes: [ + :namespace, + :api_url, + :token, + :ca_cert + ]).merge( + provider_type: :user, + platform_type: :kubernetes + ) + end + + def generate_gcp_authorize_url + state = generate_session_key_redirect(new_project_cluster_path(@project).to_s) + + @authorize_url = GoogleApi::CloudPlatform::Client.new( + nil, callback_google_api_auth_url, + state: state).authorize_url + rescue GoogleApi::Auth::ConfigMissingError + # no-op + end + + def gcp_cluster + @gcp_cluster = ::Clusters::Cluster.new.tap do |cluster| + cluster.build_provider_gcp + end + end + + def user_cluster + @user_cluster = ::Clusters::Cluster.new.tap do |cluster| + cluster.build_platform_kubernetes + end + end + + def validate_gcp_token + @valid_gcp_token = GoogleApi::CloudPlatform::Client.new(token_in_session, nil) + .validate_token(expires_at_in_session) + end + + def token_in_session + session[GoogleApi::CloudPlatform::Client.session_key_for_token] + end + + def expires_at_in_session + @expires_at_in_session ||= + session[GoogleApi::CloudPlatform::Client.session_key_for_expires_at] + end + + def generate_session_key_redirect(uri) + GoogleApi::CloudPlatform::Client.new_session_key_for_redirect_uri do |key| + session[key] = uri + end + end + def authorize_update_cluster! access_denied! unless can?(current_user, :update_cluster, cluster) end diff --git a/app/helpers/clusters_helper.rb b/app/helpers/clusters_helper.rb index 0663ef187e8..c24d340d184 100644 --- a/app/helpers/clusters_helper.rb +++ b/app/helpers/clusters_helper.rb @@ -10,45 +10,4 @@ module ClustersHelper render 'projects/clusters/gcp_signup_offer_banner' end end - - def gcp_cluster - @gcp_cluster = ::Clusters::Cluster.new.tap do |cluster| - cluster.build_provider_gcp - end - end - - def user_cluster - @user_cluster = ::Clusters::Cluster.new.tap do |cluster| - cluster.build_platform_kubernetes - end - end - - def gcp_authorize_url - state = generate_session_key_redirect(new_project_cluster_path(@project).to_s) - - GoogleApi::CloudPlatform::Client.new( - nil, callback_google_api_auth_url, - state: state).authorize_url - rescue GoogleApi::Auth::ConfigMissingError - # no-op - end - - def generate_session_key_redirect(uri) - GoogleApi::CloudPlatform::Client.new_session_key_for_redirect_uri do |key| - session[key] = uri - end - end - - def token_in_session - session[GoogleApi::CloudPlatform::Client.session_key_for_token] - end - - def expires_at_in_session - session[GoogleApi::CloudPlatform::Client.session_key_for_expires_at] - end - - def valid_gcp_token - GoogleApi::CloudPlatform::Client.new(token_in_session, nil) - .validate_token(expires_at_in_session) - end end diff --git a/app/views/projects/clusters/gcp/_form.html.haml b/app/views/projects/clusters/gcp/_form.html.haml index a6589cca529..1494998f95f 100644 --- a/app/views/projects/clusters/gcp/_form.html.haml +++ b/app/views/projects/clusters/gcp/_form.html.haml @@ -4,9 +4,9 @@ - link_to_help_page = link_to(s_('ClusterIntegration|help page'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer') = s_('ClusterIntegration|Read our %{link_to_help_page} on Kubernetes cluster integration.').html_safe % { link_to_help_page: link_to_help_page} -%p= link_to('Select a different Google account', gcp_authorize_url) +%p= link_to('Select a different Google account', @authorize_url) -= form_for @gcp_cluster, html: { class: 'js-gke-cluster-creation prepend-top-20', data: { token: token_in_session } }, url: gcp_namespace_project_clusters_path(@project.namespace, @project), as: :cluster do |field| += form_for @gcp_cluster, html: { class: 'js-gke-cluster-creation prepend-top-20', data: { token: token_in_session } }, url: create_gcp_namespace_project_clusters_path(@project.namespace, @project), as: :cluster do |field| = form_errors(@gcp_cluster) .form-group = field.label :name, s_('ClusterIntegration|Kubernetes cluster name') diff --git a/app/views/projects/clusters/new.html.haml b/app/views/projects/clusters/new.html.haml index 11b7c363553..d3b3bf2a2fe 100644 --- a/app/views/projects/clusters/new.html.haml +++ b/app/views/projects/clusters/new.html.haml @@ -7,7 +7,7 @@ .row.prepend-top-default .col-md-3 - = render 'projects/clusters/sidebar' + = render 'sidebar' .col-md-9.js-toggle-container %ul.nav-links.nav-tabs.gitlab-tabs.nav{ role: 'tablist' } %li.nav-item{ role: 'presentation' } @@ -15,16 +15,16 @@ %span Create new Cluster on GKE %li.nav-item{ role: 'presentation' } %a.nav-link{ href: '#add-user-cluster-pane', id: 'add-user-cluster-tab', class: active_when(active_tab == 'user'), data: { toggle: 'tab' }, role: 'tab' } - %span Add existing cluster + %span Add user cluster .tab-content.gitlab-tab-content .tab-pane{ id: 'create-gcp-cluster-pane', class: active_when(active_tab == 'gcp'), role: 'tabpanel' } = render 'projects/clusters/gcp/header' - - if valid_gcp_token + - if @valid_gcp_token = render 'projects/clusters/gcp/form' - - elsif gcp_authorize_url + - elsif @authorize_url .signin-with-google - = link_to(image_tag('auth_buttons/signin_with_google.png', width: '191px'), gcp_authorize_url) + = link_to(image_tag('auth_buttons/signin_with_google.png', width: '191px'), @authorize_url) = _('or') = link_to('create a new Google account', 'https://accounts.google.com/SignUpWithoutGmail?service=cloudconsole&continue=https%3A%2F%2Fconsole.cloud.google.com%2Ffreetrial%3Futm_campaign%3D2018_cpanel%26utm_source%3Dgitlab%26utm_medium%3Dreferral', target: '_blank', rel: 'noopener noreferrer') - else diff --git a/app/views/projects/clusters/user/_form.html.haml b/app/views/projects/clusters/user/_form.html.haml index 4167c8787ff..2f090e3fb86 100644 --- a/app/views/projects/clusters/user/_form.html.haml +++ b/app/views/projects/clusters/user/_form.html.haml @@ -1,4 +1,4 @@ -= form_for @user_cluster, url: user_namespace_project_clusters_path(@project.namespace, @project), as: :cluster do |field| += form_for @user_cluster, url: create_user_namespace_project_clusters_path(@project.namespace, @project), as: :cluster do |field| = form_errors(@user_cluster) .form-group = field.label :name, s_('ClusterIntegration|Kubernetes cluster name') diff --git a/config/routes/project.rb b/config/routes/project.rb index f672291a9f1..082a1166c41 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -206,10 +206,8 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do resources :clusters, except: [:edit, :create] do collection do - scope :providers do - post '/user', to: 'clusters/user#create' - post '/gcp', to: 'clusters/gcp#create' - end + post :create_gcp + post :create_user end member do diff --git a/spec/controllers/projects/clusters/gcp_controller_spec.rb b/spec/controllers/projects/clusters/gcp_controller_spec.rb deleted file mode 100644 index acaf2a1b948..00000000000 --- a/spec/controllers/projects/clusters/gcp_controller_spec.rb +++ /dev/null @@ -1,95 +0,0 @@ -require 'spec_helper' - -describe Projects::Clusters::GcpController do - include AccessMatchersForController - include GoogleApi::CloudPlatformHelpers - - set(:project) { create(:project) } - - describe 'POST create' do - let(:params) do - { - cluster: { - name: 'new-cluster', - provider_gcp_attributes: { - gcp_project_id: '111' - } - } - } - end - - before do - allow_any_instance_of(GoogleApi::CloudPlatform::Client) - .to receive(:projects_zones_clusters_create) do - OpenStruct.new( - self_link: 'projects/gcp-project-12345/zones/us-central1-a/operations/ope-123', - status: 'RUNNING' - ) - end - end - - describe 'functionality' do - let(:user) { create(:user) } - - before do - project.add_master(user) - sign_in(user) - end - - context 'when access token is valid' do - before do - stub_google_api_validate_token - end - - it 'creates a new cluster' do - expect(ClusterProvisionWorker).to receive(:perform_async) - expect { go }.to change { Clusters::Cluster.count } - .and change { Clusters::Providers::Gcp.count } - expect(response).to redirect_to(project_cluster_path(project, project.clusters.first)) - expect(project.clusters.first).to be_gcp - expect(project.clusters.first).to be_kubernetes - end - end - - context 'when access token is expired' do - before do - stub_google_api_expired_token - end - - it 'redirects to new clusters form' do - puts described_class - expect(go).to redirect_to(new_project_cluster_path(project)) - end - end - - context 'when access token is not stored in session' do - it 'redirects to new clusters form' do - expect(go).to redirect_to(new_project_cluster_path(project)) - end - end - end - - describe 'security' do - before do - allow_any_instance_of(described_class) - .to receive(:token_in_session).and_return('token') - allow_any_instance_of(described_class) - .to receive(:expires_at_in_session).and_return(1.hour.since.to_i.to_s) - allow(WaitForClusterCreationWorker).to receive(:perform_in).and_return(nil) - end - - it { expect { go }.to be_allowed_for(:admin) } - it { expect { go }.to be_allowed_for(:owner).of(project) } - it { expect { go }.to be_allowed_for(:master).of(project) } - it { expect { go }.to be_denied_for(:developer).of(project) } - it { expect { go }.to be_denied_for(:reporter).of(project) } - it { expect { go }.to be_denied_for(:guest).of(project) } - it { expect { go }.to be_denied_for(:user) } - it { expect { go }.to be_denied_for(:external) } - end - - def go - post :create, params.merge(namespace_id: project.namespace, project_id: project) - end - end -end diff --git a/spec/controllers/projects/clusters/user_controller_spec.rb b/spec/controllers/projects/clusters/user_controller_spec.rb deleted file mode 100644 index eba1c62a1f2..00000000000 --- a/spec/controllers/projects/clusters/user_controller_spec.rb +++ /dev/null @@ -1,57 +0,0 @@ -require 'spec_helper' - -describe Projects::Clusters::UserController do - include AccessMatchersForController - - set(:project) { create(:project) } - - describe 'POST create' do - let(:params) do - { - cluster: { - name: 'new-cluster', - platform_kubernetes_attributes: { - api_url: 'http://my-url', - token: 'test', - namespace: 'aaa' - } - } - } - end - - describe 'functionality' do - let(:user) { create(:user) } - - before do - project.add_master(user) - sign_in(user) - end - - context 'when creates a cluster' do - it 'creates a new cluster' do - expect(ClusterProvisionWorker).to receive(:perform_async) - expect { go }.to change { Clusters::Cluster.count } - .and change { Clusters::Platforms::Kubernetes.count } - expect(response).to redirect_to(project_cluster_path(project, project.clusters.first)) - expect(project.clusters.first).to be_user - expect(project.clusters.first).to be_kubernetes - end - end - end - - describe 'security' do - it { expect { go }.to be_allowed_for(:admin) } - it { expect { go }.to be_allowed_for(:owner).of(project) } - it { expect { go }.to be_allowed_for(:master).of(project) } - it { expect { go }.to be_denied_for(:developer).of(project) } - it { expect { go }.to be_denied_for(:reporter).of(project) } - it { expect { go }.to be_denied_for(:guest).of(project) } - it { expect { go }.to be_denied_for(:user) } - it { expect { go }.to be_denied_for(:external) } - end - - def go - post :create, params.merge(namespace_id: project.namespace, project_id: project) - end - end -end diff --git a/spec/controllers/projects/clusters_controller_spec.rb b/spec/controllers/projects/clusters_controller_spec.rb index a3aab70ab77..e47ccdc9aea 100644 --- a/spec/controllers/projects/clusters_controller_spec.rb +++ b/spec/controllers/projects/clusters_controller_spec.rb @@ -3,7 +3,6 @@ require 'spec_helper' describe Projects::ClustersController do include AccessMatchersForController include GoogleApi::CloudPlatformHelpers - include ClustersHelper set(:project) { create(:project) } @@ -76,13 +75,12 @@ describe Projects::ClustersController do end describe 'GET new' do - describe 'functionality for gcp cluster' do + describe 'functionality for new cluster' do let(:user) { create(:user) } before do project.add_master(user) sign_in(user) - @project = project end context 'when omniauth has been configured' do @@ -95,10 +93,10 @@ describe Projects::ClustersController do allow(SecureRandom).to receive(:hex).and_return(key) end - it 'has gcp_authorize_url' do + it 'has authorize_url' do go - expect(gcp_authorize_url).to include(key) + expect(assigns(:authorize_url)).to include(key) expect(session[session_key_for_redirect_uri]).to eq(new_project_cluster_path(project)) end end @@ -108,10 +106,10 @@ describe Projects::ClustersController do stub_omniauth_setting(providers: []) end - it 'does not have gcp_authorize_url' do + it 'does not have authorize_url' do go - expect(gcp_authorize_url).to be_nil + expect(assigns(:authorize_url)).to be_nil end end @@ -132,15 +130,15 @@ describe Projects::ClustersController do stub_google_api_expired_token end - it { expect(valid_gcp_token).to be_falsey } + it { expect(@valid_gcp_token).to be_falsey } end context 'when access token is not stored in session' do - it { expect(valid_gcp_token).to be_falsey } + it { expect(@valid_gcp_token).to be_falsey } end end - describe 'functionality for user cluster' do + describe 'functionality for existing cluster' do let(:user) { create(:user) } before do @@ -171,6 +169,136 @@ describe Projects::ClustersController do end end + describe 'POST create for new cluster' do + let(:params) do + { + cluster: { + name: 'new-cluster', + provider_gcp_attributes: { + gcp_project_id: 'gcp-project-12345' + } + } + } + end + + describe 'functionality' do + let(:user) { create(:user) } + + before do + project.add_master(user) + sign_in(user) + end + + context 'when access token is valid' do + before do + stub_google_api_validate_token + end + + it 'creates a new cluster' do + expect(ClusterProvisionWorker).to receive(:perform_async) + expect { go }.to change { Clusters::Cluster.count } + .and change { Clusters::Providers::Gcp.count } + expect(response).to redirect_to(project_cluster_path(project, project.clusters.first)) + expect(project.clusters.first).to be_gcp + expect(project.clusters.first).to be_kubernetes + end + end + + context 'when access token is expired' do + before do + stub_google_api_expired_token + end + + it { expect(@valid_gcp_token).to be_falsey } + end + + context 'when access token is not stored in session' do + it { expect(@valid_gcp_token).to be_falsey } + end + end + + describe 'security' do + before do + allow_any_instance_of(described_class) + .to receive(:token_in_session).and_return('token') + allow_any_instance_of(described_class) + .to receive(:expires_at_in_session).and_return(1.hour.since.to_i.to_s) + allow_any_instance_of(GoogleApi::CloudPlatform::Client) + .to receive(:projects_zones_clusters_create) do + OpenStruct.new( + self_link: 'projects/gcp-project-12345/zones/us-central1-a/operations/ope-123', + status: 'RUNNING' + ) + end + + allow(WaitForClusterCreationWorker).to receive(:perform_in).and_return(nil) + end + + it { expect { go }.to be_allowed_for(:admin) } + it { expect { go }.to be_allowed_for(:owner).of(project) } + it { expect { go }.to be_allowed_for(:master).of(project) } + it { expect { go }.to be_denied_for(:developer).of(project) } + it { expect { go }.to be_denied_for(:reporter).of(project) } + it { expect { go }.to be_denied_for(:guest).of(project) } + it { expect { go }.to be_denied_for(:user) } + it { expect { go }.to be_denied_for(:external) } + end + + def go + post :create_gcp, params.merge(namespace_id: project.namespace, project_id: project) + end + end + + describe 'POST create for existing cluster' do + let(:params) do + { + cluster: { + name: 'new-cluster', + platform_kubernetes_attributes: { + api_url: 'http://my-url', + token: 'test', + namespace: 'aaa' + } + } + } + end + + describe 'functionality' do + let(:user) { create(:user) } + + before do + project.add_master(user) + sign_in(user) + end + + context 'when creates a cluster' do + it 'creates a new cluster' do + expect(ClusterProvisionWorker).to receive(:perform_async) + expect { go }.to change { Clusters::Cluster.count } + .and change { Clusters::Platforms::Kubernetes.count } + expect(response).to redirect_to(project_cluster_path(project, project.clusters.first)) + expect(project.clusters.first).to be_user + expect(project.clusters.first).to be_kubernetes + end + end + end + + describe 'security' do + it { expect { go }.to be_allowed_for(:admin) } + it { expect { go }.to be_allowed_for(:owner).of(project) } + it { expect { go }.to be_allowed_for(:master).of(project) } + it { expect { go }.to be_denied_for(:developer).of(project) } + it { expect { go }.to be_denied_for(:reporter).of(project) } + it { expect { go }.to be_denied_for(:guest).of(project) } + it { expect { go }.to be_denied_for(:user) } + it { expect { go }.to be_denied_for(:external) } + end + + def go + post :create_user, params.merge(namespace_id: project.namespace, project_id: project) + end + end + describe 'GET status' do let(:cluster) { create(:cluster, :providing_by_gcp, projects: [project]) } diff --git a/spec/features/projects/clusters/gcp_spec.rb b/spec/features/projects/clusters/gcp_spec.rb index 31c0735f577..0ebe3459a65 100644 --- a/spec/features/projects/clusters/gcp_spec.rb +++ b/spec/features/projects/clusters/gcp_spec.rb @@ -20,10 +20,6 @@ feature 'Gcp Cluster', :js do .to receive(:token_in_session).and_return('token') allow_any_instance_of(Projects::ClustersController) .to receive(:expires_at_in_session).and_return(1.hour.since.to_i.to_s) - allow_any_instance_of(Projects::Clusters::GcpController) - .to receive(:token_in_session).and_return('token') - allow_any_instance_of(Projects::Clusters::GcpController) - .to receive(:expires_at_in_session).and_return(1.hour.since.to_i.to_s) end context 'when user does not have a cluster and visits cluster index page' do From 3e5899ad9839d3ef8a920da8b6948ba4d0fa2c06 Mon Sep 17 00:00:00 2001 From: Dennis Tang Date: Tue, 19 Jun 2018 15:10:40 +0000 Subject: [PATCH 42/46] Update index.js --- app/assets/javascripts/pages/projects/index.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/pages/projects/index.js b/app/assets/javascripts/pages/projects/index.js index ba8bc5bc5fa..0658ae8ec23 100644 --- a/app/assets/javascripts/pages/projects/index.js +++ b/app/assets/javascripts/pages/projects/index.js @@ -5,7 +5,11 @@ import ShortcutsNavigation from '../../shortcuts_navigation'; document.addEventListener('DOMContentLoaded', () => { const page = document.body.dataset.page; - const newClusterViews = ['projects:clusters:new', 'projects:clusters:create']; + const newClusterViews = [ + 'projects:clusters:new', + 'projects:clusters:create_gcp', + 'projects:clusters:create_user', + ]; if (newClusterViews.indexOf(page) > -1) { gcpSignupOffer(); From 4481e471bff586e201646c9cf718ad1e110605ea Mon Sep 17 00:00:00 2001 From: Dennis Tang Date: Wed, 20 Jun 2018 22:11:57 +0200 Subject: [PATCH 43/46] fix a couple oopsies --- app/views/projects/clusters/new.html.haml | 2 +- qa/qa/page/project/operations/kubernetes/add.rb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/views/projects/clusters/new.html.haml b/app/views/projects/clusters/new.html.haml index d3b3bf2a2fe..a38003f5750 100644 --- a/app/views/projects/clusters/new.html.haml +++ b/app/views/projects/clusters/new.html.haml @@ -15,7 +15,7 @@ %span Create new Cluster on GKE %li.nav-item{ role: 'presentation' } %a.nav-link{ href: '#add-user-cluster-pane', id: 'add-user-cluster-tab', class: active_when(active_tab == 'user'), data: { toggle: 'tab' }, role: 'tab' } - %span Add user cluster + %span Add existing cluster .tab-content.gitlab-tab-content .tab-pane{ id: 'create-gcp-cluster-pane', class: active_when(active_tab == 'gcp'), role: 'tabpanel' } diff --git a/qa/qa/page/project/operations/kubernetes/add.rb b/qa/qa/page/project/operations/kubernetes/add.rb index 9b3c482fa6c..b89ea347909 100644 --- a/qa/qa/page/project/operations/kubernetes/add.rb +++ b/qa/qa/page/project/operations/kubernetes/add.rb @@ -5,11 +5,11 @@ module QA module Kubernetes class Add < Page::Base view 'app/views/projects/clusters/new.html.haml' do - element :add_kubernetes_cluster_button, "link_to s_('ClusterIntegration|Add an existing Kubernetes cluster')" + element :add_kubernetes_cluster_button, "link_to s_('ClusterIntegration|Add existing cluster')" end def add_existing_cluster - click_on 'Add an existing Kubernetes cluster' + click_on 'Add existing cluster' end end end From 203d1026008effeeba5c1f98dba768448473f9fe Mon Sep 17 00:00:00 2001 From: Dennis Tang Date: Wed, 20 Jun 2018 21:42:08 +0000 Subject: [PATCH 44/46] fix qa selectors --- qa/qa/page/project/operations/kubernetes/add.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qa/qa/page/project/operations/kubernetes/add.rb b/qa/qa/page/project/operations/kubernetes/add.rb index b89ea347909..11ebe10fb18 100644 --- a/qa/qa/page/project/operations/kubernetes/add.rb +++ b/qa/qa/page/project/operations/kubernetes/add.rb @@ -5,7 +5,7 @@ module QA module Kubernetes class Add < Page::Base view 'app/views/projects/clusters/new.html.haml' do - element :add_kubernetes_cluster_button, "link_to s_('ClusterIntegration|Add existing cluster')" + element :add_existing_cluster_button, "Add existing cluster" end def add_existing_cluster From 3c51d36dbecf815273f23846c61e7d4c4971165f Mon Sep 17 00:00:00 2001 From: Dennis Tang Date: Thu, 21 Jun 2018 09:17:18 +0000 Subject: [PATCH 45/46] Update index.md --- doc/user/project/clusters/index.md | 1240 +++++++++++++++++++--------- 1 file changed, 831 insertions(+), 409 deletions(-) diff --git a/doc/user/project/clusters/index.md b/doc/user/project/clusters/index.md index 67f8ec4f1d8..19d5a57751b 100644 --- a/doc/user/project/clusters/index.md +++ b/doc/user/project/clusters/index.md @@ -1,423 +1,845 @@ -# Connecting GitLab with a Kubernetes cluster +# Auto DevOps -> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/35954) in GitLab 10.1. - -Connect your project to Google Kubernetes Engine (GKE) or an existing Kubernetes -cluster in a few steps. - -## Overview - -With a Kubernetes cluster associated to your project, you can use -[Review Apps](../../../ci/review_apps/index.md), deploy your applications, run -your pipelines, and much more, in an easy way. - -There are two options when adding a new cluster to your project; either associate -your account with Google Kubernetes Engine (GKE) so that you can [create new -clusters](#adding-and-creating-a-new-gke-cluster-via-gitlab) from within GitLab, -or provide the credentials to an [existing Kubernetes cluster](#adding-an-existing-kubernetes-cluster). - -## Adding and creating a new GKE cluster via GitLab - -NOTE: **Note:** -You need Maintainer [permissions] and above to access the Kubernetes page. - -Before proceeding, make sure the following requirements are met: - -* The [Google authentication integration](../../../integration/google.md) must - be enabled in GitLab at the instance level. If that's not the case, ask your - GitLab administrator to enable it. -* Your associated Google account must have the right privileges to manage - clusters on GKE. That would mean that a [billing - account](https://cloud.google.com/billing/docs/how-to/manage-billing-account) - must be set up and that you have to have permissions to access it. - -- You must have Maintainer [permissions] in order to be able to access the - **Kubernetes** page. - -* You must have [Cloud Billing API](https://cloud.google.com/billing/) enabled -* You must have [Resource Manager - API](https://cloud.google.com/resource-manager/) - -If all of the above requirements are met, you can proceed to create and add a -new Kubernetes cluster that will be hosted on GKE to your project: - -1. Navigate to your project's **Operations > Kubernetes** page. -1. Click on **Add Kubernetes cluster**. -1. Click on **Create with Google Kubernetes Engine**. -1. Connect your Google account if you haven't done already by clicking the - **Sign in with Google** button. -1. Fill in the requested values: - -* **Kubernetes cluster name** - The name you wish to give the cluster. -* **Environment scope** - The [associated environment](#setting-the-environment-scope) to this cluster. -* **Google Cloud Platform project** - The project you created in your GCP - console that will host the Kubernetes cluster. This must **not** be confused - with the project ID. Learn more about [Google Cloud Platform projects](https://cloud.google.com/resource-manager/docs/creating-managing-projects). -* **Zone** - The [zone](https://cloud.google.com/compute/docs/regions-zones/) - under which the cluster will be created. -* **Number of nodes** - The number of nodes you wish the cluster to have. -* **Machine type** - The [machine type](https://cloud.google.com/compute/docs/machine-types) - of the Virtual Machine instance that the cluster will be based on. - -1. Finally, click the **Create Kubernetes cluster** button. - -After a few moments, your cluster should be created. If something goes wrong, -you will be notified. - -You can now proceed to install some pre-defined applications and then -enable the Cluster integration. - -## Adding an existing Kubernetes cluster - -NOTE: **Note:** -You need Maintainer [permissions] and above to access the Kubernetes page. - -To add an existing Kubernetes cluster to your project: - -1. Navigate to your project's **Operations > Kubernetes** page. -1. Click on **Add Kubernetes cluster**. -1. Click on **Add an existing Kubernetes cluster** and fill in the details: - * **Kubernetes cluster name** (required) - The name you wish to give the cluster. - * **Environment scope** (required)- The - [associated environment](#setting-the-environment-scope) to this cluster. - - **API URL** (required) - - It's the URL that GitLab uses to access the Kubernetes API. Kubernetes - exposes several APIs, we want the "base" URL that is common to all of them, - e.g., `https://kubernetes.example.com` rather than `https://kubernetes.example.com/api/v1`. - - **CA certificate** (optional) - - If the API is using a self-signed TLS certificate, you'll also need to include - the `ca.crt` contents here. - - **Token** - - GitLab authenticates against Kubernetes using service tokens, which are - scoped to a particular `namespace`. If you don't have a service token yet, - you can follow the - [Kubernetes documentation](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/) - to create one. You can also view or create service tokens in the - [Kubernetes dashboard](https://kubernetes.io/docs/tasks/access-application-cluster/web-ui-dashboard/#config) - (under **Config > Secrets**). - - **Project namespace** (optional) - The following apply: - * By default you don't have to fill it in; by leaving it blank, GitLab will - create one for you. - * Each project should have a unique namespace. - * The project namespace is not necessarily the namespace of the secret, if - you're using a secret with broader permissions, like the secret from `default`. - * You should **not** use `default` as the project namespace. - * If you or someone created a secret specifically for the project, usually - with limited permissions, the secret's namespace and project namespace may - be the same. -1. Finally, click the **Add Kubernetes cluster** button. - -After a few moments, your cluster should be created. If something goes wrong, -you will be notified. - -You can now proceed to install some pre-defined applications and then -enable the Kubernetes cluster integration. - -## Security implications - -CAUTION: **Important:** -The whole cluster security is based on a model where [developers](../../permissions.md) -are trusted, so **only trusted users should be allowed to control your clusters**. - -The default cluster configuration grants access to a wide set of -functionalities needed to successfully build and deploy a containerized -application. Bare in mind that the same credentials are used for all the -applications running on the cluster. - -When GitLab creates the cluster, it enables and uses the legacy -[Attribute-based access control (ABAC)](https://kubernetes.io/docs/admin/authorization/abac/). -The newer [RBAC](https://kubernetes.io/docs/admin/authorization/rbac/) -authorization will be supported in a -[future release](https://gitlab.com/gitlab-org/gitlab-ce/issues/29398). - -### Security of GitLab Runners - -GitLab Runners have the [privileged mode](https://docs.gitlab.com/runner/executors/docker.html#the-privileged-mode) -enabled by default, which allows them to execute special commands and running -Docker in Docker. This functionality is needed to run some of the [Auto DevOps] -jobs. This implies the containers are running in privileged mode and you should, -therefore, be aware of some important details. - -The privileged flag gives all capabilities to the running container, which in -turn can do almost everything that the host can do. Be aware of the -inherent security risk associated with performing `docker run` operations on -arbitrary images as they effectively have root access. - -If you don't want to use GitLab Runner in privileged mode, first make sure that -you don't have it installed via the applications, and then use the -[Runner's Helm chart](../../../install/kubernetes/gitlab_runner_chart.md) to -install it manually. - -## Installing applications - -GitLab provides a one-click install for various applications which will be -added directly to your configured cluster. Those applications are needed for -[Review Apps](../../../ci/review_apps/index.md) and [deployments](../../../ci/environments.md). - -<<<<<<< HEAD -| Application | GitLab version | Description | -| --------------------------------------------------------------------------- | :------------: | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [Helm Tiller](https://docs.helm.sh/) | 10.2+ | Helm is a package manager for Kubernetes and is required to install all the other applications. It will be automatically installed as a dependency when you try to install a different app. It is installed in its own pod inside the cluster which can run the `helm` CLI in a safe environment. | -| [Ingress](https://kubernetes.io/docs/concepts/services-networking/ingress/) | 10.2+ | Ingress can provide load balancing, SSL termination, and name-based virtual hosting. It acts as a web proxy for your applications and is useful if you want to use [Auto DevOps] or deploy your own web apps. | -| [Prometheus](https://prometheus.io/docs/introduction/overview/) | 10.4+ | Prometheus is an open-source monitoring and alerting system useful to supervise your deployed applications | -| [GitLab Runner](https://docs.gitlab.com/runner/) | 10.6+ | GitLab Runner is the open source project that is used to run your jobs and send the results back to GitLab. It is used in conjunction with [GitLab CI/CD](https://about.gitlab.com/features/gitlab-ci-cd/), the open-source continuous integration service included with GitLab that coordinates the jobs. When installing the GitLab Runner via the applications, it will run in **privileged mode** by default. Make sure you read the [security implications](#security-implications) before doing so. | -| [JupyterHub](http://jupyter.org/) | 11.0+ | The Jupyter Notebook is an open-source web application that allows you to create and share documents that contain live code, equations, visualizations and narrative text. | -======= -| Application | GitLab version | Description | -| ----------- | :------------: | ----------- | -| [Helm Tiller](https://docs.helm.sh/) | 10.2+ | Helm is a package manager for Kubernetes and is required to install all the other applications. It will be automatically installed as a dependency when you try to install a different app. It is installed in its own pod inside the cluster which can run the `helm` CLI in a safe environment. | -| [Ingress](https://kubernetes.io/docs/concepts/services-networking/ingress/) | 10.2+ | Ingress can provide load balancing, SSL termination, and name-based virtual hosting. It acts as a web proxy for your applications and is useful if you want to use [Auto DevOps] or deploy your own web apps. | -| [Prometheus](https://prometheus.io/docs/introduction/overview/) | 10.4+ | Prometheus is an open-source monitoring and alerting system useful to supervise your deployed applications | -| [GitLab Runner](https://docs.gitlab.com/runner/) | 10.6+ | GitLab Runner is the open source project that is used to run your jobs and send the results back to GitLab. It is used in conjunction with [GitLab CI/CD](https://about.gitlab.com/features/gitlab-ci-cd/), the open-source continuous integration service included with GitLab that coordinates the jobs. When installing the GitLab Runner via the applications, it will run in **privileged mode** by default. Make sure you read the [security implications](#security-implications) before doing so. | -| [JupyterHub](http://jupyter.org/) | 11.0+ | [JupyterHub](https://jupyterhub.readthedocs.io/en/stable/) is a multi-user service for managing notebooks across a team. [Jupyter Notebooks](https://jupyter-notebook.readthedocs.io/en/latest/) provide a web-based interactive programming environment used for data analysis, visualization, and machine learning. **Note**: Authentication will be enabled for any user of the GitLab server via OAuth2. HTTPS will be supported in a future release. | - -> > > > > > > origin/master - -## Getting the external IP address - -NOTE: **Note:** -You need a load balancer installed in your cluster in order to obtain the -external IP address with the following procedure. It can be deployed using the -[**Ingress** application](#installing-applications). - -In order to publish your web application, you first need to find the external IP -address associated to your load balancer. - -### Let GitLab fetch the IP address - -> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17052) in GitLab 10.6. - -If you installed the Ingress [via the **Applications**](#installing-applications), -you should see the Ingress IP address on this same page within a few minutes. -If you don't see this, GitLab might not be able to determine the IP address of -your ingress application in which case you should manually determine it. - -### Manually determining the IP address - -If the cluster is on GKE, click on the **Google Kubernetes Engine** link in the -**Advanced settings**, or go directly to the -[Google Kubernetes Engine dashboard](https://console.cloud.google.com/kubernetes/) -and select the proper project and cluster. Then click on **Connect** and execute -the `gcloud` command in a local terminal or using the **Cloud Shell**. - -If the cluster is not on GKE, follow the specific instructions for your -Kubernetes provider to configure `kubectl` with the right credentials. - -If you installed the Ingress [via the **Applications**](#installing-applications), -run the following command: - -```bash -kubectl get svc --namespace=gitlab-managed-apps ingress-nginx-ingress-controller -o jsonpath='{.status.loadBalancer.ingress[0].ip} ' -``` - -Otherwise, you can list the IP addresses of all load balancers: - -```bash -kubectl get svc --all-namespaces -o jsonpath='{range.items[?(@.status.loadBalancer.ingress)]}{.status.loadBalancer.ingress[*].ip} ' -``` - -> **Note**: Some Kubernetes clusters return a hostname instead, like [Amazon EKS](https://aws.amazon.com/eks/). For these platforms, run: -> -> ```bash -> kubectl get service ingress-nginx-ingress-controller -n gitlab-managed-apps -o jsonpath="{.status.loadBalancer.ingress[0].hostname}"`. -> ``` - -The output is the external IP address of your cluster. This information can then -be used to set up DNS entries and forwarding rules that allow external access to -your deployed applications. - -### Using a static IP - -By default, an ephemeral external IP address is associated to the cluster's load -balancer. If you associate the ephemeral IP with your DNS and the IP changes, -your apps will not be able to be reached, and you'd have to change the DNS -record again. In order to avoid that, you should change it into a static -reserved IP. - -[Read how to promote an ephemeral external IP address in GKE.](https://cloud.google.com/compute/docs/ip-addresses/reserve-static-external-ip-address#promote_ephemeral_ip) - -### Pointing your DNS at the cluster IP - -Once you've set up the static IP, you should associate it to a [wildcard DNS -record](https://en.wikipedia.org/wiki/Wildcard_DNS_record), in order to be able -to reach your apps. This heavily depends on your domain provider, but in case -you aren't sure, just create an A record with a wildcard host like -`*.example.com.`. - -## Setting the environment scope - -NOTE: **Note:** -This is only available for [GitLab Premium][ee] where you can add more than -one Kubernetes cluster. - -When adding more than one Kubernetes clusters to your project, you need to -differentiate them with an environment scope. The environment scope associates -clusters and [environments](../../../ci/environments.md) in an 1:1 relationship -similar to how the -[environment-specific variables](../../../ci/variables/README.md#limiting-environment-scopes-of-variables) -work. - -The default environment scope is `*`, which means all jobs, regardless of their -environment, will use that cluster. Each scope can only be used by a single -cluster in a project, and a validation error will occur if otherwise. -Also, jobs that don't have an environment keyword set will not be able to access any cluster. - ---- - -For example, let's say the following Kubernetes clusters exist in a project: - -| Cluster | Environment scope | -| ----------- | ----------------- | -| Development | `*` | -| Staging | `staging/*` | -| Production | `production/*` | - -And the following environments are set in [`.gitlab-ci.yml`](../../../ci/yaml/README.md): - -```yaml -stages: -- test -- deploy - -test: - stage: test - script: sh test - -deploy to staging: - stage: deploy - script: make deploy - environment: - name: staging/$CI_COMMIT_REF_NAME - url: https://staging.example.com/ - -deploy to production: - stage: deploy - script: make deploy - environment: - name: production/$CI_COMMIT_REF_NAME - url: https://example.com/ -``` - -The result will then be: - -* The development cluster will be used for the "test" job. -* The staging cluster will be used for the "deploy to staging" job. -* The production cluster will be used for the "deploy to production" job. - -## Multiple Kubernetes clusters - -> Introduced in [GitLab Premium][ee] 10.3. - -With GitLab Premium, you can associate more than one Kubernetes clusters to your -project. That way you can have different clusters for different environments, -like dev, staging, production, etc. - -Simply add another cluster, like you did the first time, and make sure to -[set an environment scope](#setting-the-environment-scope) that will -differentiate the new cluster with the rest. - -## Deployment variables - -The Kubernetes cluster integration exposes the following -[deployment variables](../../../ci/variables/README.md#deployment-variables) in the -GitLab CI/CD build environment. - -| Variable | Description | -| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `KUBE_URL` | Equal to the API URL. | -| `KUBE_TOKEN` | The Kubernetes token. | -| `KUBE_NAMESPACE` | The Kubernetes namespace is auto-generated if not specified. The default value is `-`. You can overwrite it to use different one if needed, otherwise the `KUBE_NAMESPACE` variable will receive the default value. | -| `KUBE_CA_PEM_FILE` | Only present if a custom CA bundle was specified. Path to a file containing PEM data. | -| `KUBE_CA_PEM` | (**deprecated**) Only if a custom CA bundle was specified. Raw PEM data. | -| `KUBECONFIG` | Path to a file containing `kubeconfig` for this deployment. CA bundle would be embedded if specified. | - -## Enabling or disabling the Kubernetes cluster integration - -After you have successfully added your cluster information, you can enable the -Kubernetes cluster integration: - -1. Click the "Enabled/Disabled" switch -1. Hit **Save** for the changes to take effect - -You can now start using your Kubernetes cluster for your deployments. - -To disable the Kubernetes cluster integration, follow the same procedure. - -## Removing the Kubernetes cluster integration - -NOTE: **Note:** -You need Maintainer [permissions] and above to remove a Kubernetes cluster integration. - -NOTE: **Note:** -When you remove a cluster, you only remove its relation to GitLab, not the -cluster itself. To remove the cluster, you can do so by visiting the GKE -dashboard or using `kubectl`. - -To remove the Kubernetes cluster integration from your project, simply click on the -**Remove integration** button. You will then be able to follow the procedure -and add a Kubernetes cluster again. - -## What you can get with the Kubernetes integration - -Here's what you can do with GitLab if you enable the Kubernetes integration. - -### Deploy Boards - -> Available in [GitLab Premium][ee]. - -GitLab's Deploy Boards offer a consolidated view of the current health and -status of each CI [environment](../../../ci/environments.md) running on Kubernetes, -displaying the status of the pods in the deployment. Developers and other -teammates can view the progress and status of a rollout, pod by pod, in the -workflow they already use without any need to access Kubernetes. - -[> Read more about Deploy Boards](https://docs.gitlab.com/ee/user/project/deploy_boards.html) - -### Canary Deployments - -> Available in [GitLab Premium][ee]. - -Leverage [Kubernetes' Canary deployments](https://kubernetes.io/docs/concepts/cluster-administration/manage-deployment/#canary-deployments) -and visualize your canary deployments right inside the Deploy Board, without -the need to leave GitLab. - -[> Read more about Canary Deployments](https://docs.gitlab.com/ee/user/project/canary_deployments.html) - -### Kubernetes monitoring - -Automatically detect and monitor Kubernetes metrics. Automatic monitoring of -[NGINX ingress](../integrations/prometheus_library/nginx.md) is also supported. - -[> Read more about Kubernetes monitoring](../integrations/prometheus_library/kubernetes.md) - -### Auto DevOps +> [Introduced][ce-37115] in GitLab 10.0. Auto DevOps automatically detects, builds, tests, deploys, and monitors your applications. -To make full use of Auto DevOps(Auto Deploy, Auto Review Apps, and Auto Monitoring) -you will need the Kubernetes project integration enabled. +## Overview -[> Read more about Auto DevOps](../../../topics/autodevops/index.md) +With Auto DevOps, the software development process becomes easier to set up +as every project can have a complete workflow from verification to monitoring +without needing to configure anything. Just push your code and GitLab takes +care of everything else. This makes it easier to start new projects and brings +consistency to how applications are set up throughout a company. -### Web terminals +## Comparison to application platforms and PaaS + +Auto DevOps provides functionality described by others as an application +platform or as a Platform as a Service (PaaS). It takes inspiration from the +innovative work done by [Heroku](https://www.heroku.com/) and goes beyond it +in a couple of ways: + +1. Auto DevOps works with any Kubernetes cluster, you're not limited to running + on GitLab's infrastructure (note that many features also work without Kubernetes). +1. There is no additional cost (no markup on the infrastructure costs), and you + can use a self-hosted Kubernetes cluster or Containers as a Service on any + public cloud (for example [Google Kubernetes Engine](https://cloud.google.com/kubernetes-engine/)). +1. Auto DevOps has more features including security testing, performance testing, + and code quality testing. +1. It offers an incremental graduation path. If you need advanced customizations + you can start modifying the templates without having to start over on a + completely different platform. + +## Features + +Comprised of a set of stages, Auto DevOps brings these best practices to your +project in an easy and automatic way: + +1. [Auto Build](#auto-build) +1. [Auto Test](#auto-test) +1. [Auto Code Quality](#auto-code-quality) +1. [Auto SAST (Static Application Security Testing)](#auto-sast) +1. [Auto Dependency Scanning](#auto-dependency-scanning) +1. [Auto License Management](#auto-license-management) +1. [Auto Container Scanning](#auto-container-scanning) +1. [Auto Review Apps](#auto-review-apps) +1. [Auto DAST (Dynamic Application Security Testing)](#auto-dast) +1. [Auto Deploy](#auto-deploy) +1. [Auto Browser Performance Testing](#auto-browser-performance-testing) +1. [Auto Monitoring](#auto-monitoring) + +As Auto DevOps relies on many different components, it's good to have a basic +knowledge of the following: + +- [Kubernetes](https://kubernetes.io/docs/home/) +- [Helm](https://docs.helm.sh/) +- [Docker](https://docs.docker.com) +- [GitLab Runner](https://docs.gitlab.com/runner/) +- [Prometheus](https://prometheus.io/docs/introduction/overview/) + +Auto DevOps provides great defaults for all the stages; you can, however, +[customize](#customizing) almost everything to your needs. + +For an overview on the creation of Auto DevOps, read the blog post [From 2/3 of the Self-Hosted Git Market, to the Next-Generation CI System, to Auto DevOps](https://about.gitlab.com/2017/06/29/whats-next-for-gitlab-ci/). + +## Requirements + +TIP: **Tip:** +For self-hosted installations, the easiest way to make use of Auto DevOps is to +install GitLab inside a Kubernetes cluster using the [GitLab Omnibus Helm Chart] +which automatically installs and configures everything you need! + +To make full use of Auto DevOps, you will need: + +1. **GitLab Runner** (needed for all stages) - Your Runner needs to be + configured to be able to run Docker. Generally this means using the + [Docker](https://docs.gitlab.com/runner/executors/docker.html) or [Kubernetes + executor](https://docs.gitlab.com/runner/executors/kubernetes.html), with + [privileged mode enabled](https://docs.gitlab.com/runner/executors/docker.html#use-docker-in-docker-with-privileged-mode). + The Runners do not need to be installed in the Kubernetes cluster, but the + Kubernetes executor is easy to use and is automatically autoscaling. + Docker-based Runners can be configured to autoscale as well, using [Docker + Machine](https://docs.gitlab.com/runner/install/autoscaling.html). Runners + should be registered as [shared Runners](../../ci/runners/README.md#registering-a-shared-runner) + for the entire GitLab instance, or [specific Runners](../../ci/runners/README.md#registering-a-specific-runner) + that are assigned to specific projects. +1. **Base domain** (needed for Auto Review Apps and Auto Deploy) - You will need + a domain configured with wildcard DNS which is gonna be used by all of your + Auto DevOps applications. [Read the specifics](#auto-devops-base-domain). +1. **Kubernetes** (needed for Auto Review Apps, Auto Deploy, and Auto Monitoring) - + To enable deployments, you will need Kubernetes 1.5+. You need a [Kubernetes cluster][kubernetes-clusters] + for the project, or a Kubernetes [default service template](../../user/project/integrations/services_templates.md) + for the entire GitLab installation. + 1. **A load balancer** - You can use NGINX ingress by deploying it to your + Kubernetes cluster using the + [`nginx-ingress`](https://github.com/kubernetes/charts/tree/master/stable/nginx-ingress) + Helm chart. + 1. **Wildcard TLS termination** - You can deploy the + [`kube-lego`](https://github.com/kubernetes/charts/tree/master/stable/kube-lego) + Helm chart to your Kubernetes cluster to automatically issue certificates + for your domains using Let's Encrypt. +1. **Prometheus** (needed for Auto Monitoring) - To enable Auto Monitoring, you + will need Prometheus installed somewhere (inside or outside your cluster) and + configured to scrape your Kubernetes cluster. To get response metrics + (in addition to system metrics), you need to + [configure Prometheus to monitor NGINX](../../user/project/integrations/prometheus_library/nginx_ingress.md#configuring-prometheus-to-monitor-for-nginx-ingress-metrics). + The [Prometheus service](../../user/project/integrations/prometheus.md) + integration needs to be enabled for the project, or enabled as a + [default service template](../../user/project/integrations/services_templates.md) + for the entire GitLab installation. NOTE: **Note:** -Introduced in GitLab 8.15. You must be the project owner or have `maintainer` permissions -to use terminals. Support is limited to the first container in the -first pod of your environment. +If you do not have Kubernetes or Prometheus installed, then Auto Review Apps, +Auto Deploy, and Auto Monitoring will be silently skipped. -When enabled, the Kubernetes service adds [web terminal](../../../ci/environments.md#web-terminals) -support to your [environments](../../../ci/environments.md). This is based on the `exec` functionality found in -Docker and Kubernetes, so you get a new shell session within your existing -containers. To use this integration, you should deploy to Kubernetes using -the deployment variables above, ensuring any pods you create are labelled with -`app=$CI_ENVIRONMENT_SLUG`. GitLab will do the rest! +## Auto DevOps base domain -## Read more +The Auto DevOps base domain is required if you want to make use of [Auto +Review Apps](#auto-review-apps) and [Auto Deploy](#auto-deploy). It can be defined +in three places: -* [Connecting and deploying to an Amazon EKS cluster](eks_and_gitlab/index.md) +- either under the project's CI/CD settings while [enabling Auto DevOps](#enabling-auto-devops) +- or in instance-wide settings in the **admin area > Settings** under the "Continuous Integration and Delivery" section +- or at the project or group level as a variable: `AUTO_DEVOPS_DOMAIN` (required if you want to use [multiple clusters](#using-multiple-kubernetes-clusters)) -[permissions]: ../../permissions.md +A wildcard DNS A record matching the base domain(s) is required, for example, +given a base domain of `example.com`, you'd need a DNS entry like: + +``` +*.example.com 3600 A 1.2.3.4 +``` + +In this case, `example.com` is the domain name under which the deployed apps will be served, +and `1.2.3.4` is the IP address of your load balancer; generally NGINX +([see requirements](#requirements)). How to set up the DNS record is beyond +the scope of this document; you should check with your DNS provider. + +Alternatively you can use free public services like [nip.io](http://nip.io) or +[nip.io](http://nip.io) which provide automatic wildcard DNS without any +configuration. Just set the Auto DevOps base domain to `1.2.3.4.nip.io` or +`1.2.3.4.nip.io`. + +Once set up, all requests will hit the load balancer, which in turn will route +them to the Kubernetes pods that run your application(s). + +NOTE: **Note:** +If GitLab is installed using the [GitLab Omnibus Helm Chart], there are two +options: provide a static IP, or have one assigned. For more information see the +relevant docs on the [network prerequisites](../../install/kubernetes/gitlab_omnibus.md#networking-prerequisites). + +## Using multiple Kubernetes clusters **[PREMIUM]** + +When using Auto DevOps, you may want to deploy different environments to +different Kubernetes clusters. This is possible due to the 1:1 connection that +[exists between them](../../user/project/clusters/index.md#multiple-kubernetes-clusters). + +In the [Auto DevOps template](https://gitlab.com/gitlab-org/gitlab-ci-yml/blob/master/Auto-DevOps.gitlab-ci.yml) +(used behind the scenes by Auto DevOps), there are currently 3 defined environment names that you need to know: + +- `review/` (every environment starting with `review/`) +- `staging` +- `production` + +Those environments are tied to jobs that use [Auto Deploy](#auto-deploy), so +except for the environment scope, they would also need to have a different +domain they would be deployed to. This is why you need to define a separate +`AUTO_DEVOPS_DOMAIN` variable for all the above +[based on the environment](../../ci/variables/README.md#limiting-environment-scopes-of-variables). + +The following table is an example of how the three different clusters would +be configured. + +| Cluster name | Cluster environment scope | `AUTO_DEVOPS_DOMAIN` variable value | Variable environment scope | Notes | +| ------------ | -------------- | ----------------------------- | ------------- | ------ | +| review | `review/*` | `review.example.com` | `review/*` | The review cluster which will run all [Review Apps](../../ci/review_apps/index.md). `*` is a wildcard, which means it will be used by every environment name starting with `review/`. | +| staging | `staging` | `staging.example.com` | `staging` | (Optional) The staging cluster which will run the deployments of the staging environments. You need to [enable it first](#deploy-policy-for-staging-and-production-environments). | +| production | `production` | `example.com` | `production` | The production cluster which will run the deployments of the production environment. You can use [incremental rollouts](#incremental-rollout-to-production). | + +To add a different cluster for each environment: + +1. Navigate to your project's **Operations > Kubernetes** and create the Kubernetes clusters + with their respective environment scope as described from the table above. + + ![Auto DevOps multiple clusters](img/autodevops_multiple_clusters.png) + +1. After the clusters are created, navigate to each one and install Helm Tiller + and Ingress. +1. Make sure you have [configured your DNS](#auto-devops-base-domain) with the + specified Auto DevOps domains. +1. Navigate to your project's **Settings > CI/CD > Variables** and add + the `AUTO_DEVOPS_DOMAIN` variables with their respective environment + scope. + + ![Auto DevOps domain variables](img/autodevops_domain_variables.png) + +Now that all is configured, you can test your setup by creating a merge request +and verifying that your app is deployed as a review app in the Kubernetes +cluster with the `review/*` environment scope. Similarly, you can check the +other environments. + +## Quick start + +If you are using GitLab.com, see our [quick start guide](quick_start_guide.md) +for using Auto DevOps with GitLab.com and an external Kubernetes cluster on +Google Cloud. + +## Enabling Auto DevOps + +If you haven't done already, read the [requirements](#requirements) to make +full use of Auto DevOps. If this is your fist time, we recommend you follow the +[quick start guide](quick_start_guide.md). + +To enable Auto DevOps to your project: + +1. Check that your project doesn't have a `.gitlab-ci.yml`, or remove it otherwise +1. Go to your project's **Settings > CI/CD > Auto DevOps** +1. Select "Enable Auto DevOps" +1. Optionally, but recommended, add in the [base domain](#auto-devops-base-domain) + that will be used by Kubernetes to [deploy your application](#auto-deploy) + and choose the [deployment strategy](#deployment-strategy) +1. Hit **Save changes** for the changes to take effect + +Once saved, an Auto DevOps pipeline will be triggered on the default branch. + +NOTE: **Note:** +For GitLab versions 10.0 - 10.2, when enabling Auto DevOps, a pipeline needs to be +manually triggered either by pushing a new commit to the repository or by visiting +`https://example.gitlab.com///pipelines/new` and creating +a new pipeline for your default branch, generally `master`. + +NOTE: **Note:** +If you are a GitLab Administrator, you can enable Auto DevOps instance wide +in **Admin Area > Settings > Continuous Integration and Deployment**. Doing that, +all the projects that haven't explicitly set an option will have Auto DevOps +enabled by default. + +### Deployment strategy + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/38542) in GitLab 11.0. + +You can change the deployment strategy used by Auto DevOps by going to your +project's **Settings > CI/CD > Auto DevOps**. + +The available options are: + +- **Continuous deployment to production** - enables [Auto Deploy](#auto-deploy) + by setting the [`STAGING_ENABLED`](#deploy-policy-for-staging-and-production-environments) and + [`INCREMENTAL_ROLLOUT_ENABLED`](#incremental-rollout-to-production) variables + to false. +- **Automatic deployment to staging, manual deployment to production** - sets the + [`STAGING_ENABLED`](#deploy-policy-for-staging-and-production-environments) and + [`INCREMENTAL_ROLLOUT_ENABLED`](#incremental-rollout-to-production) variables + to true, and the user is responsible for manually deploying to staging and production. + +## Stages of Auto DevOps + +The following sections describe the stages of Auto DevOps. Read them carefully +to understand how each one works. + +### Auto Build + +Auto Build creates a build of the application in one of two ways: + +- If there is a `Dockerfile`, it will use `docker build` to create a Docker image. +- Otherwise, it will use [Herokuish](https://github.com/gliderlabs/herokuish) + and [Heroku buildpacks](https://devcenter.heroku.com/articles/buildpacks) + to automatically detect and build the application into a Docker image. + +Either way, the resulting Docker image is automatically pushed to the +[Container Registry][container-registry] and tagged with the commit SHA. + +CAUTION: **Important:** +If you are also using Auto Review Apps and Auto Deploy and choose to provide +your own `Dockerfile`, make sure you expose your application to port +`5000` as this is the port assumed by the default Helm chart. + +### Auto Test + +Auto Test automatically runs the appropriate tests for your application using +[Herokuish](https://github.com/gliderlabs/herokuish) and [Heroku +buildpacks](https://devcenter.heroku.com/articles/buildpacks) by analyzing +your project to detect the language and framework. Several languages and +frameworks are detected automatically, but if your language is not detected, +you may succeed with a [custom buildpack](#custom-buildpacks). Check the +[currently supported languages](#currently-supported-languages). + +NOTE: **Note:** +Auto Test uses tests you already have in your application. If there are no +tests, it's up to you to add them. + +### Auto Code Quality + +Auto Code Quality uses the +[Code Quality image](https://gitlab.com/gitlab-org/security-products/codequality) to run +static analysis and other code checks on the current code. The report is +created, and is uploaded as an artifact which you can later download and check +out. + +In GitLab Starter, differences between the source and +target branches are also +[shown in the merge request widget](https://docs.gitlab.com/ee/user/project/merge_requests/code_quality_diff.html). + +### Auto SAST **[ULTIMATE]** + +> Introduced in [GitLab Ultimate][ee] 10.3. + +Static Application Security Testing (SAST) uses the +[SAST Docker image](https://gitlab.com/gitlab-org/security-products/sast) to run static +analysis on the current code and checks for potential security issues. Once the +report is created, it's uploaded as an artifact which you can later download and +check out. + +In GitLab Ultimate, any security warnings are also +[shown in the merge request widget](https://docs.gitlab.com/ee//user/project/merge_requests/sast.html). + +### Auto Dependency Scanning **[ULTIMATE]** + +> Introduced in [GitLab Ultimate][ee] 10.7. + +Dependency Scanning uses the +[Dependency Scanning Docker image](https://gitlab.com/gitlab-org/security-products/dependency-scanning) +to run analysis on the project dependencies and checks for potential security issues. Once the +report is created, it's uploaded as an artifact which you can later download and +check out. + +In GitLab Ultimate, any security warnings are also +[shown in the merge request widget](https://docs.gitlab.com/ee//user/project/merge_requests/dependency_scanning.html). + +### Auto License Management **[ULTIMATE]** + +> Introduced in [GitLab Ultimate][ee] 11.0. + +License Management uses the +[License Management Docker image](https://gitlab.com/gitlab-org/security-products/license_management) +to search the project dependencies for their license. Once the +report is created, it's uploaded as an artifact which you can later download and +check out. + +In GitLab Ultimate, any licenses are also +[shown in the merge request widget](https://docs.gitlab.com/ee//user/project/merge_requests/license_management.html). + +### Auto Container Scanning + +> Introduced in GitLab 10.4. + +Vulnerability Static Analysis for containers uses +[Clair](https://github.com/coreos/clair) to run static analysis on a +Docker image and checks for potential security issues. Once the report is +created, it's uploaded as an artifact which you can later download and +check out. + +In GitLab Ultimate, any security warnings are also +[shown in the merge request widget](https://docs.gitlab.com/ee//user/project/merge_requests/container_scanning.html). + +### Auto Review Apps + +NOTE: **Note:** +This is an optional step, since many projects do not have a Kubernetes cluster +available. If the [requirements](#requirements) are not met, the job will +silently be skipped. + +CAUTION: **Caution:** +Your apps should *not* be manipulated outside of Helm (using Kubernetes directly.) +This can cause confusion with Helm not detecting the change, and subsequent +deploys with Auto DevOps can undo your changes. Also, if you change something +and want to undo it by deploying again, Helm may not detect that anything changed +in the first place, and thus not realize that it needs to re-apply the old config. + +[Review Apps][review-app] are temporary application environments based on the +branch's code so developers, designers, QA, product managers, and other +reviewers can actually see and interact with code changes as part of the review +process. Auto Review Apps create a Review App for each branch. + +The Review App will have a unique URL based on the project name, the branch +name, and a unique number, combined with the Auto DevOps base domain. For +example, `user-project-branch-1234.example.com`. A link to the Review App shows +up in the merge request widget for easy discovery. When the branch is deleted, +for example after the merge request is merged, the Review App will automatically +be deleted. + +### Auto DAST **[ULTIMATE]** + +> Introduced in [GitLab Ultimate][ee] 10.4. + +Dynamic Application Security Testing (DAST) uses the +popular open source tool [OWASP ZAProxy](https://github.com/zaproxy/zaproxy) +to perform an analysis on the current code and checks for potential security +issues. Once the report is created, it's uploaded as an artifact which you can +later download and check out. + +In GitLab Ultimate, any security warnings are also +[shown in the merge request widget](https://docs.gitlab.com/ee//user/project/merge_requests/dast.html). + +### Auto Browser Performance Testing **[PREMIUM]** + +> Introduced in [GitLab Premium][ee] 10.4. + +Auto Browser Performance Testing utilizes the [Sitespeed.io container](https://hub.docker.com/r/sitespeedio/sitespeed.io/) to measure the performance of a web page. A JSON report is created and uploaded as an artifact, which includes the overall performance score for each page. By default, the root page of Review and Production environments will be tested. If you would like to add additional URL's to test, simply add the paths to a file named `.gitlab-urls.txt` in the root directory, one per line. For example: + +``` +/ +/features +/direction +``` + +In GitLab Premium, performance differences between the source +and target branches are [shown in the merge request widget](https://docs.gitlab.com/ee//user/project/merge_requests/browser_performance_testing.html). + +### Auto Deploy + +NOTE: **Note:** +This is an optional step, since many projects do not have a Kubernetes cluster +available. If the [requirements](#requirements) are not met, the job will +silently be skipped. + +CAUTION: **Caution:** +Your apps should *not* be manipulated outside of Helm (using Kubernetes directly.) +This can cause confusion with Helm not detecting the change, and subsequent +deploys with Auto DevOps can undo your changes. Also, if you change something +and want to undo it by deploying again, Helm may not detect that anything changed +in the first place, and thus not realize that it needs to re-apply the old config. + +After a branch or merge request is merged into the project's default branch (usually +`master`), Auto Deploy deploys the application to a `production` environment in +the Kubernetes cluster, with a namespace based on the project name and unique +project ID, for example `project-4321`. + +Auto Deploy doesn't include deployments to staging or canary by default, but the +[Auto DevOps template] contains job definitions for these tasks if you want to +enable them. + +You can make use of [environment variables](#helm-chart-variables) to automatically +scale your pod replicas. + +It's important to note that when a project is deployed to a Kubernetes cluster, +it relies on a Docker image that has been pushed to the +[GitLab Container Registry](../../user/project/container_registry.md). Kubernetes +fetches this image and uses it to run the application. If the project is public, +the image can be accessed by Kubernetes without any authentication, allowing us +to have deployments more usable. If the project is private/internal, the +Registry requires credentials to pull the image. Currently, this is addressed +by providing `CI_JOB_TOKEN` as the password that can be used, but this token will +no longer be valid as soon as the deployment job finishes. This means that +Kubernetes can run the application, but in case it should be restarted or +executed somewhere else, it cannot be accessed again. + +> [Introduced][ce-19507] in GitLab 11.0. + +For internal and private projects a [GitLab Deploy Token](../../user/project/deploy_tokens/index.md###gitlab-deploy-token) +will be automatically created, when Auto DevOps is enabled and the Auto DevOps settings are saved. This Deploy Token +can be used for permanent access to the registry. + +Note: **Note** +When the GitLab Deploy Token has been manually revoked, it won't be automatically created. + +### Auto Monitoring + +NOTE: **Note:** +Check the [requirements](#requirements) for Auto Monitoring to make this stage +work. + +Once your application is deployed, Auto Monitoring makes it possible to monitor +your application's server and response metrics right out of the box. Auto +Monitoring uses [Prometheus](../../user/project/integrations/prometheus.md) to +get system metrics such as CPU and memory usage directly from +[Kubernetes](../../user/project/integrations/prometheus_library/kubernetes.md), +and response metrics such as HTTP error rates, latency, and throughput from the +[NGINX server](../../user/project/integrations/prometheus_library/nginx_ingress.md). + +The metrics include: + +- **Response Metrics:** latency, throughput, error rate +- **System Metrics:** CPU utilization, memory utilization + +If GitLab has been deployed using the [GitLab Omnibus Helm Chart], no +configuration is required. + +If you have installed GitLab using a different method, you need to: + +1. [Deploy Prometheus](../../user/project/integrations/prometheus.md#configuring-your-own-prometheus-server-within-kubernetes) into your Kubernetes cluster +1. If you would like response metrics, ensure you are running at least version + 0.9.0 of NGINX Ingress and + [enable Prometheus metrics](https://github.com/kubernetes/ingress-nginx/blob/master/docs/examples/customization/custom-vts-metrics-prometheus/nginx-vts-metrics-conf.yaml). +1. Finally, [annotate](https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/) + the NGINX Ingress deployment to be scraped by Prometheus using + `prometheus.io/scrape: "true"` and `prometheus.io/port: "10254"`. + +To view the metrics, open the +[Monitoring dashboard for a deployed environment](../../ci/environments.md#monitoring-environments). + +![Auto Metrics](img/auto_monitoring.png) + +## Customizing + +While Auto DevOps provides great defaults to get you started, you can customize +almost everything to fit your needs; from custom [buildpacks](#custom-buildpacks), +to [`Dockerfile`s](#custom-dockerfile), [Helm charts](#custom-helm-chart), or +even copying the complete [CI/CD configuration](#customizing-gitlab-ci-yml) +into your project to enable staging and canary deployments, and more. + +### Custom buildpacks + +If the automatic buildpack detection fails for your project, or if you want to +use a custom buildpack, you can override the buildpack(s) using a project variable +or a `.buildpacks` file in your project: + +- **Project variable** - Create a project variable `BUILDPACK_URL` with the URL + of the buildpack to use. +- **`.buildpacks` file** - Add a file in your project's repo called `.buildpacks` + and add the URL of the buildpack to use on a line in the file. If you want to + use multiple buildpacks, you can enter them in, one on each line. + +CAUTION: **Caution:** +Using multiple buildpacks isn't yet supported by Auto DevOps. + +### Custom `Dockerfile` + +If your project has a `Dockerfile` in the root of the project repo, Auto DevOps +will build a Docker image based on the Dockerfile rather than using buildpacks. +This can be much faster and result in smaller images, especially if your +Dockerfile is based on [Alpine](https://hub.docker.com/_/alpine/). + +### Custom Helm Chart + +Auto DevOps uses [Helm](https://helm.sh/) to deploy your application to Kubernetes. +You can override the Helm chart used by bundling up a chart into your project +repo or by specifying a project variable: + +- **Bundled chart** - If your project has a `./chart` directory with a `Chart.yaml` + file in it, Auto DevOps will detect the chart and use it instead of the [default + one](https://gitlab.com/charts/charts.gitlab.io/tree/master/charts/auto-deploy-app). + This can be a great way to control exactly how your application is deployed. +- **Project variable** - Create a [project variable](../../ci/variables/README.md#secret-variables) + `AUTO_DEVOPS_CHART` with the URL of a custom chart to use. + +### Customizing `.gitlab-ci.yml` + +If you want to modify the CI/CD pipeline used by Auto DevOps, you can copy the +[Auto DevOps template] into your project's repo and edit as you see fit. + +Assuming that your project is new or it doesn't have a `.gitlab-ci.yml` file +present: + +1. From your project home page, either click on the "Set up CI/CD" button, or click + on the plus button and (`+`), then "New file" +1. Pick `.gitlab-ci.yml` as the template type +1. Select "Auto-DevOps" from the template dropdown +1. Edit the template or add any jobs needed +1. Give an appropriate commit message and hit "Commit changes" + +TIP: **Tip:** The Auto DevOps template includes useful comments to help you +customize it. For example, if you want deployments to go to a staging environment +instead of directly to a production one, you can enable the `staging` job by +renaming `.staging` to `staging`. Then make sure to uncomment the `when` key of +the `production` job to turn it into a manual action instead of deploying +automatically. + +### PostgreSQL database support + +In order to support applications that require a database, +[PostgreSQL][postgresql] is provisioned by default. The credentials to access +the database are preconfigured, but can be customized by setting the associated +[variables](#environment-variables). These credentials can be used for defining a +`DATABASE_URL` of the format: + +```yaml +postgres://user:password@postgres-host:postgres-port/postgres-database +``` + +### Environment variables + +The following variables can be used for setting up the Auto DevOps domain, +providing a custom Helm chart, or scaling your application. PostgreSQL can be +also be customized, and you can easily use a [custom buildpack](#custom-buildpacks). + +| **Variable** | **Description** | +| ------------ | --------------- | +| `AUTO_DEVOPS_DOMAIN` | The [Auto DevOps domain](#auto-devops-domain); by default set automatically by the [Auto DevOps setting](#enabling-auto-devops). | +| `AUTO_DEVOPS_CHART` | The Helm Chart used to deploy your apps; defaults to the one [provided by GitLab](https://gitlab.com/charts/charts.gitlab.io/tree/master/charts/auto-deploy-app). | +| `REPLICAS` | The number of replicas to deploy; defaults to 1. | +| `PRODUCTION_REPLICAS` | The number of replicas to deploy in the production environment. This takes precedence over `REPLICAS`; defaults to 1. | +| `CANARY_REPLICAS` | The number of canary replicas to deploy for [Canary Deployments](https://docs.gitlab.com/ee/user/project/canary_deployments.html); defaults to 1 | +| `CANARY_PRODUCTION_REPLICAS` | The number of canary replicas to deploy for [Canary Deployments](https://docs.gitlab.com/ee/user/project/canary_deployments.html) in the production environment. This takes precedence over `CANARY_REPLICAS`; defaults to 1 | +| `POSTGRES_ENABLED` | Whether PostgreSQL is enabled; defaults to `"true"`. Set to `false` to disable the automatic deployment of PostgreSQL. | +| `POSTGRES_USER` | The PostgreSQL user; defaults to `user`. Set it to use a custom username. | +| `POSTGRES_PASSWORD` | The PostgreSQL password; defaults to `testing-password`. Set it to use a custom password. | +| `POSTGRES_DB` | The PostgreSQL database name; defaults to the value of [`$CI_ENVIRONMENT_SLUG`](../../ci/variables/README.md#predefined-variables-environment-variables). Set it to use a custom database name. | +| `BUILDPACK_URL` | The buildpack's full URL. It can point to either Git repositories or a tarball URL. For Git repositories, it is possible to point to a specific `ref`, for example `https://github.com/heroku/heroku-buildpack-ruby.git#v142` | +| `SAST_CONFIDENCE_LEVEL` | The minimum confidence level of security issues you want to be reported; `1` for Low, `2` for Medium, `3` for High; defaults to `3`.| +| `DEP_SCAN_DISABLE_REMOTE_CHECKS` | Whether remote Dependency Scanning checks are disabled; defaults to `"false"`. Set to `"true"` to disable checks that send data to GitLab central servers. [Read more about remote checks](https://gitlab.com/gitlab-org/security-products/dependency-scanning#remote-checks).| +| `STAGING_ENABLED` | From GitLab 10.8, this variable can be used to define a [deploy policy for staging and production environments](#deploy-policy-for-staging-and-production-environments). | +| `CANARY_ENABLED` | From GitLab 11.0, this variable can be used to define a [deploy policy for canary environments](#deploy-policy-for-canary-environments). | +| `INCREMENTAL_ROLLOUT_ENABLED`| From GitLab 10.8, this variable can be used to enable an [incremental rollout](#incremental-rollout-to-production) of your application for the production environment. | +| `TEST_DISABLED` | From GitLab 11.0, this variable can be used to disable the `test` job. If the variable is present, the job will not be created. | +| `CODEQUALITY_DISABLED` | From GitLab 11.0, this variable can be used to disable the `codequality` job. If the variable is present, the job will not be created. | +| `SAST_DISABLED` | From GitLab 11.0, this variable can be used to disable the `sast` job. If the variable is present, the job will not be created. | +| `DEPENDENCY_SCANNING_DISABLED` | From GitLab 11.0, this variable can be used to disable the `dependency_scanning` job. If the variable is present, the job will not be created. | +| `CONTAINER_SCANNING_DISABLED` | From GitLab 11.0, this variable can be used to disable the `sast:container` job. If the variable is present, the job will not be created. | +| `REVIEW_DISABLED` | From GitLab 11.0, this variable can be used to disable the `review` and the manual `review:stop` job. If the variable is present, these jobs will not be created. | +| `DAST_DISABLED` | From GitLab 11.0, this variable can be used to disable the `dast` job. If the variable is present, the job will not be created. | +| `PERFORMANCE_DISABLED` | From GitLab 11.0, this variable can be used to disable the `performance` job. If the variable is present, the job will not be created. | + +TIP: **Tip:** +Set up the replica variables using a +[project variable](../../ci/variables/README.md#secret-variables) +and scale your application by just redeploying it! + +CAUTION: **Caution:** +You should *not* scale your application using Kubernetes directly. This can +cause confusion with Helm not detecting the change, and subsequent deploys with +Auto DevOps can undo your changes. + +#### Advanced replica variables setup + +Apart from the two replica-related variables for production mentioned above, +you can also use others for different environments. + +There's a very specific mapping between Kubernetes' label named `track`, +GitLab CI/CD environment names, and the replicas environment variable. +The general rule is: `TRACK_ENV_REPLICAS`. Where: + +- `TRACK`: The capitalized value of the `track` + [Kubernetes label](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/) + in the Helm Chart app definition. If not set, it will not be taken into account + to the variable name. +- `ENV`: The capitalized environment name of the deploy job that is set in + `.gitlab-ci.yml`. + +That way, you can define your own `TRACK_ENV_REPLICAS` variables with which +you will be able to scale the pod's replicas easily. + +In the example below, the environment's name is `qa` and it deploys the track +`foo` which would result in looking for the `FOO_QA_REPLICAS` environment +variable: + +```yaml +QA testing: + stage: deploy + environment: + name: qa + script: + - deploy foo +``` + +The track `foo` being referenced would also need to be defined in the +application's Helm chart, like: + +```yaml +replicaCount: 1 +image: + repository: gitlab.example.com/group/project + tag: stable + pullPolicy: Always + secrets: + - name: gitlab-registry +application: + track: foo + tier: web +service: + enabled: true + name: web + type: ClusterIP + url: http://my.host.com/ + externalPort: 5000 + internalPort: 5000 +``` + +#### Deploy policy for staging and production environments + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ci-yml/merge_requests/160) +in GitLab 10.8. + +TIP: **Tip:** +You can also set this inside your [project's settings](#deployment-strategy). + +The normal behavior of Auto DevOps is to use Continuous Deployment, pushing +automatically to the `production` environment every time a new pipeline is run +on the default branch. However, there are cases where you might want to use a +staging environment and deploy to production manually. For this scenario, the +`STAGING_ENABLED` environment variable was introduced. + +If `STAGING_ENABLED` is defined in your project (e.g., set `STAGING_ENABLED` to +`1` as a secret variable), then the application will be automatically deployed +to a `staging` environment, and a `production_manual` job will be created for +you when you're ready to manually deploy to production. + +#### Deploy policy for canary environments **[PREMIUM]** + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ci-yml/merge_requests/171) +in GitLab 11.0. + +A [canary environment](https://docs.gitlab.com/ee/user/project/canary_deployments.html) can be used +before any changes are deployed to production. + +If `CANARY_ENABLED` is defined in your project (e.g., set `CANARY_ENABLED` to +`1` as a secret variable) then two manual jobs will be created: + +- `canary` which will deploy the application to the canary environment +- `production_manual` which is to be used by you when you're ready to manually + deploy to production. + +#### Incremental rollout to production **[PREMIUM]** + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/5415) in GitLab 10.8. + +TIP: **Tip:** +You can also set this inside your [project's settings](#deployment-strategy). + +When you have a new version of your app to deploy in production, you may want +to use an incremental rollout to replace just a few pods with the latest code. +This will allow you to first check how the app is behaving, and later manually +increasing the rollout up to 100%. + +If `INCREMENTAL_ROLLOUT_ENABLED` is defined in your project (e.g., set +`INCREMENTAL_ROLLOUT_ENABLED` to `1` as a secret variable), then instead of the +standard `production` job, 4 different +[manual jobs](../../ci/pipelines.md#manual-actions-from-the-pipeline-graph) +will be created: + +1. `rollout 10%` +1. `rollout 25%` +1. `rollout 50%` +1. `rollout 100%` + +The percentage is based on the `REPLICAS` variable and defines the number of +pods you want to have for your deployment. If you say `10`, and then you run +the `10%` rollout job, there will be `1` new pod + `9` old ones. + +To start a job, click on the play icon next to the job's name. You are not +required to go from `10%` to `100%`, you can jump to whatever job you want. +You can also scale down by running a lower percentage job, just before hitting +`100%`. Once you get to `100%`, you cannot scale down, and you'd have to roll +back by redeploying the old version using the +[rollback button](../../ci/environments.md#rolling-back-changes) in the +environment page. + +Below, you can see how the pipeline will look if the rollout or staging +variables are defined. + +- **Without `INCREMENTAL_ROLLOUT_ENABLED` and without `STAGING_ENABLED`** + + ![Staging and rollout disabled](img/rollout_staging_disabled.png) + +- **Without `INCREMENTAL_ROLLOUT_ENABLED` and with `STAGING_ENABLED`** + + ![Staging enabled](img/staging_enabled.png) + +- **With `INCREMENTAL_ROLLOUT_ENABLED` and without `STAGING_ENABLED`** + + ![Rollout enabled](img/rollout_enabled.png) + +- **With `INCREMENTAL_ROLLOUT_ENABLED` and with `STAGING_ENABLED`** + + ![Rollout and staging enabled](img/rollout_staging_enabled.png) + +## Currently supported languages + +NOTE: **Note:** +Not all buildpacks support Auto Test yet, as it's a relatively new +enhancement. All of Heroku's [officially supported +languages](https://devcenter.heroku.com/articles/heroku-ci#currently-supported-languages) +support it, and some third-party buildpacks as well e.g., Go, Node, Java, PHP, +Python, Ruby, Gradle, Scala, and Elixir all support Auto Test, but notably the +multi-buildpack does not. + +As of GitLab 10.0, the supported buildpacks are: + +``` +- heroku-buildpack-multi v1.0.0 +- heroku-buildpack-ruby v168 +- heroku-buildpack-nodejs v99 +- heroku-buildpack-clojure v77 +- heroku-buildpack-python v99 +- heroku-buildpack-java v53 +- heroku-buildpack-gradle v23 +- heroku-buildpack-scala v78 +- heroku-buildpack-play v26 +- heroku-buildpack-php v122 +- heroku-buildpack-go v72 +- heroku-buildpack-erlang fa17af9 +- buildpack-nginx v8 +``` + +## Limitations + +The following restrictions apply. + +### Private project support + +CAUTION: **Caution:** Private project support in Auto DevOps is experimental. + +When a project has been marked as private, GitLab's [Container +Registry][container-registry] requires authentication when downloading +containers. Auto DevOps will automatically provide the required authentication +information to Kubernetes, allowing temporary access to the registry. +Authentication credentials will be valid while the pipeline is running, allowing +for a successful initial deployment. + +After the pipeline completes, Kubernetes will no longer be able to access the +Container Registry. **Restarting a pod, scaling a service, or other actions which +require on-going access to the registry may fail**. On-going secure access is +planned for a subsequent release. + +## Troubleshooting + +- Auto Build and Auto Test may fail in detecting your language/framework. There + may be no buildpack for your application, or your application may be missing the + key files the buildpack is looking for. For example, for ruby apps, you must + have a `Gemfile` to be properly detected, even though it is possible to write a + Ruby app without a `Gemfile`. Try specifying a [custom + buildpack](#custom-buildpacks). +- Auto Test may fail because of a mismatch between testing frameworks. In this + case, you may need to customize your `.gitlab-ci.yml` with your test commands. + +### Disable the banner instance wide + +If an administrator would like to disable the banners on an instance level, this +feature can be disabled either through the console: + +```sh +sudo gitlab-rails console +``` + +Then run: + +```ruby +Feature.get(:auto_devops_banner_disabled).enable +``` + +Or through the HTTP API with an admin access token: + +```sh +curl --data "value=true" --header "PRIVATE-TOKEN: personal_access_token" https://gitlab.example.com/api/v4/features/auto_devops_banner_disabled +``` + +[ce-37115]: https://gitlab.com/gitlab-org/gitlab-ce/issues/37115 +[kubernetes-clusters]: ../../user/project/clusters/index.md +[docker-in-docker]: ../../docker/using_docker_build.md#use-docker-in-docker-executor +[review-app]: ../../ci/review_apps/index.md +[container-registry]: ../../user/project/container_registry.md +[postgresql]: https://www.postgresql.org/ +[Auto DevOps template]: https://gitlab.com/gitlab-org/gitlab-ci-yml/blob/master/Auto-DevOps.gitlab-ci.yml +[GitLab Omnibus Helm Chart]: ../../install/kubernetes/gitlab_omnibus.md [ee]: https://about.gitlab.com/products/ -[auto devops]: ../../../topics/autodevops/index.md +[ce-19507]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/19507 \ No newline at end of file From 12d340427ff0a93bb494972c5eded7cf7f6419d6 Mon Sep 17 00:00:00 2001 From: Dennis Tang Date: Thu, 21 Jun 2018 09:19:58 +0000 Subject: [PATCH 46/46] Update index.md --- doc/user/project/clusters/index.md | 1224 +++++++++------------------- 1 file changed, 393 insertions(+), 831 deletions(-) diff --git a/doc/user/project/clusters/index.md b/doc/user/project/clusters/index.md index 19d5a57751b..58a483bb3b2 100644 --- a/doc/user/project/clusters/index.md +++ b/doc/user/project/clusters/index.md @@ -1,845 +1,407 @@ -# Auto DevOps +# Connecting GitLab with a Kubernetes cluster -> [Introduced][ce-37115] in GitLab 10.0. +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/35954) in GitLab 10.1. + +Connect your project to Google Kubernetes Engine (GKE) or an existing Kubernetes +cluster in a few steps. + +## Overview + +With a Kubernetes cluster associated to your project, you can use +[Review Apps](../../../ci/review_apps/index.md), deploy your applications, run +your pipelines, and much more, in an easy way. + +There are two options when adding a new cluster to your project; either associate +your account with Google Kubernetes Engine (GKE) so that you can [create new +clusters](#adding-and-creating-a-new-gke-cluster-via-gitlab) from within GitLab, +or provide the credentials to an [existing Kubernetes cluster](#adding-an-existing-kubernetes-cluster). + +## Adding and creating a new GKE cluster via GitLab + +NOTE: **Note:** +You need Maintainer [permissions] and above to access the Kubernetes page. + +Before proceeding, make sure the following requirements are met: + +- The [Google authentication integration](../../../integration/google.md) must + be enabled in GitLab at the instance level. If that's not the case, ask your + GitLab administrator to enable it. +- Your associated Google account must have the right privileges to manage + clusters on GKE. That would mean that a [billing + account](https://cloud.google.com/billing/docs/how-to/manage-billing-account) + must be set up and that you have to have permissions to access it. +- You must have Maintainer [permissions] in order to be able to access the + **Kubernetes** page. +- You must have [Cloud Billing API](https://cloud.google.com/billing/) enabled +- You must have [Resource Manager + API](https://cloud.google.com/resource-manager/) + +If all of the above requirements are met, you can proceed to create and add a +new Kubernetes cluster that will be hosted on GKE to your project: + +1. Navigate to your project's **Operations > Kubernetes** page. +1. Click on **Add Kubernetes cluster**. +1. Click on **Create with Google Kubernetes Engine**. +1. Connect your Google account if you haven't done already by clicking the + **Sign in with Google** button. +1. Fill in the requested values: + - **Kubernetes cluster name** - The name you wish to give the cluster. + - **Environment scope** - The [associated environment](#setting-the-environment-scope) to this cluster. + - **Google Cloud Platform project** - The project you created in your GCP + console that will host the Kubernetes cluster. This must **not** be confused + with the project ID. Learn more about [Google Cloud Platform projects](https://cloud.google.com/resource-manager/docs/creating-managing-projects). + - **Zone** - The [zone](https://cloud.google.com/compute/docs/regions-zones/) + under which the cluster will be created. + - **Number of nodes** - The number of nodes you wish the cluster to have. + - **Machine type** - The [machine type](https://cloud.google.com/compute/docs/machine-types) + of the Virtual Machine instance that the cluster will be based on. +1. Finally, click the **Create Kubernetes cluster** button. + +After a few moments, your cluster should be created. If something goes wrong, +you will be notified. + +You can now proceed to install some pre-defined applications and then +enable the Cluster integration. + +## Adding an existing Kubernetes cluster + +NOTE: **Note:** +You need Maintainer [permissions] and above to access the Kubernetes page. + +To add an existing Kubernetes cluster to your project: + +1. Navigate to your project's **Operations > Kubernetes** page. +1. Click on **Add Kubernetes cluster**. +1. Click on **Add an existing Kubernetes cluster** and fill in the details: + - **Kubernetes cluster name** (required) - The name you wish to give the cluster. + - **Environment scope** (required)- The + [associated environment](#setting-the-environment-scope) to this cluster. + - **API URL** (required) - + It's the URL that GitLab uses to access the Kubernetes API. Kubernetes + exposes several APIs, we want the "base" URL that is common to all of them, + e.g., `https://kubernetes.example.com` rather than `https://kubernetes.example.com/api/v1`. + - **CA certificate** (optional) - + If the API is using a self-signed TLS certificate, you'll also need to include + the `ca.crt` contents here. + - **Token** - + GitLab authenticates against Kubernetes using service tokens, which are + scoped to a particular `namespace`. If you don't have a service token yet, + you can follow the + [Kubernetes documentation](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/) + to create one. You can also view or create service tokens in the + [Kubernetes dashboard](https://kubernetes.io/docs/tasks/access-application-cluster/web-ui-dashboard/#config) + (under **Config > Secrets**). + - **Project namespace** (optional) - The following apply: + - By default you don't have to fill it in; by leaving it blank, GitLab will + create one for you. + - Each project should have a unique namespace. + - The project namespace is not necessarily the namespace of the secret, if + you're using a secret with broader permissions, like the secret from `default`. + - You should **not** use `default` as the project namespace. + - If you or someone created a secret specifically for the project, usually + with limited permissions, the secret's namespace and project namespace may + be the same. +1. Finally, click the **Create Kubernetes cluster** button. + +After a few moments, your cluster should be created. If something goes wrong, +you will be notified. + +You can now proceed to install some pre-defined applications and then +enable the Kubernetes cluster integration. + +## Security implications + +CAUTION: **Important:** +The whole cluster security is based on a model where [developers](../../permissions.md) +are trusted, so **only trusted users should be allowed to control your clusters**. + +The default cluster configuration grants access to a wide set of +functionalities needed to successfully build and deploy a containerized +application. Bare in mind that the same credentials are used for all the +applications running on the cluster. + +When GitLab creates the cluster, it enables and uses the legacy +[Attribute-based access control (ABAC)](https://kubernetes.io/docs/admin/authorization/abac/). +The newer [RBAC](https://kubernetes.io/docs/admin/authorization/rbac/) +authorization will be supported in a +[future release](https://gitlab.com/gitlab-org/gitlab-ce/issues/29398). + +### Security of GitLab Runners + +GitLab Runners have the [privileged mode](https://docs.gitlab.com/runner/executors/docker.html#the-privileged-mode) +enabled by default, which allows them to execute special commands and running +Docker in Docker. This functionality is needed to run some of the [Auto DevOps] +jobs. This implies the containers are running in privileged mode and you should, +therefore, be aware of some important details. + +The privileged flag gives all capabilities to the running container, which in +turn can do almost everything that the host can do. Be aware of the +inherent security risk associated with performing `docker run` operations on +arbitrary images as they effectively have root access. + +If you don't want to use GitLab Runner in privileged mode, first make sure that +you don't have it installed via the applications, and then use the +[Runner's Helm chart](../../../install/kubernetes/gitlab_runner_chart.md) to +install it manually. + +## Installing applications + +GitLab provides a one-click install for various applications which will be +added directly to your configured cluster. Those applications are needed for +[Review Apps](../../../ci/review_apps/index.md) and [deployments](../../../ci/environments.md). + +| Application | GitLab version | Description | +| ----------- | :------------: | ----------- | +| [Helm Tiller](https://docs.helm.sh/) | 10.2+ | Helm is a package manager for Kubernetes and is required to install all the other applications. It will be automatically installed as a dependency when you try to install a different app. It is installed in its own pod inside the cluster which can run the `helm` CLI in a safe environment. | +| [Ingress](https://kubernetes.io/docs/concepts/services-networking/ingress/) | 10.2+ | Ingress can provide load balancing, SSL termination, and name-based virtual hosting. It acts as a web proxy for your applications and is useful if you want to use [Auto DevOps] or deploy your own web apps. | +| [Prometheus](https://prometheus.io/docs/introduction/overview/) | 10.4+ | Prometheus is an open-source monitoring and alerting system useful to supervise your deployed applications | +| [GitLab Runner](https://docs.gitlab.com/runner/) | 10.6+ | GitLab Runner is the open source project that is used to run your jobs and send the results back to GitLab. It is used in conjunction with [GitLab CI/CD](https://about.gitlab.com/features/gitlab-ci-cd/), the open-source continuous integration service included with GitLab that coordinates the jobs. When installing the GitLab Runner via the applications, it will run in **privileged mode** by default. Make sure you read the [security implications](#security-implications) before doing so. | +| [JupyterHub](http://jupyter.org/) | 11.0+ | [JupyterHub](https://jupyterhub.readthedocs.io/en/stable/) is a multi-user service for managing notebooks across a team. [Jupyter Notebooks](https://jupyter-notebook.readthedocs.io/en/latest/) provide a web-based interactive programming environment used for data analysis, visualization, and machine learning. **Note**: Authentication will be enabled for any user of the GitLab server via OAuth2. HTTPS will be supported in a future release. | + +## Getting the external IP address + +NOTE: **Note:** +You need a load balancer installed in your cluster in order to obtain the +external IP address with the following procedure. It can be deployed using the +[**Ingress** application](#installing-applications). + +In order to publish your web application, you first need to find the external IP +address associated to your load balancer. + +### Let GitLab fetch the IP address + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17052) in GitLab 10.6. + +If you installed the Ingress [via the **Applications**](#installing-applications), +you should see the Ingress IP address on this same page within a few minutes. +If you don't see this, GitLab might not be able to determine the IP address of +your ingress application in which case you should manually determine it. + +### Manually determining the IP address + +If the cluster is on GKE, click on the **Google Kubernetes Engine** link in the +**Advanced settings**, or go directly to the +[Google Kubernetes Engine dashboard](https://console.cloud.google.com/kubernetes/) +and select the proper project and cluster. Then click on **Connect** and execute +the `gcloud` command in a local terminal or using the **Cloud Shell**. + +If the cluster is not on GKE, follow the specific instructions for your +Kubernetes provider to configure `kubectl` with the right credentials. + +If you installed the Ingress [via the **Applications**](#installing-applications), +run the following command: + +```bash +kubectl get svc --namespace=gitlab-managed-apps ingress-nginx-ingress-controller -o jsonpath='{.status.loadBalancer.ingress[0].ip} ' +``` + +Otherwise, you can list the IP addresses of all load balancers: + +```bash +kubectl get svc --all-namespaces -o jsonpath='{range.items[?(@.status.loadBalancer.ingress)]}{.status.loadBalancer.ingress[*].ip} ' +``` + +> **Note**: Some Kubernetes clusters return a hostname instead, like [Amazon EKS](https://aws.amazon.com/eks/). For these platforms, run: +> ```bash +> kubectl get service ingress-nginx-ingress-controller -n gitlab-managed-apps -o jsonpath="{.status.loadBalancer.ingress[0].hostname}"`. +> ``` + +The output is the external IP address of your cluster. This information can then +be used to set up DNS entries and forwarding rules that allow external access to +your deployed applications. + +### Using a static IP + +By default, an ephemeral external IP address is associated to the cluster's load +balancer. If you associate the ephemeral IP with your DNS and the IP changes, +your apps will not be able to be reached, and you'd have to change the DNS +record again. In order to avoid that, you should change it into a static +reserved IP. + +[Read how to promote an ephemeral external IP address in GKE.](https://cloud.google.com/compute/docs/ip-addresses/reserve-static-external-ip-address#promote_ephemeral_ip) + +### Pointing your DNS at the cluster IP + +Once you've set up the static IP, you should associate it to a [wildcard DNS +record](https://en.wikipedia.org/wiki/Wildcard_DNS_record), in order to be able +to reach your apps. This heavily depends on your domain provider, but in case +you aren't sure, just create an A record with a wildcard host like +`*.example.com.`. + +## Setting the environment scope + +NOTE: **Note:** +This is only available for [GitLab Premium][ee] where you can add more than +one Kubernetes cluster. + +When adding more than one Kubernetes clusters to your project, you need to +differentiate them with an environment scope. The environment scope associates +clusters and [environments](../../../ci/environments.md) in an 1:1 relationship +similar to how the +[environment-specific variables](../../../ci/variables/README.md#limiting-environment-scopes-of-variables) +work. + +The default environment scope is `*`, which means all jobs, regardless of their +environment, will use that cluster. Each scope can only be used by a single +cluster in a project, and a validation error will occur if otherwise. +Also, jobs that don't have an environment keyword set will not be able to access any cluster. + +--- + +For example, let's say the following Kubernetes clusters exist in a project: + +| Cluster | Environment scope | +| ---------- | ------------------- | +| Development| `*` | +| Staging | `staging/*` | +| Production | `production/*` | + +And the following environments are set in [`.gitlab-ci.yml`](../../../ci/yaml/README.md): + +```yaml +stages: +- test +- deploy + +test: + stage: test + script: sh test + +deploy to staging: + stage: deploy + script: make deploy + environment: + name: staging/$CI_COMMIT_REF_NAME + url: https://staging.example.com/ + +deploy to production: + stage: deploy + script: make deploy + environment: + name: production/$CI_COMMIT_REF_NAME + url: https://example.com/ +``` + +The result will then be: + +- The development cluster will be used for the "test" job. +- The staging cluster will be used for the "deploy to staging" job. +- The production cluster will be used for the "deploy to production" job. + +## Multiple Kubernetes clusters + +> Introduced in [GitLab Premium][ee] 10.3. + +With GitLab Premium, you can associate more than one Kubernetes clusters to your +project. That way you can have different clusters for different environments, +like dev, staging, production, etc. + +Simply add another cluster, like you did the first time, and make sure to +[set an environment scope](#setting-the-environment-scope) that will +differentiate the new cluster with the rest. + +## Deployment variables + +The Kubernetes cluster integration exposes the following +[deployment variables](../../../ci/variables/README.md#deployment-variables) in the +GitLab CI/CD build environment. + +| Variable | Description | +| -------- | ----------- | +| `KUBE_URL` | Equal to the API URL. | +| `KUBE_TOKEN` | The Kubernetes token. | +| `KUBE_NAMESPACE` | The Kubernetes namespace is auto-generated if not specified. The default value is `-`. You can overwrite it to use different one if needed, otherwise the `KUBE_NAMESPACE` variable will receive the default value. | +| `KUBE_CA_PEM_FILE` | Only present if a custom CA bundle was specified. Path to a file containing PEM data. | +| `KUBE_CA_PEM` | (**deprecated**) Only if a custom CA bundle was specified. Raw PEM data. | +| `KUBECONFIG` | Path to a file containing `kubeconfig` for this deployment. CA bundle would be embedded if specified. | + +## Enabling or disabling the Kubernetes cluster integration + +After you have successfully added your cluster information, you can enable the +Kubernetes cluster integration: + +1. Click the "Enabled/Disabled" switch +1. Hit **Save** for the changes to take effect + +You can now start using your Kubernetes cluster for your deployments. + +To disable the Kubernetes cluster integration, follow the same procedure. + +## Removing the Kubernetes cluster integration + +NOTE: **Note:** +You need Maintainer [permissions] and above to remove a Kubernetes cluster integration. + +NOTE: **Note:** +When you remove a cluster, you only remove its relation to GitLab, not the +cluster itself. To remove the cluster, you can do so by visiting the GKE +dashboard or using `kubectl`. + +To remove the Kubernetes cluster integration from your project, simply click on the +**Remove integration** button. You will then be able to follow the procedure +and add a Kubernetes cluster again. + +## What you can get with the Kubernetes integration + +Here's what you can do with GitLab if you enable the Kubernetes integration. + +### Deploy Boards + +> Available in [GitLab Premium][ee]. + +GitLab's Deploy Boards offer a consolidated view of the current health and +status of each CI [environment](../../../ci/environments.md) running on Kubernetes, +displaying the status of the pods in the deployment. Developers and other +teammates can view the progress and status of a rollout, pod by pod, in the +workflow they already use without any need to access Kubernetes. + +[> Read more about Deploy Boards](https://docs.gitlab.com/ee/user/project/deploy_boards.html) + +### Canary Deployments + +> Available in [GitLab Premium][ee]. + +Leverage [Kubernetes' Canary deployments](https://kubernetes.io/docs/concepts/cluster-administration/manage-deployment/#canary-deployments) +and visualize your canary deployments right inside the Deploy Board, without +the need to leave GitLab. + +[> Read more about Canary Deployments](https://docs.gitlab.com/ee/user/project/canary_deployments.html) + +### Kubernetes monitoring + +Automatically detect and monitor Kubernetes metrics. Automatic monitoring of +[NGINX ingress](../integrations/prometheus_library/nginx.md) is also supported. + +[> Read more about Kubernetes monitoring](../integrations/prometheus_library/kubernetes.md) + +### Auto DevOps Auto DevOps automatically detects, builds, tests, deploys, and monitors your applications. -## Overview +To make full use of Auto DevOps(Auto Deploy, Auto Review Apps, and Auto Monitoring) +you will need the Kubernetes project integration enabled. -With Auto DevOps, the software development process becomes easier to set up -as every project can have a complete workflow from verification to monitoring -without needing to configure anything. Just push your code and GitLab takes -care of everything else. This makes it easier to start new projects and brings -consistency to how applications are set up throughout a company. +[> Read more about Auto DevOps](../../../topics/autodevops/index.md) -## Comparison to application platforms and PaaS - -Auto DevOps provides functionality described by others as an application -platform or as a Platform as a Service (PaaS). It takes inspiration from the -innovative work done by [Heroku](https://www.heroku.com/) and goes beyond it -in a couple of ways: - -1. Auto DevOps works with any Kubernetes cluster, you're not limited to running - on GitLab's infrastructure (note that many features also work without Kubernetes). -1. There is no additional cost (no markup on the infrastructure costs), and you - can use a self-hosted Kubernetes cluster or Containers as a Service on any - public cloud (for example [Google Kubernetes Engine](https://cloud.google.com/kubernetes-engine/)). -1. Auto DevOps has more features including security testing, performance testing, - and code quality testing. -1. It offers an incremental graduation path. If you need advanced customizations - you can start modifying the templates without having to start over on a - completely different platform. - -## Features - -Comprised of a set of stages, Auto DevOps brings these best practices to your -project in an easy and automatic way: - -1. [Auto Build](#auto-build) -1. [Auto Test](#auto-test) -1. [Auto Code Quality](#auto-code-quality) -1. [Auto SAST (Static Application Security Testing)](#auto-sast) -1. [Auto Dependency Scanning](#auto-dependency-scanning) -1. [Auto License Management](#auto-license-management) -1. [Auto Container Scanning](#auto-container-scanning) -1. [Auto Review Apps](#auto-review-apps) -1. [Auto DAST (Dynamic Application Security Testing)](#auto-dast) -1. [Auto Deploy](#auto-deploy) -1. [Auto Browser Performance Testing](#auto-browser-performance-testing) -1. [Auto Monitoring](#auto-monitoring) - -As Auto DevOps relies on many different components, it's good to have a basic -knowledge of the following: - -- [Kubernetes](https://kubernetes.io/docs/home/) -- [Helm](https://docs.helm.sh/) -- [Docker](https://docs.docker.com) -- [GitLab Runner](https://docs.gitlab.com/runner/) -- [Prometheus](https://prometheus.io/docs/introduction/overview/) - -Auto DevOps provides great defaults for all the stages; you can, however, -[customize](#customizing) almost everything to your needs. - -For an overview on the creation of Auto DevOps, read the blog post [From 2/3 of the Self-Hosted Git Market, to the Next-Generation CI System, to Auto DevOps](https://about.gitlab.com/2017/06/29/whats-next-for-gitlab-ci/). - -## Requirements - -TIP: **Tip:** -For self-hosted installations, the easiest way to make use of Auto DevOps is to -install GitLab inside a Kubernetes cluster using the [GitLab Omnibus Helm Chart] -which automatically installs and configures everything you need! - -To make full use of Auto DevOps, you will need: - -1. **GitLab Runner** (needed for all stages) - Your Runner needs to be - configured to be able to run Docker. Generally this means using the - [Docker](https://docs.gitlab.com/runner/executors/docker.html) or [Kubernetes - executor](https://docs.gitlab.com/runner/executors/kubernetes.html), with - [privileged mode enabled](https://docs.gitlab.com/runner/executors/docker.html#use-docker-in-docker-with-privileged-mode). - The Runners do not need to be installed in the Kubernetes cluster, but the - Kubernetes executor is easy to use and is automatically autoscaling. - Docker-based Runners can be configured to autoscale as well, using [Docker - Machine](https://docs.gitlab.com/runner/install/autoscaling.html). Runners - should be registered as [shared Runners](../../ci/runners/README.md#registering-a-shared-runner) - for the entire GitLab instance, or [specific Runners](../../ci/runners/README.md#registering-a-specific-runner) - that are assigned to specific projects. -1. **Base domain** (needed for Auto Review Apps and Auto Deploy) - You will need - a domain configured with wildcard DNS which is gonna be used by all of your - Auto DevOps applications. [Read the specifics](#auto-devops-base-domain). -1. **Kubernetes** (needed for Auto Review Apps, Auto Deploy, and Auto Monitoring) - - To enable deployments, you will need Kubernetes 1.5+. You need a [Kubernetes cluster][kubernetes-clusters] - for the project, or a Kubernetes [default service template](../../user/project/integrations/services_templates.md) - for the entire GitLab installation. - 1. **A load balancer** - You can use NGINX ingress by deploying it to your - Kubernetes cluster using the - [`nginx-ingress`](https://github.com/kubernetes/charts/tree/master/stable/nginx-ingress) - Helm chart. - 1. **Wildcard TLS termination** - You can deploy the - [`kube-lego`](https://github.com/kubernetes/charts/tree/master/stable/kube-lego) - Helm chart to your Kubernetes cluster to automatically issue certificates - for your domains using Let's Encrypt. -1. **Prometheus** (needed for Auto Monitoring) - To enable Auto Monitoring, you - will need Prometheus installed somewhere (inside or outside your cluster) and - configured to scrape your Kubernetes cluster. To get response metrics - (in addition to system metrics), you need to - [configure Prometheus to monitor NGINX](../../user/project/integrations/prometheus_library/nginx_ingress.md#configuring-prometheus-to-monitor-for-nginx-ingress-metrics). - The [Prometheus service](../../user/project/integrations/prometheus.md) - integration needs to be enabled for the project, or enabled as a - [default service template](../../user/project/integrations/services_templates.md) - for the entire GitLab installation. +### Web terminals NOTE: **Note:** -If you do not have Kubernetes or Prometheus installed, then Auto Review Apps, -Auto Deploy, and Auto Monitoring will be silently skipped. +Introduced in GitLab 8.15. You must be the project owner or have `maintainer` permissions +to use terminals. Support is limited to the first container in the +first pod of your environment. -## Auto DevOps base domain +When enabled, the Kubernetes service adds [web terminal](../../../ci/environments.md#web-terminals) +support to your [environments](../../../ci/environments.md). This is based on the `exec` functionality found in +Docker and Kubernetes, so you get a new shell session within your existing +containers. To use this integration, you should deploy to Kubernetes using +the deployment variables above, ensuring any pods you create are labelled with +`app=$CI_ENVIRONMENT_SLUG`. GitLab will do the rest! -The Auto DevOps base domain is required if you want to make use of [Auto -Review Apps](#auto-review-apps) and [Auto Deploy](#auto-deploy). It can be defined -in three places: +## Read more -- either under the project's CI/CD settings while [enabling Auto DevOps](#enabling-auto-devops) -- or in instance-wide settings in the **admin area > Settings** under the "Continuous Integration and Delivery" section -- or at the project or group level as a variable: `AUTO_DEVOPS_DOMAIN` (required if you want to use [multiple clusters](#using-multiple-kubernetes-clusters)) +- [Connecting and deploying to an Amazon EKS cluster](eks_and_gitlab/index.md) -A wildcard DNS A record matching the base domain(s) is required, for example, -given a base domain of `example.com`, you'd need a DNS entry like: - -``` -*.example.com 3600 A 1.2.3.4 -``` - -In this case, `example.com` is the domain name under which the deployed apps will be served, -and `1.2.3.4` is the IP address of your load balancer; generally NGINX -([see requirements](#requirements)). How to set up the DNS record is beyond -the scope of this document; you should check with your DNS provider. - -Alternatively you can use free public services like [nip.io](http://nip.io) or -[nip.io](http://nip.io) which provide automatic wildcard DNS without any -configuration. Just set the Auto DevOps base domain to `1.2.3.4.nip.io` or -`1.2.3.4.nip.io`. - -Once set up, all requests will hit the load balancer, which in turn will route -them to the Kubernetes pods that run your application(s). - -NOTE: **Note:** -If GitLab is installed using the [GitLab Omnibus Helm Chart], there are two -options: provide a static IP, or have one assigned. For more information see the -relevant docs on the [network prerequisites](../../install/kubernetes/gitlab_omnibus.md#networking-prerequisites). - -## Using multiple Kubernetes clusters **[PREMIUM]** - -When using Auto DevOps, you may want to deploy different environments to -different Kubernetes clusters. This is possible due to the 1:1 connection that -[exists between them](../../user/project/clusters/index.md#multiple-kubernetes-clusters). - -In the [Auto DevOps template](https://gitlab.com/gitlab-org/gitlab-ci-yml/blob/master/Auto-DevOps.gitlab-ci.yml) -(used behind the scenes by Auto DevOps), there are currently 3 defined environment names that you need to know: - -- `review/` (every environment starting with `review/`) -- `staging` -- `production` - -Those environments are tied to jobs that use [Auto Deploy](#auto-deploy), so -except for the environment scope, they would also need to have a different -domain they would be deployed to. This is why you need to define a separate -`AUTO_DEVOPS_DOMAIN` variable for all the above -[based on the environment](../../ci/variables/README.md#limiting-environment-scopes-of-variables). - -The following table is an example of how the three different clusters would -be configured. - -| Cluster name | Cluster environment scope | `AUTO_DEVOPS_DOMAIN` variable value | Variable environment scope | Notes | -| ------------ | -------------- | ----------------------------- | ------------- | ------ | -| review | `review/*` | `review.example.com` | `review/*` | The review cluster which will run all [Review Apps](../../ci/review_apps/index.md). `*` is a wildcard, which means it will be used by every environment name starting with `review/`. | -| staging | `staging` | `staging.example.com` | `staging` | (Optional) The staging cluster which will run the deployments of the staging environments. You need to [enable it first](#deploy-policy-for-staging-and-production-environments). | -| production | `production` | `example.com` | `production` | The production cluster which will run the deployments of the production environment. You can use [incremental rollouts](#incremental-rollout-to-production). | - -To add a different cluster for each environment: - -1. Navigate to your project's **Operations > Kubernetes** and create the Kubernetes clusters - with their respective environment scope as described from the table above. - - ![Auto DevOps multiple clusters](img/autodevops_multiple_clusters.png) - -1. After the clusters are created, navigate to each one and install Helm Tiller - and Ingress. -1. Make sure you have [configured your DNS](#auto-devops-base-domain) with the - specified Auto DevOps domains. -1. Navigate to your project's **Settings > CI/CD > Variables** and add - the `AUTO_DEVOPS_DOMAIN` variables with their respective environment - scope. - - ![Auto DevOps domain variables](img/autodevops_domain_variables.png) - -Now that all is configured, you can test your setup by creating a merge request -and verifying that your app is deployed as a review app in the Kubernetes -cluster with the `review/*` environment scope. Similarly, you can check the -other environments. - -## Quick start - -If you are using GitLab.com, see our [quick start guide](quick_start_guide.md) -for using Auto DevOps with GitLab.com and an external Kubernetes cluster on -Google Cloud. - -## Enabling Auto DevOps - -If you haven't done already, read the [requirements](#requirements) to make -full use of Auto DevOps. If this is your fist time, we recommend you follow the -[quick start guide](quick_start_guide.md). - -To enable Auto DevOps to your project: - -1. Check that your project doesn't have a `.gitlab-ci.yml`, or remove it otherwise -1. Go to your project's **Settings > CI/CD > Auto DevOps** -1. Select "Enable Auto DevOps" -1. Optionally, but recommended, add in the [base domain](#auto-devops-base-domain) - that will be used by Kubernetes to [deploy your application](#auto-deploy) - and choose the [deployment strategy](#deployment-strategy) -1. Hit **Save changes** for the changes to take effect - -Once saved, an Auto DevOps pipeline will be triggered on the default branch. - -NOTE: **Note:** -For GitLab versions 10.0 - 10.2, when enabling Auto DevOps, a pipeline needs to be -manually triggered either by pushing a new commit to the repository or by visiting -`https://example.gitlab.com///pipelines/new` and creating -a new pipeline for your default branch, generally `master`. - -NOTE: **Note:** -If you are a GitLab Administrator, you can enable Auto DevOps instance wide -in **Admin Area > Settings > Continuous Integration and Deployment**. Doing that, -all the projects that haven't explicitly set an option will have Auto DevOps -enabled by default. - -### Deployment strategy - -> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/38542) in GitLab 11.0. - -You can change the deployment strategy used by Auto DevOps by going to your -project's **Settings > CI/CD > Auto DevOps**. - -The available options are: - -- **Continuous deployment to production** - enables [Auto Deploy](#auto-deploy) - by setting the [`STAGING_ENABLED`](#deploy-policy-for-staging-and-production-environments) and - [`INCREMENTAL_ROLLOUT_ENABLED`](#incremental-rollout-to-production) variables - to false. -- **Automatic deployment to staging, manual deployment to production** - sets the - [`STAGING_ENABLED`](#deploy-policy-for-staging-and-production-environments) and - [`INCREMENTAL_ROLLOUT_ENABLED`](#incremental-rollout-to-production) variables - to true, and the user is responsible for manually deploying to staging and production. - -## Stages of Auto DevOps - -The following sections describe the stages of Auto DevOps. Read them carefully -to understand how each one works. - -### Auto Build - -Auto Build creates a build of the application in one of two ways: - -- If there is a `Dockerfile`, it will use `docker build` to create a Docker image. -- Otherwise, it will use [Herokuish](https://github.com/gliderlabs/herokuish) - and [Heroku buildpacks](https://devcenter.heroku.com/articles/buildpacks) - to automatically detect and build the application into a Docker image. - -Either way, the resulting Docker image is automatically pushed to the -[Container Registry][container-registry] and tagged with the commit SHA. - -CAUTION: **Important:** -If you are also using Auto Review Apps and Auto Deploy and choose to provide -your own `Dockerfile`, make sure you expose your application to port -`5000` as this is the port assumed by the default Helm chart. - -### Auto Test - -Auto Test automatically runs the appropriate tests for your application using -[Herokuish](https://github.com/gliderlabs/herokuish) and [Heroku -buildpacks](https://devcenter.heroku.com/articles/buildpacks) by analyzing -your project to detect the language and framework. Several languages and -frameworks are detected automatically, but if your language is not detected, -you may succeed with a [custom buildpack](#custom-buildpacks). Check the -[currently supported languages](#currently-supported-languages). - -NOTE: **Note:** -Auto Test uses tests you already have in your application. If there are no -tests, it's up to you to add them. - -### Auto Code Quality - -Auto Code Quality uses the -[Code Quality image](https://gitlab.com/gitlab-org/security-products/codequality) to run -static analysis and other code checks on the current code. The report is -created, and is uploaded as an artifact which you can later download and check -out. - -In GitLab Starter, differences between the source and -target branches are also -[shown in the merge request widget](https://docs.gitlab.com/ee/user/project/merge_requests/code_quality_diff.html). - -### Auto SAST **[ULTIMATE]** - -> Introduced in [GitLab Ultimate][ee] 10.3. - -Static Application Security Testing (SAST) uses the -[SAST Docker image](https://gitlab.com/gitlab-org/security-products/sast) to run static -analysis on the current code and checks for potential security issues. Once the -report is created, it's uploaded as an artifact which you can later download and -check out. - -In GitLab Ultimate, any security warnings are also -[shown in the merge request widget](https://docs.gitlab.com/ee//user/project/merge_requests/sast.html). - -### Auto Dependency Scanning **[ULTIMATE]** - -> Introduced in [GitLab Ultimate][ee] 10.7. - -Dependency Scanning uses the -[Dependency Scanning Docker image](https://gitlab.com/gitlab-org/security-products/dependency-scanning) -to run analysis on the project dependencies and checks for potential security issues. Once the -report is created, it's uploaded as an artifact which you can later download and -check out. - -In GitLab Ultimate, any security warnings are also -[shown in the merge request widget](https://docs.gitlab.com/ee//user/project/merge_requests/dependency_scanning.html). - -### Auto License Management **[ULTIMATE]** - -> Introduced in [GitLab Ultimate][ee] 11.0. - -License Management uses the -[License Management Docker image](https://gitlab.com/gitlab-org/security-products/license_management) -to search the project dependencies for their license. Once the -report is created, it's uploaded as an artifact which you can later download and -check out. - -In GitLab Ultimate, any licenses are also -[shown in the merge request widget](https://docs.gitlab.com/ee//user/project/merge_requests/license_management.html). - -### Auto Container Scanning - -> Introduced in GitLab 10.4. - -Vulnerability Static Analysis for containers uses -[Clair](https://github.com/coreos/clair) to run static analysis on a -Docker image and checks for potential security issues. Once the report is -created, it's uploaded as an artifact which you can later download and -check out. - -In GitLab Ultimate, any security warnings are also -[shown in the merge request widget](https://docs.gitlab.com/ee//user/project/merge_requests/container_scanning.html). - -### Auto Review Apps - -NOTE: **Note:** -This is an optional step, since many projects do not have a Kubernetes cluster -available. If the [requirements](#requirements) are not met, the job will -silently be skipped. - -CAUTION: **Caution:** -Your apps should *not* be manipulated outside of Helm (using Kubernetes directly.) -This can cause confusion with Helm not detecting the change, and subsequent -deploys with Auto DevOps can undo your changes. Also, if you change something -and want to undo it by deploying again, Helm may not detect that anything changed -in the first place, and thus not realize that it needs to re-apply the old config. - -[Review Apps][review-app] are temporary application environments based on the -branch's code so developers, designers, QA, product managers, and other -reviewers can actually see and interact with code changes as part of the review -process. Auto Review Apps create a Review App for each branch. - -The Review App will have a unique URL based on the project name, the branch -name, and a unique number, combined with the Auto DevOps base domain. For -example, `user-project-branch-1234.example.com`. A link to the Review App shows -up in the merge request widget for easy discovery. When the branch is deleted, -for example after the merge request is merged, the Review App will automatically -be deleted. - -### Auto DAST **[ULTIMATE]** - -> Introduced in [GitLab Ultimate][ee] 10.4. - -Dynamic Application Security Testing (DAST) uses the -popular open source tool [OWASP ZAProxy](https://github.com/zaproxy/zaproxy) -to perform an analysis on the current code and checks for potential security -issues. Once the report is created, it's uploaded as an artifact which you can -later download and check out. - -In GitLab Ultimate, any security warnings are also -[shown in the merge request widget](https://docs.gitlab.com/ee//user/project/merge_requests/dast.html). - -### Auto Browser Performance Testing **[PREMIUM]** - -> Introduced in [GitLab Premium][ee] 10.4. - -Auto Browser Performance Testing utilizes the [Sitespeed.io container](https://hub.docker.com/r/sitespeedio/sitespeed.io/) to measure the performance of a web page. A JSON report is created and uploaded as an artifact, which includes the overall performance score for each page. By default, the root page of Review and Production environments will be tested. If you would like to add additional URL's to test, simply add the paths to a file named `.gitlab-urls.txt` in the root directory, one per line. For example: - -``` -/ -/features -/direction -``` - -In GitLab Premium, performance differences between the source -and target branches are [shown in the merge request widget](https://docs.gitlab.com/ee//user/project/merge_requests/browser_performance_testing.html). - -### Auto Deploy - -NOTE: **Note:** -This is an optional step, since many projects do not have a Kubernetes cluster -available. If the [requirements](#requirements) are not met, the job will -silently be skipped. - -CAUTION: **Caution:** -Your apps should *not* be manipulated outside of Helm (using Kubernetes directly.) -This can cause confusion with Helm not detecting the change, and subsequent -deploys with Auto DevOps can undo your changes. Also, if you change something -and want to undo it by deploying again, Helm may not detect that anything changed -in the first place, and thus not realize that it needs to re-apply the old config. - -After a branch or merge request is merged into the project's default branch (usually -`master`), Auto Deploy deploys the application to a `production` environment in -the Kubernetes cluster, with a namespace based on the project name and unique -project ID, for example `project-4321`. - -Auto Deploy doesn't include deployments to staging or canary by default, but the -[Auto DevOps template] contains job definitions for these tasks if you want to -enable them. - -You can make use of [environment variables](#helm-chart-variables) to automatically -scale your pod replicas. - -It's important to note that when a project is deployed to a Kubernetes cluster, -it relies on a Docker image that has been pushed to the -[GitLab Container Registry](../../user/project/container_registry.md). Kubernetes -fetches this image and uses it to run the application. If the project is public, -the image can be accessed by Kubernetes without any authentication, allowing us -to have deployments more usable. If the project is private/internal, the -Registry requires credentials to pull the image. Currently, this is addressed -by providing `CI_JOB_TOKEN` as the password that can be used, but this token will -no longer be valid as soon as the deployment job finishes. This means that -Kubernetes can run the application, but in case it should be restarted or -executed somewhere else, it cannot be accessed again. - -> [Introduced][ce-19507] in GitLab 11.0. - -For internal and private projects a [GitLab Deploy Token](../../user/project/deploy_tokens/index.md###gitlab-deploy-token) -will be automatically created, when Auto DevOps is enabled and the Auto DevOps settings are saved. This Deploy Token -can be used for permanent access to the registry. - -Note: **Note** -When the GitLab Deploy Token has been manually revoked, it won't be automatically created. - -### Auto Monitoring - -NOTE: **Note:** -Check the [requirements](#requirements) for Auto Monitoring to make this stage -work. - -Once your application is deployed, Auto Monitoring makes it possible to monitor -your application's server and response metrics right out of the box. Auto -Monitoring uses [Prometheus](../../user/project/integrations/prometheus.md) to -get system metrics such as CPU and memory usage directly from -[Kubernetes](../../user/project/integrations/prometheus_library/kubernetes.md), -and response metrics such as HTTP error rates, latency, and throughput from the -[NGINX server](../../user/project/integrations/prometheus_library/nginx_ingress.md). - -The metrics include: - -- **Response Metrics:** latency, throughput, error rate -- **System Metrics:** CPU utilization, memory utilization - -If GitLab has been deployed using the [GitLab Omnibus Helm Chart], no -configuration is required. - -If you have installed GitLab using a different method, you need to: - -1. [Deploy Prometheus](../../user/project/integrations/prometheus.md#configuring-your-own-prometheus-server-within-kubernetes) into your Kubernetes cluster -1. If you would like response metrics, ensure you are running at least version - 0.9.0 of NGINX Ingress and - [enable Prometheus metrics](https://github.com/kubernetes/ingress-nginx/blob/master/docs/examples/customization/custom-vts-metrics-prometheus/nginx-vts-metrics-conf.yaml). -1. Finally, [annotate](https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/) - the NGINX Ingress deployment to be scraped by Prometheus using - `prometheus.io/scrape: "true"` and `prometheus.io/port: "10254"`. - -To view the metrics, open the -[Monitoring dashboard for a deployed environment](../../ci/environments.md#monitoring-environments). - -![Auto Metrics](img/auto_monitoring.png) - -## Customizing - -While Auto DevOps provides great defaults to get you started, you can customize -almost everything to fit your needs; from custom [buildpacks](#custom-buildpacks), -to [`Dockerfile`s](#custom-dockerfile), [Helm charts](#custom-helm-chart), or -even copying the complete [CI/CD configuration](#customizing-gitlab-ci-yml) -into your project to enable staging and canary deployments, and more. - -### Custom buildpacks - -If the automatic buildpack detection fails for your project, or if you want to -use a custom buildpack, you can override the buildpack(s) using a project variable -or a `.buildpacks` file in your project: - -- **Project variable** - Create a project variable `BUILDPACK_URL` with the URL - of the buildpack to use. -- **`.buildpacks` file** - Add a file in your project's repo called `.buildpacks` - and add the URL of the buildpack to use on a line in the file. If you want to - use multiple buildpacks, you can enter them in, one on each line. - -CAUTION: **Caution:** -Using multiple buildpacks isn't yet supported by Auto DevOps. - -### Custom `Dockerfile` - -If your project has a `Dockerfile` in the root of the project repo, Auto DevOps -will build a Docker image based on the Dockerfile rather than using buildpacks. -This can be much faster and result in smaller images, especially if your -Dockerfile is based on [Alpine](https://hub.docker.com/_/alpine/). - -### Custom Helm Chart - -Auto DevOps uses [Helm](https://helm.sh/) to deploy your application to Kubernetes. -You can override the Helm chart used by bundling up a chart into your project -repo or by specifying a project variable: - -- **Bundled chart** - If your project has a `./chart` directory with a `Chart.yaml` - file in it, Auto DevOps will detect the chart and use it instead of the [default - one](https://gitlab.com/charts/charts.gitlab.io/tree/master/charts/auto-deploy-app). - This can be a great way to control exactly how your application is deployed. -- **Project variable** - Create a [project variable](../../ci/variables/README.md#secret-variables) - `AUTO_DEVOPS_CHART` with the URL of a custom chart to use. - -### Customizing `.gitlab-ci.yml` - -If you want to modify the CI/CD pipeline used by Auto DevOps, you can copy the -[Auto DevOps template] into your project's repo and edit as you see fit. - -Assuming that your project is new or it doesn't have a `.gitlab-ci.yml` file -present: - -1. From your project home page, either click on the "Set up CI/CD" button, or click - on the plus button and (`+`), then "New file" -1. Pick `.gitlab-ci.yml` as the template type -1. Select "Auto-DevOps" from the template dropdown -1. Edit the template or add any jobs needed -1. Give an appropriate commit message and hit "Commit changes" - -TIP: **Tip:** The Auto DevOps template includes useful comments to help you -customize it. For example, if you want deployments to go to a staging environment -instead of directly to a production one, you can enable the `staging` job by -renaming `.staging` to `staging`. Then make sure to uncomment the `when` key of -the `production` job to turn it into a manual action instead of deploying -automatically. - -### PostgreSQL database support - -In order to support applications that require a database, -[PostgreSQL][postgresql] is provisioned by default. The credentials to access -the database are preconfigured, but can be customized by setting the associated -[variables](#environment-variables). These credentials can be used for defining a -`DATABASE_URL` of the format: - -```yaml -postgres://user:password@postgres-host:postgres-port/postgres-database -``` - -### Environment variables - -The following variables can be used for setting up the Auto DevOps domain, -providing a custom Helm chart, or scaling your application. PostgreSQL can be -also be customized, and you can easily use a [custom buildpack](#custom-buildpacks). - -| **Variable** | **Description** | -| ------------ | --------------- | -| `AUTO_DEVOPS_DOMAIN` | The [Auto DevOps domain](#auto-devops-domain); by default set automatically by the [Auto DevOps setting](#enabling-auto-devops). | -| `AUTO_DEVOPS_CHART` | The Helm Chart used to deploy your apps; defaults to the one [provided by GitLab](https://gitlab.com/charts/charts.gitlab.io/tree/master/charts/auto-deploy-app). | -| `REPLICAS` | The number of replicas to deploy; defaults to 1. | -| `PRODUCTION_REPLICAS` | The number of replicas to deploy in the production environment. This takes precedence over `REPLICAS`; defaults to 1. | -| `CANARY_REPLICAS` | The number of canary replicas to deploy for [Canary Deployments](https://docs.gitlab.com/ee/user/project/canary_deployments.html); defaults to 1 | -| `CANARY_PRODUCTION_REPLICAS` | The number of canary replicas to deploy for [Canary Deployments](https://docs.gitlab.com/ee/user/project/canary_deployments.html) in the production environment. This takes precedence over `CANARY_REPLICAS`; defaults to 1 | -| `POSTGRES_ENABLED` | Whether PostgreSQL is enabled; defaults to `"true"`. Set to `false` to disable the automatic deployment of PostgreSQL. | -| `POSTGRES_USER` | The PostgreSQL user; defaults to `user`. Set it to use a custom username. | -| `POSTGRES_PASSWORD` | The PostgreSQL password; defaults to `testing-password`. Set it to use a custom password. | -| `POSTGRES_DB` | The PostgreSQL database name; defaults to the value of [`$CI_ENVIRONMENT_SLUG`](../../ci/variables/README.md#predefined-variables-environment-variables). Set it to use a custom database name. | -| `BUILDPACK_URL` | The buildpack's full URL. It can point to either Git repositories or a tarball URL. For Git repositories, it is possible to point to a specific `ref`, for example `https://github.com/heroku/heroku-buildpack-ruby.git#v142` | -| `SAST_CONFIDENCE_LEVEL` | The minimum confidence level of security issues you want to be reported; `1` for Low, `2` for Medium, `3` for High; defaults to `3`.| -| `DEP_SCAN_DISABLE_REMOTE_CHECKS` | Whether remote Dependency Scanning checks are disabled; defaults to `"false"`. Set to `"true"` to disable checks that send data to GitLab central servers. [Read more about remote checks](https://gitlab.com/gitlab-org/security-products/dependency-scanning#remote-checks).| -| `STAGING_ENABLED` | From GitLab 10.8, this variable can be used to define a [deploy policy for staging and production environments](#deploy-policy-for-staging-and-production-environments). | -| `CANARY_ENABLED` | From GitLab 11.0, this variable can be used to define a [deploy policy for canary environments](#deploy-policy-for-canary-environments). | -| `INCREMENTAL_ROLLOUT_ENABLED`| From GitLab 10.8, this variable can be used to enable an [incremental rollout](#incremental-rollout-to-production) of your application for the production environment. | -| `TEST_DISABLED` | From GitLab 11.0, this variable can be used to disable the `test` job. If the variable is present, the job will not be created. | -| `CODEQUALITY_DISABLED` | From GitLab 11.0, this variable can be used to disable the `codequality` job. If the variable is present, the job will not be created. | -| `SAST_DISABLED` | From GitLab 11.0, this variable can be used to disable the `sast` job. If the variable is present, the job will not be created. | -| `DEPENDENCY_SCANNING_DISABLED` | From GitLab 11.0, this variable can be used to disable the `dependency_scanning` job. If the variable is present, the job will not be created. | -| `CONTAINER_SCANNING_DISABLED` | From GitLab 11.0, this variable can be used to disable the `sast:container` job. If the variable is present, the job will not be created. | -| `REVIEW_DISABLED` | From GitLab 11.0, this variable can be used to disable the `review` and the manual `review:stop` job. If the variable is present, these jobs will not be created. | -| `DAST_DISABLED` | From GitLab 11.0, this variable can be used to disable the `dast` job. If the variable is present, the job will not be created. | -| `PERFORMANCE_DISABLED` | From GitLab 11.0, this variable can be used to disable the `performance` job. If the variable is present, the job will not be created. | - -TIP: **Tip:** -Set up the replica variables using a -[project variable](../../ci/variables/README.md#secret-variables) -and scale your application by just redeploying it! - -CAUTION: **Caution:** -You should *not* scale your application using Kubernetes directly. This can -cause confusion with Helm not detecting the change, and subsequent deploys with -Auto DevOps can undo your changes. - -#### Advanced replica variables setup - -Apart from the two replica-related variables for production mentioned above, -you can also use others for different environments. - -There's a very specific mapping between Kubernetes' label named `track`, -GitLab CI/CD environment names, and the replicas environment variable. -The general rule is: `TRACK_ENV_REPLICAS`. Where: - -- `TRACK`: The capitalized value of the `track` - [Kubernetes label](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/) - in the Helm Chart app definition. If not set, it will not be taken into account - to the variable name. -- `ENV`: The capitalized environment name of the deploy job that is set in - `.gitlab-ci.yml`. - -That way, you can define your own `TRACK_ENV_REPLICAS` variables with which -you will be able to scale the pod's replicas easily. - -In the example below, the environment's name is `qa` and it deploys the track -`foo` which would result in looking for the `FOO_QA_REPLICAS` environment -variable: - -```yaml -QA testing: - stage: deploy - environment: - name: qa - script: - - deploy foo -``` - -The track `foo` being referenced would also need to be defined in the -application's Helm chart, like: - -```yaml -replicaCount: 1 -image: - repository: gitlab.example.com/group/project - tag: stable - pullPolicy: Always - secrets: - - name: gitlab-registry -application: - track: foo - tier: web -service: - enabled: true - name: web - type: ClusterIP - url: http://my.host.com/ - externalPort: 5000 - internalPort: 5000 -``` - -#### Deploy policy for staging and production environments - -> [Introduced](https://gitlab.com/gitlab-org/gitlab-ci-yml/merge_requests/160) -in GitLab 10.8. - -TIP: **Tip:** -You can also set this inside your [project's settings](#deployment-strategy). - -The normal behavior of Auto DevOps is to use Continuous Deployment, pushing -automatically to the `production` environment every time a new pipeline is run -on the default branch. However, there are cases where you might want to use a -staging environment and deploy to production manually. For this scenario, the -`STAGING_ENABLED` environment variable was introduced. - -If `STAGING_ENABLED` is defined in your project (e.g., set `STAGING_ENABLED` to -`1` as a secret variable), then the application will be automatically deployed -to a `staging` environment, and a `production_manual` job will be created for -you when you're ready to manually deploy to production. - -#### Deploy policy for canary environments **[PREMIUM]** - -> [Introduced](https://gitlab.com/gitlab-org/gitlab-ci-yml/merge_requests/171) -in GitLab 11.0. - -A [canary environment](https://docs.gitlab.com/ee/user/project/canary_deployments.html) can be used -before any changes are deployed to production. - -If `CANARY_ENABLED` is defined in your project (e.g., set `CANARY_ENABLED` to -`1` as a secret variable) then two manual jobs will be created: - -- `canary` which will deploy the application to the canary environment -- `production_manual` which is to be used by you when you're ready to manually - deploy to production. - -#### Incremental rollout to production **[PREMIUM]** - -> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/5415) in GitLab 10.8. - -TIP: **Tip:** -You can also set this inside your [project's settings](#deployment-strategy). - -When you have a new version of your app to deploy in production, you may want -to use an incremental rollout to replace just a few pods with the latest code. -This will allow you to first check how the app is behaving, and later manually -increasing the rollout up to 100%. - -If `INCREMENTAL_ROLLOUT_ENABLED` is defined in your project (e.g., set -`INCREMENTAL_ROLLOUT_ENABLED` to `1` as a secret variable), then instead of the -standard `production` job, 4 different -[manual jobs](../../ci/pipelines.md#manual-actions-from-the-pipeline-graph) -will be created: - -1. `rollout 10%` -1. `rollout 25%` -1. `rollout 50%` -1. `rollout 100%` - -The percentage is based on the `REPLICAS` variable and defines the number of -pods you want to have for your deployment. If you say `10`, and then you run -the `10%` rollout job, there will be `1` new pod + `9` old ones. - -To start a job, click on the play icon next to the job's name. You are not -required to go from `10%` to `100%`, you can jump to whatever job you want. -You can also scale down by running a lower percentage job, just before hitting -`100%`. Once you get to `100%`, you cannot scale down, and you'd have to roll -back by redeploying the old version using the -[rollback button](../../ci/environments.md#rolling-back-changes) in the -environment page. - -Below, you can see how the pipeline will look if the rollout or staging -variables are defined. - -- **Without `INCREMENTAL_ROLLOUT_ENABLED` and without `STAGING_ENABLED`** - - ![Staging and rollout disabled](img/rollout_staging_disabled.png) - -- **Without `INCREMENTAL_ROLLOUT_ENABLED` and with `STAGING_ENABLED`** - - ![Staging enabled](img/staging_enabled.png) - -- **With `INCREMENTAL_ROLLOUT_ENABLED` and without `STAGING_ENABLED`** - - ![Rollout enabled](img/rollout_enabled.png) - -- **With `INCREMENTAL_ROLLOUT_ENABLED` and with `STAGING_ENABLED`** - - ![Rollout and staging enabled](img/rollout_staging_enabled.png) - -## Currently supported languages - -NOTE: **Note:** -Not all buildpacks support Auto Test yet, as it's a relatively new -enhancement. All of Heroku's [officially supported -languages](https://devcenter.heroku.com/articles/heroku-ci#currently-supported-languages) -support it, and some third-party buildpacks as well e.g., Go, Node, Java, PHP, -Python, Ruby, Gradle, Scala, and Elixir all support Auto Test, but notably the -multi-buildpack does not. - -As of GitLab 10.0, the supported buildpacks are: - -``` -- heroku-buildpack-multi v1.0.0 -- heroku-buildpack-ruby v168 -- heroku-buildpack-nodejs v99 -- heroku-buildpack-clojure v77 -- heroku-buildpack-python v99 -- heroku-buildpack-java v53 -- heroku-buildpack-gradle v23 -- heroku-buildpack-scala v78 -- heroku-buildpack-play v26 -- heroku-buildpack-php v122 -- heroku-buildpack-go v72 -- heroku-buildpack-erlang fa17af9 -- buildpack-nginx v8 -``` - -## Limitations - -The following restrictions apply. - -### Private project support - -CAUTION: **Caution:** Private project support in Auto DevOps is experimental. - -When a project has been marked as private, GitLab's [Container -Registry][container-registry] requires authentication when downloading -containers. Auto DevOps will automatically provide the required authentication -information to Kubernetes, allowing temporary access to the registry. -Authentication credentials will be valid while the pipeline is running, allowing -for a successful initial deployment. - -After the pipeline completes, Kubernetes will no longer be able to access the -Container Registry. **Restarting a pod, scaling a service, or other actions which -require on-going access to the registry may fail**. On-going secure access is -planned for a subsequent release. - -## Troubleshooting - -- Auto Build and Auto Test may fail in detecting your language/framework. There - may be no buildpack for your application, or your application may be missing the - key files the buildpack is looking for. For example, for ruby apps, you must - have a `Gemfile` to be properly detected, even though it is possible to write a - Ruby app without a `Gemfile`. Try specifying a [custom - buildpack](#custom-buildpacks). -- Auto Test may fail because of a mismatch between testing frameworks. In this - case, you may need to customize your `.gitlab-ci.yml` with your test commands. - -### Disable the banner instance wide - -If an administrator would like to disable the banners on an instance level, this -feature can be disabled either through the console: - -```sh -sudo gitlab-rails console -``` - -Then run: - -```ruby -Feature.get(:auto_devops_banner_disabled).enable -``` - -Or through the HTTP API with an admin access token: - -```sh -curl --data "value=true" --header "PRIVATE-TOKEN: personal_access_token" https://gitlab.example.com/api/v4/features/auto_devops_banner_disabled -``` - -[ce-37115]: https://gitlab.com/gitlab-org/gitlab-ce/issues/37115 -[kubernetes-clusters]: ../../user/project/clusters/index.md -[docker-in-docker]: ../../docker/using_docker_build.md#use-docker-in-docker-executor -[review-app]: ../../ci/review_apps/index.md -[container-registry]: ../../user/project/container_registry.md -[postgresql]: https://www.postgresql.org/ -[Auto DevOps template]: https://gitlab.com/gitlab-org/gitlab-ci-yml/blob/master/Auto-DevOps.gitlab-ci.yml -[GitLab Omnibus Helm Chart]: ../../install/kubernetes/gitlab_omnibus.md +[permissions]: ../../permissions.md [ee]: https://about.gitlab.com/products/ -[ce-19507]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/19507 \ No newline at end of file +[Auto DevOps]: ../../../topics/autodevops/index.md