From 0b359521ec5bcea015d005db689fc425d1846ebd Mon Sep 17 00:00:00 2001 From: "adam.richards" Date: Fri, 18 Jun 2021 10:29:14 -0600 Subject: [PATCH 1/4] add api calls for projects --- clients/gitlab/client.go | 108 +++++++++++++----- .../gitlab/api/gitlab_api_adhoc_test.go | 32 +++--- 2 files changed, 98 insertions(+), 42 deletions(-) diff --git a/clients/gitlab/client.go b/clients/gitlab/client.go index c254baa..6ca9eb5 100644 --- a/clients/gitlab/client.go +++ b/clients/gitlab/client.go @@ -1,14 +1,36 @@ +/* +Copyright 2021. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + package gitlab // Facade built around go-gitlab // https://github.com/xanzy/go-gitlab // https://docs.gitlab.com/ee/api/api_resources.html +// https://docs.gitlab.com/ee/api/users.html +// https://docs.gitlab.com/ee/api/groups.html +// https://docs.gitlab.com/ee/api/projects.html +// https://docs.gitlab.com/ee/api/pipelines.html +// https://docs.gitlab.com/ee/api/container_registry.html +// https://docs.gitlab.com/ee/api/packages.html // https://pkg.go.dev/github.com/xanzy/go-gitlab?utm_source=godoc#section-documentation import ( "fmt" "github.com/romana/rlog" - gitlab "github.com/xanzy/go-gitlab" + gogitlab "github.com/xanzy/go-gitlab" "net/http" ) @@ -16,7 +38,7 @@ const itemsPerPage = 50 // Client - type Client struct { - client *gitlab.Client + client *gogitlab.Client token string apiURL string } @@ -27,11 +49,11 @@ func NewClient(apiURL string, token string, httpClient *http.Client) Client { // token gitlab user access token // httpClient optional injection of http client object to be used by gitlab api - var gitlabClient *gitlab.Client + var gitlabClient *gogitlab.Client if httpClient == nil { - gitlabClient, _ = gitlab.NewClient(token, gitlab.WithBaseURL(apiURL)) + gitlabClient, _ = gogitlab.NewClient(token, gogitlab.WithBaseURL(apiURL)) } else { - gitlabClient, _ = gitlab.NewClient(token, gitlab.WithBaseURL(apiURL), gitlab.WithHTTPClient(httpClient)) + gitlabClient, _ = gogitlab.NewClient(token, gogitlab.WithBaseURL(apiURL), gogitlab.WithHTTPClient(httpClient)) } rlog.Debug("Created new Client instance") @@ -43,15 +65,15 @@ func NewClient(apiURL string, token string, httpClient *http.Client) Client { } // GetUsers - -func (r Client) GetUsers(search *string) ([]*gitlab.User, error) { - listOptions := gitlab.ListOptions{ +func (r Client) GetUsers(search *string) ([]*gogitlab.User, error) { + listOptions := gogitlab.ListOptions{ Page: 1, PerPage: itemsPerPage, } // some valid values path, name var orderBy = "name" - var opts = gitlab.ListUsersOptions{ + var opts = gogitlab.ListUsersOptions{ ListOptions: listOptions, OrderBy: &orderBy, } @@ -61,7 +83,7 @@ func (r Client) GetUsers(search *string) ([]*gitlab.User, error) { opts.Search = search } - userList := []*gitlab.User{} + userList := []*gogitlab.User{} var more = true for more { users, res, err := r.client.Users.ListUsers(&opts) @@ -84,8 +106,8 @@ func (r Client) GetUsers(search *string) ([]*gitlab.User, error) { } // AddUser - -func (r Client) AddUser(user *gitlab.User, password string) (*gitlab.User, int, error) { - var opts = gitlab.CreateUserOptions{ +func (r Client) AddUser(user *gogitlab.User, password string) (*gogitlab.User, int, error) { + var opts = gogitlab.CreateUserOptions{ Username: &user.Username, Email: &user.Email, Name: &user.Name, @@ -101,11 +123,12 @@ func (r Client) AddUser(user *gitlab.User, password string) (*gitlab.User, int, // DeleteUser - func (r Client) DeleteUser(username string) (int, error) { - var opts = gitlab.ListUsersOptions{Username: &username} + var opts = gogitlab.ListUsersOptions{Username: &username} users, lres, err := r.client.Users.ListUsers(&opts) if err != nil { return lres.StatusCode, err } + // TODO: check return array has only one record res, err := r.client.Users.DeleteUser(users[0].ID) if err != nil { return res.StatusCode, err @@ -114,16 +137,27 @@ func (r Client) DeleteUser(username string) (int, error) { return res.StatusCode, nil } +// GetGroup - +func (r Client) GetGroup(groupID int) (*gogitlab.Group, int, error) { + group, res, err := r.client.Groups.GetGroup(groupID) + if err != nil { + return nil, 0, err + } + + rlog.Debug(fmt.Sprintf("Completed call to GetGroup. groupID: %d", group.ID)) + return group, res.StatusCode, nil +} + // GetGroups - -func (r Client) GetGroups(search *string) ([]*gitlab.Group, error) { - listOptions := gitlab.ListOptions{ +func (r Client) GetGroups(search *string) ([]*gogitlab.Group, error) { + listOptions := gogitlab.ListOptions{ Page: 1, PerPage: itemsPerPage, } // some valid values path, name var orderBy = "path" - var opts = gitlab.ListGroupsOptions{ + var opts = gogitlab.ListGroupsOptions{ ListOptions: listOptions, OrderBy: &orderBy, } @@ -132,7 +166,7 @@ func (r Client) GetGroups(search *string) ([]*gitlab.Group, error) { if search != nil { opts.Search = search } - groupList := []*gitlab.Group{} + groupList := []*gogitlab.Group{} var more = true for more { groups, res, err := r.client.Groups.ListGroups(&opts) @@ -155,10 +189,11 @@ func (r Client) GetGroups(search *string) ([]*gitlab.Group, error) { } // AddGroup - -func (r Client) AddGroup(group *gitlab.Group) (*gitlab.Group, int, error) { +func (r Client) AddGroup(group *gogitlab.Group) (*gogitlab.Group, int, error) { + // https://stackoverflow.com/questions/65081356/how-do-you-create-a-project-in-a-specific-group-via-gitlab-api // force visibility to private - var visibility = gitlab.PrivateVisibility - var opts = gitlab.CreateGroupOptions{Name: &group.Name, + var visibility = gogitlab.PrivateVisibility + var opts = gogitlab.CreateGroupOptions{Name: &group.Name, Path: &group.Path, Description: &group.Description, Visibility: &visibility} @@ -179,8 +214,8 @@ func (r Client) DeleteGroup(groupID int) (int, error) { } // GetProject - -func (r Client) GetProject(projectID int) (*gitlab.Project, error) { - opts := gitlab.GetProjectOptions{} +func (r Client) GetProject(projectID int) (*gogitlab.Project, error) { + opts := gogitlab.GetProjectOptions{} project, res, err := r.client.Projects.GetProject(projectID, &opts) if err != nil { return nil, err @@ -191,15 +226,15 @@ func (r Client) GetProject(projectID int) (*gitlab.Project, error) { } // GetProjects - -func (r Client) GetProjects(search *string) ([]*gitlab.Project, error) { - listOptions := gitlab.ListOptions{ +func (r Client) GetProjects(search *string) ([]*gogitlab.Project, error) { + listOptions := gogitlab.ListOptions{ Page: 1, PerPage: itemsPerPage, } // some valid values path, name var orderBy = "path" - var opts = gitlab.ListProjectsOptions{ + var opts = gogitlab.ListProjectsOptions{ ListOptions: listOptions, OrderBy: &orderBy, } @@ -208,10 +243,10 @@ func (r Client) GetProjects(search *string) ([]*gitlab.Project, error) { if search != nil { opts.Search = search } - projectList := []*gitlab.Project{} + projectList := []*gogitlab.Project{} var more = true for more { - opts := gitlab.ListProjectsOptions{} + opts := gogitlab.ListProjectsOptions{} projects, res, err := r.client.Projects.ListProjects(&opts) if err != nil { return nil, err @@ -230,3 +265,24 @@ func (r Client) GetProjects(search *string) ([]*gitlab.Project, error) { rlog.Debug(fmt.Sprintf("Completed call to GetProjectss. Records returned: %d", len(projectList))) return projectList, nil } + +// AddProject - +func (r Client) AddProject(project *gogitlab.Project, groupID int) (*gogitlab.Project, int, error) { + // force visibility to private + var visibility = gogitlab.PrivateVisibility + // copy customizable settings from argument, hard code other options as desired + var opts = gogitlab.CreateProjectOptions{ + Name: &project.Name, + Path: &project.Path, + Description: &project.Description, + NamespaceID: &groupID, + // hard coded values + Visibility: &visibility, + } + + newProject, res, err := r.client.Projects.CreateProject(&opts) + if err != nil { + return nil, res.StatusCode, err + } + return newProject, res.StatusCode, nil +} diff --git a/integration-tests/gitlab/api/gitlab_api_adhoc_test.go b/integration-tests/gitlab/api/gitlab_api_adhoc_test.go index c3f4cca..ce31e9b 100644 --- a/integration-tests/gitlab/api/gitlab_api_adhoc_test.go +++ b/integration-tests/gitlab/api/gitlab_api_adhoc_test.go @@ -22,7 +22,7 @@ func getClient_AdHoc() (gitlab.Client, error) { var client = gitlab.NewClient(gitlabAPIURL, gitlabAPIToken,nil) return client, nil } -func TestClient_AdHoc_getUsers(t *testing.T) { +func TestClient_AdHoc_getUsers_All(t *testing.T) { t.Run("test", func(t *testing.T) { c, err := getClient_AdHoc() if err != nil { @@ -36,11 +36,11 @@ func TestClient_AdHoc_getUsers(t *testing.T) { } t.Logf("GetUsers %d", len(got)) for x:=0;x Date: Fri, 18 Jun 2021 14:44:55 -0600 Subject: [PATCH 2/4] add subgroup support --- clients/gitlab/client.go | 72 ++++++++--- clients/gitlab/client_test.go | 2 +- .../gitlab/api/gitlab_api_adhoc_test.go | 60 +++++++++ .../gitlab/api/gitlab_api_test.go | 121 +++++++++++++++++- 4 files changed, 232 insertions(+), 23 deletions(-) diff --git a/clients/gitlab/client.go b/clients/gitlab/client.go index 6ca9eb5..2ed6a04 100644 --- a/clients/gitlab/client.go +++ b/clients/gitlab/client.go @@ -16,16 +16,24 @@ limitations under the License. package gitlab +/* // Facade built around go-gitlab -// https://github.com/xanzy/go-gitlab -// https://docs.gitlab.com/ee/api/api_resources.html -// https://docs.gitlab.com/ee/api/users.html -// https://docs.gitlab.com/ee/api/groups.html -// https://docs.gitlab.com/ee/api/projects.html -// https://docs.gitlab.com/ee/api/pipelines.html -// https://docs.gitlab.com/ee/api/container_registry.html -// https://docs.gitlab.com/ee/api/packages.html -// https://pkg.go.dev/github.com/xanzy/go-gitlab?utm_source=godoc#section-documentation +// CAUTIONS: + There may exists race conditions when deleting resources. + example: delete a group and then get group. + the delete is being processed by gitlab, but still may appear in + calls such as getGroup for a short time + References: + https://github.com/xanzy/go-gitlab + https://docs.gitlab.com/ee/api/api_resources.html + https://docs.gitlab.com/ee/api/users.html + https://docs.gitlab.com/ee/api/groups.html + https://docs.gitlab.com/ee/api/projects.html + https://docs.gitlab.com/ee/api/pipelines.html + https://docs.gitlab.com/ee/api/container_registry.html + https://docs.gitlab.com/ee/api/packages.html + https://pkg.go.dev/github.com/xanzy/go-gitlab?utm_source=godoc#section-documentation +*/ import ( "fmt" @@ -64,8 +72,25 @@ func NewClient(apiURL string, token string, httpClient *http.Client) Client { } } +// GetUser - +func (r Client) GetUser(userID int) (*gogitlab.User, int, error) { + getCustomeAttributes := true + opts := gogitlab.GetUsersOptions{ + WithCustomAttributes: &getCustomeAttributes, + } + user, res, err := r.client.Users.GetUser(userID, opts) + if err != nil { + return nil, 0, err + } + + rlog.Debug(fmt.Sprintf("Completed call to GetUser userID: %d", user.ID)) + return user, res.StatusCode, nil +} + // GetUsers - func (r Client) GetUsers(search *string) ([]*gogitlab.User, error) { + getCustomeAttributes := true + listOptions := gogitlab.ListOptions{ Page: 1, PerPage: itemsPerPage, @@ -74,8 +99,9 @@ func (r Client) GetUsers(search *string) ([]*gogitlab.User, error) { // some valid values path, name var orderBy = "name" var opts = gogitlab.ListUsersOptions{ - ListOptions: listOptions, - OrderBy: &orderBy, + ListOptions: listOptions, + OrderBy: &orderBy, + WithCustomAttributes: &getCustomeAttributes, } // if search defined add it to opts @@ -123,12 +149,18 @@ func (r Client) AddUser(user *gogitlab.User, password string) (*gogitlab.User, i // DeleteUser - func (r Client) DeleteUser(username string) (int, error) { + // expect return status code of http.StatusNoContent 204 or http.StatusNotFound 404 + var opts = gogitlab.ListUsersOptions{Username: &username} users, lres, err := r.client.Users.ListUsers(&opts) if err != nil { return lres.StatusCode, err } - // TODO: check return array has only one record + if len(users) == 0 { + statusCode := http.StatusNotFound + rlog.Debug(fmt.Sprintf("Completed call to DeleteUser. Status Code: %d", statusCode)) + return statusCode, nil + } res, err := r.client.Users.DeleteUser(users[0].ID) if err != nil { return res.StatusCode, err @@ -189,13 +221,18 @@ func (r Client) GetGroups(search *string) ([]*gogitlab.Group, error) { } // AddGroup - -func (r Client) AddGroup(group *gogitlab.Group) (*gogitlab.Group, int, error) { +func (r Client) AddGroup(group *gogitlab.Group, parentID *int) (*gogitlab.Group, int, error) { // https://stackoverflow.com/questions/65081356/how-do-you-create-a-project-in-a-specific-group-via-gitlab-api + // SubGroup will be created if a parentID is supplied // force visibility to private var visibility = gogitlab.PrivateVisibility - var opts = gogitlab.CreateGroupOptions{Name: &group.Name, - Path: &group.Path, Description: &group.Description, - Visibility: &visibility} + var opts = gogitlab.CreateGroupOptions{ + Name: &group.Name, + Path: &group.Path, + Description: &group.Description, + ParentID: parentID, + Visibility: &visibility, + } newGroup, res, err := r.client.Groups.CreateGroup(&opts) if err != nil { @@ -243,10 +280,10 @@ func (r Client) GetProjects(search *string) ([]*gogitlab.Project, error) { if search != nil { opts.Search = search } + projectList := []*gogitlab.Project{} var more = true for more { - opts := gogitlab.ListProjectsOptions{} projects, res, err := r.client.Projects.ListProjects(&opts) if err != nil { return nil, err @@ -260,7 +297,6 @@ func (r Client) GetProjects(search *string) ([]*gogitlab.Project, error) { } else { more = false } - } rlog.Debug(fmt.Sprintf("Completed call to GetProjectss. Records returned: %d", len(projectList))) return projectList, nil diff --git a/clients/gitlab/client_test.go b/clients/gitlab/client_test.go index 5185c49..e798863 100644 --- a/clients/gitlab/client_test.go +++ b/clients/gitlab/client_test.go @@ -390,7 +390,7 @@ func TestClient_AddGroup(t *testing.T) { token: tt.fields.token, apiURL: tt.fields.apiURL, } - got, _, err := r.AddGroup(tt.args.group) + got, _, err := r.AddGroup(tt.args.group, nil) if (err != nil) != tt.wantErr { t.Errorf("Client.AddGroup() error = %v, wantErr %v", err, tt.wantErr) return diff --git a/integration-tests/gitlab/api/gitlab_api_adhoc_test.go b/integration-tests/gitlab/api/gitlab_api_adhoc_test.go index ce31e9b..38aec6a 100644 --- a/integration-tests/gitlab/api/gitlab_api_adhoc_test.go +++ b/integration-tests/gitlab/api/gitlab_api_adhoc_test.go @@ -7,6 +7,8 @@ import ( "errors" "os" gitlab "valkyrie.dso.mil/valkyrie-api/clients/gitlab" + + gogitlab "github.com/xanzy/go-gitlab" ) // getClient_AdHoc - @@ -79,6 +81,23 @@ func TestClient_AdHoc_getUsers_None(t *testing.T) { }) } +func TestClient_AdHoc_deleteUser_None(t *testing.T) { + t.Run("test", func(t *testing.T) { + c, err := getClient_AdHoc() + if err != nil { + t.Errorf("Client.GetUsers() error = %v", err) + return + } + username := "notfound" + statusCode, err := c.DeleteUser(username) + if err != nil { + t.Errorf("Client.DeleteUser() error = %v", err) + return + } + t.Logf("DeleteUser success: %d", statusCode) + }) +} + func TestClient_AdHoc_getGroups_All(t *testing.T) { t.Run("test", func(t *testing.T) { c, err := getClient_AdHoc() @@ -118,6 +137,47 @@ func TestClient_AdHoc_getGroups_Search(t *testing.T) { }) } +func TestClient_AdHoc_addGroups_SubGroup(t *testing.T) { + t.Run("test", func(t *testing.T) { + c, err := getClient_AdHoc() + if err != nil { + t.Errorf("Client.getClient() error = %v", err) + return + } + s := string("adam-top-group") + groupList, err := c.GetGroups(&s) + if err != nil { + t.Errorf("Client.GetGroups() error = %v", err) + return + } + + if len(groupList) == 0 { + var statusCode int + + groupName := "adam root" + groupPath := "adam-root" + groupObj := gogitlab.Group{Name: groupName, Path: groupPath} + group, statusCode, err := c.AddGroup(&groupObj, nil) + if err != nil { + t.Errorf("AddGroup error = %v", err) + return + } + t.Logf("AddGroup: %s %d", group.FullPath, statusCode) + + groupName = "adam subgroup" + groupPath = "adam-subgroup" + groupObj = gogitlab.Group{Name: groupName, Path: groupPath} + subgroup, statusCode, err := c.AddGroup(&groupObj, &group.ID) + if err != nil { + t.Errorf("AddGroup error = %v", err) + return + } + t.Logf("AddGroup: %s %d", subgroup.FullPath, statusCode) + } + + }) +} + func TestClient_AdHoc_getProjects_ALL(t *testing.T) { t.Run("test", func(t *testing.T) { diff --git a/integration-tests/gitlab/api/gitlab_api_test.go b/integration-tests/gitlab/api/gitlab_api_test.go index 3f4e54e..84dcdc4 100644 --- a/integration-tests/gitlab/api/gitlab_api_test.go +++ b/integration-tests/gitlab/api/gitlab_api_test.go @@ -13,6 +13,7 @@ import ( ) const testItemCount = 65 + // getClient - func getClient() (gitlab.Client, error) { gitlabAPIURL, ok := os.LookupEnv("GITLAB_API_URL") @@ -23,7 +24,7 @@ func getClient() (gitlab.Client, error) { if !ok { return gitlab.Client{}, errors.New("env variable GITLAB_API_TOKEN undefinded") } - var client = gitlab.NewClient(gitlabAPIURL, gitlabAPIToken,nil) + var client = gitlab.NewClient(gitlabAPIURL, gitlabAPIToken, nil) return client, nil } @@ -136,7 +137,7 @@ func TestClient_addGroups(t *testing.T) { name := "test group " + fmt.Sprintf("%d", i) groupObj := gogitlab.Group{Name: name, Path: path} - got, statusCode, err := c.AddGroup(&groupObj) + got, statusCode, err := c.AddGroup(&groupObj, nil) if err != nil { t.Errorf("Client.AddGroup() error = %v %d", err, statusCode) @@ -198,9 +199,121 @@ func TestClient_getProjectsCount(t *testing.T) { } got, err := c.GetProjects(nil) if err != nil { - t.Errorf("Client.GetGroups() error = %v", err) + t.Errorf("Client.GetProjects() error = %v", err) + return + } + t.Logf("Projects - number of projects %d", len(got)) + }) +} + +func TestClient_addProjects1(t *testing.T) { + t.Run("test", func(t *testing.T) { + c, err := getClient() + if err != nil { + t.Errorf("Client.GetUsers() error = %v", err) + return + } + var group *gogitlab.Group + var statusCode int + + // find or create the group to hold projects + groupPath := "test-group-adam1" + groupName := "test group adam1" + groupList, err := c.GetGroups(&groupPath) + if err != nil { + t.Errorf("AddGroup error = %v", err) + return + } + + if len(groupList) == 0 { + groupObj := gogitlab.Group{Name: groupName, Path: groupPath} + group, statusCode, err = c.AddGroup(&groupObj, nil) + if err != nil { + t.Errorf("AddGroup error = %v", err) + return + } + t.Logf("AddGroup: %s %d", group.FullPath, statusCode) + } else { + group = groupList[0] + t.Logf("GetGroups: %s", group.FullPath) + } + + for i := 1; i <= testItemCount; i++ { + projectName := "test project " + fmt.Sprintf("%d", i) + + projectObj := gogitlab.Project{Name: projectName} + got, statusCode, err := c.AddProject(&projectObj, group.ID) + + if err != nil { + t.Errorf("Client.AddProject() error = %v %d", err, statusCode) + } else { + t.Logf("AddProject: [%s, %s, %d]", got.NameWithNamespace, got.HTTPURLToRepo, statusCode) + } + } + + }) +} + +func TestClient_addProjects2(t *testing.T) { + t.Run("test", func(t *testing.T) { + c, err := getClient() + if err != nil { + t.Errorf("Client.GetUsers() error = %v", err) + return + } + + var group *gogitlab.Group + var statusCode int + // find or create the group to hold projects + groupPath := "testX-group-adam2" + groupName := "testX group adam2" + groupList, err := c.GetGroups(&groupPath) + if err != nil { + t.Errorf("AddGroup error = %v", err) + return + } + + if len(groupList) == 0 { + groupObj := gogitlab.Group{Name: groupName, Path: groupPath} + group, statusCode, err = c.AddGroup(&groupObj, nil) + if err != nil { + t.Errorf("AddGroup error = %v", err) + return + } + t.Logf("AddGroup: %s %d", group.FullPath, statusCode) + } else { + group = groupList[0] + t.Logf("GetGroups: %s", group.FullPath) + } + + for i := 1; i <= testItemCount; i++ { + projectName := "test project " + fmt.Sprintf("%d", i) + + projectObj := gogitlab.Project{Name: projectName} + got, statusCode, err := c.AddProject(&projectObj, group.ID) + + if err != nil { + t.Errorf("Client.AddProject() error = %v %d", err, statusCode) + } else { + t.Logf("AddProject: [%s, %s, %d]", got.NameWithNamespace, got.HTTPURLToRepo, statusCode) + } + } + + }) +} + +func TestClient_getProjectsCount2(t *testing.T) { + t.Run("test", func(t *testing.T) { + c, err := getClient() + if err != nil { + t.Errorf("Client.getClient() error = %v", err) + return + } + got, err := c.GetProjects(nil) + if err != nil { + t.Errorf("Client.GetProjects() error = %v", err) return } t.Logf("Projects - number of projects %d", len(got)) }) -} \ No newline at end of file +} -- GitLab From a011b6d8794c30ec266bc4ffdb64aeec6b624b4e Mon Sep 17 00:00:00 2001 From: "adam.richards" Date: Mon, 21 Jun 2021 09:08:24 -0600 Subject: [PATCH 3/4] update README.md --- README.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 948202f..235747f 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,21 @@ ## Valkyrie Automated CICD Provisioning Tool ### Golang Frameworks -- rlog - - https://github.com/romana/rlog +- kubebuilder + - https://book.kubebuilder.io/introduction.html + - tutorial: https://itnext.io/building-an-operator-for-kubernetes-with-kubebuilder-17cbd3f07761 - go-gitlab - https://github.com/xanzy/go-gitlab - https://docs.gitlab.com/ee/api/api_resources.html - https://pkg.go.dev/github.com/xanzy/go-gitlab?utm_source=godoc#section-documentation - http-mock - https://github.com/jarcoal/httpmock - +- rlog + - https://github.com/romana/rlog +- Kubernetes Operator + - why: https://itnext.io/why-should-you-build-your-own-kubernetes-operator-f60c25df0b1c + - example: https://github.com/embano1/weatheroperator + - example: https://github.com/kudobuilder/operators ### lint fmt vet - run linter - `golint -set_exit_status ./... ` -- GitLab From a89438e96be0b3e7f4fd2ec12017b96c6ec05ec6 Mon Sep 17 00:00:00 2001 From: "adam.richards" Date: Mon, 21 Jun 2021 12:08:06 -0600 Subject: [PATCH 4/4] unit tests rlog conf --- README.md | 11 +- clients/gitlab/client.go | 13 +- clients/gitlab/client_test.go | 505 +++++++++++++++--- .../gitlab/api/gitlab_api_adhoc_test.go | 2 +- .../gitlab/api/gitlab_api_test.go | 2 +- rlog.conf | 10 + 6 files changed, 460 insertions(+), 83 deletions(-) create mode 100644 rlog.conf diff --git a/README.md b/README.md index 235747f..386bea0 100644 --- a/README.md +++ b/README.md @@ -16,11 +16,16 @@ - why: https://itnext.io/why-should-you-build-your-own-kubernetes-operator-f60c25df0b1c - example: https://github.com/embano1/weatheroperator - example: https://github.com/kudobuilder/operators -### lint fmt vet + +### Enable rlog configuration file +- set the environment variable RLOG_CONF_FILE +- example + - `export RLOG_CONF_FILE=$(pwd)/rlog.conf` +### Run Golang lint fmt vet +- use make + - `make fmt vet lint` - run linter - `golint -set_exit_status ./... ` -- use make - - `make fmt vet lint` ### Testing - run tests locally diff --git a/clients/gitlab/client.go b/clients/gitlab/client.go index 2ed6a04..94827ff 100644 --- a/clients/gitlab/client.go +++ b/clients/gitlab/client.go @@ -148,7 +148,17 @@ func (r Client) AddUser(user *gogitlab.User, password string) (*gogitlab.User, i } // DeleteUser - -func (r Client) DeleteUser(username string) (int, error) { +func (r Client) DeleteUser(userID int) (int, error) { + res, err := r.client.Users.DeleteUser(userID) + if err != nil { + return res.StatusCode, err + } + rlog.Debug(fmt.Sprintf("Completed call to DeleteUser. Status Code: %d", res.StatusCode)) + return res.StatusCode, nil +} + +// DeleteUserByUsername - convenience method +func (r Client) DeleteUserByUsername(username string) (int, error) { // expect return status code of http.StatusNoContent 204 or http.StatusNotFound 404 var opts = gogitlab.ListUsersOptions{Username: &username} @@ -161,6 +171,7 @@ func (r Client) DeleteUser(username string) (int, error) { rlog.Debug(fmt.Sprintf("Completed call to DeleteUser. Status Code: %d", statusCode)) return statusCode, nil } + res, err := r.client.Users.DeleteUser(users[0].ID) if err != nil { return res.StatusCode, err diff --git a/clients/gitlab/client_test.go b/clients/gitlab/client_test.go index e798863..5927ed3 100644 --- a/clients/gitlab/client_test.go +++ b/clients/gitlab/client_test.go @@ -48,6 +48,74 @@ func TestNewClient(t *testing.T) { } } +func TestClient_GetUser(t *testing.T) { + // setup a http client for use in mocking + testHTTPClient := &http.Client{} + httpmock.ActivateNonDefault(testHTTPClient) + defer httpmock.DeactivateAndReset() + + testUserID := 1 + testUsername := "testusername" + testUser := gogitlab.User{ID: testUserID, Username: testUsername} + + // use regex url matcher + httpmock.RegisterResponder("GET", + `=~^https://test/api/v4/users.*`, + httpmock.NewJsonResponderOrPanic(200, testUser), + ) + + // test objects + testAPIUrl := "https://test/api/v4/" + testToken := "token" + // create a gitlab Client object, inject http client to allow for mocking using httpmock + testGitlabClient, _ := gogitlab.NewClient(testToken, gogitlab.WithBaseURL(testAPIUrl), gogitlab.WithHTTPClient(testHTTPClient)) + + type fields struct { + client *gogitlab.Client + token string + apiURL string + } + type args struct { + userID int + } + tests := []struct { + name string + fields fields + args args + want *gogitlab.User + wantErr bool + }{ + { + name: "GetUser 1", + fields: fields{client: testGitlabClient, token: testToken, apiURL: testAPIUrl}, + args: args{userID: 1}, + want: &testUser, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := Client{ + client: tt.fields.client, + token: tt.fields.token, + apiURL: tt.fields.apiURL, + } + + user, statusCode, err := r.GetUser(tt.args.userID) + if (err != nil) != tt.wantErr { + t.Errorf("Client.GetUsers() error = %v, wantErr %v", err, tt.wantErr) + return + } + + t.Logf("user id %d, username %s statusCode: %d", user.ID, user.Username, statusCode) + + if !reflect.DeepEqual(user, tt.want) { + t.Errorf("Client.GetUsers() = %v, want %v", user, tt.want) + } + }) + } +} + func TestClient_GetUsers(t *testing.T) { // setup a http client for use in mocking testHTTPClient := &http.Client{} @@ -55,12 +123,18 @@ func TestClient_GetUsers(t *testing.T) { defer httpmock.DeactivateAndReset() // empty gitlab User array + testEmptyUserArray := []*gogitlab.User{} testUserArray := []*gogitlab.User{} testUser1 := gogitlab.User{ID: 1, Username: "joedirt"} testUser2 := gogitlab.User{ID: 2, Username: "spongebob"} testUserArray = append(testUserArray, &testUser1, &testUser2) // use regex url matcher + // ORDER MATTERS! more restrictive REGEX first + httpmock.RegisterResponder("GET", + `=~^https://test/api/v4/users.*EMPTY.*`, + httpmock.NewJsonResponderOrPanic(200, testEmptyUserArray), + ) httpmock.RegisterResponder("GET", `=~^https://test/api/v4/users.*`, httpmock.NewJsonResponderOrPanic(200, testUserArray), @@ -68,6 +142,7 @@ func TestClient_GetUsers(t *testing.T) { // test objects search := "searchstring" + testEmptySearch := "EMPTY" testAPIUrl := "https://test/api/v4/" testToken := "token" // create a gitlab Client object, inject http client to allow for mocking using httpmock @@ -102,6 +177,13 @@ func TestClient_GetUsers(t *testing.T) { want: testUserArray, wantErr: false, }, + { + name: "GetUsers Empty", + fields: fields{client: testGitlabClient, token: testToken, apiURL: testAPIUrl}, + args: args{search: &testEmptySearch}, + want: testEmptyUserArray, + wantErr: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -197,6 +279,73 @@ func TestClient_DeleteUser(t *testing.T) { httpmock.ActivateNonDefault(testHTTPClient) defer httpmock.DeactivateAndReset() + testUserID := 1 + testUsername := "testusername" + testUser := gogitlab.User{ID: testUserID, Username: testUsername} + testUserArray := []*gogitlab.User{} + testUserArray = append(testUserArray, &testUser) + + httpmock.RegisterResponder("DELETE", + `=~^https://test/api/v4/users.*`, + httpmock.NewJsonResponderOrPanic(202, testUser), + ) + + // test objects + testAPIUrl := "https://test/api/v4/" + testToken := "token" + // create a gitlab Client object, inject http client to allow for mocking using httpmock + testGitlabClient, _ := gogitlab.NewClient(testToken, gogitlab.WithBaseURL(testAPIUrl), gogitlab.WithHTTPClient(testHTTPClient)) + + type fields struct { + client *gogitlab.Client + token string + apiURL string + } + type args struct { + userID int + } + tests := []struct { + name string + fields fields + args args + want int + wantErr bool + }{ + { + name: "DeleteUser 1", + fields: fields{client: testGitlabClient, token: testToken, apiURL: testAPIUrl}, + args: args{userID: testUserID}, + want: 202, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := Client{ + client: tt.fields.client, + token: tt.fields.token, + apiURL: tt.fields.apiURL, + } + got, err := r.DeleteUser(tt.args.userID) + if (err != nil) != tt.wantErr { + t.Errorf("Client.DeleteUser() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("Client.DeleteUser() = %v, want %v", got, tt.want) + return + } + t.Logf("Client.DeleteUser() statusCode = %d ", got) + }) + } +} + +func TestClient_DeleteUserByUsername(t *testing.T) { + // setup a http client for use in mocking + testHTTPClient := &http.Client{} + httpmock.ActivateNonDefault(testHTTPClient) + defer httpmock.DeactivateAndReset() + testUser := gogitlab.User{ID: 1, Username: "joedirt"} testUserArray := []*gogitlab.User{} testUserArray = append(testUserArray, &testUser) @@ -247,20 +396,87 @@ func TestClient_DeleteUser(t *testing.T) { token: tt.fields.token, apiURL: tt.fields.apiURL, } - got, err := r.DeleteUser(tt.args.username) + got, err := r.DeleteUserByUsername(tt.args.username) if (err != nil) != tt.wantErr { - t.Errorf("Client.DeleteUser() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("Client.DeleteUserByUsername() error = %v, wantErr %v", err, tt.wantErr) return } if got != tt.want { - t.Errorf("Client.DeleteUser() = %v, want %v", got, tt.want) + t.Errorf("Client.DeleteUserByUsername() = %v, want %v", got, tt.want) return } - t.Logf("Client.DeleteUser() statusCode = %d ", got) + t.Logf("Client.DeleteUserByUsername() statusCode = %d ", got) }) } } +func TestClient_GetGroup(t *testing.T) { + // setup a http client for use in mocking + testHTTPClient := &http.Client{} + httpmock.ActivateNonDefault(testHTTPClient) + defer httpmock.DeactivateAndReset() + + testGroupID := 1 + testGroupName := "testgroupname" + testGroup := gogitlab.Group{ID: testGroupID, Name: testGroupName} + + // use regex url matcher + httpmock.RegisterResponder("GET", + `=~^https://test/api/v4/groups.*`, + httpmock.NewJsonResponderOrPanic(200, testGroup), + ) + + // test objects + testAPIUrl := "https://test/api/v4/" + testToken := "token" + // create a gitlab Client object, inject http client to allow for mocking using httpmock + testGitlabClient, _ := gogitlab.NewClient(testToken, gogitlab.WithBaseURL(testAPIUrl), gogitlab.WithHTTPClient(testHTTPClient)) + + type fields struct { + client *gogitlab.Client + token string + apiURL string + } + type args struct { + userID int + } + tests := []struct { + name string + fields fields + args args + want *gogitlab.Group + wantErr bool + }{ + { + name: "GetGroup 1", + fields: fields{client: testGitlabClient, token: testToken, apiURL: testAPIUrl}, + args: args{userID: 1}, + want: &testGroup, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := Client{ + client: tt.fields.client, + token: tt.fields.token, + apiURL: tt.fields.apiURL, + } + group, statusCode, err := r.GetGroup(tt.args.userID) + if (err != nil) != tt.wantErr { + t.Errorf("Client.GetGroup() error = %v, wantErr %v", err, tt.wantErr) + return + } + + t.Logf("user id %d, username %s statusCode: %d", group.ID, group.FullName, statusCode) + + if !reflect.DeepEqual(group, tt.want) { + t.Errorf("Client.GetUsers() = %v, want %v", group, tt.want) + } + }) + } +} func TestClient_GetGroups(t *testing.T) { // setup a http client for use in mocking testHTTPClient := &http.Client{} @@ -476,76 +692,211 @@ func TestClient_DeleteGroup(t *testing.T) { } } -// func TestClient_GetProject(t *testing.T) { -// type fields struct { -// client *gitlab.Client -// token string -// apiURL string -// } -// type args struct { -// projectID int -// } -// tests := []struct { -// name string -// fields fields -// args args -// want *gitlab.Project -// wantErr bool -// }{ -// // TODO: Add test cases. -// } -// for _, tt := range tests { -// t.Run(tt.name, func(t *testing.T) { -// r := Client{ -// client: tt.fields.client, -// token: tt.fields.token, -// apiURL: tt.fields.apiURL, -// } -// got, err := r.GetProject(tt.args.projectID) -// if (err != nil) != tt.wantErr { -// t.Errorf("Client.GetProject() error = %v, wantErr %v", err, tt.wantErr) -// return -// } -// if !reflect.DeepEqual(got, tt.want) { -// t.Errorf("Client.GetProject() = %v, want %v", got, tt.want) -// } -// }) -// } -// } - -// func TestClient_GetProjects(t *testing.T) { -// type fields struct { -// client *gitlab.Client -// token string -// apiURL string -// } -// type args struct { -// search *string -// } -// tests := []struct { -// name string -// fields fields -// args args -// want []*gitlab.Project -// wantErr bool -// }{ -// // TODO: Add test cases. -// } -// for _, tt := range tests { -// t.Run(tt.name, func(t *testing.T) { -// r := Client{ -// client: tt.fields.client, -// token: tt.fields.token, -// apiURL: tt.fields.apiURL, -// } -// got, err := r.GetProjects(tt.args.search) -// if (err != nil) != tt.wantErr { -// t.Errorf("Client.GetProjects() error = %v, wantErr %v", err, tt.wantErr) -// return -// } -// if !reflect.DeepEqual(got, tt.want) { -// t.Errorf("Client.GetProjects() = %v, want %v", got, tt.want) -// } -// }) -// } -// } +func TestClient_GetProject(t *testing.T) { + + // setup a http client for use in mocking + testHTTPClient := &http.Client{} + httpmock.ActivateNonDefault(testHTTPClient) + defer httpmock.DeactivateAndReset() + + testProjectID := 1 + testProjectName := "testProjectName" + testProject := gogitlab.Project{ID: testProjectID, Name: testProjectName} + + // use regex url matcher + httpmock.RegisterResponder("GET", + `=~^https://test/api/v4/projects.*`, + httpmock.NewJsonResponderOrPanic(200, testProject), + ) + + // test objects + testAPIUrl := "https://test/api/v4/" + testToken := "token" + // create a gitlab Client object, inject http client to allow for mocking using httpmock + testGitlabClient, _ := gogitlab.NewClient(testToken, gogitlab.WithBaseURL(testAPIUrl), gogitlab.WithHTTPClient(testHTTPClient)) + + type fields struct { + client *gogitlab.Client + token string + apiURL string + } + type args struct { + projectID int + } + tests := []struct { + name string + fields fields + args args + want *gogitlab.Project + wantErr bool + }{ + { + name: "GetGroup 1", + fields: fields{client: testGitlabClient, token: testToken, apiURL: testAPIUrl}, + args: args{projectID: 1}, + want: &testProject, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := Client{ + client: tt.fields.client, + token: tt.fields.token, + apiURL: tt.fields.apiURL, + } + got, err := r.GetProject(tt.args.projectID) + if (err != nil) != tt.wantErr { + t.Errorf("Client.GetProject() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Client.GetProject() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestClient_GetProjects(t *testing.T) { + // setup a http client for use in mocking + testHTTPClient := &http.Client{} + httpmock.ActivateNonDefault(testHTTPClient) + defer httpmock.DeactivateAndReset() + + // empty gitlab User array + testProjectArray := []*gogitlab.Project{} + testProject1 := gogitlab.Project{ID: 1, Name: "joedirt"} + testProject2 := gogitlab.Project{ID: 2, Name: "spongebob"} + testProjectArray = append(testProjectArray, &testProject1, &testProject2) + + // use regex url matcher + httpmock.RegisterResponder("GET", + `=~^https://test/api/v4/projects.*`, + httpmock.NewJsonResponderOrPanic(200, testProjectArray), + ) + // test objects + testAPIUrl := "https://test/api/v4/" + testToken := "token" + // create a gitlab Client object, inject http client to allow for mocking using httpmock + testGitlabClient, _ := gogitlab.NewClient(testToken, gogitlab.WithBaseURL(testAPIUrl), gogitlab.WithHTTPClient(testHTTPClient)) + + // test objects + search := "searchstring" + type fields struct { + client *gogitlab.Client + token string + apiURL string + } + type args struct { + search *string + } + tests := []struct { + name string + fields fields + args args + want []*gogitlab.Project + wantErr bool + }{ + { + name: "GetProjects 1", + fields: fields{client: testGitlabClient, token: testToken, apiURL: testAPIUrl}, + args: args{search: nil}, + want: testProjectArray, + wantErr: false, + }, + { + name: "GetProjects 2", + fields: fields{client: testGitlabClient, token: testToken, apiURL: testAPIUrl}, + args: args{search: &search}, + want: testProjectArray, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := Client{ + client: tt.fields.client, + token: tt.fields.token, + apiURL: tt.fields.apiURL, + } + got, err := r.GetProjects(tt.args.search) + if (err != nil) != tt.wantErr { + t.Errorf("Client.GetProjects() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Client.GetProjects() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestClient_AddProject(t *testing.T) { + + // setup a http client for use in mocking + testHTTPClient := &http.Client{} + httpmock.ActivateNonDefault(testHTTPClient) + defer httpmock.DeactivateAndReset() + + testProjectID := 1 + testProjectName := "testProjectName" + testProject := gogitlab.Project{ID: testProjectID, Name: testProjectName} + + httpmock.RegisterResponder("POST", + "https://test/api/v4/projects", + httpmock.NewJsonResponderOrPanic(200, testProject), + ) + + // test objects + testAPIUrl := "https://test/api/v4/" + testToken := "token" + // create a gitlab Client object, inject http client to allow for mocking using httpmock + testGitlabClient, _ := gogitlab.NewClient(testToken, gogitlab.WithBaseURL(testAPIUrl), gogitlab.WithHTTPClient(testHTTPClient)) + + type fields struct { + client *gogitlab.Client + token string + apiURL string + } + type args struct { + project *gogitlab.Project + groupID int + } + tests := []struct { + name string + fields fields + args args + want *gogitlab.Project + want1 int + wantErr bool + }{ + { + name: "AddGroup 1", + fields: fields{client: testGitlabClient, token: testToken, apiURL: testAPIUrl}, + args: args{project: &testProject}, + want1: 200, + want: &testProject, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := Client{ + client: tt.fields.client, + token: tt.fields.token, + apiURL: tt.fields.apiURL, + } + got, got1, err := r.AddProject(tt.args.project, tt.args.groupID) + if (err != nil) != tt.wantErr { + t.Errorf("Client.AddProject() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Client.AddProject() got = %v, want %v", got, tt.want) + } + if got1 != tt.want1 { + t.Errorf("Client.AddProject() got1 = %v, want %v", got1, tt.want1) + } + }) + } +} diff --git a/integration-tests/gitlab/api/gitlab_api_adhoc_test.go b/integration-tests/gitlab/api/gitlab_api_adhoc_test.go index 38aec6a..0f27134 100644 --- a/integration-tests/gitlab/api/gitlab_api_adhoc_test.go +++ b/integration-tests/gitlab/api/gitlab_api_adhoc_test.go @@ -89,7 +89,7 @@ func TestClient_AdHoc_deleteUser_None(t *testing.T) { return } username := "notfound" - statusCode, err := c.DeleteUser(username) + statusCode, err := c.DeleteUserByUsername(username) if err != nil { t.Errorf("Client.DeleteUser() error = %v", err) return diff --git a/integration-tests/gitlab/api/gitlab_api_test.go b/integration-tests/gitlab/api/gitlab_api_test.go index 84dcdc4..a0a3cfd 100644 --- a/integration-tests/gitlab/api/gitlab_api_test.go +++ b/integration-tests/gitlab/api/gitlab_api_test.go @@ -96,7 +96,7 @@ func TestClient_deleteUsers(t *testing.T) { for i := 1; i <= testItemCount; i++ { username := "testuser" + fmt.Sprintf("%d", i) - statusCode, err := c.DeleteUser(username) + statusCode, err := c.DeleteUserByUsername(username) if err != nil { t.Errorf("Client.DeleteUser() error = %v %d", err, statusCode) diff --git a/rlog.conf b/rlog.conf new file mode 100644 index 0000000..725cd1f --- /dev/null +++ b/rlog.conf @@ -0,0 +1,10 @@ +# Comment lines start with a '#' +# export RLOG_CONF_FILE= +RLOG_LOG_LEVEL = DEBUG +RLOG_LOG_STREAM = stdout +# time format - use ! to prevent override by environment variable +RLOG_TIME_FORMAT="2006/01/06 15:04:05.00" +# RLOG_TIME_FORMAT= UnixDate +# optional list caller information +RLOG_CALLER_INFO= 1 +# RLOG_LOG_FILE = /var/log/myapp.log -- GitLab