Verified Commit 69b78fec authored by Timo Furrer's avatar Timo Furrer Committed by GitLab
Browse files

Merge branch 'rc-runner-scopes' into 'main'

Implement runner controller instance-level runner scope support

See merge request !2765
parents 9d535179 0ea75d4b
Loading
Loading
Loading
Loading
Loading
+41 −0
Original line number Diff line number Diff line
@@ -32,6 +32,19 @@ type (
		// GitLab API docs:
		// https://docs.gitlab.com/api/runner_controllers/#remove-instance-level-scope
		RemoveRunnerControllerInstanceScope(rid int64, options ...RequestOptionFunc) (*Response, error)
		// AddRunnerControllerRunnerScope adds a runner scope to a runner
		// controller. This is an admin-only endpoint. The runner must be an
		// instance-level runner.
		//
		// GitLab API docs:
		// https://docs.gitlab.com/api/runner_controllers/#add-runner-scope
		AddRunnerControllerRunnerScope(rid, runnerID int64, options ...RequestOptionFunc) (*RunnerControllerRunnerLevelScoping, *Response, error)
		// RemoveRunnerControllerRunnerScope removes a runner scope from a runner
		// controller. This is an admin-only endpoint.
		//
		// GitLab API docs:
		// https://docs.gitlab.com/api/runner_controllers/#remove-runner-scope
		RemoveRunnerControllerRunnerScope(rid, runnerID int64, options ...RequestOptionFunc) (*Response, error)
	}

	// RunnerControllerScopesService handles communication with the runner
@@ -57,12 +70,23 @@ type RunnerControllerInstanceLevelScoping struct {
	UpdatedAt *time.Time `json:"updated_at"`
}

// RunnerControllerRunnerLevelScoping represents a runner-level scoping for a
// GitLab runner controller.
//
// GitLab API docs: https://docs.gitlab.com/api/runner_controllers/#runner-controller-scopes
type RunnerControllerRunnerLevelScoping struct {
	RunnerID  int64      `json:"runner_id"`
	CreatedAt *time.Time `json:"created_at"`
	UpdatedAt *time.Time `json:"updated_at"`
}

// RunnerControllerScopes represents all scopes configured for a GitLab runner
// controller.
//
// GitLab API docs: https://docs.gitlab.com/api/runner_controllers/#runner-controller-scopes
type RunnerControllerScopes struct {
	InstanceLevelScopings []*RunnerControllerInstanceLevelScoping `json:"instance_level_scopings"`
	RunnerLevelScopings   []*RunnerControllerRunnerLevelScoping   `json:"runner_level_scopings"`
}

func (s *RunnerControllerScopesService) ListRunnerControllerScopes(rid int64, options ...RequestOptionFunc) (*RunnerControllerScopes, *Response, error) {
@@ -88,3 +112,20 @@ func (s *RunnerControllerScopesService) RemoveRunnerControllerInstanceScope(rid
	)
	return resp, err
}

func (s *RunnerControllerScopesService) AddRunnerControllerRunnerScope(rid, runnerID int64, options ...RequestOptionFunc) (*RunnerControllerRunnerLevelScoping, *Response, error) {
	return do[*RunnerControllerRunnerLevelScoping](s.client,
		withMethod(http.MethodPost),
		withPath("runner_controllers/%d/scopes/runners/%d", rid, runnerID),
		withRequestOpts(options...),
	)
}

