Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Release #859

Merged
merged 2 commits into from
Mar 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions api/project/project.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,6 @@ type ProjectEnvironmentVariable struct {
// components.
type ProjectClient interface {
ListAllEnvironmentVariables(vcs, org, project string) ([]*ProjectEnvironmentVariable, error)
GetEnvironmentVariable(vcs, org, project, envName string) (*ProjectEnvironmentVariable, error)
CreateEnvironmentVariable(vcs, org, project string, v ProjectEnvironmentVariable) (*ProjectEnvironmentVariable, error)
}
53 changes: 53 additions & 0 deletions api/project/project_rest.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ type listAllProjectEnvVarsResponse struct {
NextPageToken string `json:"next_page_token"`
}

type createProjectEnvVarRequest struct {
Name string `json:"name"`
Value string `json:"value"`
}

// NewProjectRestClient returns a new projectRestClient satisfying the api.ProjectInterface
// interface via the REST API.
func NewProjectRestClient(config settings.Config) (*projectRestClient, error) {
Expand Down Expand Up @@ -102,3 +107,51 @@ func (c *projectRestClient) listEnvironmentVariables(params *listProjectEnvVarsP
}
return &resp, nil
}

// GetEnvironmentVariable retrieves and returns a variable with the given name.
// If the response status code is 404, nil is returned.
func (c *projectRestClient) GetEnvironmentVariable(vcs string, org string, project string, envName string) (*ProjectEnvironmentVariable, error) {
path := fmt.Sprintf("project/%s/%s/%s/envvar/%s", vcs, org, project, envName)
req, err := c.client.NewRequest("GET", &url.URL{Path: path}, nil)
if err != nil {
return nil, err
}

var resp projectEnvVarResponse
code, err := c.client.DoRequest(req, &resp)
if err != nil {
if code == 404 {
// Note: 404 may mean that the project isn't found.
// The cause can't be distinguished except by the response text.
return nil, nil
}
return nil, err
}
return &ProjectEnvironmentVariable{
Name: resp.Name,
Value: resp.Value,
}, nil
}

// CreateEnvironmentVariable creates a variable on the given project.
// This returns the variable if successfully created.
func (c *projectRestClient) CreateEnvironmentVariable(vcs string, org string, project string, v ProjectEnvironmentVariable) (*ProjectEnvironmentVariable, error) {
path := fmt.Sprintf("project/%s/%s/%s/envvar", vcs, org, project)
req, err := c.client.NewRequest("POST", &url.URL{Path: path}, &createProjectEnvVarRequest{
Name: v.Name,
Value: v.Value,
})
if err != nil {
return nil, err
}

var resp projectEnvVarResponse
_, err = c.client.DoRequest(req, &resp)
if err != nil {
return nil, err
}
return &ProjectEnvironmentVariable{
Name: resp.Name,
Value: resp.Value,
}, nil
}
163 changes: 163 additions & 0 deletions api/project/project_rest_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package project_test

import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
Expand Down Expand Up @@ -158,3 +159,165 @@ func Test_projectRestClient_ListAllEnvironmentVariables(t *testing.T) {
})
}
}

func Test_projectRestClient_GetEnvironmentVariable(t *testing.T) {
const (
vcsType = "github"
orgName = "test-org"
projName = "test-proj"
)
tests := []struct {
name string
handler http.HandlerFunc
envName string
want *project.ProjectEnvironmentVariable
wantErr bool
}{
{
name: "Should handle a successful request",
handler: func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, r.Header.Get("circle-token"), "token")
assert.Equal(t, r.Header.Get("accept"), "application/json")
assert.Equal(t, r.Header.Get("user-agent"), version.UserAgent())

assert.Equal(t, r.Method, "GET")
assert.Equal(t, r.URL.Path, fmt.Sprintf("/api/v2/project/%s/%s/%s/envvar/test1", vcsType, orgName, projName))

w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_, err := w.Write([]byte(`
{
"name": "foo",
"value": "xxxx1234"
}`))
assert.NilError(t, err)
},
envName: "test1",
want: &project.ProjectEnvironmentVariable{
Name: "foo",
Value: "xxxx1234",
},
},
{
name: "Should handle an error request",
handler: func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("content-type", "application/json")
w.WriteHeader(http.StatusInternalServerError)
_, err := w.Write([]byte(`{"message": "error"}`))
assert.NilError(t, err)
},
wantErr: true,
},
{
name: "Should handle an 404 error as a valid request",
handler: func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("content-type", "application/json")
w.WriteHeader(http.StatusNotFound)
_, err := w.Write([]byte(`{"message": "Environment variable not found."}`))
assert.NilError(t, err)
},
want: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
server := httptest.NewServer(tt.handler)
defer server.Close()

