Skip to content

Commit dd24e16

Browse files
authored
feat: support ramping up write API requests (#465)
* feat: support ramping up write API requests * release: v0.6.5
1 parent 90bc4e5 commit dd24e16

34 files changed

+11193
-335
lines changed

.golangci.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,12 @@ linters-settings:
3636
- github.com/openfga/go-sdk
3737
- github.com/openfga/language
3838
- github.com/openfga/openfga
39+
- github.com/rung/go-safecast
3940
- github.com/schollz/progressbar/v3
4041
- github.com/spf13/cobra
4142
- github.com/spf13/pflag
4243
- github.com/spf13/viper
44+
- golang.org/x/time/rate
4345
- google.golang.org/protobuf/encoding/protojson
4446
- google.golang.org/protobuf/types/known/structpb
4547
- gopkg.in/yaml.v3
@@ -75,3 +77,8 @@ issues:
7577
- path: "cmd/tuple/write(.*).go"
7678
linters:
7779
- lll
80+
- path: _test.go
81+
linters:
82+
- err113
83+
- funlen
84+
- lll

CHANGELOG.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
11
# Changelog
22

3-
#### [Unreleased](https://github.com/openfga/cli/compare/v0.6.4...HEAD)
3+
#### [Unreleased](https://github.com/openfga/cli/compare/v0.6.5...HEAD)
4+
5+
#### [0.6.5](https://github.com/openfga/cli/compare/v0.6.4...v0.6.5) (2025-03-24)
6+
7+
Added:
8+
- Support for RPS ramp up for tuple writes, which can be helpful when importing a large amount of tuples (#463)
9+
On `fga tuple write` we now support the following flags: `--max-rps` and `--rampup-period-in-sec`. If one is set, both are required.
10+
e.g. `--max-rps 10 --rampup-period-in-sec 10`
11+
If these flags are set the CLI will start ramping up requests from 1RPS to the configured max RPS over the configured period
12+
13+
Changed:
14+
- The deprecated `fga tuple import` has now been aliased to `fga tuple write` (#463)
415

516

617
#### [0.6.4](https://github.com/openfga/cli/compare/v0.6.3...v0.6.4) (2025-02-07)

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -674,11 +674,14 @@ fga tuple **write** <user> <relation> <object> --store-id=<store-id>
674674
* `--max-tuples-per-write`: Max tuples to send in a single write (optional, default=1)
675675
* `--max-parallel-requests`: Max requests to send in parallel (optional, default=4)
676676
* `--hide-imported-tuples`: When importing from a file, do not output successfully imported tuples in the command output (optional, default=false)
677+
* `--max-rps`: Max requests per second, when set the CLI will ramp up requests from 1RPS to the set value over the set period. Used in conjunction with `--rampup-period-in-sec` (optional)
678+
* `--rampup-period-in-sec`: Time in seconds to wait between each batch of tuples when ramping up. Used in conjunction with `--max-rps` (optional)
677679

678680
###### Example (with arguments)
679681
- `fga tuple write --store-id=01H0H015178Y2V4CX10C2KGHF4 user:anne can_view document:roadmap`
680682
- `fga tuple write --store-id=01H0H015178Y2V4CX10C2KGHF4 user:anne can_view document:roadmap --condition-name inOffice --condition-context '{"office_ip":"10.0.1.10"}'`
681683
- `fga tuple write --store-id=01H0H015178Y2V4CX10C2KGHF4 --model-id=01GXSA8YR785C4FYS3C0RTG7B1 --file tuples.json`
684+
- `fga tuple write --store-id=01H0H015178Y2V4CX10C2KGHF4 --file tuples.csv --max-tuples-per-write 10 --max-parallel-requests 5 --max-rps 10 --rampup-period-in-sec 10`
682685

683686
###### Response
684687
```json5

cmd/model/get_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ func TestGetModelNoAuthModelID(t *testing.T) {
2929

3030
var expectedResponse client.ClientReadAuthorizationModelResponse
3131

32-
modelJSON := `{"authorization_model":{"id":"01GXSA8YR785C4FYS3C0RTG7B1","schema_version":"1.1","type_definitions":[{"relations":{"viewer":{"this":{}}},"type":"github-repo"}]}}` //nolint:all
32+
modelJSON := `{"authorization_model":{"id":"01GXSA8YR785C4FYS3C0RTG7B1","schema_version":"1.1","type_definitions":[{"relations":{"viewer":{"this":{}}},"type":"github-repo"}]}}`
3333
if err := json.Unmarshal([]byte(modelJSON), &expectedResponse); err != nil {
3434
t.Fatalf("%v", err)
3535
}
@@ -63,7 +63,7 @@ func TestGetModelAuthModelID(t *testing.T) {
6363

6464
var expectedResponse client.ClientReadAuthorizationModelResponse
6565

66-
modelJSON := `{"authorization_model":{"id":"01GXSA8YR785C4FYS3C0RTG7B1","schema_version":"1.1","type_definitions":[{"relations":{"viewer":{"this":{}}},"type":"github-repo"}]}}` //nolint:all
66+
modelJSON := `{"authorization_model":{"id":"01GXSA8YR785C4FYS3C0RTG7B1","schema_version":"1.1","type_definitions":[{"relations":{"viewer":{"this":{}}},"type":"github-repo"}]}}`
6767
if err := json.Unmarshal([]byte(modelJSON), &expectedResponse); err != nil {
6868
t.Fatalf("%v", err)
6969
}

cmd/model/list_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import (
1515

1616
var errMockList = errors.New("mock error")
1717

18-
const model1JSON = `{"id":"01GXSA8YR785C4FYS3C0RTG7B1","schema_version":"1.1","type_definitions":[{"relations":{"viewer":{"this":{}}},"type":"github-repo"}]}` //nolint:all
18+
const model1JSON = `{"id":"01GXSA8YR785C4FYS3C0RTG7B1","schema_version":"1.1","type_definitions":[{"relations":{"viewer":{"this":{}}},"type":"github-repo"}]}`
1919

2020
func TestListModelsEmpty(t *testing.T) {
2121
t.Parallel()
@@ -163,7 +163,7 @@ func TestListModelsMultiPage(t *testing.T) {
163163

164164
var model2 openfga.AuthorizationModel
165165

166-
model2JSON := `{"id":"01GXSA8YR785C4FYS3C0RTG7B2","schema_version":"1.1","type_definitions":[{"relations":{"viewer":{"this":{}}},"type":"github-repo"}]}` //nolint:all
166+
model2JSON := `{"id":"01GXSA8YR785C4FYS3C0RTG7B2","schema_version":"1.1","type_definitions":[{"relations":{"viewer":{"this":{}}},"type":"github-repo"}]}`
167167
if err := json.Unmarshal([]byte(model2JSON), &model2); err != nil {
168168
t.Fatalf("%v", err)
169169
}

cmd/model/write.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,11 @@ import (
2828
"github.com/openfga/cli/internal/authorizationmodel"
2929
"github.com/openfga/cli/internal/cmdutils"
3030
"github.com/openfga/cli/internal/output"
31+
"github.com/openfga/cli/internal/utils"
3132
)
3233

3334
func Write(
35+
ctx context.Context,
3436
fgaClient client.SdkClient,
3537
inputModel authorizationmodel.AuthzModel,
3638
) (*client.ClientWriteAuthorizationModelResponse, error) {
@@ -40,7 +42,7 @@ func Write(
4042
Conditions: inputModel.GetConditions(),
4143
}
4244

43-
model, err := fgaClient.WriteAuthorizationModel(context.Background()).Body(body).Execute()
45+
model, err := fgaClient.WriteAuthorizationModel(ctx).Body(body).Execute()
4446
if err != nil {
4547
return nil, fmt.Errorf("failed to write model due to %w", err)
4648
}
@@ -77,14 +79,18 @@ fga model write --store-id=01H0H015178Y2V4CX10C2KGHF4 '{"type_definitions":[{"ty
7779
return err //nolint:wrapcheck
7880
}
7981

82+
debug, _ := cmd.Flags().GetBool("debug")
83+
8084
authModel := authorizationmodel.AuthzModel{}
8185

8286
err = authModel.ReadModelFromString(inputModel, writeInputFormat)
8387
if err != nil {
8488
return err //nolint:wrapcheck
8589
}
8690

87-
response, err := Write(fgaClient, authModel)
91+
ctx := utils.WithDebugContext(cmd.Context(), debug)
92+
93+
response, err := Write(ctx, fgaClient, authModel)
8894
if err != nil {
8995
return err
9096
}

cmd/model/write_test.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ func TestWriteModelFail(t *testing.T) {
2222
defer mockCtrl.Finish()
2323
mockFgaClient := mockclient.NewMockSdkClient(mockCtrl)
2424

25-
modelJSONTxt := `{"schema_version":"1.1","type_definitions":[{"relations":{"viewer":{"this":{}}},"type":"github-repo"}],"conditions":{}}` //nolint:lll
25+
modelJSONTxt := `{"schema_version":"1.1","type_definitions":[{"relations":{"viewer":{"this":{}}},"type":"github-repo"}],"conditions":{}}`
2626
body := &client.ClientWriteAuthorizationModelRequest{}
2727

2828
err := json.Unmarshal([]byte(modelJSONTxt), &body)
@@ -36,7 +36,7 @@ func TestWriteModelFail(t *testing.T) {
3636
mockRequest := mockclient.NewMockSdkClientWriteAuthorizationModelRequestInterface(mockCtrl)
3737
mockRequest.EXPECT().Body(*body).Return(mockExecute)
3838

39-
mockFgaClient.EXPECT().WriteAuthorizationModel(context.Background()).Return(mockRequest)
39+
mockFgaClient.EXPECT().WriteAuthorizationModel(gomock.Any()).Return(mockRequest)
4040

4141
model := authorizationmodel.AuthzModel{}
4242

@@ -45,7 +45,7 @@ func TestWriteModelFail(t *testing.T) {
4545
return
4646
}
4747

48-
_, err = Write(mockFgaClient, model)
48+
_, err = Write(context.TODO(), mockFgaClient, model)
4949
if err == nil {
5050
t.Fatalf("Expect error but there is none")
5151
}
@@ -58,7 +58,7 @@ func TestWriteModel(t *testing.T) {
5858
defer mockCtrl.Finish()
5959
mockFgaClient := mockclient.NewMockSdkClient(mockCtrl)
6060

61-
modelJSONTxt := `{"schema_version":"1.1","type_definitions":[{"relations":{"viewer":{"this":{}}},"type":"github-repo"}],"conditions":{}}` //nolint:lll
61+
modelJSONTxt := `{"schema_version":"1.1","type_definitions":[{"relations":{"viewer":{"this":{}}},"type":"github-repo"}],"conditions":{}}`
6262

6363
body := &client.ClientWriteAuthorizationModelRequest{}
6464

@@ -78,7 +78,7 @@ func TestWriteModel(t *testing.T) {
7878
mockRequest := mockclient.NewMockSdkClientWriteAuthorizationModelRequestInterface(mockCtrl)
7979
mockRequest.EXPECT().Body(*body).Return(mockExecute)
8080

81-
mockFgaClient.EXPECT().WriteAuthorizationModel(context.Background()).Return(mockRequest)
81+
mockFgaClient.EXPECT().WriteAuthorizationModel(gomock.Any()).Return(mockRequest)
8282

8383
model := authorizationmodel.AuthzModel{}
8484

@@ -87,7 +87,7 @@ func TestWriteModel(t *testing.T) {
8787
return
8888
}
8989

90-
output, err := Write(mockFgaClient, model)
90+
output, err := Write(context.TODO(), mockFgaClient, model)
9191
if err != nil {
9292
t.Fatal(err)
9393
}

cmd/query/expand_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ func TestExpandWithNoError(t *testing.T) {
5858

5959
mockExecute := mock_client.NewMockSdkClientExpandRequestInterface(mockCtrl)
6060

61-
expandResponseTxt := `{"tree":{"root":{"name":"document:roadmap#viewer","union":{"nodes":[{"name": "document:roadmap#viewer","leaf":{"users":{"users":["user:81684243-9356-4421-8fbf-a4f8d36aa31b"]}}}]}}}}` //nolint:all
61+
expandResponseTxt := `{"tree":{"root":{"name":"document:roadmap#viewer","union":{"nodes":[{"name": "document:roadmap#viewer","leaf":{"users":{"users":["user:81684243-9356-4421-8fbf-a4f8d36aa31b"]}}}]}}}}`
6262

6363
expectedResponse := client.ClientExpandResponse{}
6464
if err := json.Unmarshal([]byte(expandResponseTxt), &expectedResponse); err != nil {
@@ -100,7 +100,7 @@ func TestExpandWithConsistency(t *testing.T) {
100100

101101
mockExecute := mock_client.NewMockSdkClientExpandRequestInterface(mockCtrl)
102102

103-
expandResponseTxt := `{"tree":{"root":{"name":"document:roadmap#viewer","union":{"nodes":[{"name": "document:roadmap#viewer","leaf":{"users":{"users":["user:81684243-9356-4421-8fbf-a4f8d36aa31b"]}}}]}}}}` //nolint:all
103+
expandResponseTxt := `{"tree":{"root":{"name":"document:roadmap#viewer","union":{"nodes":[{"name": "document:roadmap#viewer","leaf":{"users":{"users":["user:81684243-9356-4421-8fbf-a4f8d36aa31b"]}}}]}}}}`
104104

105105
expectedResponse := client.ClientExpandResponse{}
106106
if err := json.Unmarshal([]byte(expandResponseTxt), &expectedResponse); err != nil {

cmd/query/list-relations_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ func TestListRelationsLatestAuthModelListError(t *testing.T) {
107107

108108
var expectedResponse client.ClientReadAuthorizationModelResponse
109109

110-
modelJSON := `{"authorization_model":{"id":"01GXSA8YR785C4FYS3C0RTG7B1","schema_version":"1.1","type_definitions":[{"relations":{"viewer":{"this":{}}},"type":"doc"}]}}` //nolint:all
110+
modelJSON := `{"authorization_model":{"id":"01GXSA8YR785C4FYS3C0RTG7B1","schema_version":"1.1","type_definitions":[{"relations":{"viewer":{"this":{}}},"type":"doc"}]}}`
111111
if err := json.Unmarshal([]byte(modelJSON), &expectedResponse); err != nil {
112112
t.Fatalf("%v", err)
113113
}
@@ -173,7 +173,7 @@ func TestListRelationsLatestAuthModelEmpty(t *testing.T) {
173173

174174
var expectedResponse client.ClientReadAuthorizationModelResponse
175175

176-
modelJSON := `{"authorization_model":{"id":"01GXSA8YR785C4FYS3C0RTG7B1","schema_version":"1.1","type_definitions":[{"relations":{"viewer":{"this":{}}},"type":"doc"},{"relations":{},"type":"user"}]}}` //nolint:all
176+
modelJSON := `{"authorization_model":{"id":"01GXSA8YR785C4FYS3C0RTG7B1","schema_version":"1.1","type_definitions":[{"relations":{"viewer":{"this":{}}},"type":"doc"},{"relations":{},"type":"user"}]}}`
177177
if err := json.Unmarshal([]byte(modelJSON), &expectedResponse); err != nil {
178178
t.Fatalf("%v", err)
179179
}
@@ -222,7 +222,7 @@ func TestListRelationsLatestAuthModelList(t *testing.T) {
222222

223223
var expectedResponse client.ClientReadAuthorizationModelResponse
224224

225-
modelJSON := `{"authorization_model":{"id":"01GXSA8YR785C4FYS3C0RTG7B1","schema_version":"1.1","type_definitions":[{"relations":{"viewer":{"this":{}}},"type":"doc"}]}}` //nolint:all
225+
modelJSON := `{"authorization_model":{"id":"01GXSA8YR785C4FYS3C0RTG7B1","schema_version":"1.1","type_definitions":[{"relations":{"viewer":{"this":{}}},"type":"doc"}]}}`
226226
if err := json.Unmarshal([]byte(modelJSON), &expectedResponse); err != nil {
227227
t.Fatalf("%v", err)
228228
}

cmd/root.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,9 @@ func init() {
6868
rootCmd.PersistentFlags().String("client-id", "", "Client ID. Sent to the Token Issuer during the Client Credentials flow") //nolint:lll
6969
rootCmd.PersistentFlags().String("client-secret", "", "Client Secret. Sent to the Token Issuer during the Client Credentials flow") //nolint:lll
7070
rootCmd.PersistentFlags().StringArray("api-scopes", []string{}, "API Scopes (repeat option for multiple values). Used in the Client Credentials flow") //nolint:lll
71+
rootCmd.PersistentFlags().Bool("debug", false, "Enable debug mode - can print more detailed information for debugging")
7172

73+
_ = rootCmd.Flags().MarkHidden("debug")
7274
rootCmd.MarkFlagsRequiredTogether(
7375
"api-token-issuer",
7476
"client-id",

0 commit comments

Comments
 (0)