Skip to content
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
44 changes: 44 additions & 0 deletions internal/commands/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ type Opts struct {
Client *client.Client
Quiet bool
Debug bool
DryRun bool
Headers []string
URL *url.URL
Attributes map[string]string
Expand Down Expand Up @@ -197,6 +198,7 @@ func NewCmdAPI(ctx *cmd.Context) *cmd.Command {

opts.Debug = ctx.Profile.GetVerbosity() == "debug" || ctx.Profile.GetVerbosity() == "trace"
opts.Quiet = ctx.Profile.IsQuiet()
opts.DryRun = ctx.IsDryRun()

return runAPI(opts)
},
Expand Down Expand Up @@ -233,6 +235,13 @@ func runAPI(opts *Opts) error {
requestHeaders.Set("Accept", "application/vnd.api+json")
}

// In dry-run mode, skip mutating requests and report what would have happened.
if opts.DryRun && isMutationMethod(method) {
fmt.Fprintf(opts.IO.Err(), "%s would send %s %s\n", opts.IO.ColorScheme().DryRunLabel(), method, opts.URL.String())
writeDryRunRequest(opts.IO.Err(), method, opts.URL, requestHeaders, body)
return nil
}

// Make the request
response, err := opts.Client.RawRequest(opts.ShutdownCtx, &client.Request{
Method: method,
Expand Down Expand Up @@ -503,3 +512,38 @@ func writeHeaders(w io.Writer, headers http.Header) {
fmt.Fprintf(w, "%s: %s\n", key, strings.Join(headers.Values(key), ", "))
}
}

func isMutationMethod(method string) bool {
switch strings.ToUpper(method) {
case http.MethodPost, http.MethodPatch, http.MethodPut, http.MethodDelete:
return true
default:
return false
}
}

func writeDryRunRequest(w io.Writer, method string, u *url.URL, headers http.Header, body []byte) {
fmt.Fprintf(w, "> %s %s\n", method, u.String())
keys := make([]string, 0, len(headers))
for key := range headers {
keys = append(keys, key)
}
sort.Strings(keys)
for _, key := range keys {
fmt.Fprintf(w, "%s: %s\n", key, strings.Join(headers.Values(key), ", "))
}
if len(body) == 0 {
return
}
fmt.Fprintln(w)
_, _ = w.Write(formatDryRunBody(body))
fmt.Fprintln(w)
}

func formatDryRunBody(body []byte) []byte {
var formatted bytes.Buffer
if err := json.Indent(&formatted, body, "", " "); err == nil {
return formatted.Bytes()
}
return body
}
26 changes: 26 additions & 0 deletions internal/commands/api/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"net/http"
"net/http/httptest"
"net/url"
"strings"
"sync"
"testing"

Expand Down Expand Up @@ -600,3 +601,28 @@ func serverURL(r *http.Request) string {
}
return scheme + "://" + r.Host
}

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

io := iostreams.Test()
u, err := url.Parse("https://example.com/api/v2/projects")
if err != nil {
t.Fatal(err)
}
headers := http.Header{
"Accept": []string{"application/vnd.api+json"},
"Content-Type": []string{"application/vnd.api+json"},
}
body := []byte(`{"data":{"type":"projects"}}`)

writeDryRunRequest(io.Err(), http.MethodPost, u, headers, body)

output := io.Error.String()
if !strings.Contains(output, "> POST https://example.com/api/v2/projects") {
t.Fatalf("expected request line, got %q", output)
}
if !strings.Contains(output, `"type": "projects"`) {
t.Fatalf("expected body, got %q", output)
}
}
2 changes: 1 addition & 1 deletion internal/commands/profile/profile.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ func NewCmdProfile(ctx *cmd.Context) *cmd.Command {
{{ template "mdCodeOrBold" "tfcloud" }} has several global flags that have matching profile properties.
Examples are the {{ template "mdCodeOrBold" "verbosity" }} and
{{ template "mdCodeOrBold" "organization" }} properties and their respective flags
{{ template "mdCodeOrBold" "--verbose" }} and {{ template "mdCodeOrBold" "--organization" }}.
{{ template "mdCodeOrBold" "--debug" }} and {{ template "mdCodeOrBold" "--organization" }}.
The difference between properties and flags is that flags apply only on the invoked command,
while properties are persistent across all invocations. Thus profiles allow you to conviently
maintain the same settings across command executions and multiple profiles allow you to easily
Expand Down
7 changes: 7 additions & 0 deletions internal/commands/profile/profiles/activate.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ func NewCmdActivate(ctx *cmd.Context) *cmd.Command {
NoAuthRequired: true,
RunF: func(_ *cmd.Command, args []string) error {
opts.Name = args[0]
opts.DryRun = ctx.IsDryRun()
l, err := profile.NewLoader()
if err != nil {
return err
Expand All @@ -61,6 +62,7 @@ type ActivateOpts struct {
IO iostreams.IOStreams
Profiles *profile.Loader
Name string
DryRun bool
}

func activateRun(opts *ActivateOpts) error {
Expand All @@ -85,6 +87,11 @@ func activateRun(opts *ActivateOpts) error {
return fmt.Errorf("profile %q does not exist", opts.Name)
}

if opts.DryRun {
fmt.Fprintf(opts.IO.Err(), "%s would activate profile %q\n", opts.IO.ColorScheme().DryRunLabel(), opts.Name)
return nil
}

// Save the new active profile
active.Name = opts.Name
if err := active.Write(); err != nil {
Expand Down
25 changes: 25 additions & 0 deletions internal/commands/profile/profiles/activate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,28 @@ func TestActivate(t *testing.T) {
})
}
}

func TestActivateDryRun(t *testing.T) {
t.Parallel()
r := require.New(t)
l := profile.TestLoader(t)
io := iostreams.Test()

for _, name := range []string{"foo", "bar"} {
p, err := l.NewProfile(name)
r.NoError(err)
r.NoError(p.Write())
}
active, err := l.GetActiveProfile()
r.NoError(err)
active.Name = "foo"
r.NoError(active.Write())

opts := &ActivateOpts{IO: io, Profiles: l, Name: "bar", DryRun: true}
r.NoError(activateRun(opts))
r.Contains(io.Error.String(), `would activate profile "bar"`)

newActive, err := l.GetActiveProfile()
r.NoError(err)
r.Equal("foo", newActive.Name)
}
11 changes: 11 additions & 0 deletions internal/commands/profile/profiles/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ func NewCmdCreate(ctx *cmd.Context) *cmd.Command {
NoAuthRequired: true,
RunF: func(_ *cmd.Command, args []string) error {
opts.Name = args[0]
opts.DryRun = ctx.IsDryRun()
l, err := profile.NewLoader()
if err != nil {
return err
Expand All @@ -84,6 +85,7 @@ type CreateOpts struct {
Name string
NoActivate bool
Hostname string
DryRun bool
}

func createRun(opts *CreateOpts) error {
Expand Down Expand Up @@ -111,6 +113,15 @@ func createRun(opts *CreateOpts) error {
p.Hostname = opts.Hostname
}

if opts.DryRun {
cs := opts.IO.ColorScheme()
fmt.Fprintf(opts.IO.Err(), "%s would create profile %q\n", cs.DryRunLabel(), opts.Name)
if !opts.NoActivate {
fmt.Fprintf(opts.IO.Err(), "%s would activate profile %q\n", cs.DryRunLabel(), opts.Name)
}
return nil
}

// Save the profile
if err := p.Write(); err != nil {
return fmt.Errorf("failed to save new profile: %w", err)
Expand Down
22 changes: 22 additions & 0 deletions internal/commands/profile/profiles/create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,25 @@ func TestCreate(t *testing.T) {
r.Contains(profiles, p1)
r.Contains(profiles, p2)
}

func TestCreateDryRun(t *testing.T) {
t.Parallel()
r := require.New(t)
l := profile.TestLoader(t)
io := iostreams.Test()

opts := &CreateOpts{
IO: io,
Profiles: l,
Name: "dry_run_profile",
}

opts.DryRun = true
r.NoError(createRun(opts))
r.Contains(io.Error.String(), `would create profile "dry_run_profile"`)
r.Contains(io.Error.String(), `would activate profile "dry_run_profile"`)

profiles, err := l.ListProfiles()
r.NoError(err)
r.NotContains(profiles, "dry_run_profile")
}
11 changes: 10 additions & 1 deletion internal/commands/profile/profiles/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ func NewCmdDelete(ctx *cmd.Context) *cmd.Command {
}
opts.Profiles = l
opts.Names = args
opts.DryRun = ctx.IsDryRun()
return deleteRun(opts)
},
}
Expand All @@ -75,7 +76,8 @@ type DeleteOpts struct {
IO iostreams.IOStreams
Profiles *profile.Loader

Names []string
Names []string
DryRun bool
}

func deleteRun(opts *DeleteOpts) error {
Expand Down Expand Up @@ -125,6 +127,13 @@ func deleteRun(opts *DeleteOpts) error {
}
}

if opts.DryRun {
for _, toDelete := range opts.Names {
fmt.Fprintf(opts.IO.Err(), "%s would delete profile %q\n", cs.DryRunLabel(), toDelete)
}
return nil
}

for _, toDelete := range opts.Names {
if err := opts.Profiles.DeleteProfile(toDelete); err != nil {
return fmt.Errorf("failed to delete profile %q: %w", toDelete, err)
Expand Down
25 changes: 25 additions & 0 deletions internal/commands/profile/profiles/delete_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,3 +136,28 @@ func TestDelete(t *testing.T) {
})
}
}

func TestDeleteDryRun(t *testing.T) {
t.Parallel()
r := require.New(t)
l := profile.TestLoader(t)
io := iostreams.Test()

for _, name := range []string{"foo", "bar"} {
p, err := l.NewProfile(name)
r.NoError(err)
r.NoError(p.Write())
}
active, err := l.GetActiveProfile()
r.NoError(err)
active.Name = "foo"
r.NoError(active.Write())

opts := &DeleteOpts{IO: io, Profiles: l, Names: []string{"bar"}, DryRun: true}
r.NoError(deleteRun(opts))
r.Contains(io.Error.String(), `would delete profile "bar"`)

profiles, err := l.ListProfiles()
r.NoError(err)
r.Contains(profiles, "bar")
}
21 changes: 15 additions & 6 deletions internal/commands/profile/profiles/rename.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ func NewCmdRename(ctx *cmd.Context) *cmd.Command {
return err
}
opts.Profiles = l
opts.DryRun = ctx.IsDryRun()
return renameRun(opts)
},
}
Expand All @@ -76,6 +77,7 @@ type RenameOpts struct {
Profiles *profile.Loader
ExistingName string
NewName string
DryRun bool
}

func renameRun(opts *RenameOpts) error {
Expand Down Expand Up @@ -108,8 +110,21 @@ func renameRun(opts *RenameOpts) error {
return fmt.Errorf("a profile with name %q already exists", opts.NewName)
}

active, err := opts.Profiles.GetActiveProfile()
if err != nil {
return fmt.Errorf("failed to get active profile: %w", err)
}

// Update the name and save.
existing.Name = opts.NewName
if opts.DryRun {
cs := opts.IO.ColorScheme()
fmt.Fprintf(opts.IO.Err(), "%s would rename profile %q to %q\n", cs.DryRunLabel(), opts.ExistingName, opts.NewName)
if active.Name == opts.ExistingName {
fmt.Fprintf(opts.IO.Err(), "%s would activate profile %q\n", cs.DryRunLabel(), opts.NewName)
}
return nil
}
if err := existing.Write(); err != nil {
return fmt.Errorf("error saving renamed profile: %w", err)
}
Expand All @@ -122,12 +137,6 @@ func renameRun(opts *RenameOpts) error {
return fmt.Errorf("failed to delete old profile: %w", err)
}

// Get the active profile
active, err := opts.Profiles.GetActiveProfile()
if err != nil {
return fmt.Errorf("failed to get active profile: %w", err)
}

// If the active profile was the profile that we just renamed, update to the
// new name.
if active.Name == opts.ExistingName {
Expand Down
27 changes: 27 additions & 0 deletions internal/commands/profile/profiles/rename_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,30 @@ func TestRename(t *testing.T) {
})
}
}

func TestRenameDryRun(t *testing.T) {
t.Parallel()
r := require.New(t)
l := profile.TestLoader(t)
io := iostreams.Test()

for _, name := range []string{"foo", "bar"} {
p, err := l.NewProfile(name)
r.NoError(err)
r.NoError(p.Write())
}
active, err := l.GetActiveProfile()
r.NoError(err)
active.Name = "bar"
r.NoError(active.Write())

opts := &RenameOpts{IO: io, Profiles: l, ExistingName: "bar", NewName: "baz", DryRun: true}
r.NoError(renameRun(opts))
r.Contains(io.Error.String(), `would rename profile "bar" to "baz"`)
r.Contains(io.Error.String(), `would activate profile "baz"`)

profiles, err := l.ListProfiles()
r.NoError(err)
r.Contains(profiles, "bar")
r.NotContains(profiles, "baz")
}
2 changes: 1 addition & 1 deletion internal/commands/profile/property_docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ func addCoreProperties(b *availablePropertiesBuilder) {
b.AddProperty("", "quiet", "If True, prompts will be disabled and output will be minimized.")
b.AddProperty("", "verbosity", `
Default logging verbosity for {{ template "mdCodeOrBold" "tfcloud" }} commands. This is the
equivalent of using the global {{ template "mdCodeOrBold" "--verbose" }} flag. Supported log levels:
equivalent of using the global {{ template "mdCodeOrBold" "--debug" }} flag. Supported log levels:
{{ template "mdCodeOrBold" "trace" }}, {{ template "mdCodeOrBold" "debug" }},
{{ template "mdCodeOrBold" "info" }}, {{ template "mdCodeOrBold" "warn" }}, and
{{ template "mdCodeOrBold" "error" }}.`)
Expand Down
Loading
Loading