p, err := getProjectRestClient(server)
assert.NilError(t, err)

got, err := p.GetEnvironmentVariable(vcsType, orgName, projName, tt.envName)
if (err != nil) != tt.wantErr {
t.Errorf("projectRestClient.GetEnvironmentVariable() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("projectRestClient.GetEnvironmentVariable() = %v, want %v", got, tt.want)
}
})
}
}

func Test_projectRestClient_CreateEnvironmentVariable(t *testing.T) {
const (
vcsType = "github"
orgName = "test-org"
projName = "test-proj"
)
tests := []struct {
name string
handler http.HandlerFunc
variable project.ProjectEnvironmentVariable
want *project.ProjectEnvironmentVariable
wantErr bool
}{
{
name: "Should handle a successful request",
handler: func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, r.Header.Get("circle-token"), "token")
assert.Equal(t, r.Header.Get("accept"), "application/json")
assert.Equal(t, r.Header.Get("user-agent"), version.UserAgent())

assert.Equal(t, r.Method, "POST")
assert.Equal(t, r.URL.Path, fmt.Sprintf("/api/v2/project/%s/%s/%s/envvar", vcsType, orgName, projName))
var pv project.ProjectEnvironmentVariable
err := json.NewDecoder(r.Body).Decode(&pv)
assert.NilError(t, err)
assert.Equal(t, pv, project.ProjectEnvironmentVariable{
Name: "foo",
Value: "test1234",
})

w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_, err = w.Write([]byte(`
{
"name": "foo",
"value": "xxxx1234"
}`))
assert.NilError(t, err)
},
variable: project.ProjectEnvironmentVariable{
Name: "foo",
Value: "test1234",
},
want: &project.ProjectEnvironmentVariable{
Name: "foo",
Value: "xxxx1234",
},
},
{
name: "Should handle an error request",
handler: func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("content-type", "application/json")
w.WriteHeader(http.StatusInternalServerError)
_, err := w.Write([]byte(`{"message": "error"}`))
assert.NilError(t, err)
},
variable: project.ProjectEnvironmentVariable{
Name: "bar",
Value: "testbar",
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
server := httptest.NewServer(tt.handler)
defer server.Close()

p, err := getProjectRestClient(server)
assert.NilError(t, err)

got, err := p.CreateEnvironmentVariable(vcsType, orgName, projName, tt.variable)
if (err != nil) != tt.wantErr {
t.Errorf("projectRestClient.CreateEnvironmentVariable() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("projectRestClient.CreateEnvironmentVariable() = %v, want %v", got, tt.want)
}
})
}
}
59 changes: 59 additions & 0 deletions cmd/project/environment_variable.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package project

import (
"fmt"
"strings"

projectapi "github.com/CircleCI-Public/circleci-cli/api/project"
"github.com/CircleCI-Public/circleci-cli/cmd/validator"
"github.com/olekukonko/tablewriter"
Expand All @@ -23,7 +26,21 @@ func newProjectEnvironmentVariableCommand(ops *projectOpts, preRunE validator.Va
Args: cobra.ExactArgs(3),
}

var envValue string
createVarCommand := &cobra.Command{
Short: "Create an environment variable of a project. The value is read from stdin.",
Use: "create <vcs-type> <org-name> <project-name> <env-name>",
PreRunE: preRunE,
RunE: func(cmd *cobra.Command, args []string) error {
return createProjectEnvironmentVariable(cmd, ops.client, ops.reader, args[0], args[1], args[2], args[3], envValue)
},
Args: cobra.ExactArgs(4),
}

createVarCommand.Flags().StringVar(&envValue, "env-value", "", "An environment variable value to be created. You can also pass it by stdin without this option.")

cmd.AddCommand(listVarsCommand)
cmd.AddCommand(createVarCommand)
return cmd
}

