Skip to content

Commit 61e7c32

Browse files
committed
feat: add update of exporters config.
Also, fix a blocking stop of the app when the exporter config is erroned.
1 parent 4dbfd29 commit 61e7c32

36 files changed

+1713
-163
lines changed

docs/api/README.md

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2377,6 +2377,62 @@ Status Code **200**
23772377
This operation does not require authentication
23782378
</aside>
23792379

2380+
## Update exporter
2381+
2382+
<a id="opIdv2UpdateExporter"></a>
2383+
2384+
> Code samples
2385+
2386+
```http
2387+
PUT http://localhost:8080/v2/_/exporters/{exporterID} HTTP/1.1
2388+
Host: localhost:8080
2389+
Content-Type: application/json
2390+
Accept: application/json
2391+
2392+
```
2393+
2394+
`PUT /v2/_/exporters/{exporterID}`
2395+
2396+
> Body parameter
2397+
2398+
```json
2399+
{
2400+
"driver": "string",
2401+
"config": {}
2402+
}
2403+
```
2404+
2405+
<h3 id="update-exporter-parameters">Parameters</h3>
2406+
2407+
|Name|In|Type|Required|Description|
2408+
|---|---|---|---|---|
2409+
|body|body|[V2ExporterConfiguration](#schemav2exporterconfiguration)|true|none|
2410+
|exporterID|path|string|true|The exporter id|
2411+
2412+
> Example responses
2413+
2414+
> default Response
2415+
2416+
```json
2417+
{
2418+
"errorCode": "VALIDATION",
2419+
"errorMessage": "[VALIDATION] invalid 'cursor' query param",
2420+
"details": "https://play.numscript.org/?payload=eyJlcnJvciI6ImFjY291bnQgaGFkIGluc3VmZmljaWVudCBmdW5kcyJ9"
2421+
}
2422+
```
2423+
2424+
<h3 id="update-exporter-responses">Responses</h3>
2425+
2426+
|Status|Meaning|Description|Schema|
2427+
|---|---|---|---|
2428+
|204|[No Content](https://tools.ietf.org/html/rfc7231#section-6.3.5)|Exporter updated|None|
2429+
|default|Default|Error|[V2ErrorResponse](#schemav2errorresponse)|
2430+
2431+
<aside class="warning">
2432+
To perform this operation, you must be authenticated by means of one of the following methods:
2433+
Authorization ( Scopes: ledger:write )
2434+
</aside>
2435+
23802436
## Delete exporter
23812437

23822438
<a id="opIdv2DeleteExporter"></a>
@@ -5483,6 +5539,25 @@ and
54835539

54845540
*None*
54855541

5542+
<h2 id="tocS_V2UpdateExporterRequest">V2UpdateExporterRequest</h2>
5543+
<!-- backwards compatibility -->
5544+
<a id="schemav2updateexporterrequest"></a>
5545+
<a id="schema_V2UpdateExporterRequest"></a>
5546+
<a id="tocSv2updateexporterrequest"></a>
5547+
<a id="tocsv2updateexporterrequest"></a>
5548+
5549+
```json
5550+
{
5551+
"driver": "string",
5552+
"config": {}
5553+
}
5554+
5555+
```
5556+
5557+
### Properties
5558+
5559+
*None*
5560+
54865561
<h2 id="tocS_V2PipelineConfiguration">V2PipelineConfiguration</h2>
54875562
<!-- backwards compatibility -->
54885563
<a id="schemav2pipelineconfiguration"></a>

internal/api/common/mocks_system_controller_test.go

Lines changed: 28 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/api/v1/mocks_system_controller_test.go

Lines changed: 28 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package v2
2+
3+
import (
4+
"errors"
5+
"github.com/formancehq/go-libs/v3/api"
6+
ledger "github.com/formancehq/ledger/internal"
7+
"github.com/formancehq/ledger/internal/api/common"
8+
systemcontroller "github.com/formancehq/ledger/internal/controller/system"
9+
"github.com/go-chi/chi/v5"
10+
"net/http"
11+
)
12+
13+
func updateExporter(systemController systemcontroller.Controller) func(w http.ResponseWriter, r *http.Request) {
14+
return func(w http.ResponseWriter, r *http.Request) {
15+
exporterID := chi.URLParam(r, "exporterID")
16+
common.WithBody[ledger.ExporterConfiguration](w, r, func(req ledger.ExporterConfiguration) {
17+
err := systemController.UpdateExporter(r.Context(), exporterID, req)
18+
if err != nil {
19+
switch {
20+
case errors.Is(err, systemcontroller.ErrInvalidDriverConfiguration{}):
21+
api.BadRequest(w, "VALIDATION", err)
22+
case errors.Is(err, systemcontroller.ErrExporterNotFound("")):
23+
api.NotFound(w, err)
24+
default:
25+
api.InternalServerError(w, r, err)
26+
}
27+
return
28+
}
29+
30+
w.WriteHeader(http.StatusNoContent)
31+
})
32+
}
33+
}
34+
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
package v2
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"github.com/formancehq/go-libs/v3/auth"
7+
"github.com/formancehq/go-libs/v3/logging"
8+
ledger "github.com/formancehq/ledger/internal"
9+
systemcontroller "github.com/formancehq/ledger/internal/controller/system"
10+
"net/http"
11+
"net/http/httptest"
12+
"testing"
13+
14+
"github.com/google/uuid"
15+
"github.com/pkg/errors"
16+
17+
sharedapi "github.com/formancehq/go-libs/v3/api"
18+
"github.com/stretchr/testify/require"
19+
"go.uber.org/mock/gomock"
20+
)
21+
22+
func TestUpdateExporter(t *testing.T) {
23+
t.Parallel()
24+
25+
ctx := logging.TestingContext()
26+
27+
type testCase struct {
28+
name string
29+
exporterID string
30+
exporterConfiguration ledger.ExporterConfiguration
31+
returnError error
32+
expectErrorStatusCode int
33+
expectErrorCode string
34+
}
35+
for _, testCase := range []testCase{
36+
{
37+
name: "nominal",
38+
exporterID: uuid.NewString(),
39+
exporterConfiguration: ledger.ExporterConfiguration{
40+
Driver: "http",
41+
Config: json.RawMessage(`{"url":"http://example.com"}`),
42+
},
43+
},
44+
{
45+
name: "invalid driver configuration",
46+
exporterID: uuid.NewString(),
47+
exporterConfiguration: ledger.ExporterConfiguration{
48+
Driver: "http",
49+
Config: json.RawMessage(`{"url":"invalid"}`),
50+
},
51+
returnError: systemcontroller.NewErrInvalidDriverConfiguration("http", errors.New("invalid config")),
52+
expectErrorStatusCode: http.StatusBadRequest,
53+
expectErrorCode: "VALIDATION",
54+
},
55+
{
56+
name: "exporter not found",
57+
exporterID: uuid.NewString(),
58+
exporterConfiguration: ledger.ExporterConfiguration{
59+
Driver: "http",
60+
Config: json.RawMessage(`{"url":"http://example.com"}`),
61+
},
62+
returnError: systemcontroller.NewErrExporterNotFound(""),
63+
expectErrorStatusCode: http.StatusNotFound,
64+
expectErrorCode: "NOT_FOUND",
65+
},
66+
{
67+
name: "unknown error",
68+
exporterID: uuid.NewString(),
69+
exporterConfiguration: ledger.ExporterConfiguration{
70+
Driver: "http",
71+
Config: json.RawMessage(`{"url":"http://example.com"}`),
72+
},
73+
expectErrorCode: "INTERNAL",
74+
expectErrorStatusCode: http.StatusInternalServerError,
75+
returnError: errors.New("any error"),
76+
},
77+
} {
78+
testCase := testCase
79+
t.Run(testCase.name, func(t *testing.T) {
80+
t.Parallel()
81+
82+
systemController, _ := newTestingSystemController(t, false)
83+
systemController.EXPECT().
84+
UpdateExporter(gomock.Any(), testCase.exporterID, testCase.exporterConfiguration).
85+
Return(testCase.returnError)
86+
87+
router := NewRouter(systemController, auth.NewNoAuth(), "develop", WithExporters(true))
88+
89+
data, err := json.Marshal(testCase.exporterConfiguration)
90+
require.NoError(t, err)
91+
92+
req := httptest.NewRequest(http.MethodPut, "/_/exporters/"+testCase.exporterID, bytes.NewBuffer(data))
93+
req = req.WithContext(ctx)
94+
rsp := httptest.NewRecorder()
95+
96+
router.ServeHTTP(rsp, req)
97+
98+
if testCase.expectErrorCode != "" {
99+
require.Equal(t, testCase.expectErrorStatusCode, rsp.Code)
100+
errorResponse := sharedapi.ErrorResponse{}
101+
require.NoError(t, json.NewDecoder(rsp.Body).Decode(&errorResponse))
102+
require.Equal(t, testCase.expectErrorCode, errorResponse.ErrorCode)
103+
} else {
104+
require.Equal(t, http.StatusNoContent, rsp.Code)
105+
}
106+
})
107+
}
108+
}
109+

internal/api/v2/mocks_system_controller_test.go

Lines changed: 28 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/api/v2/routes.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ func NewRouter(
4040
router.Route("/exporters", func(router chi.Router) {
4141
router.Get("/", listExporters(systemController))
4242
router.Get("/{exporterID}", getExporter(systemController))
43+
router.Put("/{exporterID}", updateExporter(systemController))
4344
router.Delete("/{exporterID}", deleteExporter(systemController))
4445
router.Post("/", createExporter(systemController))
4546
})

internal/controller/system/controller.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
type ReplicationBackend interface {
2727
ListExporters(ctx context.Context) (*bunpaginate.Cursor[ledger.Exporter], error)
2828
CreateExporter(ctx context.Context, configuration ledger.ExporterConfiguration) (*ledger.Exporter, error)
29+
UpdateExporter(ctx context.Context, id string, configuration ledger.ExporterConfiguration) error
2930
DeleteExporter(ctx context.Context, id string) error
3031
GetExporter(ctx context.Context, id string) (*ledger.Exporter, error)
3132

@@ -84,6 +85,13 @@ func (ctrl *DefaultController) CreateExporter(ctx context.Context, configuration
8485
return ret, nil
8586
}
8687

88+
// UpdateExporter can return following errors:
89+
// * ErrInvalidDriverConfiguration
90+
// * ErrExporterNotFound
91+
func (ctrl *DefaultController) UpdateExporter(ctx context.Context, id string, configuration ledger.ExporterConfiguration) error {
92+
return ctrl.replicationBackend.UpdateExporter(ctx, id, configuration)
93+
}
94+
8795
// DeleteExporter can return following errors:
8896
// ErrExporterNotFound
8997
func (ctrl *DefaultController) DeleteExporter(ctx context.Context, id string) error {

0 commit comments

Comments
 (0)