From cf6c705a00eb4de22549ab40c642d4e3ce2877bf Mon Sep 17 00:00:00 2001 From: Hunter-Thompson Date: Wed, 10 May 2023 21:09:23 +0530 Subject: [PATCH] feat(repo): push/pull/admin permission for repo --- apis/settings/v1beta1/repository_types.go | 8 + .../settings/v1beta1/zz_generated.deepcopy.go | 35 ++++ .../settings.github.com_repositories.yaml | 15 ++ config/crd/kustomization.yaml | 2 + config/manager/manager.yaml | 2 +- controllers/settings/repository.go | 167 ++++++++++++++++++ controllers/settings/repository_controller.go | 5 + docs/install.yaml | 17 +- 8 files changed, 249 insertions(+), 2 deletions(-) diff --git a/apis/settings/v1beta1/repository_types.go b/apis/settings/v1beta1/repository_types.go index 8c3ebef..5c82e6e 100644 --- a/apis/settings/v1beta1/repository_types.go +++ b/apis/settings/v1beta1/repository_types.go @@ -68,6 +68,14 @@ type RepositorySpec struct { MergeCommitTitle *string `json:"mergeCommitTitle,omitempty"` // Can be one of: PR_BODY, PR_TITLE, BLANK MergeCommitMessage *string `json:"mergeCommitMessage,omitempty"` + + RepositoryCollaborators *RepositoryCollaborators `json:"repositoryCollaborators,omitempty"` +} + +type RepositoryCollaborators struct { + PushPermission []string `json:"pushPermission,omitempty"` + PullPermission []string `json:"pullPermission,omitempty"` + AdminPermission []string `json:"adminPermission,omitempty"` } // RepositoryStatus defines the observed state of Repository diff --git a/apis/settings/v1beta1/zz_generated.deepcopy.go b/apis/settings/v1beta1/zz_generated.deepcopy.go index 948faa9..690e749 100644 --- a/apis/settings/v1beta1/zz_generated.deepcopy.go +++ b/apis/settings/v1beta1/zz_generated.deepcopy.go @@ -52,6 +52,36 @@ func (in *Repository) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RepositoryCollaborators) DeepCopyInto(out *RepositoryCollaborators) { + *out = *in + if in.PushPermission != nil { + in, out := &in.PushPermission, &out.PushPermission + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.PullPermission != nil { + in, out := &in.PullPermission, &out.PullPermission + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.AdminPermission != nil { + in, out := &in.AdminPermission, &out.AdminPermission + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RepositoryCollaborators. +func (in *RepositoryCollaborators) DeepCopy() *RepositoryCollaborators { + if in == nil { + return nil + } + out := new(RepositoryCollaborators) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *RepositoryList) DeepCopyInto(out *RepositoryList) { *out = *in @@ -186,6 +216,11 @@ func (in *RepositorySpec) DeepCopyInto(out *RepositorySpec) { *out = new(string) **out = **in } + if in.RepositoryCollaborators != nil { + in, out := &in.RepositoryCollaborators, &out.RepositoryCollaborators + *out = new(RepositoryCollaborators) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RepositorySpec. diff --git a/config/crd/bases/settings.github.com_repositories.yaml b/config/crd/bases/settings.github.com_repositories.yaml index 0bb550c..0050ee1 100644 --- a/config/crd/bases/settings.github.com_repositories.yaml +++ b/config/crd/bases/settings.github.com_repositories.yaml @@ -97,6 +97,21 @@ spec: description: Either `true` to make the repository private, or `false` to make it public. type: boolean + repositoryCollaborators: + properties: + adminPermission: + items: + type: string + type: array + pullPermission: + items: + type: string + type: array + pushPermission: + items: + type: string + type: array + type: object squashMergeCommitMessage: description: 'Can be one of: PR_BODY, COMMIT_MESSAGES, BLANK' type: string diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index 3bd8a38..03eb9a3 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -11,12 +11,14 @@ patchesStrategicMerge: # patches here are for enabling the conversion webhook for each CRD #- patches/webhook_in_repositories.yaml #- patches/webhook_in_teams.yaml +#- patches/webhook_in_collaborators.yaml #+kubebuilder:scaffold:crdkustomizewebhookpatch # [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. # patches here are for enabling the CA injection for each CRD #- patches/cainjection_in_repositories.yaml #- patches/cainjection_in_teams.yaml +#- patches/cainjection_in_collaborators.yaml #+kubebuilder:scaffold:crdkustomizecainjectionpatch # the following config is for teaching kustomize how to do kustomization for CRDs. diff --git a/config/manager/manager.yaml b/config/manager/manager.yaml index 6df3380..a14397a 100644 --- a/config/manager/manager.yaml +++ b/config/manager/manager.yaml @@ -38,7 +38,7 @@ spec: - /manager args: - --leader-elect - image: hunterthompson/github-operator:v1.0.0 + image: hunterthompson/github-operator:v1.0.2 name: manager envFrom: - secretRef: diff --git a/controllers/settings/repository.go b/controllers/settings/repository.go index 9f57c7b..7655368 100644 --- a/controllers/settings/repository.go +++ b/controllers/settings/repository.go @@ -116,3 +116,170 @@ func (r *RepositoryReconciler) EditRepoSettings(ctx context.Context, repo *setti reqLogger.Info("edited repository") return nil } + +func (r *RepositoryReconciler) EditRepoCollaboraters(ctx context.Context, repo *settingsv1beta1.Repository, reqLogger logr.Logger) error { + ghClient := gh.Login(ctx) + + if repo.Spec.RepositoryCollaborators == nil { + return nil + } + + opt := &github.ListMembersOptions{ + ListOptions: github.ListOptions{PerPage: 10}, + } + + var allMembers []*github.User + for { + users, resp, err := ghClient.Organizations.ListMembers(ctx, repo.Spec.Organization, opt) + if err != nil { + return err + } + allMembers = append(allMembers, users...) + if resp.NextPage == 0 { + break + } + opt.Page = resp.NextPage + } + + for _, adminUser := range repo.Spec.RepositoryCollaborators.AdminPermission { + err := addAdminPerm(ctx, repo, ghClient, adminUser, allMembers, reqLogger) + if err != nil { + return fmt.Errorf("failed to addAdminPerm: %w", err) + } + } + + for _, pullUser := range repo.Spec.RepositoryCollaborators.PullPermission { + err := addPullPerm(ctx, repo, ghClient, pullUser, allMembers, reqLogger) + if err != nil { + return fmt.Errorf("failed to addPullPerm: %w", err) + } + } + + for _, pushUser := range repo.Spec.RepositoryCollaborators.PushPermission { + err := addPushPerm(ctx, repo, ghClient, pushUser, allMembers, reqLogger) + if err != nil { + return fmt.Errorf("failed to addPushPerm: %w", err) + } + } + + return nil +} + +func addAdminPerm(ctx context.Context, repo *settingsv1beta1.Repository, ghClient *github.Client, adminUser string, allMembers []*github.User, reqLogger logr.Logger) error { + permLevel, _, err := ghClient.Repositories.GetPermissionLevel(ctx, repo.Spec.Organization, repo.GetName(), adminUser) + if err != nil { + if strings.Contains(err.Error(), "is not a user []") { + reqLogger.Error(err, "not a user") + return nil + } + return fmt.Errorf("failed to get perm level for repo: %w", err) + } + + if permLevel.GetPermission() == "admin" { + reqLogger.Info(adminUser + " already has admin permission") + return nil + } + + add := false + + for _, member := range allMembers { + // only add if user is actually a part of the org + if member.GetLogin() == adminUser { + add = true + break + } + } + + if add { + _, _, err = ghClient.Repositories.AddCollaborator(ctx, repo.Spec.Organization, repo.GetName(), adminUser, &github.RepositoryAddCollaboratorOptions{ + Permission: "admin", + }) + if err != nil { + return fmt.Errorf("failed to add admin collaborator for repo: %w", err) + } + reqLogger.Info("gave " + adminUser + " admin permission") + } else { + reqLogger.Info(adminUser + " is not a part of the " + repo.Spec.Organization + " org") + } + + return nil +} + +func addPullPerm(ctx context.Context, repo *settingsv1beta1.Repository, ghClient *github.Client, pullUser string, allMembers []*github.User, reqLogger logr.Logger) error { + permLevel, _, err := ghClient.Repositories.GetPermissionLevel(ctx, repo.Spec.Organization, repo.GetName(), pullUser) + if err != nil { + if strings.Contains(err.Error(), "is not a user []") { + reqLogger.Error(err, "not a user") + return nil + } + return fmt.Errorf("failed to get perm level for repo: %w", err) + } + + if permLevel.GetPermission() == "read" || permLevel.GetPermission() == "admin" || permLevel.GetPermission() == "write" { + reqLogger.Info(pullUser + " already has read permission") + return nil + } + + add := false + + for _, member := range allMembers { + // only add if user is actually a part of the org + if member.GetLogin() == pullUser { + add = true + break + } + } + + if add { + _, _, err = ghClient.Repositories.AddCollaborator(ctx, repo.Spec.Organization, repo.GetName(), pullUser, &github.RepositoryAddCollaboratorOptions{ + Permission: "pull", + }) + if err != nil { + return fmt.Errorf("failed to add pull collaborator for repo: %w", err) + } + reqLogger.Info("gave " + pullUser + " pull permission") + } else { + reqLogger.Info(pullUser + " is not a part of the " + repo.Spec.Organization + " org") + } + return nil +} + +func addPushPerm(ctx context.Context, repo *settingsv1beta1.Repository, ghClient *github.Client, pushUser string, allMembers []*github.User, reqLogger logr.Logger) error { + permLevel, _, err := ghClient.Repositories.GetPermissionLevel(ctx, repo.Spec.Organization, repo.GetName(), pushUser) + if err != nil { + if strings.Contains(err.Error(), "is not a user []") { + reqLogger.Error(err, "not a user") + return nil + } + return fmt.Errorf("failed to get perm level for repo: %w", err) + } + + if permLevel.GetPermission() == "admin" || permLevel.GetPermission() == "write" { + reqLogger.Info(pushUser + " already has push permission") + return nil + } + + add := false + + for _, member := range allMembers { + // only add if user is actually a part of the org + if member.GetLogin() == pushUser { + add = true + break + } + } + + if add { + _, _, err = ghClient.Repositories.AddCollaborator(ctx, repo.Spec.Organization, repo.GetName(), pushUser, &github.RepositoryAddCollaboratorOptions{ + Permission: "push", + }) + if err != nil { + return fmt.Errorf("failed to add push collaborator for repo: %w", err) + } + reqLogger.Info("gave " + pushUser + " push permission") + } else { + reqLogger.Info(pushUser + " is not a part of the " + repo.Spec.Organization + " org") + } + + return nil +} diff --git a/controllers/settings/repository_controller.go b/controllers/settings/repository_controller.go index f3db681..8ca8f18 100644 --- a/controllers/settings/repository_controller.go +++ b/controllers/settings/repository_controller.go @@ -85,6 +85,11 @@ func (r *RepositoryReconciler) Reconcile(ctx context.Context, req ctrl.Request) } } + err = r.EditRepoCollaboraters(ctx, repo, reqLogger) + if err != nil { + return reconcile.Result{}, err + } + return reconcile.Result{}, nil } diff --git a/docs/install.yaml b/docs/install.yaml index 33c7598..3d5aee5 100644 --- a/docs/install.yaml +++ b/docs/install.yaml @@ -103,6 +103,21 @@ spec: description: Either `true` to make the repository private, or `false` to make it public. type: boolean + repositoryCollaborators: + properties: + adminPermission: + items: + type: string + type: array + pullPermission: + items: + type: string + type: array + pushPermission: + items: + type: string + type: array + type: object squashMergeCommitMessage: description: 'Can be one of: PR_BODY, COMMIT_MESSAGES, BLANK' type: string @@ -441,7 +456,7 @@ spec: envFrom: - secretRef: name: github-operator-secret - image: hunterthompson/github-operator:latest + image: hunterthompson/github-operator:v1.0.2 livenessProbe: httpGet: path: /healthz