Repository: xanzy/go-gitlab
Pull request #556 (2019-01-30 11:36:42)

Add project clusters API support
Adds support for Project clusters API which is available from GitLab 11.7

https://docs.gitlab.com/ee/api/project_clusters.html

## Usage

Example usage of this endpoints.

```golang
package main

import (
	"fmt"
	"github.com/xanzy/go-gitlab"
	"time"
)

func main() {
	pid := 1234
	git := gitlab.NewClient(nil, "some_token")

	// LIST CLUSTERS

	clts, _, err := git.ProjectCluster.ListClusters(pid)

	if err != nil {
		panic(err)
	}

	fmt.Printf("%v\n", clts)

	// ADD CLUSTER

	name := "New cluster"
	envScope := "*"
	npc := &gitlab.AddClusterOptions{
		Name:             &name,
		EnvironmentScope: &envScope,
		PlatformKubernetes: &gitlab.PlatformKubernetesOptions{
			APIURL:            gitlab.String("https://123.123.123.12/"),
			Token:             gitlab.String(`token`),
			CaCert:            gitlab.String(`-----BEGIN CERTIFICATE-----\r\some certificate\r\n-----END CERTIFICATE-----`),
			Namespace:         gitlab.String("some-test-namespace"),
			AuthorizationType: gitlab.String("rbac"),
		},
	}

	cluster, _, err := git.ProjectCluster.AddCluster(pid, npc)

	if err != nil {
		panic(err)
	}

	// GET CLUSTER

	clt, _, err := git.ProjectCluster.GetCluster(pid, cluster.ID)

	if err != nil {
		panic(err)
	}

	fmt.Printf("%v\n", clt)

	time.Sleep(time.Second * 3)

	// EDIT CLUSTER

	changedName := "Changed K8S"
	optEdit := gitlab.EditClusterOptions{Name: &changedName}

	cltE, _, err := git.ProjectCluster.EditCluster(pid, cluster.ID, &optEdit)

	if err != nil {
		panic(err)
	}

	fmt.Printf("%v\n", cltE)

	time.Sleep(time.Second * 3)

	// DELETE CLUSTER

	_, err = git.ProjectCluster.DeleteCluster(pid, cluster.ID)

	if err != nil {
		panic(err)
	}
}
```


Patch:
diff --git a/README.md b/README.md
index a4550ea..dfb0034 100644
--- a/README.md
+++ b/README.md
@@ -66,6 +66,7 @@ to add new and/or missing endpoints. Currently the following services are suppor
 - [ ] Project import/export
 - [x] Project Members
 - [x] Project Snippets
+- [x] Project Clusters
 - [x] Protected Branches
 - [x] Protected Tags
 - [x] Repositories
