diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 8534f71a7..00ed21d17 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,4 +4,8 @@ updates: directory: "/" schedule: interval: daily - open-pull-requests-limit: 10 \ No newline at end of file + open-pull-requests-limit: 10 + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: weekly diff --git a/.github/workflows/gosec_pr.yml b/.github/workflows/gosec_pr.yml index 71896c31f..64e63dff0 100644 --- a/.github/workflows/gosec_pr.yml +++ b/.github/workflows/gosec_pr.yml @@ -9,7 +9,7 @@ jobs: GO111MODULE: on steps: - name: Checkout Source - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Run Gosec Security Scanner on root directory uses: securego/gosec@master diff --git a/.github/workflows/integration_tests_pr.yml b/.github/workflows/integration_tests_pr.yml index c4c076b3d..b1c991ffb 100644 --- a/.github/workflows/integration_tests_pr.yml +++ b/.github/workflows/integration_tests_pr.yml @@ -21,7 +21,7 @@ jobs: steps: - uses: actions/setup-go@v4 with: - go-version: '1.19' + go-version: 'stable' - run: go version - uses: actions-ecosystem/action-regex-match@v2 id: disallowed-char-check @@ -32,7 +32,7 @@ jobs: # Check out merge commit - name: Checkout PR - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: ref: ${{ inputs.sha }} diff --git a/.github/workflows/label-sync.yml b/.github/workflows/label-sync.yml index a52d11482..bf8a624ac 100644 --- a/.github/workflows/label-sync.yml +++ b/.github/workflows/label-sync.yml @@ -9,7 +9,7 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: micnncim/action-label-syncer@3abd5ab72fda571e69fffd97bd4e0033dd5f495c # pin@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/nightly-smoke-tests.yml b/.github/workflows/nightly-smoke-tests.yml index adaa2b642..990af90c8 100644 --- a/.github/workflows/nightly-smoke-tests.yml +++ b/.github/workflows/nightly-smoke-tests.yml @@ -11,7 +11,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: ref: main diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index 0dbf2f626..ab7e530b6 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -9,7 +9,7 @@ jobs: update_release_draft: runs-on: ubuntu-latest steps: - - uses: release-drafter/release-drafter@569eb7ee3a85817ab916c8f8ff03a5bd96c9c83e # pin@v5.23.0 + - uses: release-drafter/release-drafter@65c5fb495d1e69aa8c08a3317bc44ff8aabe9772 # pin@v5.24.0 with: config-name: release-drafter.yml env: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0bdfc99f0..2ed589ca5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,7 +11,7 @@ jobs: test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/setup-go@v4 with: go-version: 'stable' @@ -59,6 +59,7 @@ jobs: SKIP_LINT: 1 - name: Convert JSON Report to XML + if: github.ref == 'refs/heads/main' && github.event_name == 'push' run: | filename=$(ls | grep -E '^[0-9]{12}_linodego_test_report\.json') @@ -76,6 +77,7 @@ jobs: GO111MODULE: on - name: Add additional information to XML report + if: github.ref == 'refs/heads/main' && github.event_name == 'push' run: | filename=$(ls | grep -E '^[0-9]{12}_linodego_test_report\.xml$') python scripts/add_to_xml_test_report.py \ @@ -85,6 +87,7 @@ jobs: --xmlfile "${filename}" - name: Upload test results to bucket + if: github.ref == 'refs/heads/main' && github.event_name == 'push' env: LINODE_CLI_OBJ_ACCESS_KEY: ${{ secrets.LINODE_CLI_OBJ_ACCESS_KEY }} LINODE_CLI_OBJ_SECRET_KEY: ${{ secrets.LINODE_CLI_OBJ_SECRET_KEY }} diff --git a/.gitignore b/.gitignore index dcb4cc633..cec47a064 100644 --- a/.gitignore +++ b/.gitignore @@ -16,8 +16,8 @@ # Common IDE paths .vscode/ +.idea/ vendor/**/ .env coverage.txt - diff --git a/account_invoices.go b/account_invoices.go index 26529aaa7..d068662fa 100644 --- a/account_invoices.go +++ b/account_invoices.go @@ -18,13 +18,15 @@ type Invoice struct { Date *time.Time `json:"-"` } -// InvoiceItem structs reflect an single billable activity associate with an Invoice +// InvoiceItem structs reflect a single billable activity associate with an Invoice type InvoiceItem struct { Label string `json:"label"` Type string `json:"type"` UnitPrice int `json:"unitprice"` Quantity int `json:"quantity"` Amount float32 `json:"amount"` + Tax float32 `json:"tax"` + Region *string `json:"region"` From *time.Time `json:"-"` To *time.Time `json:"-"` } @@ -103,7 +105,7 @@ func (i *InvoiceItem) UnmarshalJSON(b []byte) error { return nil } -// GetInvoice gets the a single Invoice matching the provided ID +// GetInvoice gets a single Invoice matching the provided ID func (c *Client) GetInvoice(ctx context.Context, invoiceID int) (*Invoice, error) { req := c.R(ctx).SetResult(&Invoice{}) e := fmt.Sprintf("account/invoices/%d", invoiceID) diff --git a/account_transfer.go b/account_transfer.go new file mode 100644 index 000000000..115573021 --- /dev/null +++ b/account_transfer.go @@ -0,0 +1,33 @@ +package linodego + +import "context" + +// AccountTransfer represents an Account's network utilization for the current month. +type AccountTransfer struct { + Billable int `json:"billable"` + Quota int `json:"quota"` + Used int `json:"used"` + + RegionTransfers []AccountTransferRegion `json:"region_transfers"` +} + +// AccountTransferRegion represents an Account's network utilization for the current month +// in a given region. +type AccountTransferRegion struct { + ID string `json:"id"` + Billable int `json:"billable"` + Quota int `json:"quota"` + Used int `json:"used"` +} + +// GetAccountTransfer gets current Account's network utilization for the current month. +func (c *Client) GetAccountTransfer(ctx context.Context) (*AccountTransfer, error) { + req := c.R(ctx).SetResult(&AccountTransfer{}) + e := "account/transfer" + r, err := coupleAPIErrors(req.Get(e)) + if err != nil { + return nil, err + } + + return r.Result().(*AccountTransfer), nil +} diff --git a/client.go b/client.go index 5262c8485..a2d676f5e 100644 --- a/client.go +++ b/client.go @@ -3,7 +3,6 @@ package linodego import ( "context" "fmt" - "io/ioutil" "log" "net/http" "net/url" @@ -82,7 +81,10 @@ type clientCacheEntry struct { ExpiryOverride *time.Duration } -type Request = resty.Request +type ( + Request = resty.Request + Logger = resty.Logger +) func init() { // Wether or not we will enable Resty debugging output @@ -121,6 +123,14 @@ func (c *Client) SetDebug(debug bool) *Client { return c } +// SetLogger allows the user to override the output +// logger for debug logs. +func (c *Client) SetLogger(logger Logger) *Client { + c.resty.SetLogger(logger) + + return c +} + // OnBeforeRequest adds a handler to the request body to run before the request is sent func (c *Client) OnBeforeRequest(m func(request *Request) error) { c.resty.OnBeforeRequest(func(client *resty.Client, req *resty.Request) error { @@ -166,7 +176,7 @@ func (c *Client) updateHostURL() { apiProto = c.apiProto } - c.resty.SetHostURL( + c.resty.SetBaseURL( fmt.Sprintf( "%s://%s/%s", apiProto, @@ -183,7 +193,7 @@ func (c *Client) SetRootCertificate(path string) *Client { } // SetToken sets the API token for all requests from this client -// Only necessary if you haven't already provided an http client to NewClient() configured with the token. +// Only necessary if you haven't already provided the http client to NewClient() configured with the token. func (c *Client) SetToken(token string) *Client { c.resty.SetHeader("Authorization", fmt.Sprintf("Bearer %s", token)) return c @@ -398,7 +408,7 @@ func NewClient(hc *http.Client) (client Client) { certPath, certPathExists := os.LookupEnv(APIHostCert) if certPathExists { - cert, err := ioutil.ReadFile(filepath.Clean(certPath)) + cert, err := os.ReadFile(filepath.Clean(certPath)) if err != nil { log.Fatalf("[ERROR] Error when reading cert at %s: %s\n", certPath, err.Error()) } diff --git a/client_test.go b/client_test.go index f969d9178..3e0c35906 100644 --- a/client_test.go +++ b/client_test.go @@ -24,39 +24,39 @@ func TestClient_SetAPIVersion(t *testing.T) { client := NewClient(nil) - if client.resty.HostURL != defaultURL { - t.Fatal(cmp.Diff(client.resty.HostURL, defaultURL)) + if client.resty.BaseURL != defaultURL { + t.Fatal(cmp.Diff(client.resty.BaseURL, defaultURL)) } client.SetBaseURL(baseURL) client.SetAPIVersion(apiVersion) - if client.resty.HostURL != expectedHost { - t.Fatal(cmp.Diff(client.resty.HostURL, expectedHost)) + if client.resty.BaseURL != expectedHost { + t.Fatal(cmp.Diff(client.resty.BaseURL, expectedHost)) } // Ensure setting twice does not cause conflicts client.SetBaseURL(updatedBaseURL) client.SetAPIVersion(updatedAPIVersion) - if client.resty.HostURL != updatedExpectedHost { - t.Fatal(cmp.Diff(client.resty.HostURL, updatedExpectedHost)) + if client.resty.BaseURL != updatedExpectedHost { + t.Fatal(cmp.Diff(client.resty.BaseURL, updatedExpectedHost)) } // Revert client.SetBaseURL(baseURL) client.SetAPIVersion(apiVersion) - if client.resty.HostURL != expectedHost { - t.Fatal(cmp.Diff(client.resty.HostURL, expectedHost)) + if client.resty.BaseURL != expectedHost { + t.Fatal(cmp.Diff(client.resty.BaseURL, expectedHost)) } // Custom protocol client.SetBaseURL(protocolBaseURL) client.SetAPIVersion(protocolAPIVersion) - if client.resty.HostURL != protocolExpectedHost { - t.Fatal(cmp.Diff(client.resty.HostURL, expectedHost)) + if client.resty.BaseURL != protocolExpectedHost { + t.Fatal(cmp.Diff(client.resty.BaseURL, expectedHost)) } } diff --git a/config.go b/config.go index 0d0b2f507..6bccf83f7 100644 --- a/config.go +++ b/config.go @@ -29,7 +29,7 @@ type LoadConfigOptions struct { SkipLoadProfile bool } -// LoadConfig loads a Linode config according to the options argument. +// LoadConfig loads a Linode config according to the option's argument. // If no options are specified, the following defaults will be used: // Path: ~/.config/linode // Profile: default diff --git a/config_test.go b/config_test.go index 2ec9d8d97..b4b3db418 100644 --- a/config_test.go +++ b/config_test.go @@ -2,7 +2,6 @@ package linodego import ( "fmt" - "io/ioutil" "os" "testing" ) @@ -43,8 +42,8 @@ func TestConfig_LoadWithDefaults(t *testing.T) { expectedURL := "https://api.cool.linode.com/v4beta" - if client.resty.HostURL != expectedURL { - t.Fatalf("mismatched host url: %s != %s", client.resty.HostURL, expectedURL) + if client.resty.BaseURL != expectedURL { + t.Fatalf("mismatched host url: %s != %s", client.resty.BaseURL, expectedURL) } if client.resty.Header.Get("Authorization") != "Bearer "+p.APIToken { @@ -89,8 +88,8 @@ func TestConfig_OverrideDefaults(t *testing.T) { expectedURL := "https://api.cool.linode.com/v4" - if client.resty.HostURL != expectedURL { - t.Fatalf("mismatched host url: %s != %s", client.resty.HostURL, expectedURL) + if client.resty.BaseURL != expectedURL { + t.Fatalf("mismatched host url: %s != %s", client.resty.BaseURL, expectedURL) } if client.resty.Header.Get("Authorization") != "Bearer "+p.APIToken { @@ -131,7 +130,7 @@ func TestConfig_NoDefaults(t *testing.T) { } func createTestConfig(t *testing.T, conf string) *os.File { - file, err := ioutil.TempFile("", "linode") + file, err := os.CreateTemp("", "linode") if err != nil { t.Fatal(err) } diff --git a/errors.go b/errors.go index d7e7ae291..702959049 100644 --- a/errors.go +++ b/errors.go @@ -2,20 +2,21 @@ package linodego import ( "fmt" - "log" "net/http" + "reflect" "strings" "github.com/go-resty/resty/v2" ) const ( + ErrorUnsupported = iota // ErrorFromString is the Code identifying Errors created by string types - ErrorFromString = 1 + ErrorFromString // ErrorFromError is the Code identifying Errors created by error types - ErrorFromError = 2 + ErrorFromError // ErrorFromStringer is the Code identifying Errors created by fmt.Stringer types - ErrorFromStringer = 3 + ErrorFromStringer ) // Error wraps the LinodeGo error with the relevant http.Response @@ -113,7 +114,7 @@ func NewError(err any) *Error { apiError, ok := e.Error().(*APIError) if !ok { - log.Fatalln("Unexpected Resty Error Response") + return &Error{Code: ErrorUnsupported, Message: "Unexpected Resty Error Response, no error"} } return &Error{ @@ -128,7 +129,6 @@ func NewError(err any) *Error { case fmt.Stringer: return &Error{Code: ErrorFromStringer, Message: e.String()} default: - log.Fatalln("Unsupported type to linodego.NewError") - panic(err) + return &Error{Code: ErrorUnsupported, Message: fmt.Sprintf("Unsupported type to linodego.NewError: %s", reflect.TypeOf(e))} } } diff --git a/errors_test.go b/errors_test.go index ee379365d..15510ec72 100644 --- a/errors_test.go +++ b/errors_test.go @@ -4,7 +4,7 @@ import ( "bytes" "context" "errors" - "io/ioutil" + "io" "net/http" "net/http/httptest" "testing" @@ -13,6 +13,58 @@ import ( "github.com/google/go-cmp/cmp" ) +type tstringer string + +func (t tstringer) String() string { + return string(t) +} + +func TestNewError(t *testing.T) { + if NewError(nil) != nil { + t.Errorf("nil error should return nil") + } + if NewError(struct{}{}).Code != ErrorUnsupported { + t.Error("empty struct should return unsupported error type") + } + err := errors.New("test") + newErr := NewError(&err) + if newErr.Message == err.Error() && newErr.Code == ErrorFromError { + t.Error("nil error should return nil") + } + + if err := NewError(&resty.Response{Request: &resty.Request{}}); err.Message != "Unexpected Resty Error Response, no error" { + t.Error("Unexpected Resty Error Response, no error") + } + + rerr := &resty.Response{ + RawResponse: &http.Response{ + StatusCode: 500, + }, + Request: &resty.Request{ + Error: &APIError{ + []APIErrorReason{ + { + Reason: "testreason", + Field: "testfield", + }, + }, + }, + }, + } + + if err := NewError(rerr); err.Message != "[testfield] testreason" { + t.Error("rest response error should should be set") + } + + if err := NewError("stringerror"); err.Message != "stringerror" || err.Code != ErrorFromString { + t.Errorf("string error should be set") + } + + if err := NewError(tstringer("teststringer")); err.Message != "teststringer" || err.Code != ErrorFromStringer { + t.Errorf("stringer error should be set") + } +} + func createTestServer(method, route, contentType, body string, statusCode int) (*httptest.Server, *Client) { h := http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { if r.Method == method && r.URL.Path == route { @@ -62,7 +114,7 @@ func TestCoupleAPIErrors_badGatewayError(t *testing.T) {