Repository: xanzy/go-gitlab
Pull request #1940 (2024-05-14 19:36:51)

Add resource group API
Adds support for the Resource Group API: https://docs.gitlab.com/ee/api/resource_groups.html

Resolves https://github.com/xanzy/go-gitlab/issues/1479


Patch:
diff --git a/gitlab.go b/gitlab.go
index 18a400f..5db676f 100644
--- a/gitlab.go
+++ b/gitlab.go
@@ -207,6 +207,7 @@ type Client struct {
 	Repositories                 *RepositoriesService
 	RepositoryFiles              *RepositoryFilesService
 	RepositorySubmodules         *RepositorySubmodulesService
+	ResourceGroup                *ResourceGroupService
 	ResourceIterationEvents      *ResourceIterationEventsService
 	ResourceLabelEvents          *ResourceLabelEventsService
 	ResourceMilestoneEvents      *ResourceMilestoneEventsService
@@ -443,6 +444,7 @@ func newClient(options ...ClientOptionFunc) (*Client, error) {
 	c.Repositories = &RepositoriesService{client: c}
 	c.RepositoryFiles = &RepositoryFilesService{client: c}
 	c.RepositorySubmodules = &RepositorySubmodulesService{client: c}
+	c.ResourceGroup = &ResourceGroupService{client: c}
 	c.ResourceIterationEvents = &ResourceIterationEventsService{client: c}
 	c.ResourceLabelEvents = &ResourceLabelEventsService{client: c}
 	c.ResourceMilestoneEvents = &ResourceMilestoneEventsService{client: c}
diff --git a/resource_group.go b/resource_group.go
new file mode 100644
index 0000000..b11cd8b
--- /dev/null
+++ b/resource_group.go
@@ -0,0 +1,165 @@
+//
+// Copyright 2021, Sander van Harmelen
+//
+// 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"
+)
+
+// ResourceGroupService handles communication with the resource
+// group related methods of the GitLab API.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/resource_groups.html
+type ResourceGroupService struct {
+	client *Client
+}
+
+// ResourceGrouop represents a GitLab Project Resource Group.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/resource_groups.html
+type ResourceGroup struct {
+	ID          int        `json:"id"`
+	Key         string     `json:"key"`
+	ProcessMode string     `json:"process_mode"`
+	CreatedAt   *time.Time `json:"created_at"`
+	UpdatedAt   *time.Time `json:"updated_at"`
+}
+
+// Gets a string representation of a ResourceGroup
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/resource_groups.html
+func (rg ResourceGroup) String() string {
+	return Stringify(rg)
+}
+
+// GetAllResourceGroupsForAProject allows you to get all resource
+// groups associated with a given project.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/resource_groups.html#get-all-resource-groups-for-a-project
+func (s *ResourceGroupService) GetAllResourceGroupsForAProject(pid interface{}, options ...RequestOptionFunc) ([]*ResourceGroup, *Response, error) {
+	project, err := parseID(pid)
+	if err != nil {
+		return nil, nil, err
+	}
+	u := fmt.Sprintf("projects/%s/resource_groups", PathEscape(project))
+
+	req, err := s.client.NewRequest(http.MethodGet, u, nil, options)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	var rgs []*ResourceGroup
+	resp, err := s.client.Do(req, &rgs)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return rgs, resp, nil
+}
+
+// GetASpecificResourceGroup allows you to get a specific
+// resource group for a given project.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/resource_groups.html#get-a-specific-resource-group
+func (s *ResourceGroupService) GetASpecificResourceGroup(pid interface{}, key string, options ...RequestOptionFunc) (*ResourceGroup, *Response, error) {
+	project, err := parseID(pid)
+	if err != nil {
+		return nil, nil, err
+	}
+	u := fmt.Sprintf("projects/%s/resource_groups/%s", PathEscape(project), key)
+
+	req, err := s.client.NewRequest(http.MethodGet, u, nil, options)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	rg := new(ResourceGroup)
+	resp, err := s.client.Do(req, rg)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return rg, resp, nil
+}
+
+// ListUpcomingJobsForASpecificResourceGroup allows you to get all
+// upcoming jobs for a specific resource group for a given project.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/resource_groups.html#list-upcoming-jobs-for-a-specific-resource-group
+func (s *ResourceGroupService) ListUpcomingJobsForASpecificResourceGroup(pid interface{}, key string, options ...RequestOptionFunc) ([]*Job, *Response, error) {
+	project, err := parseID(pid)
+	if err != nil {
+		return nil, nil, err
+	}
+	u := fmt.Sprintf("projects/%s/resource_groups/%s/upcoming_jobs", PathEscape(project), key)
+
+	req, err := s.client.NewRequest(http.MethodGet, u, nil, options)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	var js []*Job
+	resp, err := s.client.Do(req, &js)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return js, resp, nil
+}
+
+// EditAnExistingResourceGroupOptions represents the available
+// EditAnExistingResourceGroup options.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/resource_groups.html#edit-an-existing-resource-group
+type EditAnExistingResourceGroupOptions struct {
+	ProcessMode *ResourceGroupProcessMode `url:"process_mode,omitempty" json:"process_mode,omitempty"`
+}
+
+// EditAnExistingResourceGroup allows you to edit a specific
+// resource group for a given project
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/resource_groups.html#edit-an-existing-resource-group
+func (s *ResourceGroupService) EditAnExistingResourceGroup(pid interface{}, key string, opts *EditAnExistingResourceGroupOptions, options ...RequestOptionFunc) (*ResourceGroup, *Response, error) {
+	project, err := parseID(pid)
+	if err != nil {
+		return nil, nil, err
+	}
+	u := fmt.Sprintf("projects/%s/resource_groups/%s", PathEscape(project), key)
+
+	req, err := s.client.NewRequest(http.MethodPut, u, opts, options)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	rg := new(ResourceGroup)
+	resp, err := s.client.Do(req, rg)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return rg, resp, nil
+}
diff --git a/types.go b/types.go
index 39c5e47..2084fed 100644
--- a/types.go
+++ b/types.go
@@ -686,6 +686,21 @@ func ProjectCreationLevel(v ProjectCreationLevelValue) *ProjectCreationLevelValu
 	return Ptr(v)
 }
 
