Commit 440e067e authored by Timo Furrer's avatar Timo Furrer
Browse files

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

parent b9acbf32
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -30,4 +30,5 @@ For more information, see
## Subcommands

- [`create`](create.md)
- [`delete`](delete.md)
- [`list`](list.md)
+53 −0
Original line number Diff line number Diff line
---
title: glab runner-controller scope delete
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.
-->

Delete a scope from a runner controller. (EXPERIMENTAL)

## Synopsis

Delete a scope from a runner controller. This is an admin-only feature.

Currently, only instance-level scopes are supported. Use the --instance flag
to remove an instance-level scope from the runner controller.

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 delete <controller-id> [flags]
```

## Examples

```console
# Remove an instance-level scope from runner controller 42 (with confirmation)
$ glab runner-controller scope delete 42 --instance

# Remove an instance-level scope without confirmation
$ glab runner-controller scope delete 42 --instance --force

```

## Options

```plaintext
  -f, --force      Skip confirmation prompt.
      --instance   Remove an instance-level scope.
```

## Options inherited from parent commands

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

import (
	"context"
	"errors"
	"fmt"
	"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
	force        bool
}

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

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

			Currently, only instance-level scopes are supported. Use the --instance flag
			to remove an instance-level scope from the runner controller.
			%s`, text.ExperimentalString),
		Args: cobra.ExactArgs(1),
		Example: heredoc.Doc(`
			# Remove an instance-level scope from runner controller 42 (with confirmation)
			$ glab runner-controller scope delete 42 --instance

			# Remove an instance-level scope without confirmation
			$ glab runner-controller scope delete 42 --instance --force
		`),
		Annotations: map[string]string{
			mcpannotations.Destructive: "true",
		},
		RunE: func(cmd *cobra.Command, args []string) error {
			if err := opts.complete(args); err != nil {
				return err
			}
			if err := opts.validate(cmd.Context()); err != nil {
				return err
			}
			return opts.run(cmd.Context())
		},
	}

	fl := cmd.Flags()
	fl.BoolVar(&opts.instance, "instance", false, "Remove an instance-level scope.")
	fl.BoolVarP(&opts.force, "force", "f", false, "Skip confirmation prompt.")

	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) validate(ctx context.Context) error {
	if !o.force {
		if !o.io.PromptEnabled() {
			return cmdutils.FlagError{Err: errors.New("--force required when not running interactively")}
		}

		var confirmed bool
		err := o.io.Confirm(ctx, &confirmed, fmt.Sprintf("Remove instance-level scope from runner controller %d?", o.controllerID))
		if err != nil {
			return err
		}

		if !confirmed {
			return cmdutils.CancelError()
		}
	}
	return nil
}

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

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

	o.io.LogInfof("Removed instance-level scope from runner controller %d\n", o.controllerID)
	return nil
}
+110 −0
Original line number Diff line number Diff line
//go:build !integration

package delete

import (
	"errors"
	"testing"

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

	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 TestDelete(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().
		RemoveRunnerControllerInstanceScope(int64(42), gomock.Any()).
		Return(nil, nil)

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

func TestDeleteError(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().
		RemoveRunnerControllerInstanceScope(int64(42), gomock.Any()).
		Return(nil, errors.New("API error"))

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

func TestDeleteRequiresInstanceFlag(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 --force")
	require.Error(t, err)
	assert.Contains(t, err.Error(), `required flag(s) "instance" not set`)
}

func TestDeleteRequiresForceNonInteractive(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 --instance")
	require.Error(t, err)
	assert.Contains(t, err.Error(), "--force required")
}

func TestDeleteInvalidID(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 --force")
	require.Error(t, err)
	assert.Contains(t, err.Error(), "invalid")
}
+2 −0
Original line number Diff line number Diff line
@@ -5,6 +5,7 @@ import (

	"gitlab.com/gitlab-org/cli/internal/cmdutils"
	createCmd "gitlab.com/gitlab-org/cli/internal/commands/runnercontroller/scope/create"
	deleteCmd "gitlab.com/gitlab-org/cli/internal/commands/runnercontroller/scope/delete"
	listCmd "gitlab.com/gitlab-org/cli/internal/commands/runnercontroller/scope/list"
	"gitlab.com/gitlab-org/cli/internal/text"
)
@@ -17,6 +18,7 @@ func NewCmd(f cmdutils.Factory) *cobra.Command {
	}

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