Commit b9acbf32 authored by Timo Furrer's avatar Timo Furrer
Browse files

feat(runner-controller): add runner controller scope create command

parent 17584dfd
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -29,4 +29,5 @@ For more information, see

## Subcommands

- [`create`](create.md)
- [`list`](list.md)
+54 −0
Original line number Diff line number Diff line
---
title: glab runner-controller scope create
stage: Create
group: Code Review
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
---

<!--
This documentation is auto generated by a script.
Please do not edit this file directly. Run `make gen-docs` instead.
-->

Create a scope for a runner controller. (EXPERIMENTAL)

## Synopsis

Create a scope for a runner controller. This is an admin-only feature.

Currently, only instance-level scopes are supported. Use the --instance flag
to add an instance-level scope, which allows the runner controller to evaluate
jobs for all runners in the GitLab instance.

This feature is an experiment and is not ready for production use.
It might be unstable or removed at any time.
For more information, see
[https://docs.gitlab.com/policy/development_stages_support/](https://docs.gitlab.com/policy/development_stages_support/).

```plaintext
glab runner-controller scope create <controller-id> [flags]
```

## Examples

```console
# Add an instance-level scope to runner controller 42
$ glab runner-controller scope create 42 --instance

# Add an instance-level scope and output as JSON
$ glab runner-controller scope create 42 --instance --output json

```

## Options

```plaintext
      --instance        Add an instance-level scope.
  -F, --output string   Format output as: text, json. (default "text")
```

## Options inherited from parent commands

```plaintext
  -h, --help   Show help for this command.
```
+100 −0
Original line number Diff line number Diff line
package create

import (
	"context"
	"strconv"

	"github.com/MakeNowJust/heredoc/v2"
	"github.com/spf13/cobra"

	gitlab "gitlab.com/gitlab-org/api/client-go"

	"gitlab.com/gitlab-org/cli/internal/api"
	"gitlab.com/gitlab-org/cli/internal/cmdutils"
	"gitlab.com/gitlab-org/cli/internal/iostreams"
	"gitlab.com/gitlab-org/cli/internal/mcpannotations"
	"gitlab.com/gitlab-org/cli/internal/text"
)

type options struct {
	io        *iostreams.IOStreams
	apiClient func(repoHost string) (*api.Client, error)

	controllerID int64
	instance     bool
	outputFormat string
}

func NewCmd(f cmdutils.Factory) *cobra.Command {
	opts := &options{
		io:        f.IO(),
		apiClient: f.ApiClient,
	}

	cmd := &cobra.Command{
		Use:   "create <controller-id> [flags]",
		Short: `Create a scope for a runner controller. (EXPERIMENTAL)`,
		Long: heredoc.Docf(`
			Create a scope for a runner controller. This is an admin-only feature.

			Currently, only instance-level scopes are supported. Use the --instance flag
			to add an instance-level scope, which allows the runner controller to evaluate
			jobs for all runners in the GitLab instance.
			%s`, text.ExperimentalString),
		Args: cobra.ExactArgs(1),
		Example: heredoc.Doc(`
			# Add an instance-level scope to runner controller 42
			$ glab runner-controller scope create 42 --instance

			# Add an instance-level scope and output as JSON
			$ glab runner-controller scope create 42 --instance --output json
		`),
		Annotations: map[string]string{
			mcpannotations.Destructive: "true",
		},
		RunE: func(cmd *cobra.Command, args []string) error {
			if err := opts.complete(args); err != nil {
				return err
			}
			return opts.run(cmd.Context())
		},
	}

	fl := cmd.Flags()
	fl.BoolVar(&opts.instance, "instance", false, "Add an instance-level scope.")
	fl.VarP(cmdutils.NewEnumValue([]string{"text", "json"}, "text", &opts.outputFormat), "output", "F", "Format output as: text, json.")

	cobra.CheckErr(cmd.MarkFlagRequired("instance"))

	return cmd
}

func (o *options) complete(args []string) error {
	id, err := strconv.ParseInt(args[0], 10, 64)
	if err != nil {
		return cmdutils.WrapError(err, "invalid runner controller ID")
	}
	o.controllerID = id
	return nil
}

func (o *options) run(ctx context.Context) error {
	apiClient, err := o.apiClient("")
	if err != nil {
		return err
	}
	client := apiClient.Lab()

	scoping, _, err := client.RunnerControllerScopes.AddRunnerControllerInstanceScope(o.controllerID, gitlab.WithContext(ctx))
	if err != nil {
		return cmdutils.WrapError(err, "failed to add instance-level scope")
	}

	switch o.outputFormat {
	case "json":
		return o.io.PrintJSON(scoping)
	default:
		o.io.LogInfof("Added instance-level scope to runner controller %d\n", o.controllerID)
		return nil
	}
}
+131 −0
Original line number Diff line number Diff line
//go:build !integration

package create

import (
	"encoding/json"
	"errors"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"go.uber.org/mock/gomock"

	gitlab "gitlab.com/gitlab-org/api/client-go"
	gitlabtesting "gitlab.com/gitlab-org/api/client-go/testing"

	"gitlab.com/gitlab-org/cli/internal/api"
	"gitlab.com/gitlab-org/cli/internal/testing/cmdtest"
)

func TestCreate(t *testing.T) {
	t.Parallel()

	tc := gitlabtesting.NewTestClient(t)

	exec := cmdtest.SetupCmdForTest(
		t,
		NewCmd,
		false,
		cmdtest.WithApiClient(cmdtest.NewTestApiClient(t, nil, "", "", api.WithGitLabClient(tc.Client))),
	)

	fixedTime := time.Date(2025, 1, 15, 10, 30, 0, 0, time.UTC)
	tc.MockRunnerControllerScopes.EXPECT().
		AddRunnerControllerInstanceScope(int64(42), gomock.Any()).
		Return(&gitlab.RunnerControllerInstanceLevelScoping{
			CreatedAt: &fixedTime,
			UpdatedAt: &fixedTime,
		}, nil, nil)

	out, err := exec("42 --instance")
	require.NoError(t, err)
	assert.Equal(t, "Added instance-level scope to runner controller 42\n", out.OutBuf.String())
}

func TestCreateJSON(t *testing.T) {
	t.Parallel()

	tc := gitlabtesting.NewTestClient(t)

	exec := cmdtest.SetupCmdForTest(
		t,
		NewCmd,
		false,
		cmdtest.WithApiClient(cmdtest.NewTestApiClient(t, nil, "", "", api.WithGitLabClient(tc.Client))),
	)

	fixedTime := time.Date(2025, 1, 15, 10, 30, 0, 0, time.UTC)
	tc.MockRunnerControllerScopes.EXPECT().
		AddRunnerControllerInstanceScope(int64(42), gomock.Any()).
		Return(&gitlab.RunnerControllerInstanceLevelScoping{
			CreatedAt: &fixedTime,
			UpdatedAt: &fixedTime,
		}, nil, nil)

	out, err := exec("42 --instance --output json")
	require.NoError(t, err)

	var result gitlab.RunnerControllerInstanceLevelScoping
	err = json.Unmarshal(out.OutBuf.Bytes(), &result)
	require.NoError(t, err)

	assert.Equal(t, fixedTime, *result.CreatedAt)
	assert.Equal(t, fixedTime, *result.UpdatedAt)
}

func TestCreateError(t *testing.T) {
	t.Parallel()

	tc := gitlabtesting.NewTestClient(t)

	exec := cmdtest.SetupCmdForTest(
		t,
		NewCmd,
		false,
		cmdtest.WithApiClient(cmdtest.NewTestApiClient(t, nil, "", "", api.WithGitLabClient(tc.Client))),
	)

	tc.MockRunnerControllerScopes.EXPECT().
		AddRunnerControllerInstanceScope(int64(42), gomock.Any()).
		Return(nil, nil, errors.New("API error"))

	_, err := exec("42 --instance")
	require.Error(t, err)
	assert.Contains(t, err.Error(), "API error")
}

func TestCreateRequiresInstanceFlag(t *testing.T) {
	t.Parallel()

	tc := gitlabtesting.NewTestClient(t)

	exec := cmdtest.SetupCmdForTest(
		t,
		NewCmd,
		false,
		cmdtest.WithApiClient(cmdtest.NewTestApiClient(t, nil, "", "", api.WithGitLabClient(tc.Client))),
	)

	_, err := exec("42")
	require.Error(t, err)
	assert.Contains(t, err.Error(), `required flag(s) "instance" not set`)
}

func TestCreateInvalidID(t *testing.T) {
	t.Parallel()

	tc := gitlabtesting.NewTestClient(t)

	exec := cmdtest.SetupCmdForTest(
		t,
		NewCmd,
		false,
		cmdtest.WithApiClient(cmdtest.NewTestApiClient(t, nil, "", "", api.WithGitLabClient(tc.Client))),
	)

	_, err := exec("invalid --instance")
	require.Error(t, err)
	assert.Contains(t, err.Error(), "invalid")
}
+2 −0
Original line number Diff line number Diff line
@@ -4,6 +4,7 @@ import (
	"github.com/spf13/cobra"

	"gitlab.com/gitlab-org/cli/internal/cmdutils"
	createCmd "gitlab.com/gitlab-org/cli/internal/commands/runnercontroller/scope/create"
	listCmd "gitlab.com/gitlab-org/cli/internal/commands/runnercontroller/scope/list"
	"gitlab.com/gitlab-org/cli/internal/text"
)
@@ -15,6 +16,7 @@ func NewCmd(f cmdutils.Factory) *cobra.Command {
		Long:  `Manages runner controller scopes. This is an admin-only feature.` + "\n" + text.ExperimentalString,
	}

	cmd.AddCommand(createCmd.NewCmd(f))
	cmd.AddCommand(listCmd.NewCmd(f))
	return cmd
}