Repository: xanzy/go-gitlab
Pull request #1461 (2022-05-04 09:10:14)

Implementation of Agents for Kubernetes APIs
This change sets adds two new services:

* `ClusterAgents`
* `ClusterAgentTokens`

The former is already available in 14.9 and the latter is anticipated to land in 15.0, see https://gitlab.com/gitlab-org/gitlab/-/merge_requests/85704/

I suggest to wait for a merge of this PR until everything is merged and available on gitlab.com.


Patch:
diff --git a/cluster_agents.go b/cluster_agents.go
new file mode 100644
index 0000000..ed3b33e
--- /dev/null
+++ b/cluster_agents.go
@@ -0,0 +1,294 @@
+//
+// Copyright 2022, Timo Furrer <tuxtimo@gmail.com>
+//
+// 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/http"
+	"time"
+)
+
+// ClusterAgentsService handles communication with the cluster agents related
+// methods of the GitLab API.
+//
+// GitLab API docs: https://docs.gitlab.com/ee/api/cluster_agents.html
+type ClusterAgentsService struct {
+	client *Client
+}
+
+// Agent represents a GitLab agent for Kubernetes.
+//
+// GitLab API docs: https://docs.gitlab.com/ee/api/cluster_agents.html
+type Agent struct {
+	ID              int           `json:"id"`
+	Name            string        `json:"name"`
+	CreatedAt       *time.Time    `json:"created_at"`
+	CreatedByUserID int           `json:"created_by_user_id"`
+	ConfigProject   ConfigProject `json:"config_project"`
+}
+
+type ConfigProject struct {
+	ID                int        `json:"id"`
+	Description       string     `json:"description"`
+	Name              string     `json:"name"`
+	NameWithNamespace string     `json:"name_with_namespace"`
+	Path              string     `json:"path"`
+	PathWithNamespace string     `json:"path_with_namespace"`
+	CreatedAt         *time.Time `json:"created_at"`
+}
+
+func (a Agent) String() string {
+	return Stringify(a)
+}
+
+// AgentToken represents a GitLab agent token.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/cluster_agents.html#list-tokens-for-an-agent
+type AgentToken struct {
+	ID              int        `json:"id"`
+	Name            string     `json:"name"`
+	Description     string     `json:"description"`
+	AgentID         int        `json:"agent_id"`
+	Status          string     `json:"status"`
+	CreatedAt       *time.Time `json:"created_at"`
+	CreatedByUserID int        `json:"created_by_user_id"`
+	LastUsedAt      *time.Time `json:"last_used_at"`
+	Token           string     `json:"token"`
+}
+
+func (a AgentToken) String() string {
+	return Stringify(a)
+}
+
+// ListProjectAgentsOptions represents the available ListProjectAgents() options.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/cluster_agents.html#list-the-agents-for-a-project
+type ListProjectAgentsOptions ListOptions
+
+// ListProjectAgents returns a list of agents registered for the project.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/cluster_agents.html#list-the-agents-for-a-project
+func (s *ClusterAgentsService) ListProjectAgents(pid interface{}, opt *ListProjectAgentsOptions, options ...RequestOptionFunc) ([]*Agent, *Response, error) {
+	project, err := parseID(pid)
+	if err != nil {
+		return nil, nil, err
+	}
+	uri := fmt.Sprintf("projects/%s/cluster_agents", PathEscape(project))
+
+	req, err := s.client.NewRequest(http.MethodGet, uri, opt, options)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	var as []*Agent
+	resp, err := s.client.Do(req, &as)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return as, resp, err
+}
+
+// GetAgent gets a single agent details.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/cluster_agents.html#get-details-about-an-agent
+func (s *ClusterAgentsService) GetAgent(pid interface{}, id int, options ...RequestOptionFunc) (*Agent, *Response, error) {
+	project, err := parseID(pid)
+	if err != nil {
+		return nil, nil, err
+	}
+	uri := fmt.Sprintf("projects/%s/cluster_agents/%d", PathEscape(project), id)
+
+	req, err := s.client.NewRequest(http.MethodGet, uri, nil, options)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	a := new(Agent)
+	resp, err := s.client.Do(req, a)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return a, resp, err
+}
+
+// RegisterProjectAgentOptions represents the available RegisterProjectAgent()
+// options.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/cluster_agents.html#register-an-agent-with-a-project
+type RegisterProjectAgentOptions struct {
+	Name *string `url:"name,omitempty" json:"name,omitempty"`
+}
+
+// RegisterProjectAgent registers an agent to the project.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/cluster_agents.html#register-an-agent-with-a-project
+func (s *ClusterAgentsService) RegisterProjectAgent(pid interface{}, opt *RegisterProjectAgentOptions, options ...RequestOptionFunc) (*Agent, *Response, error) {
+	project, err := parseID(pid)
+	if err != nil {
+		return nil, nil, err
+	}
+	uri := fmt.Sprintf("projects/%s/cluster_agents", PathEscape(project))
+
+	req, err := s.client.NewRequest(http.MethodPost, uri, opt, options)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	a := new(Agent)
+	resp, err := s.client.Do(req, a)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return a, resp, err
+}
+
+// DeleteAgent deletes an existing agent registration.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/cluster_agents.html#delete-a-registered-agent
+func (s *ClusterAgentsService) DeleteAgent(pid interface{}, id int, options ...RequestOptionFunc) (*Response, error) {
+	project, err := parseID(pid)
+	if err != nil {
+		return nil, err
+	}
+	uri := fmt.Sprintf("projects/%s/cluster_agents/%d", PathEscape(project), id)
+
+	req, err := s.client.NewRequest(http.MethodDelete, uri, nil, options)
+	if err != nil {
+		return nil, err
+	}
+
+	return s.client.Do(req, nil)
+}
+
+// ListAgentTokensOptions represents the available ListAgentTokens() options.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/cluster_agents.html#list-tokens-for-an-agent
+type ListAgentTokensOptions ListOptions
+
+// ListAgentTokens returns a list of tokens for an agent.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/cluster_agents.html#list-tokens-for-an-agent
+func (s *ClusterAgentsService) ListAgentTokens(pid interface{}, aid int, opt *ListAgentTokensOptions, options ...RequestOptionFunc) ([]*AgentToken, *Response, error) {
+	project, err := parseID(pid)
+	if err != nil {
+		return nil, nil, err
+	}
+	uri := fmt.Sprintf("projects/%s/cluster_agents/%d/tokens", PathEscape(project), aid)
+
+	req, err := s.client.NewRequest(http.MethodGet, uri, opt, options)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	var ats []*AgentToken
+	resp, err := s.client.Do(req, &ats)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return ats, resp, err
+}
+
+// GetAgentToken gets a single agent token.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/cluster_agents.html#get-a-single-agent-token
+func (s *ClusterAgentsService) GetAgentToken(pid interface{}, aid int, id int, options ...RequestOptionFunc) (*AgentToken, *Response, error) {
+	project, err := parseID(pid)
+	if err != nil {
+		return nil, nil, err
+	}
+	uri := fmt.Sprintf("projects/%s/cluster_agents/%d/tokens/%d", PathEscape(project), aid, id)
+
+	req, err := s.client.NewRequest(http.MethodGet, uri, nil, options)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	at := new(AgentToken)
+	resp, err := s.client.Do(req, at)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return at, resp, err
+}
+
+// CreateAgentTokenOptions represents the available CreateAgentToken() options.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/cluster_agents.html#create-an-agent-token
+type CreateAgentTokenOptions struct {
+	Name        *string `url:"name,omitempty" json:"name,omitempty"`
+	Description *string `url:"description,omitempty" json:"description,omitempty"`
+}
+
+// CreateAgentToken creates a new token for an agent.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/cluster_agents.html#create-an-agent-token
+func (s *ClusterAgentsService) CreateAgentToken(pid interface{}, aid int, opt *CreateAgentTokenOptions, options ...RequestOptionFunc) (*AgentToken, *Response, error) {
+	project, err := parseID(pid)
+	if err != nil {
+		return nil, nil, err
+	}
+	uri := fmt.Sprintf("projects/%s/cluster_agents/%d/tokens", PathEscape(project), aid)
+
+	req, err := s.client.NewRequest(http.MethodPost, uri, opt, options)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	at := new(AgentToken)
+	resp, err := s.client.Do(req, at)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return at, resp, err
+}
+
+// RevokeAgentToken revokes an agent token.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/cluster_agents.html#revoke-an-agent-token
+func (s *ClusterAgentsService) RevokeAgentToken(pid interface{}, aid int, id int, options ...RequestOptionFunc) (*Response, error) {
+	project, err := parseID(pid)
+	if err != nil {
+		return nil, err
+	}
+	uri := fmt.Sprintf("projects/%s/cluster_agents/%d/tokens/%d", PathEscape(project), aid, id)
+
+	req, err := s.client.NewRequest(http.MethodDelete, uri, nil, options)
+	if err != nil {
+		return nil, err
+	}
+
+	return s.client.Do(req, nil)
+}
diff --git a/examples/cluster_agents.go b/examples/cluster_agents.go
new file mode 100644
index 0000000..510dd8e
--- /dev/null
+++ b/examples/cluster_agents.go
@@ -0,0 +1,51 @@
+//
+// Copyright 2021, Timo Furrer <tuxtimo@gmail.com>
+//
+// 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 main
+
+import (
+	"fmt"
+	"log"
+
+	"github.com/xanzy/go-gitlab"
+)
+
+func clusterAgentsExample() {
+	git, err := gitlab.NewClient("tokengoeshere")
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	projectID := 33
+
+	// Register Cluster Agent
+	clusterAgent, _, err := git.ClusterAgents.RegisterProjectAgent(projectID, &gitlab.RegisterProjectAgentOptions{
+		Name: gitlab.String("agent-2"),
+	})
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Printf("Cluster Agent: %+v\n", clusterAgent)
+
+	// List Cluster Agents
+	clusterAgents, _, err := git.ClusterAgents.ListProjectAgents(projectID, nil)
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Printf("Cluster Agents: %+v\n", clusterAgents)
+}
diff --git a/gitlab.go b/gitlab.go
index d1d59bb..5d8c04e 100644
--- a/gitlab.go
+++ b/gitlab.go
@@ -109,6 +109,7 @@ type Client struct {
 	Boards                 *IssueBoardsService
 	Branches               *BranchesService
 	BroadcastMessage       *BroadcastMessagesService
+	ClusterAgents          *ClusterAgentsService
 	CIYMLTemplate          *CIYMLTemplatesService
 	Commits                *CommitsService
 	ContainerRegistry      *ContainerRegistryService
@@ -308,6 +309,7 @@ func newClient(options ...ClientOptionFunc) (*Client, error) {
 	c.Boards = &IssueBoardsService{client: c}
 	c.Branches = &BranchesService{client: c}
 	c.BroadcastMessage = &BroadcastMessagesService{client: c}
+	c.ClusterAgents = &ClusterAgentsService{client: c}
 	c.CIYMLTemplate = &CIYMLTemplatesService{client: c}
 	c.Commits = &CommitsService{client: c}
 	c.ContainerRegistry = &ContainerRegistryService{client: c}


Test patch:
diff --git a/cluster_agents_test.go b/cluster_agents_test.go
new file mode 100644
index 0000000..b7cae91
--- /dev/null
+++ b/cluster_agents_test.go
@@ -0,0 +1,357 @@
+//
+// Copyright 2022, Timo Furrer <tuxtimo@gmail.com>
+//
+// 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/http"
+	"reflect"
+	"testing"
+	"time"
+)
+
+func ListClusterAgents(t *testing.T) {
+	mux, server, client := setup(t)
+	defer teardown(server)
+
+	mux.HandleFunc("/api/v4/projects/20/cluster_agents", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, http.MethodGet)
+		fmt.Fprint(w, `
+		[
+			{
+			  "id": 1,
+			  "name": "agent-1",
+			  "config_project": {
+				"id": 20,
+				"description": "",
+				"name": "test",
+				"name_with_namespace": "Administrator / test",
+				"path": "test",
+				"path_with_namespace": "root/test",
+				"created_at": "2022-03-20T20:42:40.221Z"
+			  },
+			  "created_at": "2022-04-20T20:42:40.221Z",
+			  "created_by_user_id": 42
+			},
+			{
+			  "id": 2,
+			  "name": "agent-2",
+			  "config_project": {
+				"id": 20,
+				"description": "",
+				"name": "test",
+				"name_with_namespace": "Administrator / test",
+				"path": "test",
+				"path_with_namespace": "root/test",
+				"created_at": "2022-03-20T20:42:40.221Z"
+			  },
+			  "created_at": "2022-04-20T20:42:40.221Z",
+			  "created_by_user_id": 42
+			}
+		  ]
+		`)
+	})
+
+	opt := &ListProjectAgentsOptions{}
+	clusterAgents, _, err := client.ClusterAgents.ListProjectAgents(20, opt)
+	if err != nil {
+		t.Errorf("ClusterAgents.ListClusterAgents returned error: %v", err)
+	}
+
+	want := []*Agent{
+		{
+			ID:   1,
+			Name: "agent-1",
+			ConfigProject: ConfigProject{
+				ID:                20,
+				Description:       "",
+				Name:              "test",
+				NameWithNamespace: "Administrator / test",
+				Path:              "test",
+				PathWithNamespace: "root/test",
+				CreatedAt:         Time(time.Date(2022, time.March, 20, 20, 42, 40, 221000000, time.UTC)),
+			},
+			CreatedAt:       Time(time.Date(2022, time.April, 20, 20, 42, 40, 221000000, time.UTC)),
+			CreatedByUserID: 42,
+		},
+		{
+			ID:   2,
+			Name: "agent-2",
+			ConfigProject: ConfigProject{
+				ID:                20,
+				Description:       "",
+				Name:              "test",
+				NameWithNamespace: "Administrator / test",
+				Path:              "test",
+				PathWithNamespace: "root/test",
+				CreatedAt:         Time(time.Date(2022, time.March, 20, 20, 42, 40, 221000000, time.UTC)),
+			},
+			CreatedAt:       Time(time.Date(2022, time.April, 20, 20, 42, 40, 221000000, time.UTC)),
+			CreatedByUserID: 42,
+		},
+	}
+
+	if !reflect.DeepEqual(want, clusterAgents) {
+		t.Errorf("ClusterAgents.ListClusterAgents returned %+v, want %+v", clusterAgents, want)
+	}
+}
+
+func GetClusterAgent(t *testing.T) {
+	mux, server, client := setup(t)
+	defer teardown(server)
+
+	mux.HandleFunc("/api/v4/projects/20/cluster_agents/1", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, http.MethodGet)
+		fmt.Fprint(w, `
+			{
+				"id": 1,
+				"name": "agent-1",
+				"config_project": {
+				"id": 20,
+				"description": "",
+				"name": "test",
+				"name_with_namespace": "Administrator / test",
+				"path": "test",
+				"path_with_namespace": "root/test",
+				"created_at": "2022-03-20T20:42:40.221Z"
+				},
+				"created_at": "2022-04-20T20:42:40.221Z",
+				"created_by_user_id": 42
+			}
+    	`)
+	})
+
+	clusterAgent, _, err := client.ClusterAgents.GetAgent(20, 1)
+	if err != nil {
+		t.Errorf("ClusterAgents.GetClusterAgent returned error: %v", err)
+	}
+
+	want := &Agent{
+		ID:   1,
+		Name: "agent-1",
+		ConfigProject: ConfigProject{
+			ID:                20,
+			Description:       "",
+			Name:              "test",
+			NameWithNamespace: "Administrator / test",
+			Path:              "test",
+			PathWithNamespace: "root/test",
+			CreatedAt:         Time(time.Date(2022, time.March, 20, 20, 42, 40, 221000000, time.UTC)),
+		},
+		CreatedAt:       Time(time.Date(2022, time.April, 20, 20, 42, 40, 221000000, time.UTC)),
+		CreatedByUserID: 42,
+	}
+	if !reflect.DeepEqual(want, clusterAgent) {
+		t.Errorf("ClusterAgents.GetClusterAgent returned %+v, want %+v", clusterAgent, want)
+	}
+}
+
+func RegisterClusterAgent(t *testing.T) {
+	mux, server, client := setup(t)
+	defer teardown(server)
+
+	mux.HandleFunc("/api/v4/projects/20/cluster_agents", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, http.MethodPost)
+		fmt.Fprint(w, `
+			{
+				"id": 1,
+				"name": "agent-1",
+				"config_project": {
+				  "id": 20,
+				  "description": "",
+				  "name": "test",
+				  "name_with_namespace": "Administrator / test",
+				  "path": "test",
+				  "path_with_namespace": "root/test",
+				  "created_at": "2022-03-20T20:42:40.221Z"
+				},
+				"created_at": "2022-04-20T20:42:40.221Z",
+				"created_by_user_id": 42
+			  }
+    	`)
+	})
+
+	opt := &RegisterProjectAgentOptions{Name: String("agent-1")}
+	clusterAgent, _, err := client.ClusterAgents.RegisterProjectAgent(20, opt)
+	if err != nil {
+		t.Errorf("ClusterAgents.RegisterClusterAgent returned error: %v", err)
+	}
+
+	want := &Agent{
+		ID:   1,
+		Name: "agent-1",
+		ConfigProject: ConfigProject{
+			ID:                20,
+			Description:       "",
+			Name:              "test",
+			NameWithNamespace: "Administrator / test",
+			Path:              "test",
+			PathWithNamespace: "root/test",
+			CreatedAt:         Time(time.Date(2022, time.March, 20, 20, 42, 40, 221000000, time.UTC)),
+		},
+		CreatedAt:       Time(time.Date(2022, time.April, 20, 20, 42, 40, 221000000, time.UTC)),
+		CreatedByUserID: 42,
+	}
+	if !reflect.DeepEqual(want, clusterAgent) {
+		t.Errorf("ClusterAgents.RegisterClusterAgent returned %+v, want %+v", clusterAgent, want)
+	}
+}
+
+func ListAgentTokens(t *testing.T) {
+	mux, server, client := setup(t)
+	defer teardown(server)
+
+	mux.HandleFunc("/api/v4/projects/20/cluster_agents/5/tokens", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, http.MethodGet)
+		fmt.Fprint(w, `
+		[
+			{
+			  "id": 1,
+			  "name": "abcd",
+			  "description": "Some token",
+			  "agent_id": 5,
+			  "status": "active",
+			  "created_at": "2022-03-25T14:12:11.497Z",
+			  "created_by_user_id": 1
+			},
+			{
+			  "id": 2,
+			  "name": "foobar",
+			  "description": null,
+			  "agent_id": 5,
+			  "status": "active",
+			  "created_at": "2022-03-25T14:12:11.497Z",
+			  "created_by_user_id": 1
+			}
+		]
+		`)
+	})
+
+	opt := &ListAgentTokensOptions{}
+	clusterAgentTokens, _, err := client.ClusterAgents.ListAgentTokens(20, 5, opt)
+	if err != nil {
+		t.Errorf("ClusterAgents.ListAgentTokens returned error: %v", err)
+	}
+
+	want := []*AgentToken{
+		{
+			ID:              1,
+			Name:            "abcd",
+			Description:     "Some token",
+			AgentID:         5,
+			Status:          "active",
+			CreatedAt:       Time(time.Date(2022, time.March, 25, 14, 12, 11, 497000000, time.UTC)),
+			CreatedByUserID: 1,
+		},
+		{
+			ID:              2,
+			Name:            "foobar",
+			Description:     "",
+			AgentID:         5,
+			Status:          "active",
+			CreatedAt:       Time(time.Date(2022, time.March, 25, 14, 12, 11, 497000000, time.UTC)),
+			CreatedByUserID: 1,
+		},
+	}
+
+	if !reflect.DeepEqual(want, clusterAgentTokens) {
+		t.Errorf("ClusterAgents.ListAgentTokens returned %+v, want %+v", clusterAgentTokens, want)
+	}
+}
+
+func GetAgentToken(t *testing.T) {
+	mux, server, client := setup(t)
+	defer teardown(server)
+
+	mux.HandleFunc("/api/v4/projects/20/cluster_agents/5/tokens/1", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, http.MethodGet)
+		fmt.Fprint(w, `
+		{
+			"id": 1,
+			"name": "abcd",
+			"description": "Some token",
+			"agent_id": 5,
+			"status": "active",
+			"created_at": "2022-03-25T14:12:11.497Z",
+			"created_by_user_id": 1,
+			"last_used_at": null
+		 }
+    	`)
+	})
+
+	clusterAgentToken, _, err := client.ClusterAgents.GetAgentToken(20, 5, 1)
+	if err != nil {
+		t.Errorf("ClusterAgents.GetAgentToken returned error: %v", err)
+	}
+
+	want := &AgentToken{
+		ID:              1,
+		Name:            "abcd",
+		Description:     "Some token",
+		AgentID:         5,
+		Status:          "active",
+		CreatedAt:       Time(time.Date(2022, time.March, 25, 14, 12, 11, 497000000, time.UTC)),
+		CreatedByUserID: 1,
+		LastUsedAt:      nil,
+	}
+	if !reflect.DeepEqual(want, clusterAgentToken) {
+		t.Errorf("ClusterAgents.GetAgentToken returned %+v, want %+v", clusterAgentToken, want)
+	}
+}
+
+func RegisterAgentToken(t *testing.T) {
+	mux, server, client := setup(t)
+	defer teardown(server)
+
+	mux.HandleFunc("/api/v4/projects/20/cluster_agents/5/tokens", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, http.MethodPost)
+		fmt.Fprint(w, `
+		{
+			"id": 1,
+			"name": "abcd",
+			"description": "Some token",
+			"agent_id": 5,
+			"status": "active",
+			"created_at": "2022-03-25T14:12:11.497Z",
+			"created_by_user_id": 1,
+			"last_used_at": null,
+			"token": "qeY8UVRisx9y3Loxo1scLxFuRxYcgeX3sxsdrpP_fR3Loq4xyg"
+		}
+    	`)
+	})
+
+	opt := &CreateAgentTokenOptions{Name: String("abcd"), Description: String("Some token")}
+	clusterAgentToken, _, err := client.ClusterAgents.CreateAgentToken(20, 5, opt)
+	if err != nil {
+		t.Errorf("ClusterAgents.CreateAgentToken returned error: %v", err)
+	}
+
+	want := &AgentToken{
+		ID:              1,
+		Name:            "abcd",
+		Description:     "Some token",
+		AgentID:         5,
+		Status:          "active",
+		CreatedAt:       Time(time.Date(2022, time.March, 25, 14, 12, 11, 497000000, time.UTC)),
+		CreatedByUserID: 1,
+		LastUsedAt:      nil,
+		Token:           "qeY8UVRisx9y3Loxo1scLxFuRxYcgeX3sxsdrpP_fR3Loq4xyg",
+	}
+	if !reflect.DeepEqual(want, clusterAgentToken) {
+		t.Errorf("ClusterAgents.CreateAgentToken returned %+v, want %+v", clusterAgentToken, want)
+	}
+}