From 5f2aa98bee8b104840658c222766d56b9b26aebe Mon Sep 17 00:00:00 2001 From: Robert Cerven Date: Wed, 14 Aug 2024 21:37:01 +0200 Subject: [PATCH] New controller watching config maps named: image-controller-additional-users which creates team in quay.io for users in the config map, and grants permissions to repositories in all imageRepositories STONEBLD-2667 Signed-off-by: Robert Cerven --- config/rbac/role.yaml | 3 +- controllers/imagerepository_controller.go | 30 +- .../imagerepository_controller_test.go | 53 ++-- controllers/users_config_map_controller.go | 274 ++++++++++++++++++ main.go | 11 + pkg/quay/api.go | 18 ++ pkg/quay/quay.go | 242 +++++++++++++++- pkg/quay/quay_test.go | 2 +- pkg/quay/test_quay_client.go | 96 +++++- 9 files changed, 681 insertions(+), 48 deletions(-) create mode 100644 controllers/users_config_map_controller.go diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index d885cdc..dfa1a22 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -71,4 +71,5 @@ rules: - get - list - watch - + - patch + - update diff --git a/controllers/imagerepository_controller.go b/controllers/imagerepository_controller.go index 4831e4d..755cb4b 100644 --- a/controllers/imagerepository_controller.go +++ b/controllers/imagerepository_controller.go @@ -448,30 +448,30 @@ func (r *ImageRepositoryReconciler) GrantAdditionalRepositoryAccess(ctx context. log.Error(err, "failed to read config map with additional users", "ConfigMapName", additionalUsersConfigMapName, l.Action, l.ActionView) return err } - additionalUsersStr, usersExist := additionalUsersConfigMap.Data[additionalUsersConfigMapKey] + _, usersExist := additionalUsersConfigMap.Data[additionalUsersConfigMapKey] if !usersExist { log.Info("Config map with additional users doesn't have the key", "ConfigMapName", additionalUsersConfigMapName, "ConfigMapKey", additionalUsersConfigMapKey, l.Action, l.ActionView) return nil } - additionalUsers := strings.Fields(strings.TrimSpace(additionalUsersStr)) - log.Info("Additional users configured in config map", "AdditionalUsers", additionalUsers) - imageRepositoryName := imageRepository.Spec.Image.Name + teamName := fmt.Sprintf("%s-team", imageRepository.Namespace) - for _, user := range additionalUsers { - err := r.QuayClient.AddPermissionsForRepositoryToAccount(r.QuayOrganization, imageRepositoryName, user, false, false) - if err != nil { - if strings.Contains(err.Error(), "Invalid username:") { - log.Info("failed to add permissions for account, because it doesn't exist", "AccountName", user) - continue - } + // get team, if team doesn't exist it will be created, we don't care about users as that will be taken care of by config map controller + // so in this case if config map exists, team already exists as well with appropriate users + log.Info("Ensure team", "TeamName", teamName) + if _, err := r.QuayClient.EnsureTeam(r.QuayOrganization, teamName); err != nil { + log.Error(err, "failed to get or create team", "TeamName", teamName, l.Action, l.ActionView) + return err + } - log.Error(err, "failed to add permissions for account", "AccountName", user, l.Action, l.ActionUpdate, l.Audit, "true") - return err - } - log.Info("Additional user access was granted for", "UserName", user) + // add repo permission to the team + log.Info("Adding repository permission to the team", "TeamName", teamName, "RepositoryName", imageRepositoryName) + if err := r.QuayClient.AddReadPermissionsForRepositoryToTeam(r.QuayOrganization, imageRepositoryName, teamName); err != nil { + log.Error(err, "failed to grant repo permission to the team", "TeamName", teamName, "RepositoryName", imageRepositoryName, l.Action, l.ActionAdd) + return err } + return nil } diff --git a/controllers/imagerepository_controller_test.go b/controllers/imagerepository_controller_test.go index 5453d09..0e853c3 100644 --- a/controllers/imagerepository_controller_test.go +++ b/controllers/imagerepository_controller_test.go @@ -476,7 +476,7 @@ var _ = Describe("Image repository controller", func() { createServiceAccount(defaultNamespace, buildPipelineServiceAccountName) }) - assertProvisionRepository := func(updateComponentAnnotation bool, additionalUser string) { + assertProvisionRepository := func(updateComponentAnnotation, grantRepoPermission bool) { isCreateRepositoryInvoked := false quay.CreateRepositoryFunc = func(repository quay.RepositoryRequest) (*quay.Repository, error) { defer GinkgoRecover() @@ -506,22 +506,37 @@ var _ = Describe("Image repository controller", func() { defer GinkgoRecover() Expect(organization).To(Equal(quay.TestQuayOrg)) Expect(imageRepository).To(Equal(expectedImageName)) - - if isRobot { - Expect(strings.HasPrefix(accountName, expectedRobotAccountPrefix)).To(BeTrue()) - if strings.HasSuffix(accountName, "_pull") { - Expect(isWrite).To(BeFalse()) - isAddPullPermissionsToAccountInvoked = true - } else { - Expect(isWrite).To(BeTrue()) - isAddPushPermissionsToAccountInvoked = true - } - } else { - Expect(accountName).To(Equal(additionalUser)) + Expect(strings.HasPrefix(accountName, expectedRobotAccountPrefix)).To(BeTrue()) + if strings.HasSuffix(accountName, "_pull") { Expect(isWrite).To(BeFalse()) + isAddPullPermissionsToAccountInvoked = true + } else { + Expect(isWrite).To(BeTrue()) + isAddPushPermissionsToAccountInvoked = true } return nil } + isEnsureTeamInvoked := false + isAddReadPermissionsForRepositoryToTeamInvoked := false + if grantRepoPermission { + quay.EnsureTeamFunc = func(organization, teamName string) ([]quay.Member, error) { + defer GinkgoRecover() + Expect(organization).To(Equal(quay.TestQuayOrg)) + expectedTeamName := fmt.Sprintf("%s-team", resourceKey.Namespace) + Expect(teamName).To(Equal(expectedTeamName)) + isEnsureTeamInvoked = true + return nil, nil + } + quay.AddReadPermissionsForRepositoryToTeamFunc = func(organization, imageRepository, teamName string) error { + defer GinkgoRecover() + Expect(organization).To(Equal(quay.TestQuayOrg)) + Expect(imageRepository).To(Equal(expectedImageName)) + expectedTeamName := fmt.Sprintf("%s-team", resourceKey.Namespace) + Expect(teamName).To(Equal(expectedTeamName)) + isAddReadPermissionsForRepositoryToTeamInvoked = true + return nil + } + } isCreateNotificationInvoked := false quay.CreateNotificationFunc = func(organization, repository string, notification quay.Notification) (*quay.Notification, error) { isCreateNotificationInvoked = true @@ -574,6 +589,10 @@ var _ = Describe("Image repository controller", func() { Eventually(func() bool { return isAddPushPermissionsToAccountInvoked }, timeout, interval).Should(BeTrue()) Eventually(func() bool { return isAddPullPermissionsToAccountInvoked }, timeout, interval).Should(BeTrue()) Eventually(func() bool { return isCreateNotificationInvoked }, timeout, interval).Should(BeTrue()) + if grantRepoPermission { + Eventually(func() bool { return isEnsureTeamInvoked }, timeout, interval).Should(BeTrue()) + Eventually(func() bool { return isAddReadPermissionsForRepositoryToTeamInvoked }, timeout, interval).Should(BeTrue()) + } waitImageRepositoryFinalizerOnImageRepository(resourceKey) @@ -654,7 +673,7 @@ var _ = Describe("Image repository controller", func() { } It("should provision image repository for component, without update component annotation", func() { - assertProvisionRepository(false, "") + assertProvisionRepository(false, false) quay.DeleteRobotAccountFunc = func(organization, robotAccountName string) (bool, error) { return true, nil @@ -666,10 +685,10 @@ var _ = Describe("Image repository controller", func() { deleteImageRepository(resourceKey) }) - It("should provision image repository for component, with update component annotation and add additional user from config map", func() { + It("should provision image repository for component, with update component annotation and grant permission to team", func() { usersConfigMapKey := types.NamespacedName{Name: additionalUsersConfigMapName, Namespace: resourceKey.Namespace} createUsersConfigMap(usersConfigMapKey, []string{"user1"}) - assertProvisionRepository(true, "user1") + assertProvisionRepository(true, true) quay.DeleteRobotAccountFunc = func(organization, robotAccountName string) (bool, error) { return true, nil @@ -683,7 +702,7 @@ var _ = Describe("Image repository controller", func() { }) It("should provision image repository for component, with update component annotation", func() { - assertProvisionRepository(true, "") + assertProvisionRepository(true, false) }) It("should regenerate tokens and update secrets", func() { diff --git a/controllers/users_config_map_controller.go b/controllers/users_config_map_controller.go new file mode 100644 index 0000000..e4fdfd3 --- /dev/null +++ b/controllers/users_config_map_controller.go @@ -0,0 +1,274 @@ +/* +Copyright 2024 Red Hat, Inc. + +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 controllers + +import ( + "context" + "fmt" + "strings" + + "github.com/go-logr/logr" + imagerepositoryv1alpha1 "github.com/konflux-ci/image-controller/api/v1alpha1" + l "github.com/konflux-ci/image-controller/pkg/logs" + "github.com/konflux-ci/image-controller/pkg/quay" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/builder" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/event" + ctrllog "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/predicate" +) + +const ( + ConfigMapFinalizer = "appstudio.openshift.io/config-map" +) + +// ConfigMapReconciler reconciles a ConfigMap object with users +type ConfigMapReconciler struct { + client.Client + Scheme *runtime.Scheme + + QuayClient quay.QuayService + BuildQuayClient func(logr.Logger) quay.QuayService + QuayOrganization string +} + +// SetupWithManager sets up the controller with the Manager. +func (r *ConfigMapReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&corev1.ConfigMap{}, builder.WithPredicates(predicate.Funcs{ + CreateFunc: func(e event.CreateEvent) bool { + new, ok := e.Object.(*corev1.ConfigMap) + if !ok { + return false + } + return IsUsersConfigMap(new) + }, + UpdateFunc: func(e event.UpdateEvent) bool { + new, ok := e.ObjectNew.(*corev1.ConfigMap) + if !ok { + return false + } + return IsUsersConfigMap(new) + }, + DeleteFunc: func(e event.DeleteEvent) bool { + new, ok := e.Object.(*corev1.ConfigMap) + if !ok { + return false + } + return IsUsersConfigMap(new) + }, + GenericFunc: func(e event.GenericEvent) bool { + new, ok := e.Object.(*corev1.ConfigMap) + if !ok { + return false + } + return IsUsersConfigMap(new) + }, + })). + Complete(r) +} + +func IsUsersConfigMap(object client.Object) bool { + if configMap, ok := object.(*corev1.ConfigMap); ok { + if configMap.Name == additionalUsersConfigMapName { + return true + } + } + return false +} + +//+kubebuilder:rbac:groups=appstudio.redhat.com,resources=imagerepositories,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=core,resources=configmaps,verbs=get;list;watch;update + +func (r *ConfigMapReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + log := ctrllog.FromContext(ctx).WithName("ConfigMap") + ctx = ctrllog.IntoContext(ctx, log) + + // fetch the config map instance + configMap := &corev1.ConfigMap{} + if err := r.Client.Get(ctx, req.NamespacedName, configMap); err != nil { + if errors.IsNotFound(err) { + // The object is deleted, nothing to do + return ctrl.Result{}, nil + } + log.Error(err, "failed to get config map", l.Action, l.ActionView) + return ctrl.Result{}, err + } + + if !controllerutil.ContainsFinalizer(configMap, ConfigMapFinalizer) { + controllerutil.AddFinalizer(configMap, ConfigMapFinalizer) + + if err := r.Client.Update(ctx, configMap); err != nil { + log.Error(err, "failed to add finalizer to config map", "ConfigMapName", additionalUsersConfigMapName, "Finalizer", ConfigMapFinalizer, l.Action, l.ActionUpdate) + return ctrl.Result{}, err + } + log.Info("finalizer added to configmap") + // leave all other steps to reconcile from update action + return ctrl.Result{}, nil + } + + teamName := fmt.Sprintf("%s-team", configMap.Namespace) + removeTeam := false + + if !configMap.DeletionTimestamp.IsZero() { + removeTeam = true + log.Info("Config map with additional users was removed, will delete team", "TeamName", teamName) + + if controllerutil.ContainsFinalizer(configMap, ConfigMapFinalizer) { + controllerutil.RemoveFinalizer(configMap, ConfigMapFinalizer) + if err := r.Client.Update(ctx, configMap); err != nil { + log.Error(err, "failed to remove config map finalizer", "ConfigMapName", additionalUsersConfigMapName, "Finalizer", ConfigMapFinalizer, l.Action, l.ActionUpdate) + return ctrl.Result{}, err + } + log.Info("finalizer removed from config map", l.Action, l.ActionDelete) + } + } + + var additionalUsers []string + if !removeTeam { + // get additional users from config map + additionalUsersStr, usersExist := configMap.Data[additionalUsersConfigMapKey] + if !usersExist { + log.Info("Config map with additional users doesn't have the key", "ConfigMapName", additionalUsersConfigMapName, "ConfigMapKey", additionalUsersConfigMapKey, l.Action, l.ActionView) + removeTeam = true + } else { + additionalUsers = strings.Fields(strings.TrimSpace(additionalUsersStr)) + log.Info("Additional users configured in config map", "AdditionalUsers", additionalUsers) + } + } + + // reread quay token + r.QuayClient = r.BuildQuayClient(log) + + // remove team if config map was removed, renamed, or doesn't contain any users + if removeTeam { + log.Info("Will remove team", "TeamName", teamName) + if err := r.QuayClient.DeleteTeam(r.QuayOrganization, teamName); err != nil { + log.Error(err, "failed to remove team", "TeamName", teamName, l.Action, l.ActionDelete) + return ctrl.Result{}, err + } + return ctrl.Result{}, nil + } + + // fetch image repositories in the namespace + imageRepositoryList := &imagerepositoryv1alpha1.ImageRepositoryList{} + if err := r.Client.List(ctx, imageRepositoryList, &client.ListOptions{Namespace: configMap.Namespace}); err != nil { + log.Error(err, "failed to list ImageRepositories", l.Action, l.ActionView) + return ctrl.Result{}, err + } + + // get image repositories names + repositoriesToAdd := []string{} + for _, repository := range imageRepositoryList.Items { + repositoriesToAdd = append(repositoriesToAdd, repository.Spec.Image.Name) + } + + // get team members, if team doesn't exist it will be created + log.Info("Ensure team", "TeamName", teamName) + teamMembers, err := r.QuayClient.EnsureTeam(r.QuayOrganization, teamName) + if err != nil { + log.Error(err, "failed to get team members", "TeamName", teamName, l.Action, l.ActionView) + return ctrl.Result{}, err + } + // get users in the team + usersInTeam := []string{} + for _, user := range teamMembers { + usersInTeam = append(usersInTeam, user.Name) + } + log.Info("Users in the team", "TeamName", teamName, "Users", usersInTeam) + + // get team permissions + teamPermissions, err := r.QuayClient.ListPermissionsForTeam(r.QuayOrganization, teamName) + if err != nil { + log.Error(err, "failed to get team permissions", "TeamName", teamName, l.Action, l.ActionView) + return ctrl.Result{}, err + } + // get repositories for which team has permissions + reposTeamPermissions := []string{} + for _, repoPermission := range teamPermissions { + reposTeamPermissions = append(reposTeamPermissions, repoPermission.Repository.Name) + } + log.Info("Team has repository permissions", "TeamName", teamName, "Repositories", reposTeamPermissions) + + // grant repo permissions to the team + for _, repoToUpdate := range repositoriesToAdd { + shouldGrantPermission := true + for _, repoWithPermission := range reposTeamPermissions { + if repoToUpdate == repoWithPermission { + shouldGrantPermission = false + break + } + } + + if shouldGrantPermission { + log.Info("Grant repository permission to the team", "TeamName", teamName, "RepositoryName", repoToUpdate) + if err := r.QuayClient.AddReadPermissionsForRepositoryToTeam(r.QuayOrganization, repoToUpdate, teamName); err != nil { + log.Error(err, "failed to grant repo permission to the team", "TeamName", teamName, "RepositoryName", repoToUpdate, l.Action, l.ActionAdd) + return ctrl.Result{}, err + } + } + } + + // add users to the team + for _, userToAdd := range additionalUsers { + shouldAdd := true + for _, userInTeam := range usersInTeam { + if userToAdd == userInTeam { + shouldAdd = false + break + } + } + + if shouldAdd { + log.Info("Add user to the team", "TeamName", teamName, "UserName", userToAdd) + if permanentError, err := r.QuayClient.AddUserToTeam(r.QuayOrganization, teamName, userToAdd); err != nil { + if !permanentError { + log.Error(err, "failed to add user to the team", "TeamName", teamName, "UserName", userToAdd, l.Action, l.ActionAdd) + return ctrl.Result{}, err + } + // if user doesn't exist just log we don't have to fail because of that + log.Info(err.Error()) + } + } + } + + // remove users from the team + for _, userInTeam := range usersInTeam { + shouldRemove := true + + for _, userToAdd := range additionalUsers { + if userInTeam == userToAdd { + shouldRemove = false + } + } + + if shouldRemove { + log.Info("Remove user from the team", "TeamName", teamName, "UserName", userInTeam) + if err := r.QuayClient.RemoveUserFromTeam(r.QuayOrganization, teamName, userInTeam); err != nil { + log.Error(err, "failed to remove user from the team", "TeamName", teamName, "UserName", userInTeam, l.Action, l.ActionDelete) + return ctrl.Result{}, err + } + } + } + + return ctrl.Result{}, nil +} diff --git a/main.go b/main.go index ec1cb0f..fa9193a 100644 --- a/main.go +++ b/main.go @@ -158,6 +158,17 @@ func main() { setupLog.Error(err, "unable to create controller", "controller", "ImageRepository") os.Exit(1) } + + if err = (&controllers.ConfigMapReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + BuildQuayClient: buildQuayClientFunc, + QuayOrganization: quayOrganization, + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "ConfigMap") + os.Exit(1) + } + //+kubebuilder:scaffold:builder if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { diff --git a/pkg/quay/api.go b/pkg/quay/api.go index ad8c850..53448a9 100644 --- a/pkg/quay/api.go +++ b/pkg/quay/api.go @@ -34,6 +34,7 @@ type RepositoryRequest struct { Description string `json:"description"` //Kind string `json:"repo_kind"` } + type RobotAccount struct { Description string `json:"description"` Created string `json:"created"` @@ -51,6 +52,23 @@ type UserAccount struct { IsOrgMember bool `json:"is_org_member"` } +type TeamPermission struct { + Repository TeamPermissionRepository `json:"repository"` + Role string `json:"role"` +} + +type TeamPermissionRepository struct { + Name string `json:"name"` + IsPublic bool `json:"is_public"` +} + +type Member struct { + Name string `json:"name"` + Kind string `json:"kind"` + IsRobot bool `json:"is_robot"` + Invited bool `json:"invited"` +} + // Quay API can sometimes return {"error": "..."} and sometimes {"error_message": "..."} without the field error // In some cases the error is send alongside the response in the {"message": "..."} field. type QuayError struct { diff --git a/pkg/quay/quay.go b/pkg/quay/quay.go index cff67de..d7a7404 100644 --- a/pkg/quay/quay.go +++ b/pkg/quay/quay.go @@ -35,7 +35,14 @@ type QuayService interface { CreateRobotAccount(organization string, robotName string) (*RobotAccount, error) DeleteRobotAccount(organization string, robotName string) (bool, error) AddPermissionsForRepositoryToAccount(organization, imageRepository, accountName string, isRobot, isWrite bool) error + AddReadPermissionsForRepositoryToTeam(organization, imageRepository, teamName string) error ListPermissionsForRepository(organization, imageRepository string) (map[string]UserAccount, error) + ListPermissionsForTeam(organization, teamName string) ([]TeamPermission, error) + AddUserToTeam(organization, teamName, userName string) (bool, error) + RemoveUserFromTeam(organization, teamName, userName string) error + DeleteTeam(organization, teamName string) error + EnsureTeam(organization, teamName string) ([]Member, error) + GetTeamMembers(organization, teamName string) ([]Member, error) RegenerateRobotAccountToken(organization string, robotName string) (*RobotAccount, error) GetAllRepositories(organization string) ([]Repository, error) GetAllRobotAccounts(organization string) ([]RobotAccount, error) @@ -369,8 +376,18 @@ func (c *QuayClient) ListPermissionsForRepository(organization, imageRepository if err != nil { return nil, fmt.Errorf("failed to Do request, error: %s", err) } + if resp.GetStatusCode() != 200 { - return nil, fmt.Errorf("error getting permissions, got status code %d", resp.GetStatusCode()) + var message string + data := &QuayError{} + if err := resp.GetJson(data); err == nil { + if data.ErrorMessage != "" { + message = data.ErrorMessage + } else { + message = data.Error + } + } + return nil, fmt.Errorf("failed to get permissions for repository: %s, got status code %d, message: %s", imageRepository, resp.GetStatusCode(), message) } type Response struct { @@ -384,6 +401,204 @@ func (c *QuayClient) ListPermissionsForRepository(organization, imageRepository return response.Permissions, nil } +// ListPermissionsForTeam list permissions for the given team +func (c *QuayClient) ListPermissionsForTeam(organization, teamName string) ([]TeamPermission, error) { + url := fmt.Sprintf("%s/organization/%s/team/%s/permissions", c.url, organization, teamName) + + resp, err := c.doRequest(url, http.MethodGet, nil) + + if err != nil { + return nil, fmt.Errorf("failed to Do request, error: %s", err) + } + + if resp.GetStatusCode() != 200 { + var message string + data := &QuayError{} + if err := resp.GetJson(data); err == nil { + if data.ErrorMessage != "" { + message = data.ErrorMessage + } else { + message = data.Error + } + } + return nil, fmt.Errorf("failed to get permissions for team: %s, got status code %d, message: %s", teamName, resp.GetStatusCode(), message) + } + + type Response struct { + Permissions []TeamPermission `json:"permissions"` + } + var response Response + if err := resp.GetJson(&response); err != nil { + return nil, err + } + + return response.Permissions, nil +} + +// AddUserToTeam adds user to the given team +// bool return value is indicating if it is permanent error (user doesn't exist) +func (c *QuayClient) AddUserToTeam(organization, teamName, userName string) (bool, error) { + url := fmt.Sprintf("%s/organization/%s/team/%s/members/%s", c.url, organization, teamName, userName) + + resp, err := c.doRequest(url, http.MethodPut, nil) + if err != nil { + return false, fmt.Errorf("failed to Do request, error: %s", err) + } + + if resp.GetStatusCode() != 200 { + // 400 is returned when user doesn't exist + if resp.GetStatusCode() == 400 { + return true, fmt.Errorf("failed to add user: %s, to the team team: %s, user doesn't exist", userName, teamName) + } + + var message string + data := &QuayError{} + if err := resp.GetJson(data); err == nil { + if data.ErrorMessage != "" { + message = data.ErrorMessage + } else { + message = data.Error + } + } + return false, fmt.Errorf("failed to add user: %s, to the team team: %s, got status code %d, message: %s", userName, teamName, resp.GetStatusCode(), message) + } + return false, nil +} + +// RemoveUserToTeam remove user from the given team +func (c *QuayClient) RemoveUserFromTeam(organization, teamName, userName string) error { + url := fmt.Sprintf("%s/organization/%s/team/%s/members/%s", c.url, organization, teamName, userName) + + resp, err := c.doRequest(url, http.MethodDelete, nil) + if err != nil { + return fmt.Errorf("failed to Do request, error: %s", err) + } + + // 400 is returned when user isn't already in the team + // 404 is returned when user doesn't exist + if resp.GetStatusCode() == 204 || resp.GetStatusCode() == 404 || resp.GetStatusCode() == 400 { + return nil + } + + var message string + data := &QuayError{} + if err := resp.GetJson(data); err == nil { + if data.ErrorMessage != "" { + message = data.ErrorMessage + } else { + message = data.Error + } + } + return fmt.Errorf("failed to remove user: %s, from the team team: %s, got status code %d, message: %s", userName, teamName, resp.GetStatusCode(), message) +} + +func (c *QuayClient) DeleteTeam(organization, teamName string) error { + url := fmt.Sprintf("%s/organization/%s/team/%s", c.url, organization, teamName) + + resp, err := c.doRequest(url, http.MethodDelete, nil) + if err != nil { + return fmt.Errorf("failed to Do request, error: %s", err) + } + + // 400 is returned when team doesn't exist + // 404 just in case + if resp.GetStatusCode() == 204 || resp.GetStatusCode() == 404 || resp.GetStatusCode() == 400 { + return nil + } + + var message string + data := &QuayError{} + if err := resp.GetJson(data); err == nil { + if data.ErrorMessage != "" { + message = data.ErrorMessage + } else { + message = data.Error + } + } + return fmt.Errorf("failed to remove team: %s, got status code %d, message: %s", teamName, resp.GetStatusCode(), message) +} + +// EnsureTeam ensures that team exists, if it doesn't it will create it +// returns list of team members +func (c *QuayClient) EnsureTeam(organization, teamName string) ([]Member, error) { + members, err := c.GetTeamMembers(organization, teamName) + + if err != nil { + return nil, err + } + // team exists + if members != nil { + return members, nil + } + + // create team + url := fmt.Sprintf("%s/organization/%s/team/%s", c.url, organization, teamName) + body := strings.NewReader(`{"role": "member"}`) + + resp, err := c.doRequest(url, http.MethodPut, body) + if err != nil { + return nil, fmt.Errorf("failed to Do request, error: %s", err) + } + + if resp.GetStatusCode() != 200 { + var message string + data := &QuayError{} + if err := resp.GetJson(data); err == nil { + if data.ErrorMessage != "" { + message = data.ErrorMessage + } else { + message = data.Error + } + } + return nil, fmt.Errorf("failed to create team: %s, got status code %d, message: %s", teamName, resp.GetStatusCode(), message) + } + + members, err = c.GetTeamMembers(organization, teamName) + if err != nil { + return nil, err + } + return members, nil +} + +// GetTeamMembers gets members of the team, when nil is returned that means that team doesn't exist +func (c *QuayClient) GetTeamMembers(organization, teamName string) ([]Member, error) { + url := fmt.Sprintf("%s/organization/%s/team/%s/members", c.url, organization, teamName) + + resp, err := c.doRequest(url, http.MethodGet, nil) + + if err != nil { + return nil, fmt.Errorf("failed to Do request, error: %s", err) + } + if resp.GetStatusCode() != 200 { + // team doesn't exist + if resp.GetStatusCode() == 404 { + return nil, nil + } + + var message string + data := &QuayError{} + if err := resp.GetJson(data); err == nil { + if data.ErrorMessage != "" { + message = data.ErrorMessage + } else { + message = data.Error + } + } + return nil, fmt.Errorf("failed to get team members for team: %s, got status code %d, message: %s", teamName, resp.GetStatusCode(), message) + } + + type Response struct { + Members []Member `json:"members"` + } + var response Response + + if err := resp.GetJson(&response); err != nil { + return nil, err + } + + return response.Members, nil +} + // AddPermissionsForRepositoryToAccount allows given account to access to the given repository. // If isWrite is true, then pull and push permissions are added, otherwise - pull access only. func (c *QuayClient) AddPermissionsForRepositoryToAccount(organization, imageRepository, accountName string, isRobot, isWrite bool) error { @@ -426,6 +641,31 @@ func (c *QuayClient) AddPermissionsForRepositoryToAccount(organization, imageRep return nil } +// AddReadPermissionsForRepositoryToTeam allows given team read access to the given repository. +func (c *QuayClient) AddReadPermissionsForRepositoryToTeam(organization, imageRepository, teamName string) error { + url := fmt.Sprintf("%s/repository/%s/%s/permissions/team/%s", c.url, organization, imageRepository, teamName) + body := strings.NewReader(`{"role": "read"}`) + + resp, err := c.doRequest(url, http.MethodPut, body) + if err != nil { + return err + } + + if resp.GetStatusCode() != 200 { + var message string + data := &QuayError{} + if err := resp.GetJson(data); err == nil { + if data.ErrorMessage != "" { + message = data.ErrorMessage + } else { + message = data.Error + } + } + return fmt.Errorf("failed to add permissions to the team: %s. Status code: %d, message: %s", teamName, resp.GetStatusCode(), message) + } + return nil +} + func (c *QuayClient) RegenerateRobotAccountToken(organization string, robotName string) (*RobotAccount, error) { url := fmt.Sprintf("%s/organization/%s/robots/%s/regenerate", c.url, organization, robotName) diff --git a/pkg/quay/quay_test.go b/pkg/quay/quay_test.go index 969c687..386c18b 100644 --- a/pkg/quay/quay_test.go +++ b/pkg/quay/quay_test.go @@ -549,7 +549,7 @@ func TestQuayClient_ListPermisssionsForRepository(t *testing.T) { { name: "server does not respond 200", statusCode: 400, - expectedErr: "error getting permissions", + expectedErr: "failed to get permissions", responseData: "", expectedPermissions: nil, }, diff --git a/pkg/quay/test_quay_client.go b/pkg/quay/test_quay_client.go index 201ff39..a3efaf1 100644 --- a/pkg/quay/test_quay_client.go +++ b/pkg/quay/test_quay_client.go @@ -30,19 +30,26 @@ type TestQuayClient struct{} var _ QuayService = (*TestQuayClient)(nil) var ( - CreateRepositoryFunc func(repository RepositoryRequest) (*Repository, error) - DeleteRepositoryFunc func(organization, imageRepository string) (bool, error) - ChangeRepositoryVisibilityFunc func(organization, imageRepository string, visibility string) error - GetRobotAccountFunc func(organization string, robotName string) (*RobotAccount, error) - CreateRobotAccountFunc func(organization string, robotName string) (*RobotAccount, error) - DeleteRobotAccountFunc func(organization string, robotName string) (bool, error) - AddPermissionsForRepositoryToAccountFunc func(organization, imageRepository, accountName string, isRobot, isWrite bool) error - ListPermissionsForRepositoryFunc func(organization, imageRepository string) (map[string]UserAccount, error) - RegenerateRobotAccountTokenFunc func(organization string, robotName string) (*RobotAccount, error) - GetNotificationsFunc func(organization, repository string) ([]Notification, error) - CreateNotificationFunc func(organization, repository string, notification Notification) (*Notification, error) - UpdateNotificationFunc func(organization, repository string, notificationUuid string, notification Notification) (*Notification, error) - DeleteNotificationFunc func(organization, repository string, notificationUuid string) (bool, error) + CreateRepositoryFunc func(repository RepositoryRequest) (*Repository, error) + DeleteRepositoryFunc func(organization, imageRepository string) (bool, error) + ChangeRepositoryVisibilityFunc func(organization, imageRepository string, visibility string) error + GetRobotAccountFunc func(organization string, robotName string) (*RobotAccount, error) + CreateRobotAccountFunc func(organization string, robotName string) (*RobotAccount, error) + DeleteRobotAccountFunc func(organization string, robotName string) (bool, error) + AddPermissionsForRepositoryToAccountFunc func(organization, imageRepository, accountName string, isRobot, isWrite bool) error + AddReadPermissionsForRepositoryToTeamFunc func(organization, imageRepository, teamName string) error + ListPermissionsForRepositoryFunc func(organization, imageRepository string) (map[string]UserAccount, error) + ListPermissionsForTeamFunc func(organization, teamName string) ([]TeamPermission, error) + AddUserToTeamFunc func(organization, teamName, userName string) (bool, error) + RemoveUserFromTeamFunc func(organization, teamName, userName string) error + DeleteTeamFunc func(organization, teamName string) error + EnsureTeamFunc func(organization, teamName string) ([]Member, error) + GetTeamMembersFunc func(organization, teamName string) ([]Member, error) + RegenerateRobotAccountTokenFunc func(organization string, robotName string) (*RobotAccount, error) + GetNotificationsFunc func(organization, repository string) ([]Notification, error) + CreateNotificationFunc func(organization, repository string, notification Notification) (*Notification, error) + UpdateNotificationFunc func(organization, repository string, notificationUuid string, notification Notification) (*Notification, error) + DeleteNotificationFunc func(organization, repository string, notificationUuid string) (bool, error) ) func ResetTestQuayClient() { @@ -53,7 +60,14 @@ func ResetTestQuayClient() { CreateRobotAccountFunc = func(organization, robotName string) (*RobotAccount, error) { return &RobotAccount{}, nil } DeleteRobotAccountFunc = func(organization, robotName string) (bool, error) { return true, nil } AddPermissionsForRepositoryToAccountFunc = func(organization, imageRepository, accountName string, isRobot, isWrite bool) error { return nil } + AddReadPermissionsForRepositoryToTeamFunc = func(organization, imageRepository, teamName string) error { return nil } ListPermissionsForRepositoryFunc = func(organization, imageRepository string) (map[string]UserAccount, error) { return nil, nil } + ListPermissionsForTeamFunc = func(organization, teamName string) ([]TeamPermission, error) { return []TeamPermission{}, nil } + AddUserToTeamFunc = func(organization, teamName, userName string) (bool, error) { return false, nil } + RemoveUserFromTeamFunc = func(organization, teamName, userName string) error { return nil } + DeleteTeamFunc = func(organization, teamName string) error { return nil } + EnsureTeamFunc = func(organization, teamName string) ([]Member, error) { return []Member{}, nil } + GetTeamMembersFunc = func(organization, teamName string) ([]Member, error) { return []Member{}, nil } RegenerateRobotAccountTokenFunc = func(organization, robotName string) (*RobotAccount, error) { return &RobotAccount{}, nil } GetNotificationsFunc = func(organization, repository string) ([]Notification, error) { return []Notification{}, nil } CreateNotificationFunc = func(organization, repository string, notification Notification) (*Notification, error) { @@ -103,11 +117,46 @@ func ResetTestQuayClientToFails() { Fail("AddPermissionsForRepositoryToAccount invoked") return nil } + AddReadPermissionsForRepositoryToTeamFunc = func(organization, imageRepository, teamName string) error { + defer GinkgoRecover() + Fail("AddPermissionsForRepositoryToTeam invoked") + return nil + } ListPermissionsForRepositoryFunc = func(organization, imageRepository string) (map[string]UserAccount, error) { defer GinkgoRecover() Fail("ListPermissionsForRepository invoked") return nil, nil } + ListPermissionsForTeamFunc = func(organization, teamName string) ([]TeamPermission, error) { + defer GinkgoRecover() + Fail("ListPermissionsForTeam invoked") + return nil, nil + } + AddUserToTeamFunc = func(organization, teamName, userName string) (bool, error) { + defer GinkgoRecover() + Fail("AddUserToTeam invoked") + return false, nil + } + RemoveUserFromTeamFunc = func(organization, teamName, userName string) error { + defer GinkgoRecover() + Fail("RemoveUserFromTeam invoked") + return nil + } + DeleteTeamFunc = func(organization, teamName string) error { + defer GinkgoRecover() + Fail("DeleteTeam invoked") + return nil + } + EnsureTeamFunc = func(organization, teamName string) ([]Member, error) { + defer GinkgoRecover() + Fail("EnsureTeam invoked") + return nil, nil + } + GetTeamMembersFunc = func(organization, teamName string) ([]Member, error) { + defer GinkgoRecover() + Fail("GetTeamMembers invoked") + return nil, nil + } RegenerateRobotAccountTokenFunc = func(organization, robotName string) (*RobotAccount, error) { defer GinkgoRecover() Fail("RegenerateRobotAccountToken invoked") @@ -156,9 +205,30 @@ func (c TestQuayClient) DeleteRobotAccount(organization string, robotName string func (c TestQuayClient) AddPermissionsForRepositoryToAccount(organization, imageRepository, accountName string, isRobot, isWrite bool) error { return AddPermissionsForRepositoryToAccountFunc(organization, imageRepository, accountName, isRobot, isWrite) } +func (c TestQuayClient) AddReadPermissionsForRepositoryToTeam(organization, imageRepository, teamName string) error { + return AddReadPermissionsForRepositoryToTeamFunc(organization, imageRepository, teamName) +} func (c TestQuayClient) ListPermissionsForRepository(organization, imageRepository string) (map[string]UserAccount, error) { return ListPermissionsForRepositoryFunc(organization, imageRepository) } +func (c TestQuayClient) ListPermissionsForTeam(organization, teamName string) ([]TeamPermission, error) { + return ListPermissionsForTeamFunc(organization, teamName) +} +func (c TestQuayClient) AddUserToTeam(organization, teamName, userName string) (bool, error) { + return AddUserToTeamFunc(organization, teamName, userName) +} +func (c TestQuayClient) RemoveUserFromTeam(organization, teamName, userName string) error { + return RemoveUserFromTeamFunc(organization, teamName, userName) +} +func (c TestQuayClient) DeleteTeam(organization, teamName string) error { + return DeleteTeamFunc(organization, teamName) +} +func (c TestQuayClient) EnsureTeam(organization, teamName string) ([]Member, error) { + return EnsureTeamFunc(organization, teamName) +} +func (c TestQuayClient) GetTeamMembers(organization, teamName string) ([]Member, error) { + return GetTeamMembersFunc(organization, teamName) +} func (c TestQuayClient) RegenerateRobotAccountToken(organization string, robotName string) (*RobotAccount, error) { return RegenerateRobotAccountTokenFunc(organization, robotName) }