From d93fc319878104c47ede3a55e6fe489c04977501 Mon Sep 17 00:00:00 2001 From: Dorian de Koning Date: Wed, 17 Aug 2022 16:38:02 +0200 Subject: [PATCH] feat: add support for project feature flags --- gitlab.go | 2 + project_feature_flags.go | 210 +++++++++++++++++++ project_feature_flags_test.go | 241 ++++++++++++++++++++++ testdata/create_project_feature_flag.json | 22 ++ testdata/get_project_feature_flag.json | 22 ++ testdata/list_project_feature_flags.json | 48 +++++ testdata/update_project_feature_flag.json | 36 ++++ 7 files changed, 581 insertions(+) create mode 100644 project_feature_flags.go create mode 100644 project_feature_flags_test.go create mode 100644 testdata/create_project_feature_flag.json create mode 100644 testdata/get_project_feature_flag.json create mode 100644 testdata/list_project_feature_flags.json create mode 100644 testdata/update_project_feature_flag.json diff --git a/gitlab.go b/gitlab.go index 8fb886941..033bbcfa3 100644 --- a/gitlab.go +++ b/gitlab.go @@ -171,6 +171,7 @@ type Client struct { ProjectAccessTokens *ProjectAccessTokensService ProjectBadges *ProjectBadgesService ProjectCluster *ProjectClustersService + ProjectFeatureFlags *ProjectFeatureFlagService ProjectImportExport *ProjectImportExportService ProjectIterations *ProjectIterationsService ProjectMembers *ProjectMembersService @@ -374,6 +375,7 @@ func newClient(options ...ClientOptionFunc) (*Client, error) { c.ProjectAccessTokens = &ProjectAccessTokensService{client: c} c.ProjectBadges = &ProjectBadgesService{client: c} c.ProjectCluster = &ProjectClustersService{client: c} + c.ProjectFeatureFlags = &ProjectFeatureFlagService{client: c} c.ProjectImportExport = &ProjectImportExportService{client: c} c.ProjectIterations = &ProjectIterationsService{client: c} c.ProjectMembers = &ProjectMembersService{client: c} diff --git a/project_feature_flags.go b/project_feature_flags.go new file mode 100644 index 000000000..73c214e89 --- /dev/null +++ b/project_feature_flags.go @@ -0,0 +1,210 @@ +package gitlab + +import ( + "fmt" + "net/http" + "time" +) + +// ProjectFeatureFlagService handles operations on gitlab project feature flags using the following api: +// GitLab API docs: https://docs.gitlab.com/ee/api/feature_flags.html +type ProjectFeatureFlagService struct { + client *Client +} + +// ProjectFeatureFlag represents a GitLab project iteration. +// +// GitLab API docs: https://docs.gitlab.com/ee/api/feature_flags.html +type ProjectFeatureFlag struct { + Name string `json:"name"` + Description string `json:"description"` + Active bool `json:"active"` + Version string `json:"version"` + CreatedAt *time.Time `json:"created_at"` + UpdatedAt *time.Time `json:"updated_at"` + Scopes []ProjectFeatureFlagScope `json:"scopes"` + Strategies []ProjectFeatureFlagStrategy `json:"strategies"` +} + +// ProjectFeatureFlagScope defines the scopes of a feature flag +type ProjectFeatureFlagScope struct { + ID int `json:"id"` + EnvironmentScope string `json:"environment_scope"` +} + +// ProjectFeatureFlagStrategy defines the strategy used for a feature flag +type ProjectFeatureFlagStrategy struct { + ID int `json:"id"` + Name string `json:"name"` + Parameters ProjectFeatureFlagStrategyParameter `json:"parameters"` + Scopes []ProjectFeatureFlagScope `json:"scopes"` +} + +// ProjectFeatureFlagStrategyParameter is used in updating and creating feature flags +type ProjectFeatureFlagStrategyParameter struct { + GroupID string `json:"groupId,omitempty"` + UserIDs string `json:"userIds,omitempty"` + Percentage string `json:"percentage,omitempty"` +} + +// UpdateProjectFeatureFlagOptions is used to specify the values when updating feature flags +type UpdateProjectFeatureFlagOptions struct { + Name string `json:"name,omitempty"` + Description *string `json:"description,omitempty"` + Active *bool `json:"active,omitempty"` + Strategies *struct { + Name string `json:"Name,omitempty"` + Parameters ProjectFeatureFlagStrategyParameter `json:"parameters,omitempty"` + Scopes []ProjectFeatureFlagScope `json:"scopes,omitempty"` + } `json:"strategies,omitempty"` +} + +// CreateProjectFeatureFlagOptions contains the options that can be specified when creating feature flags +type CreateProjectFeatureFlagOptions struct { + Name string `json:"name"` + Description *string `json:"description,omitempty"` + Version *string `json:"version,omitempty"` + Active *bool `json:"active,omitempty"` + Strategies *struct { + Name string `json:"Name,omitempty"` + Parameters ProjectFeatureFlagStrategyParameter `json:"parameters,omitempty"` + Scopes []ProjectFeatureFlagScope `json:"scopes,omitempty"` + } `json:"strategies,omitempty"` +} + +func (i ProjectFeatureFlag) String() string { + return Stringify(i) +} + +// ListProjectFeatureFlagOptions contains the options for ListProjectFeatureFlags +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/feature_flags.html#list-feature-flags-for-a-project +type ListProjectFeatureFlagOptions struct { + ListOptions + Scope *string `url:"scope,omitempty" json:"scope,omitempty"` +} + +// ListProjectFeatureFlags returns a list with the feature flags of a project. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/feature_flags.html#list-feature-flags-for-a-project +func (i *ProjectFeatureFlagService) ListProjectFeatureFlags(pid interface{}, opt *ListProjectFeatureFlagOptions, options ...RequestOptionFunc) ([]*ProjectFeatureFlag, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/feature_flags", PathEscape(project)) + + req, err := i.client.NewRequest(http.MethodGet, u, opt, options) + if err != nil { + return nil, nil, err + } + + var pis []*ProjectFeatureFlag + resp, err := i.client.Do(req, &pis) + if err != nil { + return nil, resp, err + } + + return pis, resp, err +} + +// GetProjectFeatureFlag gets a single feature flag for the specified project. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/feature_flags.html#get-a-single-feature-flag +func (s *ProjectFeatureFlagService) GetProjectFeatureFlag(pid interface{}, name string, options ...RequestOptionFunc) (*ProjectFeatureFlag, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/feature_flags/%s", PathEscape(project), name) + + req, err := s.client.NewRequest(http.MethodGet, u, nil, options) + if err != nil { + return nil, nil, err + } + + b := new(ProjectFeatureFlag) + resp, err := s.client.Do(req, b) + if err != nil { + return nil, resp, err + } + + return b, resp, err +} + +// CreateProjectFeatureFlag creates a feature flag +// +// Gitlab API docs: +// https://docs.gitlab.com/ee/api/feature_flags.html#create-a-feature-flag +func (s *ProjectFeatureFlagService) CreateProjectFeatureFlag(pid interface{}, opt *CreateProjectFeatureFlagOptions, options ...RequestOptionFunc) (*ProjectFeatureFlag, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/feature_flags", + PathEscape(project), + ) + + req, err := s.client.NewRequest(http.MethodPost, u, opt, options) + if err != nil { + return nil, nil, err + } + + var flag ProjectFeatureFlag + resp, err := s.client.Do(req, &flag) + if err != nil { + return &flag, resp, err + } + + return &flag, resp, err +} + +// UpdateProjectFeatureFlag updates a feature flag +// +// Gitlab API docs: +// https://docs.gitlab.com/ee/api/feature_flags.html#update-a-feature-flag +func (s *ProjectFeatureFlagService) UpdateProjectFeatureFlag(pid interface{}, name string, opt *UpdateProjectFeatureFlagOptions, options ...RequestOptionFunc) (*ProjectFeatureFlag, *Response, error) { + group, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/feature_flags/%s", + PathEscape(group), + name, + ) + + req, err := s.client.NewRequest(http.MethodPut, u, opt, options) + if err != nil { + return nil, nil, err + } + + var flag ProjectFeatureFlag + resp, err := s.client.Do(req, &flag) + if err != nil { + return &flag, resp, err + } + + return &flag, resp, err +} + +// DeleteProjectFeatureFlag deletes a feature flag +// +// Gitlab API docs: +// https://docs.gitlab.com/ee/api/feature_flags.html#delete-a-feature-flag +func (s *ProjectFeatureFlagService) DeleteProjectFeatureFlag(pid interface{}, name string, options ...RequestOptionFunc) (*Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, err + } + u := fmt.Sprintf("projects/%s/feature_flags/%s", PathEscape(project), name) + + req, err := s.client.NewRequest(http.MethodDelete, u, nil, options) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) +} diff --git a/project_feature_flags_test.go b/project_feature_flags_test.go new file mode 100644 index 000000000..590a3fdc5 --- /dev/null +++ b/project_feature_flags_test.go @@ -0,0 +1,241 @@ +package gitlab + +import ( + "net/http" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestListProjectFeatureFlags(t *testing.T) { + mux, server, client := setup(t) + defer teardown(server) + + mux.HandleFunc("/api/v4/projects/333/feature_flags", + func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, http.MethodGet) + mustWriteHTTPResponse(t, w, "testdata/list_project_feature_flags.json") + }) + + actual, _, err := client.ProjectFeatureFlags.ListProjectFeatureFlags(333, &ListProjectFeatureFlagOptions{}) + if err != nil { + t.Errorf("ProjectFeatureFlags.ListProjectFeatureFlags returned error: %v", err) + return + } + + createdAt1 := time.Date(2019, 11, 4, 8, 13, 51, 0, time.UTC) + updatedAt1 := time.Date(2019, 11, 4, 8, 13, 11, 0, time.UTC) + + createdAt2 := time.Date(2019, 11, 4, 8, 13, 10, 0, time.UTC) + updatedAt2 := time.Date(2019, 11, 4, 8, 13, 10, 0, time.UTC) + + expected := []*ProjectFeatureFlag{ + { + Name: "merge_train", + Description: "This feature is about merge train", + Active: true, + Version: "new_version_flag", + CreatedAt: &createdAt1, + UpdatedAt: &updatedAt1, + Scopes: []ProjectFeatureFlagScope{}, + Strategies: []ProjectFeatureFlagStrategy{ + { + ID: 1, + Name: "userWithId", + Parameters: ProjectFeatureFlagStrategyParameter{ + UserIDs: "user1", + }, + Scopes: []ProjectFeatureFlagScope{ + { + ID: 1, + EnvironmentScope: "production", + }, + }, + }, + }, + }, + { + Name: "new_live_trace", + Description: "This is a new live trace feature", + Active: true, + Version: "new_version_flag", + CreatedAt: &createdAt2, + UpdatedAt: &updatedAt2, + Scopes: []ProjectFeatureFlagScope{}, + Strategies: []ProjectFeatureFlagStrategy{ + { + ID: 2, + Name: "default", + Scopes: []ProjectFeatureFlagScope{ + { + ID: 2, + EnvironmentScope: "staging", + }, + }, + }, + }, + }, + } + + assert.Equal(t, len(expected), len(actual)) + for i := range expected { + assert.Equal(t, expected[i], actual[i]) + } +} + +func TestGetProjectFeatureFlag(t *testing.T) { + mux, server, client := setup(t) + defer teardown(server) + + mux.HandleFunc("/api/v4/projects/1/feature_flags/testing", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, http.MethodGet) + mustWriteHTTPResponse(t, w, "testdata/get_project_feature_flag.json") + }) + + actual, resp, err := client.ProjectFeatureFlags.GetProjectFeatureFlag(1, "testing") + if err != nil { + t.Fatalf("ProjectFeatureFlags.GetProjectFeatureFlag returned error: %v, response %v", err, resp) + } + + date := time.Date(2020, 05, 13, 19, 56, 33, 0, time.UTC) + expected := &ProjectFeatureFlag{ + Name: "awesome_feature", + Active: true, + Version: "new_version_flag", + CreatedAt: &date, + UpdatedAt: &date, + Scopes: []ProjectFeatureFlagScope{}, + Strategies: []ProjectFeatureFlagStrategy{ + { + ID: 36, + Name: "default", + Parameters: ProjectFeatureFlagStrategyParameter{}, + Scopes: []ProjectFeatureFlagScope{ + { + ID: 37, + EnvironmentScope: "production", + }, + }, + }, + }, + } + + assert.Equal(t, expected, actual) +} + +func TestCreateProjectFeatureFlag(t *testing.T) { + mux, server, client := setup(t) + defer teardown(server) + + mux.HandleFunc("/api/v4/projects/1/feature_flags/testing", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, http.MethodPut) + mustWriteHTTPResponse(t, w, "testdata/create_project_feature_flag.json") + }) + + actual, _, err := client.ProjectFeatureFlags.UpdateProjectFeatureFlag(1, "testing", &UpdateProjectFeatureFlagOptions{}) + if err != nil { + t.Errorf("ProjectFeatureFlags.UpdateProjectFeatureFlag returned error: %v", err) + return + } + + createdAt := time.Date(2020, 5, 13, 19, 56, 33, 0, time.UTC) + updatedAt := time.Date(2020, 5, 13, 19, 56, 33, 0, time.UTC) + + expected := &ProjectFeatureFlag{ + Name: "awesome_feature", + Active: true, + Version: "new_version_flag", + CreatedAt: &createdAt, + UpdatedAt: &updatedAt, + Scopes: []ProjectFeatureFlagScope{}, + Strategies: []ProjectFeatureFlagStrategy{ + { + ID: 36, + Name: "default", + Parameters: ProjectFeatureFlagStrategyParameter{}, + Scopes: []ProjectFeatureFlagScope{ + { + ID: 37, + EnvironmentScope: "production", + }, + }, + }, + }, + } + + assert.Equal(t, expected, actual) +} + +func TestUpdateProjectFeatureFlag(t *testing.T) { + mux, server, client := setup(t) + defer teardown(server) + + mux.HandleFunc("/api/v4/projects/1/feature_flags/testing", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, http.MethodPut) + mustWriteHTTPResponse(t, w, "testdata/update_project_feature_flag.json") + }) + + actual, _, err := client.ProjectFeatureFlags.UpdateProjectFeatureFlag(1, "testing", &UpdateProjectFeatureFlagOptions{}) + if err != nil { + t.Errorf("ProjectFeatureFlags.UpdateProjectFeatureFlag returned error: %v", err) + return + } + + createdAt := time.Date(2020, 5, 13, 20, 10, 32, 0, time.UTC) + updatedAt := time.Date(2020, 5, 13, 20, 10, 32, 0, time.UTC) + + expected := &ProjectFeatureFlag{ + Name: "awesome_feature", + Active: true, + Version: "new_version_flag", + CreatedAt: &createdAt, + UpdatedAt: &updatedAt, + Scopes: []ProjectFeatureFlagScope{}, + Strategies: []ProjectFeatureFlagStrategy{ + { + ID: 38, + Name: "gradualRolloutUserId", + Parameters: ProjectFeatureFlagStrategyParameter{ + GroupID: "default", + Percentage: "25", + }, + Scopes: []ProjectFeatureFlagScope{ + { + ID: 40, + EnvironmentScope: "staging", + }, + }, + }, + { + ID: 37, + Name: "default", + Scopes: []ProjectFeatureFlagScope{ + { + ID: 39, + EnvironmentScope: "production", + }, + }, + }, + }, + } + + assert.Equal(t, expected, actual) + +} + +func TestDeleteProjectFeatureFlag(t *testing.T) { + mux, server, client := setup(t) + defer teardown(server) + + mux.HandleFunc("/api/v4/projects/1/feature_flags/testing", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, http.MethodDelete) + }) + + _, err := client.ProjectFeatureFlags.DeleteProjectFeatureFlag(1, "testing") + if err != nil { + t.Errorf("ProjectFeatureFlags.DeleteProjectFeatureFlag returned error: %v", err) + return + } + +} diff --git a/testdata/create_project_feature_flag.json b/testdata/create_project_feature_flag.json new file mode 100644 index 000000000..e46062762 --- /dev/null +++ b/testdata/create_project_feature_flag.json @@ -0,0 +1,22 @@ +{ + "name": "awesome_feature", + "description": null, + "active": true, + "version": "new_version_flag", + "created_at": "2020-05-13T19:56:33Z", + "updated_at": "2020-05-13T19:56:33Z", + "scopes": [], + "strategies": [ + { + "id": 36, + "name": "default", + "parameters": {}, + "scopes": [ + { + "id": 37, + "environment_scope": "production" + } + ] + } + ] +} diff --git a/testdata/get_project_feature_flag.json b/testdata/get_project_feature_flag.json new file mode 100644 index 000000000..e46062762 --- /dev/null +++ b/testdata/get_project_feature_flag.json @@ -0,0 +1,22 @@ +{ + "name": "awesome_feature", + "description": null, + "active": true, + "version": "new_version_flag", + "created_at": "2020-05-13T19:56:33Z", + "updated_at": "2020-05-13T19:56:33Z", + "scopes": [], + "strategies": [ + { + "id": 36, + "name": "default", + "parameters": {}, + "scopes": [ + { + "id": 37, + "environment_scope": "production" + } + ] + } + ] +} diff --git a/testdata/list_project_feature_flags.json b/testdata/list_project_feature_flags.json new file mode 100644 index 000000000..660fd2fcf --- /dev/null +++ b/testdata/list_project_feature_flags.json @@ -0,0 +1,48 @@ +[ + { + "name":"merge_train", + "description":"This feature is about merge train", + "active": true, + "version": "new_version_flag", + "created_at":"2019-11-04T08:13:51Z", + "updated_at":"2019-11-04T08:13:11Z", + "scopes":[], + "strategies": [ + { + "id": 1, + "name": "userWithId", + "parameters": { + "userIds": "user1" + }, + "scopes": [ + { + "id": 1, + "environment_scope": "production" + } + ] + } + ] + }, + { + "name":"new_live_trace", + "description":"This is a new live trace feature", + "active": true, + "version": "new_version_flag", + "created_at":"2019-11-04T08:13:10Z", + "updated_at":"2019-11-04T08:13:10Z", + "scopes":[], + "strategies": [ + { + "id": 2, + "name": "default", + "parameters": {}, + "scopes": [ + { + "id": 2, + "environment_scope": "staging" + } + ] + } + ] + } +] diff --git a/testdata/update_project_feature_flag.json b/testdata/update_project_feature_flag.json new file mode 100644 index 000000000..f66b51ab2 --- /dev/null +++ b/testdata/update_project_feature_flag.json @@ -0,0 +1,36 @@ +{ + "name": "awesome_feature", + "description": null, + "active": true, + "version": "new_version_flag", + "created_at": "2020-05-13T20:10:32Z", + "updated_at": "2020-05-13T20:10:32Z", + "scopes": [], + "strategies": [ + { + "id": 38, + "name": "gradualRolloutUserId", + "parameters": { + "groupId": "default", + "percentage": "25" + }, + "scopes": [ + { + "id": 40, + "environment_scope": "staging" + } + ] + }, + { + "id": 37, + "name": "default", + "parameters": {}, + "scopes": [ + { + "id": 39, + "environment_scope": "production" + } + ] + } + ] +}