From 2464d43e8a4381213b419a0ee742ede9b57e35ba Mon Sep 17 00:00:00 2001 From: Robert Cerven Date: Wed, 7 Aug 2024 18:12:18 +0200 Subject: [PATCH] Grant read access to additional users on image repository creation, based on config map in the user namespace named "image-controller-additional-users" STONEBLD-2666 Signed-off-by: Robert Cerven --- config/rbac/role.yaml | 8 ++ controllers/imagerepository_controller.go | 55 +++++++++++++- .../imagerepository_controller_test.go | 73 +++++++++++++------ controllers/suite_util_test.go | 28 +++++++ pkg/quay/api.go | 7 ++ pkg/quay/quay.go | 47 +++++++++--- pkg/quay/quay_debug_test.go | 2 +- pkg/quay/quay_test.go | 70 +++++++++++++++++- pkg/quay/test_quay_client.go | 44 ++++++----- 9 files changed, 280 insertions(+), 54 deletions(-) diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 8dec3f9..d885cdc 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -63,4 +63,12 @@ rules: - get - patch - update +- apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch diff --git a/controllers/imagerepository_controller.go b/controllers/imagerepository_controller.go index 726132c..83bc4ff 100644 --- a/controllers/imagerepository_controller.go +++ b/controllers/imagerepository_controller.go @@ -49,6 +49,8 @@ const ( buildPipelineServiceAccountName = "appstudio-pipeline" updateComponentAnnotationName = "image-controller.appstudio.redhat.com/update-component-image" + additionalUsersConfigMapName = "image-controller-additional-users" + additionalUsersConfigMapKey = "quay.io" ) // ImageRepositoryReconciler reconciles a ImageRepository object @@ -79,6 +81,7 @@ func setMetricsTime(idForMetrics string, reconcileStartTime time.Time) { //+kubebuilder:rbac:groups=appstudio.redhat.com,resources=imagerepositories/status,verbs=get;update;patch //+kubebuilder:rbac:groups=appstudio.redhat.com,resources=imagerepositories/finalizers,verbs=update //+kubebuilder:rbac:groups=appstudio.redhat.com,resources=components,verbs=get;list;watch +//+kubebuilder:rbac:groups=core,resources=configmaps,verbs=get;list;watch //+kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch;create;update;patch //+kubebuilder:rbac:groups="",resources=serviceaccounts,verbs=get;list;watch;update;patch @@ -343,6 +346,11 @@ func (r *ImageRepositoryReconciler) ProvisionImageRepository(ctx context.Context } } + err = r.GrantAdditionalRepositoryAccess(ctx, imageRepository) + if err != nil { + return err + } + var notificationStatus []imagerepositoryv1alpha1.NotificationStatus if notificationStatus, err = r.SetNotifications(ctx, imageRepository); err != nil { return err @@ -411,7 +419,7 @@ func (r *ImageRepositoryReconciler) ProvisionImageRepositoryAccess(ctx context.C return nil, err } - err = r.QuayClient.AddPermissionsForRepositoryToRobotAccount(r.QuayOrganization, imageRepositoryName, robotAccount.Name, !isPullOnly) + err = r.QuayClient.AddPermissionsForRepositoryToAccount(r.QuayOrganization, imageRepositoryName, robotAccount.Name, true, !isPullOnly) if err != nil { log.Error(err, "failed to add permissions to robot account", "RobotAccountName", robotAccountName, l.Action, l.ActionUpdate, l.Audit, "true") return nil, err @@ -429,6 +437,51 @@ func (r *ImageRepositoryReconciler) ProvisionImageRepositoryAccess(ctx context.C return data, nil } +func (r *ImageRepositoryReconciler) GrantAdditionalRepositoryAccess(ctx context.Context, imageRepository *imagerepositoryv1alpha1.ImageRepository) error { + log := ctrllog.FromContext(ctx).WithName("GrantAdditionalRepositoryAccess") + ctx = ctrllog.IntoContext(ctx, log) + + additionalUsersConfigMap := &corev1.ConfigMap{} + if err := r.Client.Get(ctx, types.NamespacedName{Name: additionalUsersConfigMapName, Namespace: imageRepository.Namespace}, additionalUsersConfigMap); err != nil { + if errors.IsNotFound(err) { + log.Info("Config map with additional users doesn't exist", "ConfigMapName", additionalUsersConfigMapName, l.Action, l.ActionView) + return nil + } + log.Error(err, "failed to read config map with additional users", "ConfigMapName", additionalUsersConfigMapName, l.Action, l.ActionView) + return err + } + additionalUsersStr, 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 + + 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 + } + + 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) + } + listPerms, _ := r.QuayClient.ListPermissionsForRepository(r.QuayOrganization, imageRepositoryName) + log.Info("Repository has now following permissions", "RepositoryName", imageRepositoryName) + for _, user := range listPerms { + log.Info(fmt.Sprintf("name: %s; role: %s; is robot: %t; is org member: %t", user.Name, user.Role, user.IsRobot, user.IsOrgMember)) + } + return nil +} + // RegenerateImageRepositoryCredentials rotates robot account(s) token and updates corresponding secret(s) func (r *ImageRepositoryReconciler) RegenerateImageRepositoryCredentials(ctx context.Context, imageRepository *imagerepositoryv1alpha1.ImageRepository) error { log := ctrllog.FromContext(ctx) diff --git a/controllers/imagerepository_controller_test.go b/controllers/imagerepository_controller_test.go index 69db2da..1fd4fc2 100644 --- a/controllers/imagerepository_controller_test.go +++ b/controllers/imagerepository_controller_test.go @@ -83,14 +83,14 @@ var _ = Describe("Image repository controller", func() { Expect(strings.HasPrefix(robotName, expectedRobotAccountPrefix)).To(BeTrue()) return &quay.RobotAccount{Name: robotName, Token: pushToken}, nil } - isAddPushPermissionsToRobotAccountInvoked := false - quay.AddPermissionsForRepositoryToRobotAccountFunc = func(organization, imageRepository, robotAccountName string, isWrite bool) error { + isAddPushPermissionsToAccountInvoked := false + quay.AddPermissionsForRepositoryToAccountFunc = func(organization, imageRepository, accountName string, isRobot, isWrite bool) error { defer GinkgoRecover() - isAddPushPermissionsToRobotAccountInvoked = true + isAddPushPermissionsToAccountInvoked = true Expect(organization).To(Equal(quay.TestQuayOrg)) Expect(imageRepository).To(Equal(expectedImageName)) Expect(isWrite).To(BeTrue()) - Expect(strings.HasPrefix(robotAccountName, expectedRobotAccountPrefix)).To(BeTrue()) + Expect(strings.HasPrefix(accountName, expectedRobotAccountPrefix)).To(BeTrue()) return nil } @@ -105,7 +105,7 @@ var _ = Describe("Image repository controller", func() { Eventually(func() bool { return isCreateRepositoryInvoked }, timeout, interval).Should(BeTrue()) Eventually(func() bool { return isCreateRobotAccountInvoked }, timeout, interval).Should(BeTrue()) - Eventually(func() bool { return isAddPushPermissionsToRobotAccountInvoked }, timeout, interval).Should(BeTrue()) + Eventually(func() bool { return isAddPushPermissionsToAccountInvoked }, timeout, interval).Should(BeTrue()) Eventually(func() bool { return isCreateNotificationInvoked }, timeout, interval).Should(BeFalse()) waitImageRepositoryFinalizerOnImageRepository(resourceKey) @@ -356,14 +356,14 @@ var _ = Describe("Image repository controller", func() { Expect(strings.HasPrefix(robotName, expectedRobotAccountPrefix)).To(BeTrue()) return &quay.RobotAccount{Name: robotName, Token: pushToken}, nil } - isAddPushPermissionsToRobotAccountInvoked := false - quay.AddPermissionsForRepositoryToRobotAccountFunc = func(organization, imageRepository, robotAccountName string, isWrite bool) error { + isAddPushPermissionsToAccountInvoked := false + quay.AddPermissionsForRepositoryToAccountFunc = func(organization, imageRepository, accountName string, isRobot, isWrite bool) error { defer GinkgoRecover() - isAddPushPermissionsToRobotAccountInvoked = true + isAddPushPermissionsToAccountInvoked = true Expect(organization).To(Equal(quay.TestQuayOrg)) Expect(imageRepository).To(Equal(expectedImageName)) Expect(isWrite).To(BeTrue()) - Expect(strings.HasPrefix(robotAccountName, expectedRobotAccountPrefix)).To(BeTrue()) + Expect(strings.HasPrefix(accountName, expectedRobotAccountPrefix)).To(BeTrue()) return nil } @@ -377,7 +377,7 @@ var _ = Describe("Image repository controller", func() { Eventually(func() bool { return isCreateRepositoryInvoked }, timeout, interval).Should(BeTrue()) Eventually(func() bool { return isCreateRobotAccountInvoked }, timeout, interval).Should(BeTrue()) - Eventually(func() bool { return isAddPushPermissionsToRobotAccountInvoked }, timeout, interval).Should(BeTrue()) + Eventually(func() bool { return isAddPushPermissionsToAccountInvoked }, timeout, interval).Should(BeTrue()) Eventually(func() bool { return isCreateNotificationInvoked }, timeout, interval).Should(BeFalse()) waitImageRepositoryFinalizerOnImageRepository(resourceKey) @@ -476,7 +476,7 @@ var _ = Describe("Image repository controller", func() { createServiceAccount(defaultNamespace, buildPipelineServiceAccountName) }) - assertProvisionRepository := func(updateComponentAnnotation bool) { + assertProvisionRepository := func(updateComponentAnnotation bool, additionalUser string) { isCreateRepositoryInvoked := false quay.CreateRepositoryFunc = func(repository quay.RepositoryRequest) (*quay.Repository, error) { defer GinkgoRecover() @@ -500,22 +500,31 @@ var _ = Describe("Image repository controller", func() { isCreatePushRobotAccountInvoked = true return &quay.RobotAccount{Name: robotName, Token: pushToken}, nil } - isAddPushPermissionsToRobotAccountInvoked := false - isAddPullPermissionsToRobotAccountInvoked := false - quay.AddPermissionsForRepositoryToRobotAccountFunc = func(organization, imageRepository, robotAccountName string, isWrite bool) error { + isAddPushPermissionsToAccountInvoked := false + isAddPullPermissionsToAccountInvoked := false + quay.AddPermissionsForRepositoryToAccountFunc = func(organization, imageRepository, accountName string, isRobot, isWrite bool) error { defer GinkgoRecover() Expect(organization).To(Equal(quay.TestQuayOrg)) Expect(imageRepository).To(Equal(expectedImageName)) - Expect(strings.HasPrefix(robotAccountName, expectedRobotAccountPrefix)).To(BeTrue()) - if strings.HasSuffix(robotAccountName, "_pull") { - Expect(isWrite).To(BeFalse()) - isAddPullPermissionsToRobotAccountInvoked = true + + 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(isWrite).To(BeTrue()) - isAddPushPermissionsToRobotAccountInvoked = true + Expect(accountName).To(Equal(additionalUser)) + Expect(isWrite).To(BeFalse()) } return nil } + quay.ListPermissionsForRepositoryFunc = func(organization, imageRepository string) (map[string]quay.UserAccount, error) { + return nil, nil + } isCreateNotificationInvoked := false quay.CreateNotificationFunc = func(organization, repository string, notification quay.Notification) (*quay.Notification, error) { isCreateNotificationInvoked = true @@ -565,8 +574,8 @@ var _ = Describe("Image repository controller", func() { Eventually(func() bool { return isCreateRepositoryInvoked }, timeout, interval).Should(BeTrue()) Eventually(func() bool { return isCreatePushRobotAccountInvoked }, timeout, interval).Should(BeTrue()) Eventually(func() bool { return isCreatePullRobotAccountInvoked }, timeout, interval).Should(BeTrue()) - Eventually(func() bool { return isAddPushPermissionsToRobotAccountInvoked }, timeout, interval).Should(BeTrue()) - Eventually(func() bool { return isAddPullPermissionsToRobotAccountInvoked }, timeout, interval).Should(BeTrue()) + 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()) waitImageRepositoryFinalizerOnImageRepository(resourceKey) @@ -648,7 +657,22 @@ var _ = Describe("Image repository controller", func() { } It("should provision image repository for component, without update component annotation", func() { - assertProvisionRepository(false) + assertProvisionRepository(false, "") + + quay.DeleteRobotAccountFunc = func(organization, robotAccountName string) (bool, error) { + return true, nil + } + quay.DeleteRepositoryFunc = func(organization, imageRepository string) (bool, error) { + return true, nil + } + + deleteImageRepository(resourceKey) + }) + + It("should provision image repository for component, with update component annotation and add additional user from config map", func() { + usersConfigMapKey := types.NamespacedName{Name: additionalUsersConfigMapName, Namespace: resourceKey.Namespace} + createUsersConfigMap(usersConfigMapKey, []string{"user1"}) + assertProvisionRepository(true, "user1") quay.DeleteRobotAccountFunc = func(organization, robotAccountName string) (bool, error) { return true, nil @@ -657,11 +681,12 @@ var _ = Describe("Image repository controller", func() { return true, nil } + deleteUsersConfigMap(usersConfigMapKey) deleteImageRepository(resourceKey) }) It("should provision image repository for component, with update component annotation", func() { - assertProvisionRepository(true) + assertProvisionRepository(true, "") }) It("should regenerate tokens and update secrets", func() { diff --git a/controllers/suite_util_test.go b/controllers/suite_util_test.go index 71df7e8..07eeae8 100644 --- a/controllers/suite_util_test.go +++ b/controllers/suite_util_test.go @@ -17,6 +17,7 @@ limitations under the License. package controllers import ( + "strings" "time" . "github.com/onsi/ginkgo/v2" @@ -348,3 +349,30 @@ func deleteServiceAccount(serviceAccountKey types.NamespacedName) { return k8sErrors.IsNotFound(k8sClient.Get(ctx, serviceAccountKey, serviceAccount)) }, timeout, interval).Should(BeTrue()) } + +func createUsersConfigMap(configMapKey types.NamespacedName, users []string) { + configMapData := map[string]string{} + configMapData[additionalUsersConfigMapKey] = strings.Join(users, " ") + + usersConfigMap := corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: configMapKey.Name, Namespace: configMapKey.Namespace}, + Data: configMapData, + } + + if err := k8sClient.Create(ctx, &usersConfigMap); err != nil && !k8sErrors.IsAlreadyExists(err) { + Fail(err.Error()) + } +} + +func deleteUsersConfigMap(configMapKey types.NamespacedName) { + usersConfigMap := corev1.ConfigMap{} + if err := k8sClient.Get(ctx, configMapKey, &usersConfigMap); err != nil { + if k8sErrors.IsNotFound(err) { + return + } + Fail(err.Error()) + } + if err := k8sClient.Delete(ctx, &usersConfigMap); err != nil && !k8sErrors.IsNotFound(err) { + Fail(err.Error()) + } +} diff --git a/pkg/quay/api.go b/pkg/quay/api.go index afe2df4..ad8c850 100644 --- a/pkg/quay/api.go +++ b/pkg/quay/api.go @@ -44,6 +44,13 @@ type RobotAccount struct { Message string `json:"message"` } +type UserAccount struct { + Name string `json:"name"` + Role string `json:"role"` + IsRobot bool `json:"is_robot"` + IsOrgMember bool `json:"is_org_member"` +} + // 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 a7c656d..8e43b65 100644 --- a/pkg/quay/quay.go +++ b/pkg/quay/quay.go @@ -34,7 +34,8 @@ type QuayService interface { GetRobotAccount(organization string, robotName string) (*RobotAccount, error) CreateRobotAccount(organization string, robotName string) (*RobotAccount, error) DeleteRobotAccount(organization string, robotName string) (bool, error) - AddPermissionsForRepositoryToRobotAccount(organization, imageRepository, robotAccountName string, isWrite bool) error + AddPermissionsForRepositoryToAccount(organization, imageRepository, accountName string, isRobot, isWrite bool) error + ListPermissionsForRepository(organization, imageRepository string) (map[string]UserAccount, error) RegenerateRobotAccountToken(organization string, robotName string) (*RobotAccount, error) GetAllRepositories(organization string) ([]Repository, error) GetAllRobotAccounts(organization string) ([]RobotAccount, error) @@ -359,18 +360,46 @@ func (c *QuayClient) DeleteRobotAccount(organization string, robotName string) ( return false, errors.New(data.ErrorMessage) } -// AddPermissionsForRepositoryToRobotAccount allows given robot account to access to the given repository. +// ListPermissionsForRepository list permissions for the given repository. +func (c *QuayClient) ListPermissionsForRepository(organization, imageRepository string) (map[string]UserAccount, error) { + url := fmt.Sprintf("%s/repository/%s/%s/permissions/user/", c.url, organization, imageRepository) + + 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 { + return nil, fmt.Errorf("error getting permissions, got status code %d", resp.GetStatusCode()) + } + + type Response struct { + Permissions map[string]UserAccount `json:"permissions"` + } + var response Response + if err := resp.GetJson(&response); err != nil { + return nil, err + } + + return response.Permissions, nil +} + +// AddPermissionsForRepositoryToAccount allows given robot 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) AddPermissionsForRepositoryToRobotAccount(organization, imageRepository, robotAccountName string, isWrite bool) error { - var robotAccountFullName string - if robotName, err := handleRobotName(robotAccountName); err == nil { - robotAccountFullName = organization + "+" + robotName +func (c *QuayClient) AddPermissionsForRepositoryToAccount(organization, imageRepository, accountName string, isRobot, isWrite bool) error { + var accountFullName string + if isRobot { + if robotName, err := handleRobotName(accountName); err == nil { + accountFullName = organization + "+" + robotName + } else { + return err + } } else { - return err + accountFullName = accountName } // url := "https://quay.io/api/v1/repository/redhat-appstudio/test-repo-using-api/permissions/user/redhat-appstudio+createdbysbose" - url := fmt.Sprintf("%s/repository/%s/%s/permissions/user/%s", c.url, organization, imageRepository, robotAccountFullName) + url := fmt.Sprintf("%s/repository/%s/%s/permissions/user/%s", c.url, organization, imageRepository, accountFullName) role := "read" if isWrite { @@ -392,7 +421,7 @@ func (c *QuayClient) AddPermissionsForRepositoryToRobotAccount(organization, ima message = data.Error } } - return fmt.Errorf("failed to add permissions to the robot account. Status code: %d, message: %s", resp.GetStatusCode(), message) + return fmt.Errorf("failed to add permissions to the account: %s. Status code: %d, message: %s", accountFullName, resp.GetStatusCode(), message) } return nil } diff --git a/pkg/quay/quay_debug_test.go b/pkg/quay/quay_debug_test.go index 7cf7ac5..9e79c90 100644 --- a/pkg/quay/quay_debug_test.go +++ b/pkg/quay/quay_debug_test.go @@ -172,7 +172,7 @@ func TestAddPermissionsToRobotAccount(t *testing.T) { } quayClient := NewQuayClient(&http.Client{Transport: &http.Transport{}}, quayToken, quayApiUrl) - err := quayClient.AddPermissionsForRepositoryToRobotAccount(quayOrgName, quayImageRepoName, quayRobotAccountName, true) + err := quayClient.AddPermissionsForRepositoryToAccount(quayOrgName, quayImageRepoName, quayRobotAccountName, true, true) if err != nil { t.Fatal(err) } diff --git a/pkg/quay/quay_test.go b/pkg/quay/quay_test.go index 18ad23c..969c687 100644 --- a/pkg/quay/quay_test.go +++ b/pkg/quay/quay_test.go @@ -335,7 +335,7 @@ func TestQuayClient_AddPermissions(t *testing.T) { robotName: robotName, statusCode: 400, responseData: "{\"name: \"info\"}", - expectedErr: "failed to add permissions to the robot account", + expectedErr: "failed to add permissions to the account", }, { name: "stop if http request fails", @@ -357,7 +357,7 @@ func TestQuayClient_AddPermissions(t *testing.T) { } quayClient := NewQuayClient(client, "authtoken", testQuayApiUrl) - err := quayClient.AddPermissionsForRepositoryToRobotAccount("org", "repository", tc.robotName, true) + err := quayClient.AddPermissionsForRepositoryToAccount("org", "repository", tc.robotName, true, true) if tc.expectedErr == "" { assert.NilError(t, err) @@ -531,6 +531,72 @@ func TestQuayClient_GetAllRobotAccounts(t *testing.T) { } } +func TestQuayClient_ListPermisssionsForRepository(t *testing.T) { + testCases := []struct { + name string + statusCode int + responseData interface{} + expectedPermissions map[string]UserAccount + expectedErr string + }{ + { + name: "list permissions for repository normally", + statusCode: 200, + responseData: "{\"permissions\": {\"user1\": {\"name\": \"user1\", \"role\": \"read\", \"is_robot\": false, \"is_org_member\": true}}}", + expectedPermissions: map[string]UserAccount{"user1": UserAccount{Name: "user1", Role: "read", IsRobot: false, IsOrgMember: true}}, + expectedErr: "", + }, + { + name: "server does not respond 200", + statusCode: 400, + expectedErr: "error getting permissions", + responseData: "", + expectedPermissions: nil, + }, + { + name: "server does not respond invalid a JSON string", + statusCode: 200, + responseData: "{\"permissions\": {\"user1\": {\"name}}}", + expectedPermissions: nil, + expectedErr: "failed to unmarshal response body", + }, + { + name: "stop if http request fails", + expectedErr: "failed to Do request:", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + defer gock.Off() + + req := gock.New(testQuayApiUrl). + MatchHeader("Content-Type", "application/json"). + MatchHeader("Authorization", "Bearer authtoken"). + Get("/repository/test_org/test_repository/permissions/user/") + req.Reply(tc.statusCode).JSON(tc.responseData) + + if tc.name == "stop if http request fails" { + req.AddMatcher(gock.MatchPath).Get("another-path") + } + + client := &http.Client{Transport: &http.Transport{}} + gock.InterceptClient(client) + + quayClient := NewQuayClient(client, "authtoken", testQuayApiUrl) + permissions, err := quayClient.ListPermissionsForRepository("test_org", "test_repository") + + if tc.expectedErr == "" { + assert.NilError(t, err) + } else { + assert.ErrorContains(t, err, tc.expectedErr) + } + + assert.DeepEqual(t, tc.expectedPermissions, permissions) + }) + } +} + func TestQuayClient_handleRobotName(t *testing.T) { invalidRobotNameErr := fmt.Errorf("robot name is invalid, must match `^([a-z0-9]+(?:[._-][a-z0-9]+)*)$` (one plus sign in the middle is also allowed)") testCases := []struct { diff --git a/pkg/quay/test_quay_client.go b/pkg/quay/test_quay_client.go index a7331e6..201ff39 100644 --- a/pkg/quay/test_quay_client.go +++ b/pkg/quay/test_quay_client.go @@ -30,18 +30,19 @@ 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) - AddPermissionsForRepositoryToRobotAccountFunc func(organization, imageRepository, robotAccountName string, isWrite bool) 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 + 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) ) func ResetTestQuayClient() { @@ -51,7 +52,8 @@ func ResetTestQuayClient() { GetRobotAccountFunc = func(organization, robotName string) (*RobotAccount, error) { return &RobotAccount{}, nil } CreateRobotAccountFunc = func(organization, robotName string) (*RobotAccount, error) { return &RobotAccount{}, nil } DeleteRobotAccountFunc = func(organization, robotName string) (bool, error) { return true, nil } - AddPermissionsForRepositoryToRobotAccountFunc = func(organization, imageRepository, robotAccountName string, isWrite bool) error { return nil } + AddPermissionsForRepositoryToAccountFunc = func(organization, imageRepository, accountName string, isRobot, isWrite bool) error { return nil } + ListPermissionsForRepositoryFunc = func(organization, imageRepository string) (map[string]UserAccount, error) { return nil, 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) { @@ -96,11 +98,16 @@ func ResetTestQuayClientToFails() { Fail("DeleteRobotAccount invoked") return true, nil } - AddPermissionsForRepositoryToRobotAccountFunc = func(organization, imageRepository, robotAccountName string, isWrite bool) error { + AddPermissionsForRepositoryToAccountFunc = func(organization, imageRepository, accountName string, isRobot, isWrite bool) error { defer GinkgoRecover() - Fail("AddPermissionsForRepositoryToRobotAccount invoked") + Fail("AddPermissionsForRepositoryToAccount invoked") return nil } + ListPermissionsForRepositoryFunc = func(organization, imageRepository string) (map[string]UserAccount, error) { + defer GinkgoRecover() + Fail("ListPermissionsForRepository invoked") + return nil, nil + } RegenerateRobotAccountTokenFunc = func(organization, robotName string) (*RobotAccount, error) { defer GinkgoRecover() Fail("RegenerateRobotAccountToken invoked") @@ -146,8 +153,11 @@ func (c TestQuayClient) CreateRobotAccount(organization string, robotName string func (c TestQuayClient) DeleteRobotAccount(organization string, robotName string) (bool, error) { return DeleteRobotAccountFunc(organization, robotName) } -func (c TestQuayClient) AddPermissionsForRepositoryToRobotAccount(organization, imageRepository, robotAccountName string, isWrite bool) error { - return AddPermissionsForRepositoryToRobotAccountFunc(organization, imageRepository, robotAccountName, isWrite) +func (c TestQuayClient) AddPermissionsForRepositoryToAccount(organization, imageRepository, accountName string, isRobot, isWrite bool) error { + return AddPermissionsForRepositoryToAccountFunc(organization, imageRepository, accountName, isRobot, isWrite) +} +func (c TestQuayClient) ListPermissionsForRepository(organization, imageRepository string) (map[string]UserAccount, error) { + return ListPermissionsForRepositoryFunc(organization, imageRepository) } func (c TestQuayClient) RegenerateRobotAccountToken(organization string, robotName string) (*RobotAccount, error) { return RegenerateRobotAccountTokenFunc(organization, robotName)