-
Notifications
You must be signed in to change notification settings - Fork 29
feat: add rate limiting policy support #428
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| package ratelimitingpolicies | ||
|
|
||
| type RateLimitingPolicy struct { | ||
| ID string `json:"Id"` | ||
| IsBuiltIn bool `json:"IsBuiltIn"` | ||
| Name string `json:"Name"` | ||
| IsEnabled bool `json:"IsEnabled"` | ||
| ScopeType RateLimitingPolicyScopeType `json:"ScopeType"` | ||
| RequestsPerHour int `json:"RequestsPerHour"` | ||
| BurstLimit int `json:"BurstLimit"` | ||
| AuditMode bool `json:"AuditMode"` | ||
| } | ||
|
|
||
| type GetRateLimitingPolicyByIdRequest struct { | ||
| ID string `uri:"id"` | ||
| } | ||
|
|
||
| type ListRateLimitingPoliciesRequest struct { | ||
| Skip int `uri:"skip,omitempty"` | ||
| Take int `uri:"take,omitempty"` | ||
| } | ||
|
|
||
| type ListRateLimitingPoliciesResponse struct { | ||
| ItemType string `json:"ItemType"` | ||
| TotalResults int `json:"TotalResults"` | ||
| ItemsPerPage int `json:"ItemsPerPage"` | ||
| Items []RateLimitingPolicy `json:"Items"` | ||
| NumberOfPages int `json:"NumberOfPages"` | ||
| LastPageNumber int `json:"LastPageNumber"` | ||
| } | ||
|
|
||
| type ModifyRateLimitingPolicyCommand struct { | ||
| ID string `uri:"id" json:"-"` | ||
| Name string `json:"Name"` | ||
| IsEnabled bool `json:"IsEnabled"` | ||
| ScopeType RateLimitingPolicyScopeType `json:"ScopeType"` | ||
| RequestsPerHour int `json:"RequestsPerHour"` | ||
| BurstLimit int `json:"BurstLimit"` | ||
| AuditMode bool `json:"AuditMode"` | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| package ratelimitingpolicies | ||
|
|
||
| type RateLimitingPolicyScopeType int | ||
|
|
||
| const ( | ||
| Unauthenticated RateLimitingPolicyScopeType = iota | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just checking, this serializes across the JSON API as a string, doesn't it?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yep! |
||
| AuthenticatedHuman | ||
| AuthenticatedAgent | ||
| ) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. TIL about this |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| package ratelimitingpolicies | ||
|
|
||
| import ( | ||
| "github.com/OctopusDeploy/go-octopusdeploy/v2/internal" | ||
| "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/newclient" | ||
| ) | ||
|
|
||
| const rateLimitingPoliciesTemplate = "/api/ratelimitingpolicies{/id}{?skip,take}" | ||
|
|
||
| // GetByID returns the rate limiting policy that matches the given ID. | ||
| func GetByID(client newclient.Client, request GetRateLimitingPolicyByIdRequest) (*RateLimitingPolicy, error) { | ||
| if request.ID == "" { | ||
| return nil, internal.CreateRequiredParameterIsEmptyError("ID") | ||
| } | ||
|
|
||
| path, pathError := client.URITemplateCache().Expand(rateLimitingPoliciesTemplate, request) | ||
| if pathError != nil { | ||
| return nil, pathError | ||
| } | ||
|
|
||
| result, resultError := newclient.Get[RateLimitingPolicy](client.HttpSession(), path) | ||
| if resultError != nil { | ||
| return nil, resultError | ||
| } | ||
|
|
||
| return result, nil | ||
| } | ||
|
|
||
| // List returns a paginated collection of rate limiting policies. | ||
| func List(client newclient.Client, request ListRateLimitingPoliciesRequest) (*ListRateLimitingPoliciesResponse, error) { | ||
| path, pathError := client.URITemplateCache().Expand(rateLimitingPoliciesTemplate, request) | ||
| if pathError != nil { | ||
| return nil, pathError | ||
| } | ||
|
|
||
| result, resultError := newclient.Get[ListRateLimitingPoliciesResponse](client.HttpSession(), path) | ||
| if resultError != nil { | ||
| return nil, resultError | ||
| } | ||
|
|
||
| return result, nil | ||
| } | ||
|
|
||
| // Modify changes the rate limiting policy that matches the given ID. | ||
| func Modify(client newclient.Client, command ModifyRateLimitingPolicyCommand) (*RateLimitingPolicy, error) { | ||
| if command.ID == "" { | ||
| return nil, internal.CreateRequiredParameterIsEmptyError("ID") | ||
| } | ||
|
|
||
| path, pathError := client.URITemplateCache().Expand(rateLimitingPoliciesTemplate, command) | ||
| if pathError != nil { | ||
| return nil, pathError | ||
| } | ||
|
|
||
| result, resultError := newclient.Put[RateLimitingPolicy](client.HttpSession(), path, command) | ||
| if resultError != nil { | ||
| return nil, resultError | ||
| } | ||
|
|
||
| return result, nil | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,128 @@ | ||
| package ratelimitingpolicies | ||
|
|
||
| import ( | ||
| "encoding/json" | ||
| "testing" | ||
|
|
||
| "github.com/stretchr/testify/require" | ||
| ) | ||
|
|
||
| func TestRateLimitingPolicyScopeTypeJsonMarshal(t *testing.T) { | ||
| cases := map[RateLimitingPolicyScopeType]string{ | ||
| Unauthenticated: `"Unauthenticated"`, | ||
| AuthenticatedHuman: `"AuthenticatedHuman"`, | ||
| AuthenticatedAgent: `"AuthenticatedAgent"`, | ||
| } | ||
| for scope, expected := range cases { | ||
| jsonValue, err := json.Marshal(scope) | ||
| require.NoError(t, err) | ||
| require.JSONEq(t, expected, string(jsonValue)) | ||
|
|
||
| var enumValue RateLimitingPolicyScopeType | ||
| require.NoError(t, json.Unmarshal(jsonValue, &enumValue)) | ||
| require.Equal(t, scope, enumValue) | ||
| } | ||
| } | ||
|
|
||
| func TestRateLimitingPolicyScopeTypeJsonUnmarshalInvalid(t *testing.T) { | ||
| var scope RateLimitingPolicyScopeType | ||
| require.Error(t, json.Unmarshal([]byte(`"NotAScope"`), &scope)) | ||
| } | ||
|
|
||
| func TestRateLimitingPolicyMarshalRoundTrip(t *testing.T) { | ||
| policy := RateLimitingPolicy{ | ||
| ID: "RateLimitingPolicies-2", | ||
| IsBuiltIn: true, | ||
| Name: "Authenticated requests", | ||
| IsEnabled: true, | ||
| ScopeType: AuthenticatedHuman, | ||
| RequestsPerHour: 10_000, | ||
| BurstLimit: 5_000, | ||
| AuditMode: true, | ||
| } | ||
|
|
||
| data, err := json.Marshal(policy) | ||
| require.NoError(t, err) | ||
|
|
||
| expected := `{ | ||
| "Id": "RateLimitingPolicies-2", | ||
| "IsBuiltIn": true, | ||
| "Name": "Authenticated requests", | ||
| "IsEnabled": true, | ||
| "ScopeType": "AuthenticatedHuman", | ||
| "RequestsPerHour": 10000, | ||
| "BurstLimit": 5000, | ||
| "AuditMode": true | ||
| }` | ||
| require.JSONEq(t, expected, string(data)) | ||
|
|
||
| var received RateLimitingPolicy | ||
| require.NoError(t, json.Unmarshal(data, &received)) | ||
| require.Equal(t, policy, received) | ||
| } | ||
|
|
||
| func TestModifyRateLimitingPolicyCommandMarshal(t *testing.T) { | ||
| command := ModifyRateLimitingPolicyCommand{ | ||
| ID: "RateLimitingPolicies-1", | ||
| Name: "Changed", | ||
| IsEnabled: true, | ||
| ScopeType: Unauthenticated, | ||
| RequestsPerHour: 123, | ||
| BurstLimit: 456, | ||
| AuditMode: true, | ||
| } | ||
|
|
||
| data, err := json.Marshal(command) | ||
| require.NoError(t, err) | ||
| require.JSONEq(t, `{ | ||
| "Name": "Changed", | ||
| "IsEnabled": true, | ||
| "ScopeType": "Unauthenticated", | ||
| "RequestsPerHour": 123, | ||
| "BurstLimit": 456, | ||
| "AuditMode": true | ||
| }`, string(data)) | ||
| } | ||
|
|
||
| func TestListRateLimitingPoliciesResponseUnmarshal(t *testing.T) { | ||
| payload := `{ | ||
| "ItemType": "RateLimitingPolicy", | ||
| "TotalResults": 2, | ||
| "ItemsPerPage": 30, | ||
| "NumberOfPages": 1, | ||
| "LastPageNumber": 0, | ||
| "Items": [ | ||
| { | ||
| "Id": "RateLimitingPolicies-1", | ||
| "Name": "Human", | ||
| "IsBuiltIn": true, | ||
| "ScopeType": "AuthenticatedHuman", | ||
| "IsEnabled": true, | ||
| "RequestsPerHour": 1000, | ||
| "BurstLimit": 50, | ||
| "AuditMode": true | ||
| }, | ||
| { | ||
| "Id": "RateLimitingPolicies-2", | ||
| "Name": "Agent", | ||
| "IsBuiltIn": true, | ||
| "ScopeType": "AuthenticatedAgent", | ||
| "IsEnabled": false, | ||
| "RequestsPerHour": 500, | ||
| "BurstLimit": 25, | ||
| "AuditMode": false | ||
| } | ||
| ] | ||
| }` | ||
|
|
||
| var response ListRateLimitingPoliciesResponse | ||
| require.NoError(t, json.Unmarshal([]byte(payload), &response)) | ||
|
|
||
| require.Equal(t, 2, response.TotalResults) | ||
| require.Len(t, response.Items, 2) | ||
| require.Equal(t, AuthenticatedHuman, response.Items[0].ScopeType) | ||
| require.Equal(t, AuthenticatedAgent, response.Items[1].ScopeType) | ||
| require.False(t, response.Items[1].IsEnabled) | ||
| require.True(t, response.Items[0].AuditMode) | ||
| require.False(t, response.Items[1].AuditMode) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not 100% sure I'm doing this the right way, as while there are some examples (e.g. live status) that are similar with requests and responses, there seem to be numerous different patterns at play in this repo. This seems the most strongly aligned with our target architecture though, and aligns with the C# client too.