Skip to content
Snippets Groups Projects
Commit 12fb8a36 authored by Anders Parslov's avatar Anders Parslov Committed by Jay McCure
Browse files

feat: add environment scope for export

parent d8fa1d63
No related branches found
No related tags found
1 merge request!1741feat: add environment scope for export
......@@ -3,6 +3,10 @@ package export
import (
"encoding/json"
"fmt"
"io"
"os"
"regexp"
"strings"
"github.com/MakeNowJust/heredoc/v2"
"github.com/spf13/cobra"
......@@ -22,6 +26,7 @@ type ExportOpts struct {
ValueSet bool
Group string
OutputFormat string
Scope string
Page int
PerPage int
......@@ -77,11 +82,17 @@ func NewCmdExport(f *cmdutils.Factory, runE func(opts *ExportOpts) error) *cobra
cmd.Flags().IntVarP(&opts.Page, "page", "p", 1, "Page number.")
cmd.Flags().IntVarP(&opts.PerPage, "per-page", "P", 100, "Number of items to list per page.")
cmd.Flags().StringVarP(&opts.OutputFormat, "format", "F", "json", "Format of output: json, export, env.")
cmd.Flags().StringVarP(&opts.Scope, "scope", "s", "*", "The environment_scope of the variables. Values: '*' (default), or specific environments.")
return cmd
}
func exportRun(opts *ExportOpts) error {
var out io.Writer = os.Stdout
if opts.IO != nil && opts.IO.StdOut != nil {
out = opts.IO.StdOut
}
httpClient, err := opts.HTTPClient()
if err != nil {
return err
......@@ -93,7 +104,6 @@ func exportRun(opts *ExportOpts) error {
}
if opts.Group != "" {
createVarOpts := &gitlab.ListGroupVariablesOptions{Page: opts.Page, PerPage: opts.PerPage}
groupVariables, err := api.ListGroupVariables(httpClient, opts.Group, createVarOpts)
if err != nil {
......@@ -106,7 +116,7 @@ func exportRun(opts *ExportOpts) error {
return nil
}
return printGroupVariables(groupVariables, opts)
return printGroupVariables(groupVariables, opts, out)
} else {
createVarOpts := &gitlab.ListProjectVariablesOptions{Page: opts.Page, PerPage: opts.PerPage}
......@@ -121,50 +131,163 @@ func exportRun(opts *ExportOpts) error {
return nil
}
return printProjectVariables(projectVariables, opts)
return printProjectVariables(projectVariables, opts, out)
}
}
func matchesScope(varScope, optScope string) bool {
if varScope == "*" || optScope == "*" {
return true
}
if varScope == optScope {
return true
}
if strings.Contains(varScope, "*") {
varPattern := "^" + regexp.QuoteMeta(varScope) + "$"
optPattern := "^" + regexp.QuoteMeta(optScope) + "$"
varPattern = strings.ReplaceAll(varPattern, `\*`, ".*")
optPattern = strings.ReplaceAll(optPattern, `\*`, ".*")
matchesVar, _ := regexp.MatchString(varPattern, optScope)
matchesOpt, _ := regexp.MatchString(optPattern, varScope)
return matchesVar || matchesOpt
}
return false
}
func printGroupVariables(variables []*gitlab.GroupVariable, opts *ExportOpts) error {
func isValidEnvironmentScope(optScope string) bool {
pattern := `^[a-zA-Z0-9\s\-_/${}\x20]+$`
re, _ := regexp.Compile(pattern)
matched := re.MatchString(optScope)
return matched || optScope == "*"
}
func printGroupVariables(variables []*gitlab.GroupVariable, opts *ExportOpts, out io.Writer) error {
if !isValidEnvironmentScope((opts.Scope)) {
return fmt.Errorf("invalid environment scope: %s", opts.Scope)
}
writtenKeys := make([]string, 0)
switch opts.OutputFormat {
case "env":
for _, variable := range variables {
fmt.Printf("%s=%s\n", variable.Key, variable.Value)
if matchesScope(variable.EnvironmentScope, opts.Scope) {
if !strings.Contains(variable.EnvironmentScope, "*") {
fmt.Fprintf(out, "%s=%s\n", variable.Key, variable.Value)
writtenKeys = append(writtenKeys, variable.Key)
}
}
}
keysMap := CreateWrittenKeysMap(writtenKeys)
for _, variable := range variables {
if matchesScope(variable.EnvironmentScope, opts.Scope) {
if !(keysMap[variable.Key]) && (strings.Contains(variable.EnvironmentScope, "*")) {
fmt.Fprintf(out, "%s=%s\n", variable.Key, variable.Value)
}
}
}
case "export":
for _, variable := range variables {
fmt.Printf("export %s=%s\n", variable.Key, variable.Value)
if matchesScope(variable.EnvironmentScope, opts.Scope) {
if !strings.Contains(variable.EnvironmentScope, "*") {
fmt.Fprintf(out, "export %s=%s\n", variable.Key, variable.Value)
writtenKeys = append(writtenKeys, variable.Key)
}
}
}
keysMap := CreateWrittenKeysMap(writtenKeys)
for _, variable := range variables {
if matchesScope(variable.EnvironmentScope, opts.Scope) {
if !(keysMap[variable.Key]) && (strings.Contains(variable.EnvironmentScope, "*")) {
fmt.Fprintf(out, "export %s=%s\n", variable.Key, variable.Value)
}
}
}
case "json":
res, err := marshalJson(variables)
filteredVariables := make([]*gitlab.GroupVariable, 0)
for _, variable := range variables {
if matchesScope(variable.EnvironmentScope, opts.Scope) {
filteredVariables = append(filteredVariables, variable)
}
}
res, err := marshalJson(filteredVariables)
if err != nil {
return err
}
fmt.Println(string(res))
fmt.Fprintln(out, string(res))
default:
return fmt.Errorf("unsupported output format: %s", opts.OutputFormat)
}
return nil
}
func printProjectVariables(variables []*gitlab.ProjectVariable, opts *ExportOpts) error {
func printProjectVariables(variables []*gitlab.ProjectVariable, opts *ExportOpts, out io.Writer) error {
if !isValidEnvironmentScope((opts.Scope)) {
return fmt.Errorf("invalid environment scope: %s", opts.Scope)
}
writtenKeys := make([]string, 0)
switch opts.OutputFormat {
case "env":
for _, variable := range variables {
fmt.Printf("%s=%s\n", variable.Key, variable.Value)
if matchesScope(variable.EnvironmentScope, opts.Scope) {
if !strings.Contains(variable.EnvironmentScope, "*") {
fmt.Fprintf(out, "%s=%s\n", variable.Key, variable.Value)
writtenKeys = append(writtenKeys, variable.Key)
}
}
}
keysMap := CreateWrittenKeysMap(writtenKeys)
for _, variable := range variables {
if matchesScope(variable.EnvironmentScope, opts.Scope) {
if !(keysMap[variable.Key]) && (strings.Contains(variable.EnvironmentScope, "*")) {
fmt.Fprintf(out, "%s=%s\n", variable.Key, variable.Value)
}
}
}
case "export":
for _, variable := range variables {
fmt.Printf("export %s=%s\n", variable.Key, variable.Value)
if matchesScope(variable.EnvironmentScope, opts.Scope) {
if !strings.Contains(variable.EnvironmentScope, "*") {
fmt.Fprintf(out, "export %s=%s\n", variable.Key, variable.Value)
writtenKeys = append(writtenKeys, variable.Key)
}
}
}
keysMap := CreateWrittenKeysMap(writtenKeys)
for _, variable := range variables {
if matchesScope(variable.EnvironmentScope, opts.Scope) {
if !(keysMap[variable.Key]) && (strings.Contains(variable.EnvironmentScope, "*")) {
fmt.Fprintf(out, "export %s=%s\n", variable.Key, variable.Value)
}
}
}
case "json":
res, err := marshalJson(variables)
filteredVariables := make([]*gitlab.ProjectVariable, 0)
for _, variable := range variables {
if matchesScope(variable.EnvironmentScope, opts.Scope) {
filteredVariables = append(filteredVariables, variable)
}
}
res, err := marshalJson(filteredVariables)
if err != nil {
return err
}
fmt.Println(string(res))
fmt.Fprintln(out, string(res))
default:
return fmt.Errorf("unsupported output format: %s", opts.OutputFormat)
}
return nil
}
func CreateWrittenKeysMap(writtenKeys []string) map[string]bool {
keysMap := make(map[string]bool)
for _, key := range writtenKeys {
keysMap[key] = true
}
return keysMap
}
......@@ -5,6 +5,7 @@ import (
"net/http"
"testing"
"github.com/MakeNowJust/heredoc/v2"
"github.com/google/shlex"
"github.com/stretchr/testify/assert"
"github.com/xanzy/go-gitlab"
......@@ -95,85 +96,521 @@ func Test_exportRun_project(t *testing.T) {
mockProjectVariables := []gitlab.ProjectVariable{
{
Key: "VAR1",
Value: "value1",
Key: "VAR1",
Value: "value1",
EnvironmentScope: "dev",
},
{
Key: "VAR2",
Value: "value2",
Key: "VAR2",
Value: "value2.1",
EnvironmentScope: "prod",
},
{
Key: "VAR2",
Value: "value2.2",
EnvironmentScope: "*",
},
{
Key: "VAR3",
Value: "value3",
EnvironmentScope: "dev/a",
},
{
Key: "VAR4",
Value: "value4.1",
EnvironmentScope: "dev/b",
},
{
Key: "VAR4",
Value: "value4.2",
EnvironmentScope: "feature-1",
},
{
Key: "VAR4",
Value: "value4.3",
EnvironmentScope: "feature-2",
},
{
Key: "VAR5",
Value: "value5",
EnvironmentScope: "feature-*",
},
}
io, _, stdout, _ := iostreams.Test()
outputFormats := []string{"env", "export", "json"}
tests := []struct {
scope string
format string
expectedOutput string
}{
{
scope: "*",
format: "json",
expectedOutput: heredoc.Doc(`
[
{
"key": "VAR1",
"value": "value1",
"variable_type": "",
"protected": false,
"masked": false,
"raw": false,
"environment_scope": "dev",
"description": ""
},
{
"key": "VAR2",
"value": "value2.1",
"variable_type": "",
"protected": false,
"masked": false,
"raw": false,
"environment_scope": "prod",
"description": ""
},
{
"key": "VAR2",
"value": "value2.2",
"variable_type": "",
"protected": false,
"masked": false,
"raw": false,
"environment_scope": "*",
"description": ""
},
{
"key": "VAR3",
"value": "value3",
"variable_type": "",
"protected": false,
"masked": false,
"raw": false,
"environment_scope": "dev/a",
"description": ""
},
{
"key": "VAR4",
"value": "value4.1",
"variable_type": "",
"protected": false,
"masked": false,
"raw": false,
"environment_scope": "dev/b",
"description": ""
},
{
"key": "VAR4",
"value": "value4.2",
"variable_type": "",
"protected": false,
"masked": false,
"raw": false,
"environment_scope": "feature-1",
"description": ""
},
{
"key": "VAR4",
"value": "value4.3",
"variable_type": "",
"protected": false,
"masked": false,
"raw": false,
"environment_scope": "feature-2",
"description": ""
},
{
"key": "VAR5",
"value": "value5",
"variable_type": "",
"protected": false,
"masked": false,
"raw": false,
"environment_scope": "feature-*",
"description": ""
}
]
`),
},
{
scope: "dev/b",
format: "json",
expectedOutput: heredoc.Doc(`
[
{
"key": "VAR2",
"value": "value2.2",
"variable_type": "",
"protected": false,
"masked": false,
"raw": false,
"environment_scope": "*",
"description": ""
},
{
"key": "VAR4",
"value": "value4.1",
"variable_type": "",
"protected": false,
"masked": false,
"raw": false,
"environment_scope": "dev/b",
"description": ""
}
]
`),
},
{
scope: "*",
format: "env",
expectedOutput: "VAR1=value1\nVAR2=value2.1\nVAR3=value3\nVAR4=value4.1\nVAR4=value4.2\nVAR4=value4.3\nVAR5=value5\n",
},
{
scope: "*",
format: "export",
expectedOutput: "export VAR1=value1\nexport VAR2=value2.1\nexport VAR3=value3\nexport VAR4=value4.1\nexport VAR4=value4.2\nexport VAR4=value4.3\nexport VAR5=value5\n",
},
{
scope: "dev",
format: "env",
expectedOutput: "VAR1=value1\nVAR2=value2.2\n",
},
{
scope: "dev",
format: "export",
expectedOutput: "export VAR1=value1\nexport VAR2=value2.2\n",
},
{
scope: "prod",
format: "env",
expectedOutput: "VAR2=value2.1\n",
},
{
scope: "prod",
format: "export",
expectedOutput: "export VAR2=value2.1\n",
},
{
scope: "dev/a",
format: "env",
expectedOutput: "VAR3=value3\nVAR2=value2.2\n",
},
{
scope: "dev/a",
format: "export",
expectedOutput: "export VAR3=value3\nexport VAR2=value2.2\n",
},
{
scope: "feature-1",
format: "env",
expectedOutput: "VAR4=value4.2\nVAR2=value2.2\nVAR5=value5\n",
},
{
scope: "feature-1",
format: "export",
expectedOutput: "export VAR4=value4.2\nexport VAR2=value2.2\nexport VAR5=value5\n",
},
{
scope: "feature-2",
format: "env",
expectedOutput: "VAR4=value4.3\nVAR2=value2.2\nVAR5=value5\n",
},
{
scope: "feature-2",
format: "export",
expectedOutput: "export VAR4=value4.3\nexport VAR2=value2.2\nexport VAR5=value5\n",
},
}
for _, format := range outputFormats {
reg.RegisterResponder(http.MethodGet, "https://gitlab.com/api/v4/projects/owner%2Frepo/variables?page=1&per_page=10",
httpmock.NewJSONResponse(http.StatusOK, mockProjectVariables),
)
opts := &ExportOpts{
HTTPClient: func() (*gitlab.Client, error) {
a, _ := api.TestClient(&http.Client{Transport: reg}, "", "gitlab.com", false)
return a.Lab(), nil
},
BaseRepo: func() (glrepo.Interface, error) {
return glrepo.FromFullName("owner/repo")
},
IO: io,
Page: 1,
PerPage: 10,
OutputFormat: format,
}
_, _ = opts.HTTPClient()
err := exportRun(opts)
assert.NoError(t, err)
assert.Equal(t, "", stdout.String())
for _, test := range tests {
t.Run(test.scope+"_"+test.format, func(t *testing.T) {
reg.RegisterResponder(http.MethodGet, "https://gitlab.com/api/v4/projects/owner%2Frepo/variables?page=1&per_page=10",
httpmock.NewJSONResponse(http.StatusOK, mockProjectVariables),
)
opts := &ExportOpts{
HTTPClient: func() (*gitlab.Client, error) {
a, _ := api.TestClient(&http.Client{Transport: reg}, "", "gitlab.com", false)
return a.Lab(), nil
},
BaseRepo: func() (glrepo.Interface, error) {
return glrepo.FromFullName("owner/repo")
},
IO: io,
Page: 1,
PerPage: 10,
OutputFormat: test.format,
Scope: test.scope,
}
_, _ = opts.HTTPClient()
err := exportRun(opts)
assert.NoError(t, err)
assert.Equal(t, test.expectedOutput, stdout.String())
stdout.Reset()
})
}
}
func Test_exportRun_group(t *testing.T) {
reg := &httpmock.Mocker{
MatchURL: httpmock.FullURL,
}
defer reg.Verify(t)
mockGroupVariables := []gitlab.GroupVariable{
{
Key: "VAR1",
Value: "value1",
Key: "VAR1",
Value: "value1",
EnvironmentScope: "dev",
},
{
Key: "VAR2",
Value: "value2",
Key: "VAR2",
Value: "value2.1",
EnvironmentScope: "prod",
},
{
Key: "VAR2",
Value: "value2.2",
EnvironmentScope: "*",
},
{
Key: "VAR3",
Value: "value3",
EnvironmentScope: "dev/a",
},
{
Key: "VAR4",
Value: "value4.1",
EnvironmentScope: "dev/b",
},
{
Key: "VAR4",
Value: "value4.2",
EnvironmentScope: "feature-1",
},
{
Key: "VAR4",
Value: "value4.3",
EnvironmentScope: "feature-2",
},
{
Key: "VAR5",
Value: "value5",
EnvironmentScope: "feature-*",
},
}
reg := &httpmock.Mocker{
MatchURL: httpmock.FullURL,
}
defer reg.Verify(t)
io, _, stdout, _ := iostreams.Test()
outputFormats := []string{"env", "export", "json"}
for _, format := range outputFormats {
reg.RegisterResponder(http.MethodGet, "https://gitlab.com/api/v4/groups/GROUP/variables?page=7&per_page=77",
httpmock.NewJSONResponse(http.StatusOK, mockGroupVariables),
)
tests := []struct {
scope string
format string
expectedOutput string
}{
{
scope: "*",
format: "json",
expectedOutput: heredoc.Doc(`
[
{
"key": "VAR1",
"value": "value1",
"variable_type": "",
"protected": false,
"masked": false,
"raw": false,
"environment_scope": "dev",
"description": ""
},
{
"key": "VAR2",
"value": "value2.1",
"variable_type": "",
"protected": false,
"masked": false,
"raw": false,
"environment_scope": "prod",
"description": ""
},
{
"key": "VAR2",
"value": "value2.2",
"variable_type": "",
"protected": false,
"masked": false,
"raw": false,
"environment_scope": "*",
"description": ""
},
{
"key": "VAR3",
"value": "value3",
"variable_type": "",
"protected": false,
"masked": false,
"raw": false,
"environment_scope": "dev/a",
"description": ""
},
{
"key": "VAR4",
"value": "value4.1",
"variable_type": "",
"protected": false,
"masked": false,
"raw": false,
"environment_scope": "dev/b",
"description": ""
},
{
"key": "VAR4",
"value": "value4.2",
"variable_type": "",
"protected": false,
"masked": false,
"raw": false,
"environment_scope": "feature-1",
"description": ""
},
{
"key": "VAR4",
"value": "value4.3",
"variable_type": "",
"protected": false,
"masked": false,
"raw": false,
"environment_scope": "feature-2",
"description": ""
},
{
"key": "VAR5",
"value": "value5",
"variable_type": "",
"protected": false,
"masked": false,
"raw": false,
"environment_scope": "feature-*",
"description": ""
}
]
`),
},
{
scope: "dev/b",
format: "json",
expectedOutput: heredoc.Doc(`
[
{
"key": "VAR2",
"value": "value2.2",
"variable_type": "",
"protected": false,
"masked": false,
"raw": false,
"environment_scope": "*",
"description": ""
},
{
"key": "VAR4",
"value": "value4.1",
"variable_type": "",
"protected": false,
"masked": false,
"raw": false,
"environment_scope": "dev/b",
"description": ""
}
]
`),
},
{
scope: "*",
format: "env",
expectedOutput: "VAR1=value1\nVAR2=value2.1\nVAR3=value3\nVAR4=value4.1\nVAR4=value4.2\nVAR4=value4.3\nVAR5=value5\n",
},
{
scope: "*",
format: "export",
expectedOutput: "export VAR1=value1\nexport VAR2=value2.1\nexport VAR3=value3\nexport VAR4=value4.1\nexport VAR4=value4.2\nexport VAR4=value4.3\nexport VAR5=value5\n",
},
{
scope: "dev",
format: "env",
expectedOutput: "VAR1=value1\nVAR2=value2.2\n",
},
{
scope: "dev",
format: "export",
expectedOutput: "export VAR1=value1\nexport VAR2=value2.2\n",
},
{
scope: "prod",
format: "env",
expectedOutput: "VAR2=value2.1\n",
},
{
scope: "prod",
format: "export",
expectedOutput: "export VAR2=value2.1\n",
},
{
scope: "dev/a",
format: "env",
expectedOutput: "VAR3=value3\nVAR2=value2.2\n",
},
{
scope: "dev/a",
format: "export",
expectedOutput: "export VAR3=value3\nexport VAR2=value2.2\n",
},
{
scope: "feature-1",
format: "env",
expectedOutput: "VAR4=value4.2\nVAR2=value2.2\nVAR5=value5\n",
},
{
scope: "feature-1",
format: "export",
expectedOutput: "export VAR4=value4.2\nexport VAR2=value2.2\nexport VAR5=value5\n",
},
{
scope: "feature-2",
format: "env",
expectedOutput: "VAR4=value4.3\nVAR2=value2.2\nVAR5=value5\n",
},
{
scope: "feature-2",
format: "export",
expectedOutput: "export VAR4=value4.3\nexport VAR2=value2.2\nexport VAR5=value5\n",
},
}
opts := &ExportOpts{
HTTPClient: func() (*gitlab.Client, error) {
a, _ := api.TestClient(&http.Client{Transport: reg}, "", "gitlab.com", false)
return a.Lab(), nil
},
BaseRepo: func() (glrepo.Interface, error) {
return glrepo.FromFullName("owner/repo")
},
IO: io,
Page: 7,
PerPage: 77,
Group: "GROUP",
OutputFormat: format,
}
_, _ = opts.HTTPClient()
err := exportRun(opts)
assert.NoError(t, err)
assert.Equal(t, "", stdout.String())
for _, test := range tests {
t.Run(test.scope+"_"+test.format, func(t *testing.T) {
reg.RegisterResponder(http.MethodGet, "https://gitlab.com/api/v4/groups/group/variables?page=1&per_page=10",
httpmock.NewJSONResponse(http.StatusOK, mockGroupVariables),
)
opts := &ExportOpts{
HTTPClient: func() (*gitlab.Client, error) {
a, _ := api.TestClient(&http.Client{Transport: reg}, "", "gitlab.com", false)
return a.Lab(), nil
},
BaseRepo: func() (glrepo.Interface, error) {
return glrepo.FromFullName("owner/repo")
},
IO: io,
Page: 1,
PerPage: 10,
OutputFormat: test.format,
Scope: test.scope,
Group: "group",
}
_, _ = opts.HTTPClient()
err := exportRun(opts)
assert.NoError(t, err)
assert.Equal(t, test.expectedOutput, stdout.String())
stdout.Reset()
})
}
}
......@@ -41,6 +41,7 @@ glab variable export --group gitlab-org --per-page 1000 --page 1
-p, --page int Page number. (default 1)
-P, --per-page int Number of items to list per page. (default 100)
-R, --repo OWNER/REPO Select another repository. Can use either OWNER/REPO or `GROUP/NAMESPACE/REPO` format. Also accepts full URL or Git URL.
-s, --scope string The environment_scope of the variables. Values: '*' (default), or specific environments. (default "*")
```
## Options inherited from parent commands
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment