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
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,14 @@ Perform an API request. See `tfcloud api --help` for usage and examples.
**`tfcloud variable import [tfvars-file] [flags]`**

Import variables from a tfvars file or the process environment into the current workspace or a variable set. See `tfcloud variable import --help` for usage and examples.

#### Exit Codes

| Exit | Meaning | Solution |
|------|----------------------------------|-----------------------------|
| 0 | OK | — |
| 1 | Usage error | Read `tfcloud <cmd> --help` |
| 2 | Not Found or Authorization Error | Verify URL/ID |
| 3 | Authentication Error | `tfcloud auth login` |
| 4 | Network error | Check connectivity |
| 5 | API Server Error Persists | Try again later |
12 changes: 1 addition & 11 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,10 @@ go 1.25.5
require (
github.com/MakeNowJust/heredoc/v2 v2.0.1
github.com/dustin/go-humanize v1.0.1
github.com/go-openapi/runtime v0.29.3
github.com/hashicorp/cli v1.1.7
github.com/hashicorp/go-hclog v1.6.3
github.com/hashicorp/go-multierror v1.1.1
github.com/hashicorp/go-tfe v1.78.1-0.20260401171829-7a49f0cf5cb4
github.com/hashicorp/go-tfe v1.78.1-0.20260414223040-554d4064682a
github.com/hashicorp/go-version v1.9.0
github.com/hashicorp/hcl/v2 v2.24.0
github.com/lithammer/dedent v1.1.0
Expand Down Expand Up @@ -43,14 +42,6 @@ require (
github.com/fatih/color v1.18.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/errors v0.22.7 // indirect
github.com/go-openapi/strfmt v0.26.0 // indirect
github.com/go-openapi/swag/conv v0.25.5 // indirect
github.com/go-openapi/swag/fileutils v0.25.5 // indirect
github.com/go-openapi/swag/jsonutils v0.25.5 // indirect
github.com/go-openapi/swag/stringutils v0.25.5 // indirect
github.com/go-openapi/swag/typeutils v0.25.5 // indirect
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
Expand All @@ -67,7 +58,6 @@ require (
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/oklog/ulid/v2 v2.1.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
Expand Down
53 changes: 2 additions & 51 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -36,52 +36,8 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-openapi/analysis v0.24.3 h1:a1hrvMr8X0Xt69KP5uVTu5jH62DscmDifrLzNglAayk=
github.com/go-openapi/analysis v0.24.3/go.mod h1:Nc+dWJ/FxZbhSow5Yh3ozg5CLJioB+XXT6MdLvJUsUw=
github.com/go-openapi/errors v0.22.7 h1:JLFBGC0Apwdzw3484MmBqspjPbwa2SHvpDm0u5aGhUA=
github.com/go-openapi/errors v0.22.7/go.mod h1://QW6SD9OsWtH6gHllUCddOXDL0tk0ZGNYHwsw4sW3w=
github.com/go-openapi/jsonpointer v0.22.5 h1:8on/0Yp4uTb9f4XvTrM2+1CPrV05QPZXu+rvu2o9jcA=
github.com/go-openapi/jsonpointer v0.22.5/go.mod h1:gyUR3sCvGSWchA2sUBJGluYMbe1zazrYWIkWPjjMUY0=
github.com/go-openapi/jsonreference v0.21.5 h1:6uCGVXU/aNF13AQNggxfysJ+5ZcU4nEAe+pJyVWRdiE=
github.com/go-openapi/jsonreference v0.21.5/go.mod h1:u25Bw85sX4E2jzFodh1FOKMTZLcfifd1Q+iKKOUxExw=
github.com/go-openapi/loads v0.23.3 h1:g5Xap1JfwKkUnZdn+S0L3SzBDpcTIYzZ5Qaag0YDkKQ=
github.com/go-openapi/loads v0.23.3/go.mod h1:NOH07zLajXo8y55hom0omlHWDVVvCwBM/S+csCK8LqA=
github.com/go-openapi/runtime v0.29.3 h1:h5twGaEqxtQg40ePiYm9vFFH1q06Czd7Ot6ufdK0w/Y=
github.com/go-openapi/runtime v0.29.3/go.mod h1:8A1W0/L5eyNJvKciqZtvIVQvYO66NlB7INMSZ9bw/oI=
github.com/go-openapi/spec v0.22.4 h1:4pxGjipMKu0FzFiu/DPwN3CTBRlVM2yLf/YTWorYfDQ=
github.com/go-openapi/spec v0.22.4/go.mod h1:WQ6Ai0VPWMZgMT4XySjlRIE6GP1bGQOtEThn3gcWLtQ=
github.com/go-openapi/strfmt v0.26.0 h1:SDdQLyOEqu8W96rO1FRG1fuCtVyzmukky0zcD6gMGLU=
github.com/go-openapi/strfmt v0.26.0/go.mod h1:Zslk5VZPOISLwmWTMBIS7oiVFem1o1EI6zULY8Uer7Y=
github.com/go-openapi/swag/conv v0.25.5 h1:wAXBYEXJjoKwE5+vc9YHhpQOFj2JYBMF2DUi+tGu97g=
github.com/go-openapi/swag/conv v0.25.5/go.mod h1:CuJ1eWvh1c4ORKx7unQnFGyvBbNlRKbnRyAvDvzWA4k=
github.com/go-openapi/swag/fileutils v0.25.5 h1:B6JTdOcs2c0dBIs9HnkyTW+5gC+8NIhVBUwERkFhMWk=
github.com/go-openapi/swag/fileutils v0.25.5/go.mod h1:V3cT9UdMQIaH4WiTrUc9EPtVA4txS0TOmRURmhGF4kc=
github.com/go-openapi/swag/jsonname v0.25.5 h1:8p150i44rv/Drip4vWI3kGi9+4W9TdI3US3uUYSFhSo=
github.com/go-openapi/swag/jsonname v0.25.5/go.mod h1:jNqqikyiAK56uS7n8sLkdaNY/uq6+D2m2LANat09pKU=
github.com/go-openapi/swag/jsonutils v0.25.5 h1:XUZF8awQr75MXeC+/iaw5usY/iM7nXPDwdG3Jbl9vYo=
github.com/go-openapi/swag/jsonutils v0.25.5/go.mod h1:48FXUaz8YsDAA9s5AnaUvAmry1UcLcNVWUjY42XkrN4=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.5 h1:SX6sE4FrGb4sEnnxbFL/25yZBb5Hcg1inLeErd86Y1U=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.5/go.mod h1:/2KvOTrKWjVA5Xli3DZWdMCZDzz3uV/T7bXwrKWPquo=
github.com/go-openapi/swag/loading v0.25.5 h1:odQ/umlIZ1ZVRteI6ckSrvP6e2w9UTF5qgNdemJHjuU=
github.com/go-openapi/swag/loading v0.25.5/go.mod h1:I8A8RaaQ4DApxhPSWLNYWh9NvmX2YKMoB9nwvv6oW6g=
github.com/go-openapi/swag/mangling v0.25.5 h1:hyrnvbQRS7vKePQPHHDso+k6CGn5ZBs5232UqWZmJZw=
github.com/go-openapi/swag/mangling v0.25.5/go.mod h1:6hadXM/o312N/h98RwByLg088U61TPGiltQn71Iw0NY=
github.com/go-openapi/swag/stringutils v0.25.5 h1:NVkoDOA8YBgtAR/zvCx5rhJKtZF3IzXcDdwOsYzrB6M=
github.com/go-openapi/swag/stringutils v0.25.5/go.mod h1:PKK8EZdu4QJq8iezt17HM8RXnLAzY7gW0O1KKarrZII=
github.com/go-openapi/swag/typeutils v0.25.5 h1:EFJ+PCga2HfHGdo8s8VJXEVbeXRCYwzzr9u4rJk7L7E=
github.com/go-openapi/swag/typeutils v0.25.5/go.mod h1:itmFmScAYE1bSD8C4rS0W+0InZUBrB2xSPbWt6DLGuc=
github.com/go-openapi/swag/yamlutils v0.25.5 h1:kASCIS+oIeoc55j28T4o8KwlV2S4ZLPT6G0iq2SSbVQ=
github.com/go-openapi/swag/yamlutils v0.25.5/go.mod h1:Gek1/SjjfbYvM+Iq4QGwa/2lEXde9n2j4a3wI3pNuOQ=
github.com/go-openapi/testify/enable/yaml/v2 v2.4.1 h1:NZOrZmIb6PTv5LTFxr5/mKV/FjbUzGE7E6gLz7vFoOQ=
github.com/go-openapi/testify/enable/yaml/v2 v2.4.1/go.mod h1:r7dwsujEHawapMsxA69i+XMGZrQ5tRauhLAjV/sxg3Q=
github.com/go-openapi/testify/v2 v2.4.1 h1:zB34HDKj4tHwyUQHrUkpV0Q0iXQ6dUCOQtIqn8hE6Iw=
github.com/go-openapi/testify/v2 v2.4.1/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
github.com/go-openapi/validate v0.25.2 h1:12NsfLAwGegqbGWr2CnvT65X/Q2USJipmJ9b7xDJZz0=
github.com/go-openapi/validate v0.25.2/go.mod h1:Pgl1LpPPGFnZ+ys4/hTlDiRYQdI1ocKypgE+8Q8BLfY=
github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
Expand All @@ -96,8 +52,8 @@ github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVH
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/go-tfe v1.78.1-0.20260401171829-7a49f0cf5cb4 h1:+jBVHPQheF/zwa5Yi2A7zby+2jl6zV+IiteiOwA36ls=
github.com/hashicorp/go-tfe v1.78.1-0.20260401171829-7a49f0cf5cb4/go.mod h1:NCc9n8HN05g6Bu5v0a3JhkOoWN42DF/Jk0nDQSaZwFI=
github.com/hashicorp/go-tfe v1.78.1-0.20260414223040-554d4064682a h1:K5t3L7CDC3gtbLI28Zwlb8DwBn0+ojUEKVBmzZhQ0ck=
github.com/hashicorp/go-tfe v1.78.1-0.20260414223040-554d4064682a/go.mod h1:NCc9n8HN05g6Bu5v0a3JhkOoWN42DF/Jk0nDQSaZwFI=
github.com/hashicorp/go-version v1.9.0 h1:CeOIz6k+LoN3qX9Z0tyQrPtiB1DFYRPfCIBtaXPSCnA=
github.com/hashicorp/go-version v1.9.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/hcl/v2 v2.24.0 h1:2QJdZ454DSsYGoaE6QheQZjtKZSUs9Nh2izTWiwQxvE=
Expand Down Expand Up @@ -152,9 +108,6 @@ github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s=
github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
Expand Down Expand Up @@ -191,8 +144,6 @@ go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWv
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f h1:XdNn9LlyWAhLVp6P/i8QYBW+hlyhrhei9uErw2B5GJo=
Expand Down
77 changes: 52 additions & 25 deletions internal/pkg/cmd/command_internal.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@ import (
"errors"
"fmt"
"io"
"net"
"net/http"
"os"
"slices"
"strconv"
"strings"

"github.com/go-openapi/runtime"
"github.com/hashicorp/cli"
"github.com/hashicorp/go-tfe"
"github.com/muesli/reflow/indent"
"github.com/muesli/reflow/wordwrap"
"github.com/posener/complete"
Expand All @@ -26,21 +27,57 @@ import (
"github.com/hashicorp/tfcloud/internal/pkg/ld"
)

func (c *Command) errorToExitCode(args []string, err error) int {
io := c.io
cs := io.ColorScheme()

exitCode := 1
var exitCodeErr *ExitCodeError
var netErr *net.OpError
var apiErr *tfe.APIError

if errors.Is(err, ErrDisplayHelp) {
return cli.RunResultHelp
} else if errors.Is(err, ErrDisplayUsage) {
fmt.Fprint(io.Err(), c.usageHelp())
return 1
} else if errors.Is(err, tfe.ErrNotFound) {
fmt.Fprintf(io.Err(), "%s %s\n\n", cs.ErrorLabel(), notFoundErrorHelp(io))
return 2
} else if errors.Is(err, tfe.ErrUnauthorized) {
fmt.Fprintf(io.Err(), "%s %s\n\n", cs.ErrorLabel(), authErrorHelp(io, c.commandPath(), args))
return 3
} else if errors.As(err, &netErr) {
fmt.Fprintf(io.Err(), "%s Network error: %s\n", cs.ErrorLabel(), netErr)
return 4
} else if errors.As(err, &apiErr) {
if apiErr.StatusCode >= http.StatusInternalServerError {
fmt.Fprintf(io.Err(), "%s Server error: %s\n", cs.ErrorLabel(), apiErr)
return 5
}
} else if errors.As(err, &exitCodeErr) {
exitCode = exitCodeErr.Code
}

fmt.Fprintf(io.Err(), "%s %s\n", cs.ErrorLabel(), wordWrap(err.Error(), 120))
return exitCode
}

// Run runs the given command.
func (c *Command) Run(args []string) int {
// Get the colorscheme
io := c.getIO()
cs := c.getIO().ColorScheme()

if c.RunF == nil {
if len(c.children) != 0 {
return cli.RunResultHelp
}

fmt.Println("Command has no run function or children. This is an invalid command")
fmt.Fprintln(io.Err(), "Command has no run function or children. This is an invalid command")
return 1
}

// Get the colorscheme
io := c.getIO()
cs := c.getIO().ColorScheme()

// Parse the flags
if err := c.parseFlags(args); err != nil {
fmt.Fprintf(io.Err(), "%s %s\n", cs.ErrorLabel(), err)
Expand Down Expand Up @@ -101,30 +138,20 @@ func (c *Command) Run(args []string) int {

// Run the command
if err := c.RunF(c, parsedArgs); err != nil {
exitCode := 1
var runtimeErr runtime.ClientResponseStatus
var exitCodeErr *ExitCodeError
if errors.Is(err, ErrDisplayHelp) {
return cli.RunResultHelp
} else if errors.Is(err, ErrDisplayUsage) {
fmt.Fprint(io.Err(), c.usageHelp())
return 1
} else if errors.As(err, &runtimeErr) && runtimeErr.IsCode(http.StatusUnauthorized) {
// TODO: This runtimeErr is inaccurate for HCPTF
// Request failed because of authentication issues.
fmt.Fprintf(io.Err(), "%s %s\n\n", cs.ErrorLabel(), authErrorHelp(io, c.commandPath(), args))
return 1
} else if errors.As(err, &exitCodeErr) {
exitCode = exitCodeErr.Code
}

fmt.Fprintf(io.Err(), "%s %s\n", cs.ErrorLabel(), wordWrap(err.Error(), 120))
return exitCode
return c.errorToExitCode(args, err)
}

return 0
}

func notFoundErrorHelp(io iostreams.IOStreams) string {
return heredoc.New(io, heredoc.WithPreserveNewlines(), heredoc.WithWidth(0)).Must(`
Resource not found or you are unauthorized to this action. Check your account permissions.

{{ Bold "$ tfcloud auth info" }}
`)
}

// authErrorHelp returns a help message for recovering from authentication errors.
func authErrorHelp(io iostreams.IOStreams, commandPath string, args []string) string {
// Build the original command
Expand Down
44 changes: 44 additions & 0 deletions internal/pkg/cmd/command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,15 @@ package cmd

import (
"fmt"
"net"
"testing"

"github.com/spf13/pflag"
"github.com/stretchr/testify/require"

"github.com/hashicorp/cli"
"github.com/hashicorp/go-tfe"

"github.com/hashicorp/tfcloud/internal/pkg/iostreams"
)

Expand Down Expand Up @@ -132,3 +136,43 @@ func TestCommand_ExitCode(t *testing.T) {
r.Equal(code, root.Run([]string{}))
r.Contains(io.Error.String(), err.Error())
}

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

opErr := &net.OpError{Err: fmt.Errorf("some network error")}

tests := []struct {
err error
expected int
errContains string
}{
{err: ErrDisplayHelp, expected: cli.RunResultHelp},
{err: ErrDisplayUsage, expected: 1},
{err: tfe.ErrNotFound, expected: 2, errContains: "Resource not found or you are unauthorized to this action"},
{err: tfe.ErrUnauthorized, expected: 3, errContains: "tfcloud auth login"},
{err: opErr, expected: 4, errContains: "network error"},
{err: tfe.ErrInternalServer, expected: 5, errContains: "Internal Server Error"},
{err: fmt.Errorf("some other error"), expected: 1, errContains: "ERROR: some other error"},
}

for _, tt := range tests {
t.Run(fmt.Sprintf("err %T exits with code %d", tt.err, tt.expected), func(t *testing.T) {
r := require.New(t)

// Create the command tree
io := iostreams.Test()
root := &Command{
Name: "root",
io: io,
RunF: func(c *Command, args []string) error {
return tt.err
},
}
r.Equal(tt.expected, root.Run([]string{}))
if tt.errContains != "" {
r.Contains(io.Error.String(), tt.errContains)
}
})
}
}
Loading