Expand All @@ -44,3 +61,45 @@ func listProjectEnvironmentVariables(cmd *cobra.Command, client projectapi.Proje

return nil
}

func createProjectEnvironmentVariable(cmd *cobra.Command, client projectapi.ProjectClient, r UserInputReader, vcsType, orgName, projName, name, value string) error {
if value == "" {
val, err := r.ReadSecretString("Enter an environment variable value and press enter")
if err != nil {
return err
}
if val == "" {
return fmt.Errorf("the environment variable value must not be empty")
}
value = val
}
value = strings.Trim(value, "\r\n")

existV, err := client.GetEnvironmentVariable(vcsType, orgName, projName, name)
if err != nil {
return err
}
if existV != nil {
msg := fmt.Sprintf("The environment variable name=%s value=%s already exists. Do you overwrite it?", existV.Name, existV.Value)
if !r.AskConfirm(msg) {
fmt.Fprintln(cmd.OutOrStdout(), "Canceled")
return nil
}
}

v, err := client.CreateEnvironmentVariable(vcsType, orgName, projName, projectapi.ProjectEnvironmentVariable{
Name: name,
Value: value,
})
if err != nil {
return err
}

table := tablewriter.NewWriter(cmd.OutOrStdout())

table.SetHeader([]string{"Environment Variable", "Value"})
table.Append([]string{v.Name, v.Value})
table.Render()

return nil
}
49 changes: 45 additions & 4 deletions cmd/project/project.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,46 @@ package project
import (
projectapi "github.com/CircleCI-Public/circleci-cli/api/project"
"github.com/CircleCI-Public/circleci-cli/cmd/validator"
"github.com/CircleCI-Public/circleci-cli/prompt"

"github.com/CircleCI-Public/circleci-cli/settings"
"github.com/spf13/cobra"
)

// UserInputReader displays a message and reads a user input value
type UserInputReader interface {
ReadSecretString(msg string) (string, error)
AskConfirm(msg string) bool
}

type projectOpts struct {
client projectapi.ProjectClient
reader UserInputReader
}

// ProjectOption configures a command created by NewProjectCommand
type ProjectOption interface {
apply(*projectOpts)
}

type promptReader struct{}

func (p promptReader) ReadSecretString(msg string) (string, error) {
return prompt.ReadSecretStringFromUser(msg)
}

func (p promptReader) AskConfirm(msg string) bool {
return prompt.AskUserToConfirm(msg)
}

// NewProjectCommand generates a cobra command for managing projects
func NewProjectCommand(config *settings.Config, preRunE validator.Validator) *cobra.Command {
var opts projectOpts
func NewProjectCommand(config *settings.Config, preRunE validator.Validator, opts ...ProjectOption) *cobra.Command {
pos := projectOpts{
reader: &promptReader{},
}
for _, o := range opts {
o.apply(&pos)
}
command := &cobra.Command{
Use: "project",
Short: "Operate on projects",
Expand All @@ -23,12 +51,25 @@ func NewProjectCommand(config *settings.Config, preRunE validator.Validator) *co
if err != nil {
return err
}
opts.client = client
pos.client = client
return nil
},
}

command.AddCommand(newProjectEnvironmentVariableCommand(&opts, preRunE))
command.AddCommand(newProjectEnvironmentVariableCommand(&pos, preRunE))

return command
}

type customReaderProjectOption struct {
r UserInputReader
}

func (c customReaderProjectOption) apply(opts *projectOpts) {
opts.reader = c.r
}

// CustomReader returns a ProjectOption that sets a given UserInputReader to a project command
func CustomReader(r UserInputReader) ProjectOption {
return customReaderProjectOption{r}
}
Loading