From 6bfcfdceda4b58a51f90d32f55d525b22cf0094c Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Mon, 14 Oct 2024 22:42:39 +0000
Subject: [PATCH 1/4] chore: Update dependency cspell to v8.15.2 (#572)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
This PR contains the following updates:
| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [cspell](https://cspell.org/)
([source](https://redirect.github.com/streetsidesoftware/cspell/tree/HEAD/packages/cspell))
| [`8.15.1` ->
`8.15.2`](https://renovatebot.com/diffs/npm/cspell/8.15.1/8.15.2) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/cspell/8.15.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/cspell/8.15.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/cspell/8.15.1/8.15.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/cspell/8.15.1/8.15.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
---
### Release Notes
streetsidesoftware/cspell (cspell)
###
[`v8.15.2`](https://redirect.github.com/streetsidesoftware/cspell/blob/HEAD/packages/cspell/CHANGELOG.md#small8152-2024-10-14-small)
[Compare
Source](https://redirect.github.com/streetsidesoftware/cspell/compare/v8.15.1...v8.15.2)
- chore: Update Integration Test Performance Data
([#6361](https://redirect.github.com/streetsidesoftware/cspell/issues/6361))
([d639368](https://redirect.github.com/streetsidesoftware/cspell/commit/d639368)),
closes
[#6361](https://redirect.github.com/streetsidesoftware/cspell/issues/6361)
---
### Configuration
📅 **Schedule**: Branch creation - "after 10pm every weekday,before 5am
every weekday,every weekend" (UTC), Automerge - At any time (no schedule
defined).
🚦 **Automerge**: Enabled.
â™» **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.
🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.
---
- [ ] If you want to rebase/retry this PR, check
this box
---
This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/nobl9/nobl9-go).
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
---
package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/package.json b/package.json
index b73d1800..7873aff6 100644
--- a/package.json
+++ b/package.json
@@ -3,7 +3,7 @@
"private": true,
"type": "module",
"devDependencies": {
- "cspell": "8.15.1",
+ "cspell": "8.15.2",
"markdownlint-cli": "0.41.0",
"yaml": "2.6.0"
},
From 07d9bba110136827fdcc2cf08902229b3dd9f223 Mon Sep 17 00:00:00 2001
From: Mateusz Hawrus <48822818+nieomylnieja@users.noreply.github.com>
Date: Tue, 15 Oct 2024 10:58:42 +0200
Subject: [PATCH 2/4] feat: Add standardized API errors (#567)
## Summary
Add application level errors to the current `APIError`. Due to the
naming conflict, rename `APIError` to more general `HTTPError` and name
the application level error `APIError`.
## Release Notes
`sdk.APIError` is now an application level error. It can be accessed
through `sdk.HTTPError.Errors` field.
## Breaking Changes
The former `sdk.APIError` has been renamed `sdk.HTTPError`. The
`sdk.HTTPError.Message` field has been removed. Run `gofmt -r
'sdk.APIError -> sdk.HTTPError'` to apply the change to your codebase.
---
sdk/api_error.tmpl | 1 -
sdk/client_errors.go | 80 -----------
sdk/client_errors_test.go | 127 -----------------
sdk/http_error.tmpl | 8 ++
sdk/http_errors.go | 139 +++++++++++++++++++
sdk/http_errors_test.go | 279 ++++++++++++++++++++++++++++++++++++++
6 files changed, 426 insertions(+), 208 deletions(-)
delete mode 100644 sdk/api_error.tmpl
delete mode 100644 sdk/client_errors.go
delete mode 100644 sdk/client_errors_test.go
create mode 100644 sdk/http_error.tmpl
create mode 100644 sdk/http_errors.go
create mode 100644 sdk/http_errors_test.go
diff --git a/sdk/api_error.tmpl b/sdk/api_error.tmpl
deleted file mode 100644
index 187b041e..00000000
--- a/sdk/api_error.tmpl
+++ /dev/null
@@ -1 +0,0 @@
-{{- if .CodeText }}{{ .CodeText }}: {{ end -}}{{ .Message }} (code: {{ .Code }}{{- if .URL }}, endpoint: {{ .Method }} {{ .URL }}{{- end }}{{- if .TraceID }}, traceId: {{ .TraceID }}{{- end }})
\ No newline at end of file
diff --git a/sdk/client_errors.go b/sdk/client_errors.go
deleted file mode 100644
index e849e61d..00000000
--- a/sdk/client_errors.go
+++ /dev/null
@@ -1,80 +0,0 @@
-package sdk
-
-import (
- "bytes"
- _ "embed"
- "fmt"
- "io"
- "net/http"
- "os"
- "text/template"
-)
-
-// APIError represents an HTTP error response from the API.
-type APIError struct {
- Message string `json:"message"`
- StatusCode int `json:"statusCode"`
- Method string `json:"method"`
- URL string `json:"url"`
- TraceID string `json:"traceId,omitempty"`
-}
-
-// IsRetryable returns true if the underlying API error can be retried.
-func (r APIError) IsRetryable() bool {
- return r.StatusCode >= 500
-}
-
-// Error returns a string representation of the error.
-func (r APIError) Error() string {
- buf := bytes.Buffer{}
- buf.Grow(len(apiErrorTemplateData))
- if err := apiErrorTemplate.Execute(&buf, apiErrorTemplateFields{
- Message: r.Message,
- Method: r.Method,
- URL: r.URL,
- TraceID: r.TraceID,
- CodeText: http.StatusText(r.StatusCode),
- Code: r.StatusCode,
- }); err != nil {
- _, _ = fmt.Fprintf(os.Stderr, "failed to execute APIError template: %v\n", err)
- }
- return buf.String()
-}
-
-// processHTTPResponse processes an HTTP response and returns an error if the response is erroneous.
-func processHTTPResponse(resp *http.Response) error {
- if resp.StatusCode < 300 {
- return nil
- }
- var body string
- if resp.Body != nil {
- rawBody, _ := io.ReadAll(resp.Body)
- body = string(bytes.TrimSpace(rawBody))
- }
- respErr := APIError{
- StatusCode: resp.StatusCode,
- TraceID: resp.Header.Get(HeaderTraceID),
- Message: body,
- }
- if resp.Request != nil {
- if resp.Request.URL != nil {
- respErr.URL = resp.Request.URL.String()
- }
- respErr.Method = resp.Request.Method
- }
- return &respErr
-}
-
-//go:embed api_error.tmpl
-var apiErrorTemplateData string
-
-var apiErrorTemplate = template.Must(template.New("api_error").Parse(apiErrorTemplateData))
-
-type apiErrorTemplateFields struct {
- Message string
- Method string
- URL string
- TraceID string
- CodeText string
- Code int
-}
diff --git a/sdk/client_errors_test.go b/sdk/client_errors_test.go
deleted file mode 100644
index baad6898..00000000
--- a/sdk/client_errors_test.go
+++ /dev/null
@@ -1,127 +0,0 @@
-package sdk
-
-import (
- "bytes"
- "fmt"
- "io"
- "net/http"
- "net/url"
- "testing"
-
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-)
-
-func TestAPIError(t *testing.T) {
- t.Parallel()
- t.Run("status code smaller than 300, no error", func(t *testing.T) {
- t.Parallel()
- for code := 200; code < 300; code++ {
- require.NoError(t, processHTTPResponse(&http.Response{StatusCode: code}))
- }
- })
- t.Run("errors", func(t *testing.T) {
- t.Parallel()
- for code := 300; code < 600; code++ {
- err := processHTTPResponse(&http.Response{
- StatusCode: code,
- Body: io.NopCloser(bytes.NewBufferString("error!")),
- Header: http.Header{HeaderTraceID: []string{"123"}},
- Request: &http.Request{
- Method: http.MethodGet,
- URL: &url.URL{
- Scheme: "https",
- Host: "app.nobl9.com",
- Path: "/api/slos",
- },
- },
- })
- require.Error(t, err)
- expectedMessage := fmt.Sprintf("error! (code: %d, endpoint: GET https://app.nobl9.com/api/slos, traceId: 123)", code)
- if textCode := http.StatusText(code); textCode != "" {
- expectedMessage = textCode + ": " + expectedMessage
- }
- require.EqualError(t, err, expectedMessage)
- }
- })
- t.Run("missing trace id", func(t *testing.T) {
- t.Parallel()
- err := processHTTPResponse(&http.Response{
- StatusCode: http.StatusBadRequest,
- Body: io.NopCloser(bytes.NewBufferString("error!")),
- Request: &http.Request{
- Method: http.MethodGet,
- URL: &url.URL{
- Scheme: "https",
- Host: "app.nobl9.com",
- Path: "/api/slos",
- },
- },
- })
- require.Error(t, err)
- expectedMessage := "Bad Request: error! (code: 400, endpoint: GET https://app.nobl9.com/api/slos)"
- require.EqualError(t, err, expectedMessage)
- })
- t.Run("missing status text", func(t *testing.T) {
- t.Parallel()
- err := processHTTPResponse(&http.Response{
- StatusCode: 555,
- Body: io.NopCloser(bytes.NewBufferString("error!")),
- Request: &http.Request{
- Method: http.MethodGet,
- URL: &url.URL{
- Scheme: "https",
- Host: "app.nobl9.com",
- Path: "/api/slos",
- },
- },
- })
- require.Error(t, err)
- expectedMessage := "error! (code: 555, endpoint: GET https://app.nobl9.com/api/slos)"
- require.EqualError(t, err, expectedMessage)
- })
- t.Run("missing url", func(t *testing.T) {
- t.Parallel()
- err := processHTTPResponse(&http.Response{
- StatusCode: 555,
- Body: io.NopCloser(bytes.NewBufferString("error!")),
- })
- require.Error(t, err)
- expectedMessage := "error! (code: 555)"
- require.EqualError(t, err, expectedMessage)
- })
-}
-
-func TestAPIError_IsRetryable(t *testing.T) {
- t.Parallel()
- tests := []*http.Response{
- {
- StatusCode: http.StatusInternalServerError,
- Body: io.NopCloser(bytes.NewBufferString("operation failed due to concurrency issue but can be retried")),
- Request: &http.Request{
- Method: http.MethodPut,
- URL: &url.URL{
- Scheme: "https",
- Host: "app.nobl9.com",
- Path: "/api/apply",
- },
- },
- },
- {
- StatusCode: http.StatusInternalServerError,
- Request: &http.Request{
- Method: http.MethodPut,
- URL: &url.URL{
- Scheme: "https",
- Host: "app.nobl9.com",
- Path: "/api/apply",
- },
- },
- },
- }
- for _, test := range tests {
- err := processHTTPResponse(test)
- require.Error(t, err)
- assert.True(t, err.(*APIError).IsRetryable())
- }
-}
diff --git a/sdk/http_error.tmpl b/sdk/http_error.tmpl
new file mode 100644
index 00000000..b950811c
--- /dev/null
+++ b/sdk/http_error.tmpl
@@ -0,0 +1,8 @@
+{{- if and (len .Errors | eq 1) (not (index .Errors 0).Source) }}
+ {{- if .CodeText }}{{ .CodeText }}: {{ end -}}{{ (index .Errors 0).Title }} (code: {{ .Code }}{{- if .URL }}, endpoint: {{ .Method }} {{ .URL }}{{- end }}{{- if .TraceID }}, traceId: {{ .TraceID }}{{- end }})
+{{- else }}
+ {{- if .CodeText }}{{ .CodeText }} {{ end -}} (code: {{ .Code }}{{- if .URL }}, endpoint: {{ .Method }} {{ .URL }}{{- end }}{{- if .TraceID }}, traceId: {{ .TraceID }}{{- end }})
+ {{- range .Errors }}
+ - {{ .Title }}{{- if .Source }} (source: '{{ .Source.PropertyName }}'{{- if .Source.PropertyValue }}, value: '{{ .Source.PropertyValue }}'{{- end }}){{- end }}
+ {{- end }}
+{{- end }}
diff --git a/sdk/http_errors.go b/sdk/http_errors.go
new file mode 100644
index 00000000..eb7d25c6
--- /dev/null
+++ b/sdk/http_errors.go
@@ -0,0 +1,139 @@
+package sdk
+
+import (
+ "bytes"
+ _ "embed"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "os"
+ "strings"
+ "text/template"
+
+ "github.com/pkg/errors"
+)
+
+// HTTPError represents an HTTP error response from the API.
+type HTTPError struct {
+ // StatusCode is the HTTP status code of the response.
+ // Example: 200, 400, 404, 500.
+ StatusCode int `json:"statusCode"`
+ // Method is the HTTP method used to make the request.
+ // Example: "GET", "POST", "PUT", "DELETE".
+ Method string `json:"method"`
+ // URL is the URL of the API endpoint that was called.
+ URL string `json:"url"`
+ // TraceID is an optional, unique identifier that can be used to trace the error in Nobl9 platform.
+ // Contact [Nobl9 support] if you need help debugging the issue based on the TraceID.
+ //
+ // [Nobl9 support]: https://nobl9.com/contact/support
+ TraceID string `json:"traceId,omitempty"`
+ // Errors is a list of errors returned by the API.
+ // At least one error is always guaranteed to be set.
+ // At the very minimum it will contain just the [APIError.Title].
+ Errors []APIError `json:"errors"`
+}
+
+// APIError defines a standardized format for error responses across all Nobl9 public services.
+// It ensures that errors are communicated in a consistent and structured manner,
+// making it easier for developers to handle and debug issues.
+type APIError struct {
+ // Title is a human-readable summary of the error. It is required.
+ Title string `json:"title"`
+ // Code is an application-specific error code. It is optional.
+ Code string `json:"code,omitempty"`
+ // Source provides additional context for the source of the error. It is optional.
+ Source *APIErrorSource `json:"source,omitempty"`
+}
+
+// APIErrorSource provides additional context for the source of the [APIError].
+type APIErrorSource struct {
+ // PropertyName is an optional name of the property that caused the error.
+ // It can be a JSON path or a simple property name.
+ PropertyName string `json:"propertyName,omitempty"`
+ // PropertyValue is an optional value of the property that caused the error.
+ PropertyValue string `json:"propertyValue,omitempty"`
+}
+
+// IsRetryable returns true if the underlying API error can be retried.
+func (r HTTPError) IsRetryable() bool {
+ return r.StatusCode >= 500
+}
+
+// Error returns a string representation of the error.
+func (r HTTPError) Error() string {
+ buf := bytes.Buffer{}
+ buf.Grow(len(httpErrorTemplateData))
+ if err := httpErrorTemplate.Execute(&buf, httpErrorTemplateFields{
+ Errors: r.Errors,
+ Method: r.Method,
+ URL: r.URL,
+ TraceID: r.TraceID,
+ CodeText: http.StatusText(r.StatusCode),
+ Code: r.StatusCode,
+ }); err != nil {
+ _, _ = fmt.Fprintf(os.Stderr, "failed to execute %T template: %v\n", r, err)
+ }
+ return buf.String()
+}
+
+// processHTTPResponse processes an HTTP response and returns an error if the response is erroneous.
+func processHTTPResponse(resp *http.Response) error {
+ if resp.StatusCode < 300 {
+ return nil
+ }
+ apiErrors, err := processAPIErrors(resp)
+ if err != nil {
+ return err
+ }
+ httpErr := HTTPError{
+ StatusCode: resp.StatusCode,
+ TraceID: resp.Header.Get(HeaderTraceID),
+ Errors: apiErrors,
+ }
+ if resp.Request != nil {
+ if resp.Request.URL != nil {
+ httpErr.URL = resp.Request.URL.String()
+ }
+ httpErr.Method = resp.Request.Method
+ }
+ return &httpErr
+}
+
+// processAPIErrors processes an HTTP response and returns a list of [APIError].
+// It checks for the 'content-type' header, if it's set to 'application/json'
+// it will decode the response body directly into a slice of [APIError].
+// Otherwise, a single [APIError] is created with the response body as the [APIError.Title].
+func processAPIErrors(resp *http.Response) ([]APIError, error) {
+ if resp.Body == nil {
+ return []APIError{{Title: "unknown error"}}, nil
+ }
+ if typ := resp.Header.Get("Content-Type"); typ != "" && strings.HasPrefix(typ, "application/json") {
+ dec := json.NewDecoder(resp.Body)
+ var apiErrors []APIError
+ if err := dec.Decode(&apiErrors); err != nil {
+ return nil, errors.Wrap(err, "failed to decode JSON response body")
+ }
+ return apiErrors, nil
+ }
+ rawBody, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return nil, errors.Wrap(err, "failed to read response body")
+ }
+ return []APIError{{Title: string(bytes.TrimSpace(rawBody))}}, nil
+}
+
+//go:embed http_error.tmpl
+var httpErrorTemplateData string
+
+var httpErrorTemplate = template.Must(template.New("").Parse(strings.TrimSpace(httpErrorTemplateData)))
+
+type httpErrorTemplateFields struct {
+ Errors []APIError
+ Method string
+ URL string
+ TraceID string
+ CodeText string
+ Code int
+}
diff --git a/sdk/http_errors_test.go b/sdk/http_errors_test.go
new file mode 100644
index 00000000..191d3d11
--- /dev/null
+++ b/sdk/http_errors_test.go
@@ -0,0 +1,279 @@
+package sdk
+
+import (
+ "bytes"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestHTTPError(t *testing.T) {
+ t.Parallel()
+ t.Run("status code smaller than 300, no error", func(t *testing.T) {
+ t.Parallel()
+ for code := 200; code < 300; code++ {
+ require.NoError(t, processHTTPResponse(&http.Response{StatusCode: code}))
+ }
+ })
+ t.Run("errors", func(t *testing.T) {
+ t.Parallel()
+ for code := 300; code < 600; code++ {
+ err := processHTTPResponse(&http.Response{
+ StatusCode: code,
+ Body: io.NopCloser(bytes.NewBufferString("error!")),
+ Header: http.Header{HeaderTraceID: []string{"123"}},
+ Request: &http.Request{
+ Method: http.MethodGet,
+ URL: &url.URL{
+ Scheme: "https",
+ Host: "app.nobl9.com",
+ Path: "/api/slos",
+ },
+ },
+ })
+ require.Error(t, err)
+ expectedError := &HTTPError{
+ StatusCode: code,
+ Method: "GET",
+ URL: "https://app.nobl9.com/api/slos",
+ TraceID: "123",
+ Errors: []APIError{{Title: "error!"}},
+ }
+ require.Equal(t, expectedError, err)
+ expectedMessage := fmt.Sprintf("error! (code: %d, endpoint: GET https://app.nobl9.com/api/slos, traceId: 123)", code)
+ if textCode := http.StatusText(code); textCode != "" {
+ expectedMessage = textCode + ": " + expectedMessage
+ }
+ require.EqualError(t, err, expectedMessage)
+ }
+ })
+ t.Run("missing trace id", func(t *testing.T) {
+ t.Parallel()
+ err := processHTTPResponse(&http.Response{
+ StatusCode: http.StatusBadRequest,
+ Body: io.NopCloser(bytes.NewBufferString("error!")),
+ Request: &http.Request{
+ Method: http.MethodGet,
+ URL: &url.URL{
+ Scheme: "https",
+ Host: "app.nobl9.com",
+ Path: "/api/slos",
+ },
+ },
+ })
+ require.Error(t, err)
+ expectedError := &HTTPError{
+ StatusCode: 400,
+ Method: "GET",
+ URL: "https://app.nobl9.com/api/slos",
+ Errors: []APIError{{Title: "error!"}},
+ }
+ require.Equal(t, expectedError, err)
+ expectedMessage := "Bad Request: error! (code: 400, endpoint: GET https://app.nobl9.com/api/slos)"
+ assert.EqualError(t, err, expectedMessage)
+ })
+ t.Run("missing status text", func(t *testing.T) {
+ t.Parallel()
+ err := processHTTPResponse(&http.Response{
+ StatusCode: 555,
+ Body: io.NopCloser(bytes.NewBufferString("error!")),
+ Request: &http.Request{
+ Method: http.MethodGet,
+ URL: &url.URL{
+ Scheme: "https",
+ Host: "app.nobl9.com",
+ Path: "/api/slos",
+ },
+ },
+ })
+ require.Error(t, err)
+ expectedError := &HTTPError{
+ StatusCode: 555,
+ Method: "GET",
+ URL: "https://app.nobl9.com/api/slos",
+ Errors: []APIError{{Title: "error!"}},
+ }
+ require.Equal(t, expectedError, err)
+ expectedMessage := "error! (code: 555, endpoint: GET https://app.nobl9.com/api/slos)"
+ assert.EqualError(t, err, expectedMessage)
+ })
+ t.Run("missing url", func(t *testing.T) {
+ t.Parallel()
+ err := processHTTPResponse(&http.Response{
+ StatusCode: 555,
+ Body: io.NopCloser(bytes.NewBufferString("error!")),
+ })
+ require.Error(t, err)
+ expectedError := &HTTPError{
+ StatusCode: 555,
+ Errors: []APIError{{Title: "error!"}},
+ }
+ require.Equal(t, expectedError, err)
+ expectedMessage := "error! (code: 555)"
+ assert.EqualError(t, err, expectedMessage)
+ })
+ t.Run("missing body", func(t *testing.T) {
+ t.Parallel()
+ err := processHTTPResponse(&http.Response{
+ StatusCode: http.StatusInternalServerError,
+ Body: nil,
+ Request: &http.Request{
+ Method: http.MethodGet,
+ URL: &url.URL{
+ Scheme: "https",
+ Host: "app.nobl9.com",
+ Path: "/api/slos",
+ },
+ },
+ })
+ require.Error(t, err)
+ expectedMessage := "Internal Server Error: unknown error (code: 500, endpoint: GET https://app.nobl9.com/api/slos)"
+ assert.EqualError(t, err, expectedMessage)
+ })
+ t.Run("failed to read body", func(t *testing.T) {
+ t.Parallel()
+ readerErr := errors.New("reader error")
+ err := processHTTPResponse(&http.Response{
+ StatusCode: http.StatusInternalServerError,
+ Body: &mockReadCloser{err: readerErr},
+ })
+ require.Error(t, err)
+ assert.ErrorIs(t, err, readerErr)
+ })
+ t.Run("read JSON API errors", func(t *testing.T) {
+ t.Parallel()
+ apiErrors := []APIError{
+ {
+ Title: "error1",
+ },
+ {
+ Title: "error2",
+ Code: "some_code",
+ },
+ {
+ Title: "error3",
+ Code: "other_code",
+ Source: &APIErrorSource{
+ PropertyName: "$.data",
+ },
+ },
+ {
+ Title: "error4",
+ Code: "yet_another_code",
+ Source: &APIErrorSource{
+ PropertyName: "$.data[1].name",
+ PropertyValue: "value",
+ },
+ },
+ }
+ data, err := json.Marshal(apiErrors)
+ require.NoError(t, err)
+
+ err = processHTTPResponse(&http.Response{
+ StatusCode: http.StatusBadRequest,
+ Header: http.Header{
+ "Content-Type": []string{"application/json"},
+ HeaderTraceID: []string{"123"},
+ },
+ Body: io.NopCloser(bytes.NewBuffer(data)),
+ Request: &http.Request{
+ Method: http.MethodGet,
+ URL: &url.URL{
+ Scheme: "https",
+ Host: "app.nobl9.com",
+ Path: "/api/slos",
+ },
+ },
+ })
+ require.Error(t, err)
+ expectedError := &HTTPError{
+ StatusCode: 400,
+ Method: "GET",
+ URL: "https://app.nobl9.com/api/slos",
+ TraceID: "123",
+ Errors: apiErrors,
+ }
+ assert.Equal(t, expectedError, err)
+ expectedMessage := `Bad Request (code: 400, endpoint: GET https://app.nobl9.com/api/slos, traceId: 123)
+ - error1
+ - error2
+ - error3 (source: '$.data')
+ - error4 (source: '$.data[1].name', value: 'value')`
+ assert.EqualError(t, err, expectedMessage)
+ })
+ t.Run("failed to read JSON", func(t *testing.T) {
+ t.Parallel()
+ err := processHTTPResponse(&http.Response{
+ StatusCode: http.StatusBadRequest,
+ Header: http.Header{"Content-Type": []string{"application/json"}},
+ Body: io.NopCloser(bytes.NewBufferString(`{"this:"that"}`)),
+ })
+ require.Error(t, err)
+ assert.Error(t, err, "failed to decode JSON response body")
+ })
+ t.Run("content type with charset", func(t *testing.T) {
+ t.Parallel()
+ apiErrors := []APIError{{Title: "error"}}
+ data, err := json.Marshal(apiErrors)
+ require.NoError(t, err)
+
+ err = processHTTPResponse(&http.Response{
+ StatusCode: http.StatusBadRequest,
+ Header: http.Header{"Content-Type": []string{"application/json; charset=utf-8"}},
+ Body: io.NopCloser(bytes.NewBuffer(data)),
+ })
+ require.Error(t, err)
+ expectedError := &HTTPError{
+ StatusCode: 400,
+ Errors: apiErrors,
+ }
+ assert.Equal(t, expectedError, err)
+ })
+}
+
+type mockReadCloser struct{ err error }
+
+func (mo *mockReadCloser) Read(p []byte) (n int, err error) { return 0, mo.err }
+
+func (mo *mockReadCloser) Close() error { return nil }
+
+func TestHTTPError_IsRetryable(t *testing.T) {
+ t.Parallel()
+ tests := []*http.Response{
+ {
+ StatusCode: http.StatusInternalServerError,
+ Body: io.NopCloser(bytes.NewBufferString("operation failed due to concurrency issue but can be retried")),
+ Request: &http.Request{
+ Method: http.MethodPut,
+ URL: &url.URL{
+ Scheme: "https",
+ Host: "app.nobl9.com",
+ Path: "/api/apply",
+ },
+ },
+ },
+ {
+ StatusCode: http.StatusInternalServerError,
+ Request: &http.Request{
+ Method: http.MethodPut,
+ URL: &url.URL{
+ Scheme: "https",
+ Host: "app.nobl9.com",
+ Path: "/api/apply",
+ },
+ },
+ },
+ }
+ for _, test := range tests {
+ err := processHTTPResponse(test)
+ require.Error(t, err)
+ assert.True(t, err.(*HTTPError).IsRetryable())
+ }
+}
From eb9041d592d84d068c8a2c4674d92046d953bf6a Mon Sep 17 00:00:00 2001
From: Mateusz Karasiewicz <100848248+mkaras-nobl9@users.noreply.github.com>
Date: Wed, 16 Oct 2024 13:18:17 +0200
Subject: [PATCH 3/4] feat: extend SLO status by most recent process status
[PC-14514] (#570)
## Motivation
Adds new SLO process statuses to the `status` field.
## Summary
before:
```
apiVersion: n9/v1alpha
kind: SLO
...
status:
timeTravel:
source: user
startTime: 2024-07-22T08:10:38Z
status: completed
triggeredBy: 00udqsfk2d3Oe8ljW4x6
unit: Day
value: 30.0
targetSlo:
timeTravelStatus:
startTime: 2024-07-21T08:10:38Z
status: completed
triggeredBy: 00u6axxaftHIMOizO4x7
unit: Day
value: 30.0
updatedAt: 2024-10-03T07:52:25Z
```
after:
```
apiVersion: n9/v1alpha
kind: SLO
...
status:
compositeSlo:
startTime: 2024-10-14T03:49:06Z
status: queued
triggeredBy: 00u6axxaftHIMOizO4x7
unit: Hour
value: 9.0
errorBudgetAdjustment:
startTime: 2024-10-14T01:57:42Z
status: queued
triggeredBy: 00u6axxaftHIMOizO4x7
unit: Hour
value: 10.0
replay:
startTime: 2024-10-13T11:20:32Z
status: queued
triggeredBy: 00u6axxaftHIMOizO4x7
unit: Day
value: 1.0
targetSlo:
replay:
startTime: 2024-07-21T08:10:38Z
status: completed
triggeredBy: 00u6axxaftHIMOizO4x7
unit: Day
value: 30.0
updatedAt: 2024-10-14T11:20:31Z
```
## Related changes
https://github.com/nobl9/n9/pull/15672
## Breaking change
SLO kind:
- The `status.timeTravel` field has been renamed to `status.replay`.
- The `status.timeTravel.source` field has been removed.
- The `status.targetSlo.timeTravelStatus` field has been renamed to
`status.targetSlo.replay`.
## Release Notes
Added new fields for SLO kind status, which reflect the current state of
the last process for the given type - replay, errorBudgetAdjustment,
compositeSlo.
---
manifest/v1alpha/slo/slo.go | 25 +++++++++++++++++++++----
sdk/models/replay.go | 12 ++++++++++++
2 files changed, 33 insertions(+), 4 deletions(-)
diff --git a/manifest/v1alpha/slo/slo.go b/manifest/v1alpha/slo/slo.go
index 88d86f8a..2176c757 100644
--- a/manifest/v1alpha/slo/slo.go
+++ b/manifest/v1alpha/slo/slo.go
@@ -161,15 +161,32 @@ type AnomalyConfigAlertMethod struct {
// Status holds dynamic fields returned when the Service is fetched from Nobl9 platform.
// Status is not part of the static object definition.
type Status struct {
- UpdatedAt string `json:"updatedAt,omitempty"`
- ReplayStatus *ReplayStatus `json:"timeTravel,omitempty"`
- TargetSLOStatus *TargetSloStatus `json:"targetSlo,omitempty"`
+ UpdatedAt string `json:"updatedAt,omitempty"`
+ CompositeSLO *ProcessStatus `json:"compositeSlo,omitempty"`
+ ErrorBudgetAdjustment *ProcessStatus `json:"errorBudgetAdjustment,omitempty"`
+ Replay *ProcessStatus `json:"replay,omitempty"`
+ TargetSLO *TargetSloStatus `json:"targetSlo,omitempty"`
+ // Deprecated: use Status.Replay instead.
+ ReplayStatus *ReplayStatus `json:"timeTravel,omitempty"`
}
+type ProcessStatus struct {
+ Status string `json:"status"`
+ TriggeredBy string `json:"triggeredBy"`
+ Unit string `json:"unit"`
+ Value int `json:"value"`
+ StartTime string `json:"startTime"`
+}
+
+// TargetSloStatus represents the status of Replay a target SLO process.
type TargetSloStatus struct {
- TargetTimeTravel ReplayStatus `json:"targetTimeTravel"`
+ // Deprecated: use TargetSloStatus.Replay instead.
+ TargetTimeTravel ReplayStatus `json:"targetTimeTravel,omitempty"`
+ Replay ProcessStatus `json:"replay,omitempty"`
}
+// Deprecated: ReplayStatus exists for historical compatibility
+// and should not be used.
type ReplayStatus struct {
Source string `json:"source"`
Status string `json:"status"`
diff --git a/sdk/models/replay.go b/sdk/models/replay.go
index f57593ee..d96ba44e 100644
--- a/sdk/models/replay.go
+++ b/sdk/models/replay.go
@@ -9,6 +9,8 @@ import (
"github.com/nobl9/govy/pkg/govy"
"github.com/nobl9/govy/pkg/rules"
+
+ "github.com/nobl9/nobl9-go/manifest/v1alpha/slo"
)
// maximumAllowedReplayDuration currently is 30 days.
@@ -49,6 +51,16 @@ type ReplayStatus struct {
StartTime string `json:"startTime"`
}
+func ToProcessStatus(status ReplayStatus) slo.ProcessStatus {
+ return slo.ProcessStatus{
+ Status: status.Status,
+ TriggeredBy: status.TriggeredBy,
+ Unit: status.Unit,
+ Value: status.Value,
+ StartTime: status.StartTime,
+ }
+}
+
type ReplaySourceSLO struct {
Slo string `json:"slo"`
Project string `json:"project"`
From 24782da0f04759accc905bfc73f4886c79623939 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Thu, 17 Oct 2024 02:22:39 +0000
Subject: [PATCH 4/4] chore: Update dependency cspell to v8.15.3 (#573)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
This PR contains the following updates:
| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [cspell](https://cspell.org/)
([source](https://redirect.github.com/streetsidesoftware/cspell/tree/HEAD/packages/cspell))
| [`8.15.2` ->
`8.15.3`](https://renovatebot.com/diffs/npm/cspell/8.15.2/8.15.3) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/cspell/8.15.3?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/cspell/8.15.3?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/cspell/8.15.2/8.15.3?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/cspell/8.15.2/8.15.3?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
---
### Release Notes
streetsidesoftware/cspell (cspell)
###
[`v8.15.3`](https://redirect.github.com/streetsidesoftware/cspell/blob/HEAD/packages/cspell/CHANGELOG.md#small8153-2024-10-16-small)
[Compare
Source](https://redirect.github.com/streetsidesoftware/cspell/compare/v8.15.2...v8.15.3)
- chore: Update Integration Test Performance Data
([#6377](https://redirect.github.com/streetsidesoftware/cspell/issues/6377))
([7ff6781](https://redirect.github.com/streetsidesoftware/cspell/commit/7ff6781)),
closes
[#6377](https://redirect.github.com/streetsidesoftware/cspell/issues/6377)
---
### Configuration
📅 **Schedule**: Branch creation - "after 10pm every weekday,before 5am
every weekday,every weekend" (UTC), Automerge - At any time (no schedule
defined).
🚦 **Automerge**: Enabled.
â™» **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.
🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.
---
- [ ] If you want to rebase/retry this PR, check
this box
---
This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/nobl9/nobl9-go).
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
---
package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/package.json b/package.json
index 7873aff6..7247a04f 100644
--- a/package.json
+++ b/package.json
@@ -3,7 +3,7 @@
"private": true,
"type": "module",
"devDependencies": {
- "cspell": "8.15.2",
+ "cspell": "8.15.3",
"markdownlint-cli": "0.41.0",
"yaml": "2.6.0"
},