func (s *RunnerControllerScopesService) RemoveRunnerControllerRunnerScope(rid, runnerID int64, options ...RequestOptionFunc) (*Response, error) {
	_, resp, err := do[none](s.client,
		withMethod(http.MethodDelete),
		withPath("runner_controllers/%d/scopes/runners/%d", rid, runnerID),
		withRequestOpts(options...),
	)
	return resp, err
}
+101 −3
Original line number Diff line number Diff line
@@ -23,7 +23,8 @@ func TestListRunnerControllerScopes(t *testing.T) {
					"created_at": "2026-01-01T00:00:00.000Z",
					"updated_at": "2026-01-01T00:00:00.000Z"
				}
			]
			],
			"runner_level_scopings": []
		}`)
	})

@@ -38,6 +39,7 @@ func TestListRunnerControllerScopes(t *testing.T) {
				UpdatedAt: Ptr(time.Date(2026, time.January, 1, 0, 0, 0, 0, time.UTC)),
			},
		},
		RunnerLevelScopings: []*RunnerControllerRunnerLevelScoping{},
	}
	assert.Equal(t, want, scopes)
}
@@ -51,16 +53,18 @@ func TestListRunnerControllerScopes_Empty(t *testing.T) {
		// WHEN listing scopes for the runner controller
		testMethod(t, r, http.MethodGet)
		fmt.Fprint(w, `{
			"instance_level_scopings": []
			"instance_level_scopings": [],
			"runner_level_scopings": []
		}`)
	})

	scopes, _, err := client.RunnerControllerScopes.ListRunnerControllerScopes(1)
	assert.NoError(t, err)

	// THEN empty instance_level_scopings array is returned
	// THEN empty scopings arrays are returned
	want := &RunnerControllerScopes{
		InstanceLevelScopings: []*RunnerControllerInstanceLevelScoping{},
		RunnerLevelScopings:   []*RunnerControllerRunnerLevelScoping{},
	}
	assert.Equal(t, want, scopes)
}
@@ -109,3 +113,97 @@ func TestRemoveRunnerControllerInstanceScope(t *testing.T) {
	// THEN 204 No Content is returned
	assert.Equal(t, http.StatusNoContent, resp.StatusCode)
}

func TestListRunnerControllerScopes_WithRunnerLevelScopings(t *testing.T) {
	t.Parallel()
	mux, client := setup(t)

	// GIVEN a runner controller with runner-level scopings exists
	mux.HandleFunc("/api/v4/runner_controllers/1/scopes", func(w http.ResponseWriter, r *http.Request) {
		// WHEN listing scopes for the runner controller
		testMethod(t, r, http.MethodGet)
		fmt.Fprint(w, `{
			"instance_level_scopings": [],
			"runner_level_scopings": [
				{
					"runner_id": 5,
					"created_at": "2026-01-01T00:00:00.000Z",
					"updated_at": "2026-01-01T00:00:00.000Z"
				},
				{
					"runner_id": 10,
					"created_at": "2026-01-02T00:00:00.000Z",
					"updated_at": "2026-01-02T00:00:00.000Z"
				}
			]
		}`)
	})

	scopes, _, err := client.RunnerControllerScopes.ListRunnerControllerScopes(1)
	assert.NoError(t, err)

	// THEN the scopes are returned with runner-level scopings
	want := &RunnerControllerScopes{
		InstanceLevelScopings: []*RunnerControllerInstanceLevelScoping{},
		RunnerLevelScopings: []*RunnerControllerRunnerLevelScoping{
			{
				RunnerID:  5,
				CreatedAt: Ptr(time.Date(2026, time.January, 1, 0, 0, 0, 0, time.UTC)),
				UpdatedAt: Ptr(time.Date(2026, time.January, 1, 0, 0, 0, 0, time.UTC)),
			},
			{
				RunnerID:  10,
				CreatedAt: Ptr(time.Date(2026, time.January, 2, 0, 0, 0, 0, time.UTC)),
				UpdatedAt: Ptr(time.Date(2026, time.January, 2, 0, 0, 0, 0, time.UTC)),
			},
		},
	}
	assert.Equal(t, want, scopes)
}

func TestAddRunnerControllerRunnerScope(t *testing.T) {
	t.Parallel()
	mux, client := setup(t)

	// GIVEN a runner controller without a runner scope for runner 5 exists
	mux.HandleFunc("/api/v4/runner_controllers/1/scopes/runners/5", func(w http.ResponseWriter, r *http.Request) {
		// WHEN adding a runner scope for runner 5
		testMethod(t, r, http.MethodPost)
		w.WriteHeader(http.StatusCreated)
		fmt.Fprint(w, `{
			"runner_id": 5,
			"created_at": "2026-01-01T00:00:00.000Z",
			"updated_at": "2026-01-01T00:00:00.000Z"
		}`)
	})

	scoping, resp, err := client.RunnerControllerScopes.AddRunnerControllerRunnerScope(1, 5)
	assert.NoError(t, err)
	assert.Equal(t, http.StatusCreated, resp.StatusCode)

	// THEN the created runner-level scoping is returned
	want := &RunnerControllerRunnerLevelScoping{
		RunnerID:  5,
		CreatedAt: Ptr(time.Date(2026, time.January, 1, 0, 0, 0, 0, time.UTC)),
		UpdatedAt: Ptr(time.Date(2026, time.January, 1, 0, 0, 0, 0, time.UTC)),
	}
	assert.Equal(t, want, scoping)
}

func TestRemoveRunnerControllerRunnerScope(t *testing.T) {
	t.Parallel()
	mux, client := setup(t)

	// GIVEN a runner controller with a runner scope for runner 5 exists
	mux.HandleFunc("/api/v4/runner_controllers/1/scopes/runners/5", func(w http.ResponseWriter, r *http.Request) {
		// WHEN removing the runner scope for runner 5
		testMethod(t, r, http.MethodDelete)
		w.WriteHeader(http.StatusNoContent)
	})

	resp, err := client.RunnerControllerScopes.RemoveRunnerControllerRunnerScope(1, 5)
	assert.NoError(t, err)

	// THEN 204 No Content is returned
	assert.Equal(t, http.StatusNoContent, resp.StatusCode)
}
+89 −0
Original line number Diff line number Diff line
@@ -84,6 +84,51 @@ func (c *MockRunnerControllerScopesServiceInterfaceAddRunnerControllerInstanceSc
	return c
}

// AddRunnerControllerRunnerScope mocks base method.
func (m *MockRunnerControllerScopesServiceInterface) AddRunnerControllerRunnerScope(rid, runnerID int64, options ...gitlab.RequestOptionFunc) (*gitlab.RunnerControllerRunnerLevelScoping, *gitlab.Response, error) {
	m.ctrl.T.Helper()
	varargs := []any{rid, runnerID}
	for _, a := range options {
		varargs = append(varargs, a)
	}
	ret := m.ctrl.Call(m, "AddRunnerControllerRunnerScope", varargs...)
	ret0, _ := ret[0].(*gitlab.RunnerControllerRunnerLevelScoping)
	ret1, _ := ret[1].(*gitlab.Response)
	ret2, _ := ret[2].(error)
	return ret0, ret1, ret2
}

// AddRunnerControllerRunnerScope indicates an expected call of AddRunnerControllerRunnerScope.
func (mr *MockRunnerControllerScopesServiceInterfaceMockRecorder) AddRunnerControllerRunnerScope(rid, runnerID any, options ...any) *MockRunnerControllerScopesServiceInterfaceAddRunnerControllerRunnerScopeCall {
	mr.mock.ctrl.T.Helper()
	varargs := append([]any{rid, runnerID}, options...)
	call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddRunnerControllerRunnerScope", reflect.TypeOf((*MockRunnerControllerScopesServiceInterface)(nil).AddRunnerControllerRunnerScope), varargs...)
	return &MockRunnerControllerScopesServiceInterfaceAddRunnerControllerRunnerScopeCall{Call: call}
}

// MockRunnerControllerScopesServiceInterfaceAddRunnerControllerRunnerScopeCall wrap *gomock.Call
type MockRunnerControllerScopesServiceInterfaceAddRunnerControllerRunnerScopeCall struct {
	*gomock.Call
}

// Return rewrite *gomock.Call.Return
func (c *MockRunnerControllerScopesServiceInterfaceAddRunnerControllerRunnerScopeCall) Return(arg0 *gitlab.RunnerControllerRunnerLevelScoping, arg1 *gitlab.Response, arg2 error) *MockRunnerControllerScopesServiceInterfaceAddRunnerControllerRunnerScopeCall {
	c.Call = c.Call.Return(arg0, arg1, arg2)
	return c
}

// Do rewrite *gomock.Call.Do
func (c *MockRunnerControllerScopesServiceInterfaceAddRunnerControllerRunnerScopeCall) Do(f func(int64, int64, ...gitlab.RequestOptionFunc) (*gitlab.RunnerControllerRunnerLevelScoping, *gitlab.Response, error)) *MockRunnerControllerScopesServiceInterfaceAddRunnerControllerRunnerScopeCall {
	c.Call = c.Call.Do(f)
	return c
}

// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *MockRunnerControllerScopesServiceInterfaceAddRunnerControllerRunnerScopeCall) DoAndReturn(f func(int64, int64, ...gitlab.RequestOptionFunc) (*gitlab.RunnerControllerRunnerLevelScoping, *gitlab.Response, error)) *MockRunnerControllerScopesServiceInterfaceAddRunnerControllerRunnerScopeCall {
	c.Call = c.Call.DoAndReturn(f)
	return c
}

// ListRunnerControllerScopes mocks base method.
func (m *MockRunnerControllerScopesServiceInterface) ListRunnerControllerScopes(rid int64, options ...gitlab.RequestOptionFunc) (*gitlab.RunnerControllerScopes, *gitlab.Response, error) {
	m.ctrl.T.Helper()
@@ -172,3 +217,47 @@ func (c *MockRunnerControllerScopesServiceInterfaceRemoveRunnerControllerInstanc
	c.Call = c.Call.DoAndReturn(f)
	return c
}

// RemoveRunnerControllerRunnerScope mocks base method.
func (m *MockRunnerControllerScopesServiceInterface) RemoveRunnerControllerRunnerScope(rid, runnerID int64, options ...gitlab.RequestOptionFunc) (*gitlab.Response, error) {
	m.ctrl.T.Helper()
	varargs := []any{rid, runnerID}
	for _, a := range options {
		varargs = append(varargs, a)
	}
	ret := m.ctrl.Call(m, "RemoveRunnerControllerRunnerScope", varargs...)
	ret0, _ := ret[0].(*gitlab.Response)
	ret1, _ := ret[1].(error)
	return ret0, ret1
}

// RemoveRunnerControllerRunnerScope indicates an expected call of RemoveRunnerControllerRunnerScope.
func (mr *MockRunnerControllerScopesServiceInterfaceMockRecorder) RemoveRunnerControllerRunnerScope(rid, runnerID any, options ...any) *MockRunnerControllerScopesServiceInterfaceRemoveRunnerControllerRunnerScopeCall {
	mr.mock.ctrl.T.Helper()
	varargs := append([]any{rid, runnerID}, options...)
	call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveRunnerControllerRunnerScope", reflect.TypeOf((*MockRunnerControllerScopesServiceInterface)(nil).RemoveRunnerControllerRunnerScope), varargs...)
	return &MockRunnerControllerScopesServiceInterfaceRemoveRunnerControllerRunnerScopeCall{Call: call}
}

// MockRunnerControllerScopesServiceInterfaceRemoveRunnerControllerRunnerScopeCall wrap *gomock.Call
type MockRunnerControllerScopesServiceInterfaceRemoveRunnerControllerRunnerScopeCall struct {
	*gomock.Call
}

// Return rewrite *gomock.Call.Return
func (c *MockRunnerControllerScopesServiceInterfaceRemoveRunnerControllerRunnerScopeCall) Return(arg0 *gitlab.Response, arg1 error) *MockRunnerControllerScopesServiceInterfaceRemoveRunnerControllerRunnerScopeCall {
	c.Call = c.Call.Return(arg0, arg1)
	return c
}

// Do rewrite *gomock.Call.Do
func (c *MockRunnerControllerScopesServiceInterfaceRemoveRunnerControllerRunnerScopeCall) Do(f func(int64, int64, ...gitlab.RequestOptionFunc) (*gitlab.Response, error)) *MockRunnerControllerScopesServiceInterfaceRemoveRunnerControllerRunnerScopeCall {
	c.Call = c.Call.Do(f)
	return c
}

// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *MockRunnerControllerScopesServiceInterfaceRemoveRunnerControllerRunnerScopeCall) DoAndReturn(f func(int64, int64, ...gitlab.RequestOptionFunc) (*gitlab.Response, error)) *MockRunnerControllerScopesServiceInterfaceRemoveRunnerControllerRunnerScopeCall {
	c.Call = c.Call.DoAndReturn(f)
	return c
}