Skip to content

Commit

Permalink
feature: add variable delete command (profclems#890)
Browse files Browse the repository at this point in the history
* Move IsValidKey to new variableutils package

* add variable delete command

* remove unnecessary alias

* wip: add tests

* working tests
  • Loading branch information
bradym committed Nov 3, 2021
1 parent 83eac01 commit af35a72
Show file tree
Hide file tree
Showing 9 changed files with 387 additions and 58 deletions.
43 changes: 42 additions & 1 deletion api/variable.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package api

import "github.com/xanzy/go-gitlab"
import (
"github.com/hashicorp/go-retryablehttp"

"github.com/xanzy/go-gitlab"
)

var CreateProjectVariable = func(client *gitlab.Client, projectID interface{}, opts *gitlab.CreateProjectVariableOptions) (*gitlab.ProjectVariable, error) {
if client == nil {
Expand All @@ -26,6 +30,29 @@ var ListProjectVariables = func(client *gitlab.Client, projectID interface{}, op
return vars, nil
}

var DeleteProjectVariable = func(client *gitlab.Client, projectID interface{}, key string, scope string) error {
if client == nil {
client = apiClient.Lab()
}

var filter = func(request *retryablehttp.Request) error {
q := request.URL.Query()
q.Add("filter[environment_scope]", scope)

request.URL.RawQuery = q.Encode()

return nil
}

_, err := client.ProjectVariables.RemoveVariable(projectID, key, filter)

if err != nil {
return err
}

return nil
}

var ListGroupVariables = func(client *gitlab.Client, groupID interface{}, opts *gitlab.ListGroupVariablesOptions) ([]*gitlab.GroupVariable, error) {
if client == nil {
client = apiClient.Lab()
Expand All @@ -49,3 +76,17 @@ var CreateGroupVariable = func(client *gitlab.Client, groupID interface{}, opts

return vars, nil
}

var DeleteGroupVariable = func(client *gitlab.Client, groupID interface{}, key string) error {
if client == nil {
client = apiClient.Lab()
}

_, err := client.GroupVariables.RemoveVariable(groupID, key)

if err != nil {
return err
}

return nil
}
108 changes: 108 additions & 0 deletions commands/variable/delete/delete.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package delete

import (
"fmt"

"errors"

"github.com/MakeNowJust/heredoc"
"github.com/profclems/glab/api"
"github.com/profclems/glab/commands/cmdutils"
"github.com/profclems/glab/commands/variable/variableutils"
"github.com/profclems/glab/internal/glrepo"
"github.com/profclems/glab/pkg/iostreams"

"github.com/spf13/cobra"
"github.com/xanzy/go-gitlab"
)

type DeleteOpts struct {
HTTPClient func() (*gitlab.Client, error)
IO *iostreams.IOStreams
BaseRepo func() (glrepo.Interface, error)

Key string
Scope string
Group string
}

func NewCmdSet(f *cmdutils.Factory, runE func(opts *DeleteOpts) error) *cobra.Command {
opts := &DeleteOpts{
IO: f.IO,
}

cmd := &cobra.Command{
Use: "delete <key>",
Short: "Delete a project or group variable",
Aliases: []string{"remove"},
Args: cobra.ExactArgs(1),
Example: heredoc.Doc(`
$ glab variable delete VAR_NAME
$ glab variable delete VAR_NAME --scope=prod
$ glab variable delete VARNAME -g mygroup
`),
RunE: func(cmd *cobra.Command, args []string) (err error) {
opts.HTTPClient = f.HttpClient
opts.BaseRepo = f.BaseRepo
opts.Key = args[0]

if !variableutils.IsValidKey(opts.Key) {
err = cmdutils.FlagError{Err: fmt.Errorf("invalid key provided.\n%s", variableutils.ValidKeyMsg)}
return
} else if len(args) != 1 {
err = cmdutils.FlagError{Err: errors.New("no key provided")}
}

if cmd.Flags().Changed("scope") && opts.Group != "" {
err = cmdutils.FlagError{Err: errors.New("scope is not required for group variables")}
return
}

if runE != nil {
err = runE(opts)
return
}
err = deleteRun(opts)
return
},
}

cmd.Flags().StringVarP(&opts.Scope, "scope", "s", "*", "The environment_scope of the variable. All (*), or specific environments")
cmd.Flags().StringVarP(&opts.Group, "group", "g", "", "Delete variable from a group")

return cmd

}

func deleteRun(opts *DeleteOpts) error {
c := opts.IO.Color()
httpClient, err := opts.HTTPClient()
if err != nil {
return err
}

baseRepo, err := opts.BaseRepo()
if err != nil {
return err
}

if opts.Group == "" {
// Delete project-level variable
err = api.DeleteProjectVariable(httpClient, baseRepo.FullName(), opts.Key, opts.Scope)
if err != nil {
return err
}

fmt.Fprintf(opts.IO.StdOut, "%s Deleted variable %s with scope %s for %s\n", c.GreenCheck(), opts.Key, opts.Scope, baseRepo.FullName())
} else {
// Delete group-level variable
err = api.DeleteGroupVariable(httpClient, opts.Group, opts.Key)
if err != nil {
return err
}

fmt.Fprintf(opts.IO.StdOut, "%s Deleted variable %s for group %s\n", c.GreenCheck(), opts.Key, opts.Group)
}

return nil
}
171 changes: 171 additions & 0 deletions commands/variable/delete/delete_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
package delete

import (
"bytes"
"net/http"
"testing"

"github.com/profclems/glab/api"
"github.com/profclems/glab/internal/glrepo"
"github.com/profclems/glab/pkg/iostreams"
"github.com/xanzy/go-gitlab"

"github.com/alecthomas/assert"
"github.com/google/shlex"
"github.com/profclems/glab/commands/cmdutils"
"github.com/profclems/glab/pkg/httpmock"
)

func Test_NewCmdSet(t *testing.T) {
tests := []struct {
name string
cli string
wants DeleteOpts
stdinTTY bool
wantsErr bool
}{
{
name: "delete var",
cli: "cool_secret",
wantsErr: false,
},
{
name: "delete scoped var",
cli: "cool_secret --scope prod",
wantsErr: false,
},
{
name: "delete group var",
cli: "cool_secret -g mygroup",
wantsErr: false,
},
{
name: "delete scoped group var",
cli: "cool_secret -g mygroup --scope prod",
wantsErr: true,
},
{
name: "no name",
cli: "",
wantsErr: true,
},
{
name: "invalid characters in name",
cli: "BAD-SECRET",
wantsErr: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
io, _, _, _ := iostreams.Test()
f := &cmdutils.Factory{
IO: io,
}

io.IsInTTY = tt.stdinTTY

argv, err := shlex.Split(tt.cli)
assert.NoError(t, err)

cmd := NewCmdSet(f, func(opts *DeleteOpts) error {
return nil
})

cmd.SetArgs(argv)
cmd.SetIn(&bytes.Buffer{})
cmd.SetOut(&bytes.Buffer{})
cmd.SetErr(&bytes.Buffer{})

_, err = cmd.ExecuteC()
if tt.wantsErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
})
}
}

func Test_deleteRun(t *testing.T) {
reg := &httpmock.Mocker{
MatchURL: httpmock.PathAndQuerystring,
}
defer reg.Verify(t)

reg.RegisterResponder("DELETE", "/api/v4/projects/owner%2Frepo/variables/TEST_VAR?filter%5Benvironment_scope%5D=%2A",
httpmock.NewStringResponse(204, " "),
)

reg.RegisterResponder("DELETE", "/api/v4/projects/owner%2Frepo/variables/TEST_VAR?filter%5Benvironment_scope%5D=stage",
httpmock.NewStringResponse(204, " "),
)

reg.RegisterResponder("DELETE", "/api/v4/groups/testGroup/variables/TEST_VAR",
httpmock.NewStringResponse(204, " "),
)

var httpClient = func() (*gitlab.Client, error) {
a, _ := api.TestClient(&http.Client{Transport: reg}, "", "gitlab.com", false)
return a.Lab(), nil
}
var baseRepo = func() (glrepo.Interface, error) {
return glrepo.FromFullName("owner/repo")
}

tests := []struct {
name string
opts DeleteOpts
wantsErr bool
wantsOutput string
}{
{
name: "delete project variable no scope",
opts: DeleteOpts{
HTTPClient: httpClient,
BaseRepo: baseRepo,
Key: "TEST_VAR",
Scope: "*",
},
wantsErr: false,
wantsOutput: "✓ Deleted variable TEST_VAR with scope * for owner/repo\n",
},
{
name: "delete project variable with stage scope",
opts: DeleteOpts{
HTTPClient: httpClient,
BaseRepo: baseRepo,
Key: "TEST_VAR",
Scope: "stage",
},
wantsErr: false,
wantsOutput: "✓ Deleted variable TEST_VAR with scope stage for owner/repo\n",
},
{
name: "delete group variable",
opts: DeleteOpts{
HTTPClient: httpClient,
BaseRepo: baseRepo,
Key: "TEST_VAR",
Scope: "",
Group: "testGroup",
},
wantsErr: false,
wantsOutput: "✓ Deleted variable TEST_VAR for group testGroup\n",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, _ = tt.opts.HTTPClient()

io, _, stdout, _ := iostreams.Test()
tt.opts.IO = io
io.IsInTTY = false

err := deleteRun(&tt.opts)
assert.NoError(t, err)
assert.Equal(t, stdout.String(), tt.wantsOutput)
})
}
}
20 changes: 3 additions & 17 deletions commands/variable/set/set.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ import (
"errors"
"fmt"
"io/ioutil"
"regexp"
"strings"

"github.com/profclems/glab/pkg/iostreams"

"github.com/MakeNowJust/heredoc"
"github.com/profclems/glab/api"
"github.com/profclems/glab/commands/cmdutils"
"github.com/profclems/glab/commands/variable/variableutils"
"github.com/profclems/glab/internal/glrepo"
"github.com/spf13/cobra"
"github.com/xanzy/go-gitlab"
Expand All @@ -36,8 +36,6 @@ func NewCmdSet(f *cmdutils.Factory, runE func(opts *SetOpts) error) *cobra.Comma
IO: f.IO,
}

validKeyMsg := "A valid key must have no more than 255 characters; only A-Z, a-z, 0-9, and _ are allowed"

cmd := &cobra.Command{
Use: "set <key> <value>",
Short: "Create a new project or group variable",
Expand All @@ -59,8 +57,8 @@ func NewCmdSet(f *cmdutils.Factory, runE func(opts *SetOpts) error) *cobra.Comma

opts.Key = args[0]

if !isValidKey(opts.Key) {
err = cmdutils.FlagError{Err: fmt.Errorf("invalid key provided.\n%s", validKeyMsg)}
if !variableutils.IsValidKey(opts.Key) {
err = cmdutils.FlagError{Err: fmt.Errorf("invalid key provided.\n%s", variableutils.ValidKeyMsg)}
return
}

Expand Down Expand Up @@ -169,15 +167,3 @@ func getValue(opts *SetOpts, args []string) (string, error) {
}
return strings.TrimSpace(string(value)), nil
}

// isValidKey checks if a key is valid if it follows the following criteria:
// must have no more than 255 characters;
// only A-Z, a-z, 0-9, and _ are allowed
func isValidKey(key string) bool {
// check if key falls within range of 1-255
if len(key) > 255 || len(key) < 1 {
return false
}
keyRE := regexp.MustCompile(`^[A-Za-z0-9_]+$`)
return keyRE.MatchString(key)
}
Loading

0 comments on commit af35a72

Please sign in to comment.