diff --git a/gitlab.go b/gitlab.go
index efd3004..b5fa59b 100644
--- a/gitlab.go
+++ b/gitlab.go
@@ -320,6 +320,7 @@ type Client struct {
 	ProjectBadges         *ProjectBadgesService
 	ProjectSnippets       *ProjectSnippetsService
 	ProjectVariables      *ProjectVariablesService
+	ProjectCluster        *ProjectClustersService
 	ProtectedBranches     *ProtectedBranchesService
 	ProtectedTags         *ProtectedTagsService
 	Repositories          *RepositoriesService
@@ -461,6 +462,7 @@ func newClient(httpClient *http.Client) *Client {
 	c.ProjectBadges = &ProjectBadgesService{client: c}
 	c.ProjectSnippets = &ProjectSnippetsService{client: c}
 	c.ProjectVariables = &ProjectVariablesService{client: c}
+	c.ProjectCluster = &ProjectClustersService{client: c}
 	c.ProtectedBranches = &ProtectedBranchesService{client: c}
 	c.ProtectedTags = &ProtectedTagsService{client: c}
 	c.Repositories = &RepositoriesService{client: c}
diff --git a/project_clusters.go b/project_clusters.go
new file mode 100644
index 0000000..8f8f6ab
--- /dev/null
+++ b/project_clusters.go
@@ -0,0 +1,211 @@
+//
+// Copyright 2019, Matej Velikonja
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package gitlab
+
+import (
+	"fmt"
+	"net/url"
+	"time"
+)
+
+// ProjectClustersService handles communication with the
+// project clusters related methods of the GitLab API.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/project_clusters.html
+type ProjectClustersService struct {
+	client *Client
+}
+
+// ProjectCluster represents a GitLab Project Cluster.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/project_clusters.html
+type ProjectCluster struct {
+	ID                 int                 `json:"id"`
+	Name               string              `json:"name"`
+	CreatedAt          *time.Time          `json:"created_at"`
+	ProviderType       string              `json:"provider_type"`
+	PlatformType       string              `json:"platform_type"`
+	EnvironmentScope   string              `json:"environment_scope"`
+	ClusterType        string              `json:"cluster_type"`
+	User               *User               `json:"user"`
+	PlatformKubernetes *PlatformKubernetes `json:"platform_kubernetes"`
+	Project            *Project            `json:"project"`
+}
+
+func (v ProjectCluster) String() string {
+	return Stringify(v)
+}
+
+// PlatformKubernetes represents a GitLab Project Cluster PlatformKubernetes.
+type PlatformKubernetes struct {
+	APIURL            string `json:"api_url"`
+	Token             string `json:"token"`
+	CaCert            string `json:"ca_cert"`
+	Namespace         string `json:"namespace"`
+	AuthorizationType string `json:"authorization_type"`
+}
+
+// PlatformKubernetesOptions represents the available PlatformKubernetes options.
+type PlatformKubernetesOptions struct {
+	APIURL            *string `url:"api_url,omitempty" json:"api_url,omitempty"`
+	Token             *string `url:"token,omitempty" json:"token,omitempty"`
+	CaCert            *string `url:"ca_cert,omitempty" json:"ca_cert,omitempty"`
+	Namespace         *string `url:"namespace,omitempty" json:"namespace,omitempty"`
+	AuthorizationType *string `url:"authorization_type,omitempty" json:"authorization_type,omitempty"`
+}
+
+// ListClusters gets a list of all clusters in a project.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/project_clusters.html#list-project-clusters
+func (s *ProjectClustersService) ListClusters(pid interface{}, options ...OptionFunc) ([]*ProjectCluster, *Response, error) {
+	project, err := parseID(pid)
+	if err != nil {
+		return nil, nil, err
+	}
+	u := fmt.Sprintf("projects/%s/clusters", url.QueryEscape(project))
+
+	req, err := s.client.NewRequest("GET", u, nil, options)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	var pc []*ProjectCluster
+	resp, err := s.client.Do(req, &pc)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return pc, resp, err
+}
+
+// GetCluster gets a cluster.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/project_clusters.html#get-a-single-project-cluster
+func (s *ProjectClustersService) GetCluster(pid interface{}, cluster int, options ...OptionFunc) (*ProjectCluster, *Response, error) {
+	project, err := parseID(pid)
+	if err != nil {
+		return nil, nil, err
+	}
+	u := fmt.Sprintf("projects/%s/clusters/%d", url.QueryEscape(project), cluster)
+
+	req, err := s.client.NewRequest("GET", u, nil, options)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	var pc *ProjectCluster
+	resp, err := s.client.Do(req, &pc)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return pc, resp, err
+}
+
+// AddClusterOptions represents the available AddCluster() options.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/project_clusters.html#add-existing-cluster-to-project
+type AddClusterOptions struct {
+	Name               *string                    `url:"name,omitempty" json:"name,omitempty"`
+	Enabled            *bool                      `url:"enabled,omitempty" json:"enabled,omitempty"`
+	EnvironmentScope   *string                    `url:"environment_scope,omitempty" json:"environment_scope,omitempty"`
+	PlatformKubernetes *PlatformKubernetesOptions `url:"platform_kubernetes_attributes,omitempty" json:"platform_kubernetes_attributes,omitempty"`
+}
+
+// AddCluster adds an existing cluster to the project.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/project_clusters.html#add-existing-cluster-to-project
+func (s *ProjectClustersService) AddCluster(pid interface{}, opt *AddClusterOptions, options ...OptionFunc) (*ProjectCluster, *Response, error) {
+	project, err := parseID(pid)
+	if err != nil {
+		return nil, nil, err
+	}
+	u := fmt.Sprintf("projects/%s/clusters/user", url.QueryEscape(project))
+
+	req, err := s.client.NewRequest("POST", u, opt, options)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	pc := new(ProjectCluster)
+	resp, err := s.client.Do(req, pc)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return pc, resp, err
+}
+
+// EditClusterOptions represents the available EditCluster() options.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/project_clusters.html#edit-project-cluster
+type EditClusterOptions struct {
+	Name               *string                    `url:"name,omitempty" json:"name,omitempty"`
+	EnvironmentScope   *string                    `url:"environment_scope,omitempty" json:"environment_scope,omitempty"`
+	PlatformKubernetes *PlatformKubernetesOptions `url:"platform_kubernetes_attributes,omitempty" json:"platform_kubernetes_attributes,omitempty"`
+}
+
+// EditCluster updates an existing project cluster.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/project_clusters.html#edit-project-cluster
+func (s *ProjectClustersService) EditCluster(pid interface{}, cluster int, opt *EditClusterOptions, options ...OptionFunc) (*ProjectCluster, *Response, error) {
+	project, err := parseID(pid)
+	if err != nil {
+		return nil, nil, err
+	}
+	u := fmt.Sprintf("projects/%s/clusters/%d", url.QueryEscape(project), cluster)
+
+	req, err := s.client.NewRequest("PUT", u, opt, options)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	pc := new(ProjectCluster)
+	resp, err := s.client.Do(req, pc)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return pc, resp, err
+}
+
+// DeleteCluster deletes an existing project cluster.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/project_clusters.html#delete-project-cluster
+func (s *ProjectClustersService) DeleteCluster(pid interface{}, cluster int, options ...OptionFunc) (*Response, error) {
+	project, err := parseID(pid)
+	if err != nil {
+		return nil, err
+	}
+	u := fmt.Sprintf("projects/%s/clusters/%d", url.QueryEscape(project), cluster)
+
+	req, err := s.client.NewRequest("DELETE", u, nil, options)
+	if err != nil {
+		return nil, err
+	}
+
+	return s.client.Do(req, nil)
+}


Test patch:
diff --git a/project_clusters_test.go b/project_clusters_test.go
new file mode 100644
index 0000000..029e411
--- /dev/null
+++ b/project_clusters_test.go
@@ -0,0 +1,305 @@
+package gitlab
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+)
+
+func TestListClusters(t *testing.T) {
+	mux, server, client := setup()
+	defer teardown(server)
+	pid := 1234
+
+	mux.HandleFunc("/api/v4/projects/1234/clusters", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "GET")
+		response := `[
+  {
+    "id":18,
+    "name":"cluster-1",
+    "created_at":"2019-01-02T20:18:12.563Z",
+    "provider_type":"user",
+    "platform_type":"kubernetes",
+    "environment_scope":"*",
+    "cluster_type":"project_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",
+      "namespace":"cluster-1-namespace",
+      "authorization_type":"rbac",
+      "ca_cert":"-----BEGIN CERTIFICATE-----\r\nhFiK1L61owwDQYJKoZIhvcNAQELBQAw\r\nLzEtMCsGA1UEAxMkZDA1YzQ1YjctNzdiMS00NDY0LThjNmEtMTQ0ZDJkZjM4ZDBj\r\nMB4XDTE4MTIyNzIwMDM1MVoXDTIzMTIyNjIxMDM1MVowLzEtMCsGA1UEAxMkZDA1\r\nYzQ1YjctNzdiMS00NDY0LThjNmEtMTQ0ZDJkZjM.......-----END CERTIFICATE-----"
+    }
+  }
+]`
+		fmt.Fprint(w, response)
+	})
+
+	clusters, _, err := client.ProjectCluster.ListClusters(pid)
+
+	if err != nil {
+		t.Errorf("ProjectClusters.ListClusters returned error: %v", err)
+	}
+
+	if len(clusters) != 1 {
+		t.Errorf("expected 1 cluster; got %d", len(clusters))
+	}
+
+	if clusters[0].ID != 18 {
+		t.Errorf("expected clusterID 1; got %d", clusters[0].ID)
+	}
+}
+
+func TestGetCluster(t *testing.T) {
+	mux, server, client := setup()
+	defer teardown(server)
+	pid := 1234
+
+	mux.HandleFunc("/api/v4/projects/1234/clusters/1", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "GET")
+		response := `{
+  "id":18,
+  "name":"cluster-1",
+  "created_at":"2019-01-02T20:18:12.563Z",
+  "provider_type":"user",
+  "platform_type":"kubernetes",
+  "environment_scope":"*",
+  "cluster_type":"project_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",
+    "namespace":"cluster-1-namespace",
+    "authorization_type":"rbac",
+    "ca_cert":"-----BEGIN CERTIFICATE-----\r\nhFiK1L61owwDQYJKoZIhvcNAQELBQAw\r\nLzEtMCsGA1UEAxMkZDA1YzQ1YjctNzdiMS00NDY0LThjNmEtMTQ0ZDJkZjM4ZDBj\r\nMB4XDTE4MTIyNzIwMDM1MVoXDTIzMTIyNjIxMDM1MVowLzEtMCsGA1UEAxMkZDA1\r\nYzQ1YjctNzdiMS00NDY0LThjNmEtMTQ0ZDJkZjM.......-----END CERTIFICATE-----"
+  },
+  "project":
+  {
+    "id":26,
+    "description":"",
+    "name":"project-with-clusters-api",
+    "name_with_namespace":"Administrator / project-with-clusters-api",
+    "path":"project-with-clusters-api",
+    "path_with_namespace":"root/project-with-clusters-api",
+    "created_at":"2019-01-02T20:13:32.600Z",
+    "default_branch":null,
+    "tag_list":[],
+    "ssh_url_to_repo":"ssh://gitlab.example.com/root/project-with-clusters-api.git",
+    "http_url_to_repo":"https://gitlab.example.com/root/project-with-clusters-api.git",
+    "web_url":"https://gitlab.example.com/root/project-with-clusters-api",
+    "readme_url":null,
+    "avatar_url":null,
+    "star_count":0,
+    "forks_count":0,
+    "last_activity_at":"2019-01-02T20:13:32.600Z",
+    "namespace":
+    {
+      "id":1,
+      "name":"root",
+      "path":"root",
+      "kind":"user",
+      "full_path":"root",
+      "parent_id":null
+    }
+  }
+}`
+		fmt.Fprint(w, response)
+	})
+
+	cluster, _, err := client.ProjectCluster.GetCluster(pid, 1)
+
+	if err != nil {
+		t.Errorf("ProjectClusters.ListClusters returned error: %v", err)
+	}
+
+	if cluster.ID != 18 {
+		t.Errorf("expected clusterID 18; got %d", cluster.ID)
+	}
+}
+
+func TestAddCluster(t *testing.T) {
+	mux, server, client := setup()
+	defer teardown(server)
+	pid := 1234
+
+	mux.HandleFunc("/api/v4/projects/1234/clusters/user", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "POST")
+		response := `{
+  "id":24,
+  "name":"cluster-5",
+  "created_at":"2019-01-03T21:53:40.610Z",
+  "provider_type":"user",
+  "platform_type":"kubernetes",
+  "environment_scope":"*",
+  "cluster_type":"project_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",
+    "namespace":"cluster-5-namespace",
+    "authorization_type":"rbac",
+    "ca_cert":"-----BEGIN CERTIFICATE-----\r\nhFiK1L61owwDQYJKoZIhvcNAQELBQAw\r\nLzEtMCsGA1UEAxMkZDA1YzQ1YjctNzdiMS00NDY0LThjNmEtMTQ0ZDJkZjM4ZDBj\r\nMB4XDTE4MTIyNzIwMDM1MVoXDTIzMTIyNjIxMDM1MVowLzEtMCsGA1UEAxMkZDA1\r\nYzQ1YjctNzdiMS00NDY0LThjNmEtMTQ0ZDJkZjM.......-----END CERTIFICATE-----"
+  },
+  "project":
+  {
+    "id":26,
+    "description":"",
+    "name":"project-with-clusters-api",
+    "name_with_namespace":"Administrator / project-with-clusters-api",
+    "path":"project-with-clusters-api",
+    "path_with_namespace":"root/project-with-clusters-api",
+    "created_at":"2019-01-02T20:13:32.600Z",
+    "default_branch":null,
+    "tag_list":[],
+    "ssh_url_to_repo":"ssh:://gitlab.example.com/root/project-with-clusters-api.git",
+    "http_url_to_repo":"https://gitlab.example.com/root/project-with-clusters-api.git",
+    "web_url":"https://gitlab.example.com/root/project-with-clusters-api",
+    "readme_url":null,
+    "avatar_url":null,
+    "star_count":0,
+    "forks_count":0,
+    "last_activity_at":"2019-01-02T20:13:32.600Z",
+    "namespace":
+    {
+      "id":1,
+      "name":"root",
+      "path":"root",
+      "kind":"user",
+      "full_path":"root",
+      "parent_id":null
+    }
+  }
+}`
+		fmt.Fprint(w, response)
+	})
+
+	cluster, _, err := client.ProjectCluster.AddCluster(pid, &AddClusterOptions{})
+
+	if err != nil {
+		t.Errorf("ProjectClusters.AddCluster returned error: %v", err)
+	}
+
+	if cluster.ID != 24 {
+		t.Errorf("expected ClusterID 24; got %d", cluster.ID)
+	}
+}
+
+func TestEditCluster(t *testing.T) {
+	mux, server, client := setup()
+	defer teardown(server)
+	pid := 1234
+
+	mux.HandleFunc("/api/v4/projects/1234/clusters/1", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "PUT")
+		response := `{
+  "id":24,
+  "name":"new-cluster-name",
+  "created_at":"2019-01-03T21:53:40.610Z",
+  "provider_type":"user",
+  "platform_type":"kubernetes",
+  "environment_scope":"*",
+  "cluster_type":"project_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",
+    "namespace":"cluster-5-namespace",
+    "authorization_type":"rbac",
+    "ca_cert":null
+  },
+  "project":
+  {
+    "id":26,
+    "description":"",
+    "name":"project-with-clusters-api",
+    "name_with_namespace":"Administrator / project-with-clusters-api",
+    "path":"project-with-clusters-api",
+    "path_with_namespace":"root/project-with-clusters-api",
+    "created_at":"2019-01-02T20:13:32.600Z",
+    "default_branch":null,
+    "tag_list":[],
+    "ssh_url_to_repo":"ssh:://gitlab.example.com/root/project-with-clusters-api.git",
+    "http_url_to_repo":"https://gitlab.example.com/root/project-with-clusters-api.git",
+    "web_url":"https://gitlab.example.com/root/project-with-clusters-api",
+    "readme_url":null,
+    "avatar_url":null,
+    "star_count":0,
+    "forks_count":0,
+    "last_activity_at":"2019-01-02T20:13:32.600Z",
+    "namespace":
+    {
+      "id":1,
+      "name":"root",
+      "path":"root",
+      "kind":"user",
+      "full_path":"root",
+      "parent_id":null
+    }
+  }
+}`
+		fmt.Fprint(w, response)
+	})
+
+	cluster, _, err := client.ProjectCluster.EditCluster(pid, 1, &EditClusterOptions{})
+
+	if err != nil {
+		t.Errorf("ProjectClusters.EditCluster returned error: %v", err)
+	}
+
+	if cluster.ID != 24 {
+		t.Errorf("expected ClusterID 24; got %d", cluster.ID)
+	}
+}
+
+func TestDeleteCluster(t *testing.T) {
+	mux, server, client := setup()
+	defer teardown(server)
+
+	mux.HandleFunc("/api/v4/projects/1234/clusters/1", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "DELETE")
+		w.WriteHeader(http.StatusAccepted)
+	})
+
+	resp, err := client.ProjectCluster.DeleteCluster(1234, 1)
+	if err != nil {
+		t.Errorf("ProjectCluster.DeleteCluster returned error: %v", err)
+	}
+
+	want := http.StatusAccepted
+	got := resp.StatusCode
+	if got != want {
+		t.Errorf("ProjectCluster.DeleteCluster returned %d, want %d", got, want)
+	}
+}