+// ResourceGroupProcessMode represents a process mode for a resource group
+// within a GitLab project.
+//
+// GitLab API docs: https://docs.gitlab.com/ee/ci/resource_groups/index.html#process-modes
+type ResourceGroupProcessMode string
+
+// List of available resource group process modes.
+//
+// GitLab API docs: https://docs.gitlab.com/ee/ci/resource_groups/index.html#process-modes
+const (
+	Unordered   ResourceGroupProcessMode = "unordered"
+	OldestFirst ResourceGroupProcessMode = "oldest_first"
+	NewestFirst ResourceGroupProcessMode = "newest_first"
+)
+
 // SharedRunnersSettingValue determines whether shared runners are enabled for a
 // group’s subgroups and projects.
 //


Test patch:
diff --git a/resource_group_test.go b/resource_group_test.go
new file mode 100644
index 0000000..f2d59e1
--- /dev/null
+++ b/resource_group_test.go
@@ -0,0 +1,190 @@
+package gitlab
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestResourceGroups_GetAllResourceGroupsForAProject(t *testing.T) {
+	mux, client := setup(t)
+
+	mux.HandleFunc("/api/v4/projects/1/resource_groups", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, http.MethodGet)
+		fmt.Fprint(w, `[
+			{
+				"id": 3,
+				"key": "production",
+				"process_mode": "unordered",
+				"created_at": "2021-09-01T08:04:59.650Z",
+				"updated_at": "2021-09-01T08:04:59.650Z"
+			  }
+		]`)
+	})
+
+	date, _ := time.Parse(timeLayout, "2021-09-01T08:04:59.650Z")
+
+	want := []*ResourceGroup{{
+		ID:          3,
+		Key:         "production",
+		ProcessMode: "unordered",
+		CreatedAt:   &date,
+		UpdatedAt:   &date,
+	}}
+
+	rgs, resp, err := client.ResourceGroup.GetAllResourceGroupsForAProject(1)
+	require.NoError(t, err)
+	require.NotNil(t, resp)
+	require.ElementsMatch(t, want, rgs)
+
+	rgs, resp, err = client.ResourceGroup.GetAllResourceGroupsForAProject(1.01)
+	require.EqualError(t, err, "invalid ID type 1.01, the ID must be an int or a string")
+	require.Nil(t, resp)
+	require.Nil(t, rgs)
+
+	rgs, resp, err = client.ResourceGroup.GetAllResourceGroupsForAProject(1, errorOption)
+	require.EqualError(t, err, "RequestOptionFunc returns an error")
+	require.Nil(t, resp)
+	require.Nil(t, rgs)
+
+	rgs, resp, err = client.ResourceGroup.GetAllResourceGroupsForAProject(2)
+	require.Error(t, err)
+	require.Equal(t, http.StatusNotFound, resp.StatusCode)
+	require.Nil(t, rgs)
+}
+
+func TestResourceGroups_GetASpecificResourceGroup(t *testing.T) {
+	mux, client := setup(t)
+
+	mux.HandleFunc("/api/v4/projects/1/resource_groups/production", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, http.MethodGet)
+		fmt.Fprint(w, `
+			{
+				"id": 3,
+				"key": "production",
+				"process_mode": "unordered",
+				"created_at": "2021-09-01T08:04:59.650Z",
+				"updated_at": "2021-09-01T08:04:59.650Z"
+			  }
+		`)
+	})
+
+	date, _ := time.Parse(timeLayout, "2021-09-01T08:04:59.650Z")
+
+	want := &ResourceGroup{
+		ID:          3,
+		Key:         "production",
+		ProcessMode: "unordered",
+		CreatedAt:   &date,
+		UpdatedAt:   &date,
+	}
+
+	rg, resp, err := client.ResourceGroup.GetASpecificResourceGroup(1, "production")
+	require.NoError(t, err)
+	require.NotNil(t, resp)
+	require.Equal(t, want, rg)
+
+	rg, resp, err = client.ResourceGroup.GetASpecificResourceGroup(1.01, "production")
+	require.EqualError(t, err, "invalid ID type 1.01, the ID must be an int or a string")
+	require.Nil(t, resp)
+	require.Nil(t, rg)
+
+	rg, resp, err = client.ResourceGroup.GetASpecificResourceGroup(1, "production", errorOption)
+	require.EqualError(t, err, "RequestOptionFunc returns an error")
+	require.Nil(t, resp)
+	require.Nil(t, rg)
+
+	rg, resp, err = client.ResourceGroup.GetASpecificResourceGroup(2, "production")
+	require.Error(t, err)
+	require.Equal(t, http.StatusNotFound, resp.StatusCode)
+	require.Nil(t, rg)
+}
+
+func TestResourceGroups_ListUpcomingJobsForASpecificResourceGroup(t *testing.T) {
+	mux, client := setup(t)
+
+	mux.HandleFunc("/api/v4/projects/1/resource_groups/production/upcoming_jobs", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, http.MethodGet)
+		fmt.Fprint(w, `[
+			{
+				"id": 6
+			}
+		]`)
+	})
+
+	want := []*Job{{
+		ID: 6,
+	}}
+
+	jobs, resp, err := client.ResourceGroup.ListUpcomingJobsForASpecificResourceGroup(1, "production")
+	require.NoError(t, err)
+	require.NotNil(t, resp)
+	require.ElementsMatch(t, want, jobs)
+
+	jobs, resp, err = client.ResourceGroup.ListUpcomingJobsForASpecificResourceGroup(1.01, "production")
+	require.EqualError(t, err, "invalid ID type 1.01, the ID must be an int or a string")
+	require.Nil(t, resp)
+	require.Nil(t, jobs)
+
+	jobs, resp, err = client.ResourceGroup.ListUpcomingJobsForASpecificResourceGroup(1, "production", errorOption)
+	require.EqualError(t, err, "RequestOptionFunc returns an error")
+	require.Nil(t, resp)
+	require.Nil(t, jobs)
+
+	jobs, resp, err = client.ResourceGroup.ListUpcomingJobsForASpecificResourceGroup(2, "production")
+	require.Error(t, err)
+	require.Equal(t, http.StatusNotFound, resp.StatusCode)
+	require.Nil(t, jobs)
+}
+
+func TestResourceGroup_EditAnExistingResourceGroup(t *testing.T) {
+	mux, client := setup(t)
+
+	mux.HandleFunc("/api/v4/projects/1/resource_groups/production", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, http.MethodPut)
+		fmt.Fprint(w, `
+			{
+				"id": 3,
+				"key": "production",
+				"process_mode": "unordered",
+				"created_at": "2021-09-01T08:04:59.650Z",
+				"updated_at": "2021-09-01T08:04:59.650Z"
+			}
+		`)
+	})
+
+	date, _ := time.Parse(timeLayout, "2021-09-01T08:04:59.650Z")
+
+	want := &ResourceGroup{
+		ID:          3,
+		Key:         "production",
+		ProcessMode: "unordered",
+		CreatedAt:   &date,
+		UpdatedAt:   &date,
+	}
+
+	opts := &EditAnExistingResourceGroupOptions{ProcessMode: Ptr(OldestFirst)}
+
+	rg, resp, err := client.ResourceGroup.EditAnExistingResourceGroup(1, "production", opts)
+	require.NoError(t, err)
+	require.NotNil(t, resp)
+	require.Equal(t, want, rg)
+
+	rg, resp, err = client.ResourceGroup.EditAnExistingResourceGroup(1.01, "production", opts)
+	require.EqualError(t, err, "invalid ID type 1.01, the ID must be an int or a string")
+	require.Nil(t, resp)
+	require.Nil(t, rg)
+
+	rg, resp, err = client.ResourceGroup.EditAnExistingResourceGroup(1, "production", opts, errorOption)
+	require.EqualError(t, err, "RequestOptionFunc returns an error")
+	require.Nil(t, resp)
+	require.Nil(t, rg)
+
+	rg, resp, err = client.ResourceGroup.EditAnExistingResourceGroup(2, "production", opts)
+	require.Error(t, err)
+	require.Equal(t, http.StatusNotFound, resp.StatusCode)
+	require.Nil(t, rg)
+}