From 78cf713223a75138e152ba223abd7d30cbba0322 Mon Sep 17 00:00:00 2001
From: Dylan Griffith <dyl.griffith@gmail.com>
Date: Mon, 1 Jul 2019 16:49:00 +1000
Subject: [PATCH] Add API for CRUD group clusters

This is basically a copy of the API for project clusters.
---
 .../unreleased/55623-group-cluster-apis.yml   |   5 +
 doc/api/group_clusters.md                     | 280 +++++++++++
 lib/api/api.rb                                |   1 +
 lib/api/entities.rb                           |   4 +
 lib/api/group_clusters.rb                     | 140 ++++++
 spec/requests/api/group_clusters_spec.rb      | 452 ++++++++++++++++++
 6 files changed, 882 insertions(+)
 create mode 100644 changelogs/unreleased/55623-group-cluster-apis.yml
 create mode 100644 doc/api/group_clusters.md
 create mode 100644 lib/api/group_clusters.rb
 create mode 100644 spec/requests/api/group_clusters_spec.rb

diff --git a/changelogs/unreleased/55623-group-cluster-apis.yml b/changelogs/unreleased/55623-group-cluster-apis.yml
new file mode 100644
index 00000000000..fe987ef4a82
--- /dev/null
+++ b/changelogs/unreleased/55623-group-cluster-apis.yml
@@ -0,0 +1,5 @@
+---
+title: Add API for CRUD group clusters
+merge_request: 30213
+author:
+type: added
diff --git a/doc/api/group_clusters.md b/doc/api/group_clusters.md
new file mode 100644
index 00000000000..71a05b4d338
--- /dev/null
+++ b/doc/api/group_clusters.md
@@ -0,0 +1,280 @@
+# Group clusters API
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/30213)
+in GitLab 12.1.
+
+NOTE: **Note:**
+User will need at least maintainer access for the group to use these endpoints.
+
+## List group clusters
+
+Returns a list of group clusters.
+
+```
+GET /groups/:id/clusters
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) |
+
+Example request:
+
+```bash
+curl --header 'Private-Token: <your_access_token>' https://gitlab.example.com/api/v4/groups/26/clusters
+```
+
+Example response:
+
+```json
+[
+  {
+    "id":18,
+    "name":"cluster-1",
+    "domain":"example.com",
+    "created_at":"2019-01-02T20:18:12.563Z",
+    "provider_type":"user",
+    "platform_type":"kubernetes",
+    "environment_scope":"*",
+    "cluster_type":"group_type",
+    "user":
+    {
+      "id":1,
+      "name":"Administrator",
+      "username":"root",
+      "state":"active",
+      "avatar_url":"https://www.gravatar.com/avatar/4249f4df72b..",
+      "web_url":"https://gitlab.example.com/root"
+    },
+    "platform_kubernetes":
+    {
+      "api_url":"https://104.197.68.152",
+      "authorization_type":"rbac",
+      "ca_cert":"-----BEGIN CERTIFICATE-----\r\nhFiK1L61owwDQYJKoZIhvcNAQELBQAw\r\nLzEtMCsGA1UEAxMkZDA1YzQ1YjctNzdiMS00NDY0LThjNmEtMTQ0ZDJkZjM4ZDBj\r\nMB4XDTE4MTIyNzIwMDM1MVoXDTIzMTIyNjIxMDM1MVowLzEtMCsGA1UEAxMkZDA1\r\nYzQ1YjctNzdiMS00NDY0LThjNmEtMTQ0ZDJkZjM.......-----END CERTIFICATE-----"
+    }
+  },
+  {
+    "id":19,
+    "name":"cluster-2",
+    ...
+  }
+]
+```
+
+## Get a single group cluster
+
+Gets a single group cluster.
+
+```
+GET /groups/:id/clusters/:cluster_id
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) |
+| `cluster_id` | integer | yes | The ID of the cluster |
+
+Example request:
+
+```bash
+curl --header 'Private-Token: <your_access_token>' https://gitlab.example.com/api/v4/groups/26/clusters/18
+```
+
+Example response:
+
+```json
+{
+  "id":18,
+  "name":"cluster-1",
+  "domain":"example.com",
+  "created_at":"2019-01-02T20:18:12.563Z",
+  "provider_type":"user",
+  "platform_type":"kubernetes",
+  "environment_scope":"*",
+  "cluster_type":"group_type",
+  "user":
+  {
+    "id":1,
+    "name":"Administrator",
+    "username":"root",
+    "state":"active",
+    "avatar_url":"https://www.gravatar.com/avatar/4249f4df72b..",
+    "web_url":"https://gitlab.example.com/root"
+  },
+  "platform_kubernetes":
+  {
+    "api_url":"https://104.197.68.152",
+    "authorization_type":"rbac",
+    "ca_cert":"-----BEGIN CERTIFICATE-----\r\nhFiK1L61owwDQYJKoZIhvcNAQELBQAw\r\nLzEtMCsGA1UEAxMkZDA1YzQ1YjctNzdiMS00NDY0LThjNmEtMTQ0ZDJkZjM4ZDBj\r\nMB4XDTE4MTIyNzIwMDM1MVoXDTIzMTIyNjIxMDM1MVowLzEtMCsGA1UEAxMkZDA1\r\nYzQ1YjctNzdiMS00NDY0LThjNmEtMTQ0ZDJkZjM.......-----END CERTIFICATE-----"
+  },
+  "group":
+  {
+    "id":26,
+    "name":"group-with-clusters-api",
+    "web_url":"https://gitlab.example.com/group-with-clusters-api"
+  }
+}
+```
+
+## Add existing cluster to group
+
+Adds an existing Kubernetes cluster to the group.
+
+```
+POST /groups/:id/clusters/user
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) |
+| `name` | String | yes | The name of the cluster |
+| `domain` | String | no | The [base domain](../user/group/clusters/index.md#base-domain) of the cluster |
+| `enabled` | Boolean | no | Determines if cluster is active or not, defaults to true |
+| `managed` | Boolean | no | Determines if GitLab will manage namespaces and service accounts for this cluster, defaults to true |
+| `platform_kubernetes_attributes[api_url]` | String | yes | The URL to access the Kubernetes API |
+| `platform_kubernetes_attributes[token]` | String | yes | The token to authenticate against Kubernetes |
+| `platform_kubernetes_attributes[ca_cert]` | String | no | TLS certificate (needed if API is using a self-signed TLS certificate |
+| `platform_kubernetes_attributes[authorization_type]` | String | no | The cluster authorization type: `rbac`, `abac` or `unknown_authorization`. Defaults to `rbac`. |
+| `environment_scope` | String | no | The associated environment to the cluster. Defaults to `*` **[PREMIUM]** |
+
+Example request:
+
+```bash
+curl --header 'Private-Token: <your_access_token>' https://gitlab.example.com/api/v4/groups/26/clusters/user \
+-H "Accept: application/json" \
+-H "Content-Type:application/json" \
+--request POST --data '{"name":"cluster-5", "platform_kubernetes_attributes":{"api_url":"https://35.111.51.20","token":"12345","ca_cert":"-----BEGIN CERTIFICATE-----\r\nhFiK1L61owwDQYJKoZIhvcNAQELBQAw\r\nLzEtMCsGA1UEAxMkZDA1YzQ1YjctNzdiMS00NDY0LThjNmEtMTQ0ZDJkZjM4ZDBj\r\nMB4XDTE4MTIyNzIwMDM1MVoXDTIzMTIyNjIxMDM1MVowLzEtMCsGA1UEAxMkZDA1\r\nYzQ1YjctNzdiMS00NDY0LThjNmEtMTQ0ZDJkZjM.......-----END CERTIFICATE-----"}}'
+```
+
+Example response:
+
+```json
+{
+  "id":24,
+  "name":"cluster-5",
+  "created_at":"2019-01-03T21:53:40.610Z",
+  "provider_type":"user",
+  "platform_type":"kubernetes",
+  "environment_scope":"*",
+  "cluster_type":"group_type",
+  "user":
+  {
+    "id":1,
+    "name":"Administrator",
+    "username":"root",
+    "state":"active",
+    "avatar_url":"https://www.gravatar.com/avatar/4249f4df72b..",
+    "web_url":"https://gitlab.example.com/root"
+  },
+  "platform_kubernetes":
+  {
+    "api_url":"https://35.111.51.20",
+    "authorization_type":"rbac",
+    "ca_cert":"-----BEGIN CERTIFICATE-----\r\nhFiK1L61owwDQYJKoZIhvcNAQELBQAw\r\nLzEtMCsGA1UEAxMkZDA1YzQ1YjctNzdiMS00NDY0LThjNmEtMTQ0ZDJkZjM4ZDBj\r\nMB4XDTE4MTIyNzIwMDM1MVoXDTIzMTIyNjIxMDM1MVowLzEtMCsGA1UEAxMkZDA1\r\nYzQ1YjctNzdiMS00NDY0LThjNmEtMTQ0ZDJkZjM.......-----END CERTIFICATE-----"
+  },
+  "group":
+  {
+    "id":26,
+    "name":"group-with-clusters-api",
+    "web_url":"https://gitlab.example.com/root/group-with-clusters-api"
+  }
+}
+```
+
+## Edit group cluster
+
+Updates an existing group cluster.
+
+```
+PUT /groups/:id/clusters/:cluster_id
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) |
+| `cluster_id` | integer | yes | The ID of the cluster |
+| `name` | String | no | The name of the cluster |
+| `domain` | String | no | The [base domain](../user/group/clusters/index.md#base-domain) of the cluster |
+| `platform_kubernetes_attributes[api_url]` | String | no | The URL to access the Kubernetes API |
+| `platform_kubernetes_attributes[token]` | String | no | The token to authenticate against Kubernetes |
+| `platform_kubernetes_attributes[ca_cert]` | String | no | TLS certificate (needed if API is using a self-signed TLS certificate |
+| `environment_scope` | String | no | The associated environment to the cluster **[PREMIUM]** |
+
+NOTE: **Note:**
+`name`, `api_url`, `ca_cert` and `token` can only be updated if the cluster was added
+through the ["Add an existing Kubernetes Cluster"](../user/project/clusters/index.md#adding-an-existing-kubernetes-cluster) option or
+through the ["Add existing cluster to group"](#add-existing-cluster-to-group) endpoint.
+
+Example request:
+
+```bash
+curl --header 'Private-Token: <your_access_token>' https://gitlab.example.com/api/v4/groups/26/clusters/24 \
+-H "Content-Type:application/json" \
+--request PUT --data '{"name":"new-cluster-name","domain":"new-domain.com","api_url":"https://new-api-url.com"}'
+```
+
+Example response:
+
+```json
+{
+  "id":24,
+  "name":"new-cluster-name",
+  "domain":"new-domain.com",
+  "created_at":"2019-01-03T21:53:40.610Z",
+  "provider_type":"user",
+  "platform_type":"kubernetes",
+  "environment_scope":"*",
+  "cluster_type":"group_type",
+  "user":
+  {
+    "id":1,
+    "name":"Administrator",
+    "username":"root",
+    "state":"active",
+    "avatar_url":"https://www.gravatar.com/avatar/4249f4df72b..",
+    "web_url":"https://gitlab.example.com/root"
+  },
+  "platform_kubernetes":
+  {
+    "api_url":"https://new-api-url.com",
+    "authorization_type":"rbac",
+    "ca_cert":null
+  },
+  "group":
+  {
+    "id":26,
+    "name":"group-with-clusters-api",
+    "web_url":"https://gitlab.example.com/group-with-clusters-api"
+  }
+}
+
+```
+
+## Delete group cluster
+
+Deletes an existing group cluster.
+
+```
+DELETE /groups/:id/clusters/:cluster_id
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) |
+| `cluster_id` | integer | yes | The ID of the cluster |
+
+Example request:
+
+```bash
+curl --request DELETE --header 'Private-Token: <your_access_token>' https://gitlab.example.com/api/v4/groups/26/clusters/23'
+```
diff --git a/lib/api/api.rb b/lib/api/api.rb
index 5f1e63aea0f..cb70704d2b6 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -108,6 +108,7 @@ module API
     mount ::API::Features
     mount ::API::Files
     mount ::API::GroupBoards
+    mount ::API::GroupClusters
     mount ::API::GroupLabels
     mount ::API::GroupMilestones
     mount ::API::Groups
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 06a9089df6d..a23ea0e2a6d 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -1666,6 +1666,10 @@ module API
     class ClusterProject < Cluster
       expose :project, using: Entities::BasicProjectDetails
     end
+
+    class ClusterGroup < Cluster
+      expose :group, using: Entities::BasicGroupDetails
+    end
   end
 end
 
diff --git a/lib/api/group_clusters.rb b/lib/api/group_clusters.rb
new file mode 100644
index 00000000000..db0f8081140
--- /dev/null
+++ b/lib/api/group_clusters.rb
@@ -0,0 +1,140 @@
+# frozen_string_literal: true
+
+module API
+  class GroupClusters < Grape::API
+    include PaginationParams
+
+    before { authenticate! }
+
+    # EE::API::GroupClusters will
+    # override these methods
+    helpers do
+      params :create_params_ee do
+      end
+
+      params :update_params_ee do
+      end
+    end
+
+    params do
+      requires :id, type: String, desc: 'The ID of the group'
+    end
+    resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
+      desc 'Get all clusters from the group' do
+        success Entities::Cluster
+      end
+      params do
+        use :pagination
+      end
+      get ':id/clusters' do
+        authorize! :read_cluster, user_group
+
+        present paginate(clusters_for_current_user), with: Entities::Cluster
+      end
+
+      desc 'Get specific cluster for the group' do
+        success Entities::ClusterGroup
+      end
+      params do
+        requires :cluster_id, type: Integer, desc: 'The cluster ID'
+      end
+      get ':id/clusters/:cluster_id' do
+        authorize! :read_cluster, cluster
+
+        present cluster, with: Entities::ClusterGroup
+      end
+
+      desc 'Adds an existing cluster' do
+        success Entities::ClusterGroup
+      end
+      params do
+        requires :name, type: String, desc: 'Cluster name'
+        optional :enabled, type: Boolean, default: true, desc: 'Determines if cluster is active or not, defaults to true'
+        optional :domain, type: String, desc: 'Cluster base domain'
+        optional :managed, type: Boolean, default: true, desc: 'Determines if GitLab will manage namespaces and service accounts for this cluster, defaults to true'
+        requires :platform_kubernetes_attributes, type: Hash, desc: %q(Platform Kubernetes data) do
+          requires :api_url, type: String, allow_blank: false, desc: 'URL to access the Kubernetes API'
+          requires :token, type: String, desc: 'Token to authenticate against Kubernetes'
+          optional :ca_cert, type: String, desc: 'TLS certificate (needed if API is using a self-signed TLS certificate)'
+          optional :namespace, type: String, desc: 'Unique namespace related to Group'
+          optional :authorization_type, type: String, values: Clusters::Platforms::Kubernetes.authorization_types.keys, default: 'rbac', desc: 'Cluster authorization type, defaults to RBAC'
+        end
+        use :create_params_ee
+      end
+      post ':id/clusters/user' do
+        authorize! :add_cluster, user_group
+
+        user_cluster = ::Clusters::CreateService
+          .new(current_user, create_cluster_user_params)
+          .execute
+
+        if user_cluster.persisted?
+          present user_cluster, with: Entities::ClusterGroup
+        else
+          render_validation_error!(user_cluster)
+        end
+      end
+
+      desc 'Update an existing cluster' do
+        success Entities::ClusterGroup
+      end
+      params do
+        requires :cluster_id, type: Integer, desc: 'The cluster ID'
+        optional :name, type: String, desc: 'Cluster name'
+        optional :domain, type: String, desc: 'Cluster base domain'
+        optional :platform_kubernetes_attributes, type: Hash, desc: %q(Platform Kubernetes data) do
+          optional :api_url, type: String, desc: 'URL to access the Kubernetes API'
+          optional :token, type: String, desc: 'Token to authenticate against Kubernetes'
+          optional :ca_cert, type: String, desc: 'TLS certificate (needed if API is using a self-signed TLS certificate)'
+          optional :namespace, type: String, desc: 'Unique namespace related to Group'
+        end
+        use :update_params_ee
+      end
+      put ':id/clusters/:cluster_id' do
+        authorize! :update_cluster, cluster
+
+        update_service = Clusters::UpdateService.new(current_user, update_cluster_params)
+
+        if update_service.execute(cluster)
+          present cluster, with: Entities::ClusterGroup
+        else
+          render_validation_error!(cluster)
+        end
+      end
+
+      desc 'Remove a cluster' do
+        success Entities::ClusterGroup
+      end
+      params do
+        requires :cluster_id, type: Integer, desc: 'The Cluster ID'
+      end
+      delete ':id/clusters/:cluster_id' do
+        authorize! :admin_cluster, cluster
+
+        destroy_conditionally!(cluster)
+      end
+    end
+
+    helpers do
+      def clusters_for_current_user
+        @clusters_for_current_user ||= ClustersFinder.new(user_group, current_user, :all).execute
+      end
+
+      def cluster
+        @cluster ||= clusters_for_current_user.find(params[:cluster_id])
+      end
+
+      def create_cluster_user_params
+        declared_params.merge({
+          provider_type: :user,
+          platform_type: :kubernetes,
+          clusterable: user_group
+        })
+      end
+
+      def update_cluster_params
+        declared_params(include_missing: false).without(:cluster_id)
+      end
+    end
+  end
+end
diff --git a/spec/requests/api/group_clusters_spec.rb b/spec/requests/api/group_clusters_spec.rb
new file mode 100644
index 00000000000..46e3dd650cc
--- /dev/null
+++ b/spec/requests/api/group_clusters_spec.rb
@@ -0,0 +1,452 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe API::GroupClusters do
+  include KubernetesHelpers
+
+  let(:current_user) { create(:user) }
+  let(:developer_user) { create(:user) }
+  let(:group) { create(:group, :private) }
+
+  before do
+    group.add_developer(developer_user)
+    group.add_maintainer(current_user)
+  end
+
+  describe 'GET /groups/:id/clusters' do
+    let!(:extra_cluster) { create(:cluster, :provided_by_gcp, :group) }
+
+    let!(:clusters) do
+      create_list(:cluster, 5, :provided_by_gcp, :group, :production_environment,
+                  groups: [group])
+    end
+
+    context 'non-authorized user' do
+      it 'responds with 403' do
+        get api("/groups/#{group.id}/clusters", developer_user)
+
+        expect(response).to have_gitlab_http_status(403)
+      end
+    end
+
+    context 'authorized user' do
+      before do
+        get api("/groups/#{group.id}/clusters", current_user)
+      end
+
+      it 'responds with 200' do
+        expect(response).to have_gitlab_http_status(200)
+      end
+
+      it 'includes pagination headers' do
+        expect(response).to include_pagination_headers
+      end
+
+      it 'only include authorized clusters' do
+        cluster_ids = json_response.map { |cluster| cluster['id'] }
+
+        expect(cluster_ids).to match_array(clusters.pluck(:id))
+        expect(cluster_ids).not_to include(extra_cluster.id)
+      end
+    end
+  end
+
+  describe 'GET /groups/:id/clusters/:cluster_id' do
+    let(:cluster_id) { cluster.id }
+
+    let(:platform_kubernetes) do
+      create(:cluster_platform_kubernetes, :configured)
+    end
+
+    let(:cluster) do
+      create(:cluster, :group, :provided_by_gcp, :with_domain,
+             platform_kubernetes: platform_kubernetes,
+             user: current_user,
+             groups: [group])
+    end
+
+    context 'non-authorized user' do
+      it 'responds with 403' do
+        get api("/groups/#{group.id}/clusters/#{cluster_id}", developer_user)
+
+        expect(response).to have_gitlab_http_status(403)
+      end
+    end
+
+    context 'authorized user' do
+      before do
+        get api("/groups/#{group.id}/clusters/#{cluster_id}", current_user)
+      end
+
+      it 'returns specific cluster' do
+        expect(json_response['id']).to eq(cluster.id)
+      end
+
+      it 'returns cluster information' do
+        expect(json_response['provider_type']).to eq('gcp')
+        expect(json_response['platform_type']).to eq('kubernetes')
+        expect(json_response['environment_scope']).to eq('*')
+        expect(json_response['cluster_type']).to eq('group_type')
+        expect(json_response['domain']).to eq('example.com')
+      end
+
+      it 'returns group information' do
+        cluster_group = json_response['group']
+
+        expect(cluster_group['id']).to eq(group.id)
+        expect(cluster_group['name']).to eq(group.name)
+        expect(cluster_group['web_url']).to eq(group.web_url)
+      end
+
+      it 'returns kubernetes platform information' do
+        platform = json_response['platform_kubernetes']
+
+        expect(platform['api_url']).to eq('https://kubernetes.example.com')
+        expect(platform['ca_cert']).to be_present
+      end
+
+      it 'returns user information' do
+        user = json_response['user']
+
+        expect(user['id']).to eq(current_user.id)
+        expect(user['username']).to eq(current_user.username)
+      end
+
+      it 'returns GCP provider information' do
+        gcp_provider = json_response['provider_gcp']
+
+        expect(gcp_provider['cluster_id']).to eq(cluster.id)
+        expect(gcp_provider['status_name']).to eq('created')
+        expect(gcp_provider['gcp_project_id']).to eq('test-gcp-project')
+        expect(gcp_provider['zone']).to eq('us-central1-a')
+        expect(gcp_provider['machine_type']).to eq('n1-standard-2')
+        expect(gcp_provider['num_nodes']).to eq(3)
+        expect(gcp_provider['endpoint']).to eq('111.111.111.111')
+      end
+
+      context 'when cluster has no provider' do
+        let(:cluster) do
+          create(:cluster, :group, :provided_by_user,
+                 groups: [group])
+        end
+
+        it 'does not include GCP provider info' do
+          expect(json_response['provider_gcp']).not_to be_present
+        end
+      end
+
+      context 'with non-existing cluster' do
+        let(:cluster_id) { 123 }
+
+        it 'returns 404' do
+          expect(response).to have_gitlab_http_status(404)
+        end
+      end
+    end
+  end
+
+  shared_context 'kubernetes calls stubbed' do
+    before do
+      stub_kubeclient_discover(api_url)
+    end
+  end
+
+  describe 'POST /groups/:id/clusters/user' do
+    include_context 'kubernetes calls stubbed'
+
+    let(:api_url) { 'https://kubernetes.example.com' }
+    let(:authorization_type) { 'rbac' }
+
+    let(:platform_kubernetes_attributes) do
+      {
+        api_url: api_url,
+        token: 'sample-token',
+        authorization_type: authorization_type
+      }
+    end
+
+    let(:cluster_params) do
+      {
+        name: 'test-cluster',
+        domain: 'domain.example.com',
+        managed: false,
+        platform_kubernetes_attributes: platform_kubernetes_attributes
+      }
+    end
+
+    context 'non-authorized user' do
+      it 'responds with 403' do
+        post api("/groups/#{group.id}/clusters/user", developer_user), params: cluster_params
+
+        expect(response).to have_gitlab_http_status(403)
+      end
+    end
+
+    context 'authorized user' do
+      before do
+        post api("/groups/#{group.id}/clusters/user", current_user), params: cluster_params
+      end
+
+      context 'with valid params' do
+        it 'responds with 201' do
+          expect(response).to have_gitlab_http_status(201)
+        end
+
+        it 'creates a new Cluster::Cluster' do
+          cluster_result = Clusters::Cluster.find(json_response["id"])
+          platform_kubernetes = cluster_result.platform
+
+          expect(cluster_result).to be_user
+          expect(cluster_result).to be_kubernetes
+          expect(cluster_result.group).to eq(group)
+          expect(cluster_result.name).to eq('test-cluster')
+          expect(cluster_result.domain).to eq('domain.example.com')
+          expect(cluster_result.managed).to be_falsy
+          expect(platform_kubernetes.rbac?).to be_truthy
+          expect(platform_kubernetes.api_url).to eq(api_url)
+          expect(platform_kubernetes.token).to eq('sample-token')
+        end
+      end
+
+      context 'when user does not indicate authorization type' do
+        let(:platform_kubernetes_attributes) do
+          {
+            api_url: api_url,
+            token: 'sample-token'
+          }
+        end
+
+        it 'defaults to RBAC' do
+          cluster_result = Clusters::Cluster.find(json_response['id'])
+
+          expect(cluster_result.platform_kubernetes.rbac?).to be_truthy
+        end
+      end
+
+      context 'when user sets authorization type as ABAC' do
+        let(:authorization_type) { 'abac' }
+
+        it 'creates an ABAC cluster' do
+          cluster_result = Clusters::Cluster.find(json_response['id'])
+
+          expect(cluster_result.platform.abac?).to be_truthy
+        end
+      end
+
+      context 'with invalid params' do
+        let(:api_url) { 'invalid_api_url' }
+
+        it 'responds with 400' do
+          expect(response).to have_gitlab_http_status(400)
+        end
+
+        it 'does not create a new Clusters::Cluster' do
+          expect(group.reload.clusters).to be_empty
+        end
+
+        it 'returns validation errors' do
+          expect(json_response['message']['platform_kubernetes.api_url'].first).to be_present
+        end
+      end
+    end
+
+    context 'when user tries to add multiple clusters' do
+      before do
+        create(:cluster, :provided_by_gcp, :group,
+               groups: [group])
+
+        post api("/groups/#{group.id}/clusters/user", current_user), params: cluster_params
+      end
+
+      it 'responds with 400' do
+        expect(response).to have_gitlab_http_status(400)
+        expect(json_response['message']['base'].first).to include('Instance does not support multiple Kubernetes clusters')
+      end
+    end
+
+    context 'non-authorized user' do
+      before do
+        post api("/groups/#{group.id}/clusters/user", developer_user), params: cluster_params
+      end
+
+      it 'responds with 403' do
+        expect(response).to have_gitlab_http_status(403)
+
+        expect(json_response['message']).to eq('403 Forbidden')
+      end
+    end
+  end
+
+  describe 'PUT /groups/:id/clusters/:cluster_id' do
+    include_context 'kubernetes calls stubbed'
+
+    let(:api_url) { 'https://kubernetes.example.com' }
+
+    let(:update_params) do
+      {
+        domain: domain,
+        platform_kubernetes_attributes: platform_kubernetes_attributes
+      }
+    end
+
+    let(:domain) { 'new-domain.com' }
+    let(:platform_kubernetes_attributes) { {} }
+
+    let(:cluster) do
+      create(:cluster, :group, :provided_by_gcp,
+             groups: [group], domain: 'old-domain.com')
+    end
+
+    context 'non-authorized user' do
+      it 'responds with 403' do
+        put api("/groups/#{group.id}/clusters/#{cluster.id}", developer_user), params: update_params
+
+        expect(response).to have_gitlab_http_status(403)
+      end
+    end
+
+    context 'authorized user' do
+      before do
+        put api("/groups/#{group.id}/clusters/#{cluster.id}", current_user), params: update_params
+
+        cluster.reload
+      end
+
+      context 'with valid params' do
+        it 'responds with 200' do
+          expect(response).to have_gitlab_http_status(200)
+        end
+
+        it 'updates cluster attributes' do
+          expect(cluster.domain).to eq('new-domain.com')
+        end
+      end
+
+      context 'with invalid params' do
+        let(:domain) { 'invalid domain' }
+
+        it 'responds with 400' do
+          expect(response).to have_gitlab_http_status(400)
+        end
+
+        it 'does not update cluster attributes' do
+          expect(cluster.domain).to eq('old-domain.com')
+        end
+
+        it 'returns validation errors' do
+          expect(json_response['message']['domain'].first).to match('contains invalid characters (valid characters: [a-z0-9\\-])')
+        end
+      end
+
+      context 'with a GCP cluster' do
+        context 'when user tries to change GCP specific fields' do
+          let(:platform_kubernetes_attributes) do
+            {
+              api_url: 'https://new-api-url.com',
+              token: 'new-sample-token'
+            }
+          end
+
+          it 'responds with 400' do
+            expect(response).to have_gitlab_http_status(400)
+          end
+
+          it 'returns validation error' do
+            expect(json_response['message']['platform_kubernetes.base'].first).to eq('Cannot modify managed Kubernetes cluster')
+          end
+        end
+
+        context 'when user tries to change domain' do
+          let(:domain) { 'new-domain.com' }
+
+          it 'responds with 200' do
+            expect(response).to have_gitlab_http_status(200)
+          end
+        end
+      end
+
+      context 'with an user cluster' do
+        let(:api_url) { 'https://new-api-url.com' }
+
+        let(:cluster) do
+          create(:cluster, :group, :provided_by_user,
+                 groups: [group])
+        end
+
+        let(:platform_kubernetes_attributes) do
+          {
+            api_url: api_url,
+            token: 'new-sample-token'
+          }
+        end
+
+        let(:update_params) do
+          {
+            name: 'new-name',
+            platform_kubernetes_attributes: platform_kubernetes_attributes
+          }
+        end
+
+        it 'responds with 200' do
+          expect(response).to have_gitlab_http_status(200)
+        end
+
+        it 'updates platform kubernetes attributes' do
+          platform_kubernetes = cluster.platform_kubernetes
+
+          expect(cluster.name).to eq('new-name')
+          expect(platform_kubernetes.api_url).to eq('https://new-api-url.com')
+          expect(platform_kubernetes.token).to eq('new-sample-token')
+        end
+      end
+
+      context 'with a cluster that does not belong to user' do
+        let(:cluster) { create(:cluster, :group, :provided_by_user) }
+
+        it 'responds with 404' do
+          expect(response).to have_gitlab_http_status(404)
+        end
+      end
+    end
+  end
+
+  describe 'DELETE /groups/:id/clusters/:cluster_id' do
+    let(:cluster_params) { { cluster_id: cluster.id } }
+
+    let(:cluster) do
+      create(:cluster, :group, :provided_by_gcp,
+             groups: [group])
+    end
+
+    context 'non-authorized user' do
+      it 'responds with 403' do
+        delete api("/groups/#{group.id}/clusters/#{cluster.id}", developer_user), params: cluster_params
+
+        expect(response).to have_gitlab_http_status(403)
+      end
+    end
+
+    context 'authorized user' do
+      before do
+        delete api("/groups/#{group.id}/clusters/#{cluster.id}", current_user), params: cluster_params
+      end
+
+      it 'responds with 204' do
+        expect(response).to have_gitlab_http_status(204)
+      end
+
+      it 'deletes the cluster' do
+        expect(Clusters::Cluster.exists?(id: cluster.id)).to be_falsy
+      end
+
+      context 'with a cluster that does not belong to user' do
+        let(:cluster) { create(:cluster, :group, :provided_by_user) }
+
+        it 'responds with 404' do
+          expect(response).to have_gitlab_http_status(404)
+        end
+      end
+    end
+  end
+end
-- 
2.30.9