From 6179da8d5b7e94e2891afeb93b0d32ad0d00cdf2 Mon Sep 17 00:00:00 2001 From: Lado Golijashvili Date: Thu, 25 Apr 2024 15:28:52 +0400 Subject: [PATCH] Ruleset implementation (#19) * fix linting issues * revert pc.yaml changes * add docs to repository_types, remove debug print statements from code * update crd after adding struct documentation * reorder packages in util file to pass linting --- Makefile | 3 +- README.md | 2 + .../v1alpha1/repository_types.go | 117 +++++ .../v1alpha1/zz_generated.deepcopy.go | 299 +++++++++++ examples/organizations/repository.yaml | 43 ++ internal/clients/client.go | 5 + internal/clients/fake/client.go | 25 + internal/controller/repository/repository.go | 469 ++++++++++++++++++ .../controller/repository/repository_test.go | 101 ++++ internal/util/util.go | 94 +++- ...ons.github.crossplane.io_repositories.yaml | 158 ++++++ 11 files changed, 1311 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index d373f43..2d9d826 100644 --- a/Makefile +++ b/Makefile @@ -92,7 +92,8 @@ dev: $(KIND) $(KUBECTL) @$(KIND) create cluster --name=$(PROJECT_NAME)-dev @$(KUBECTL) cluster-info --context kind-$(PROJECT_NAME)-dev @$(INFO) Installing Crossplane CRDs - @$(KUBECTL) apply -k https://github.com/crossplane/crossplane//cluster?ref=master + # https://github.com/crossplane/crossplane/issues/5336 + @$(KUBECTL) apply --server-side -k https://github.com/crossplane/crossplane//cluster?ref=master @$(INFO) Installing Provider GitHub CRDs @$(KUBECTL) apply -R -f package/crds @$(INFO) Starting Provider GitHub controllers diff --git a/README.md b/README.md index 3eb9125..849ab30 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,8 @@ implements follwing objects with partial functionality: * team permissions * webhooks * branch protection rules + * Repository rules + * rulesets * Membership * role diff --git a/apis/organizations/v1alpha1/repository_types.go b/apis/organizations/v1alpha1/repository_types.go index fa1d324..68c34a8 100644 --- a/apis/organizations/v1alpha1/repository_types.go +++ b/apis/organizations/v1alpha1/repository_types.go @@ -34,6 +34,9 @@ type RepositoryParameters struct { BranchProtectionRules []BranchProtectionRule `json:"branchProtectionRules,omitempty"` + // RepositoryRules are the rules for the repository + RepositoryRules []RepositoryRuleset `json:"repositoryRules,omitempty"` + // Creates a new repository using a repository template CreateFromTemplate *TemplateRepo `json:"createFromTemplate,omitempty"` @@ -292,6 +295,120 @@ type BranchProtectionRestrictions struct { Apps []string `json:"apps,omitempty"` } +// RepositoryRuleset represents the rules for a repository +type RepositoryRuleset struct { + // Name is the name of the ruleset + Name string `json:"name"` + // Enforcement is the enforcement level of the ruleset, can be one of: "disabled", "active" + // +optional + Enforcement *string `json:"enforcement,omitempty"` + // Target is the target of the ruleset, can be one of: "branch", "tag" + // +optional + Target *string `json:"target,omitempty"` + // BypassActors is the list of actors that can bypass the ruleset + // +optional + BypassActors []*RulesetByPassActors `json:"bypassActors"` + // Conditions is the conditions for the ruleset, which branches or tags are included or excluded from the ruleset + // +optional + Conditions *RulesetConditions `json:"conditions,omitempty"` + // Rules is the rules for the ruleset + // +optional + Rules *Rules `json:"rules,omitempty"` +} + +type RulesetByPassActors struct { + // ActorId is the ID of the actor + // +optional + ActorId *int64 `json:"actorId,omitempty"` + // ActorType is the type of the actor, can be one of: Integration, OrganizationAdmin, RepositoryRole, Team + // +optional + ActorType *string `json:"actorType,omitempty"` + // BypassMode is the bypass mode of the actor, can be one of: "always", "pull_request" + // +optional + BypassMode *string `json:"bypassMode,omitempty"` +} + +type RulesetConditions struct { + RefName *RulesetRefName `json:"refName,omitempty"` +} + +type RulesetRefName struct { + // Include is the list of branches or tags to include + Include []string `json:"include"` + // Exclude is the list of branches or tags to exclude + Exclude []string `json:"exclude"` +} + +type Rules struct { + // Creation restricts the creation of matching branches or tags that are set in Conditions + // +optional + Creation *bool `json:"creation,omitempty"` + // Deletion restricts the deletion of matching branches or tags that are set in Conditions + // +optional + Deletion *bool `json:"deletion,omitempty"` + // Update restricts the update of matching branches or tags that are set in Conditions + // +optional + Update *bool `json:"update,omitempty"` + // RequiredLinearHistory requires a linear commit history, which prevents merge commits. + // +optional + RequiredLinearHistory *bool `json:"requiredLinearHistory,omitempty"` + // RequiredDeployments requires that deployment to specific environments are successful before merging. + // +optional + RequiredDeployments *RulesRequiredDeployments `json:"requiredDeployments,omitempty"` + // RequiredSignatures requires signed commits. + // +optional + RequiredSignatures *bool `json:"requiredSignatures,omitempty"` + // PullRequest is the rules for pull requests + // +optional + PullRequest *RulesPullRequest `json:"pullRequest,omitempty"` + // RequiredStatusChecks requires status checks to pass before merging. + // +optional + RequiredStatusChecks *RulesRequiredStatusChecks `json:"requiredStatusChecks,omitempty"` + // NonFastForward restricts force pushes to matching branches or tags that are set in Conditions + // +optional + NonFastForward *bool `json:"nonFastForward,omitempty"` +} + +type RulesRequiredDeployments struct { + // Environments is the list of environments that are required to be deployed to before merging + // +optional + Environments []string `json:"environments,omitempty"` +} + +type RulesPullRequest struct { + // DismissStaleReviewsOnPush automatically dismiss approving reviews when someone pushes a new commit. + // +optional + DismissStaleReviewsOnPush *bool `json:"dismissStaleReviewsOnPush,omitempty"` + // RequireCodeOwnerReview requires the pull request to be approved by a code owner. + // +optional + RequireCodeOwnerReview *bool `json:"requireCodeOwnerReview,omitempty"` + // RequireLastPushApproval requires the most recent push to be approved by someone other than the person who pushed it. + // +optional + RequireLastPushApproval *bool `json:"requireLastPushApproval,omitempty"` + // RequiredApprovingReviewCount specifies the number of reviewers required to approve pull requests. + // +optional + RequiredApprovingReviewCount *int `json:"requiredApprovingReviewCount,omitempty"` + // RequiredReviewThreadResolution requires all conversations on code to be resolved before a pull request can be merged. + // +optional + RequiredReviewThreadResolution *bool `json:"requiredReviewThreadResolution,omitempty"` +} + +type RulesRequiredStatusChecks struct { + // RequiredStatusChecks is the list of status checks to require in order to merge into this branch. + // +optional + RequiredStatusChecks []*RulesRequiredStatusChecksParameters `json:"requiredStatusChecks,omitempty"` + // StrictRequiredStatusChecksPolicy requires branches to be up-to-date before merging. + // +optional + StrictRequiredStatusChecksPolicy *bool `json:"strictRequiredStatusChecksPolicy,omitempty"` +} +type RulesRequiredStatusChecksParameters struct { + // Context is the name of the required check. + Context string `json:"context"` + // IntegrationId is the ID of integration that must provide this check. + // +optional + IntegrationId *int64 `json:"integrationId,omitempty"` +} + // TemplateRepo represents the configuration for creating a new repository from a template. type TemplateRepo struct { // The account owner of the template repository. The name is not case-sensitive. diff --git a/apis/organizations/v1alpha1/zz_generated.deepcopy.go b/apis/organizations/v1alpha1/zz_generated.deepcopy.go index 356d74a..d21171d 100644 --- a/apis/organizations/v1alpha1/zz_generated.deepcopy.go +++ b/apis/organizations/v1alpha1/zz_generated.deepcopy.go @@ -609,6 +609,13 @@ func (in *RepositoryParameters) DeepCopyInto(out *RepositoryParameters) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.RepositoryRules != nil { + in, out := &in.RepositoryRules, &out.RepositoryRules + *out = make([]RepositoryRuleset, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } if in.CreateFromTemplate != nil { in, out := &in.CreateFromTemplate, &out.CreateFromTemplate *out = new(TemplateRepo) @@ -690,6 +697,52 @@ func (in *RepositoryPermissions) DeepCopy() *RepositoryPermissions { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RepositoryRuleset) DeepCopyInto(out *RepositoryRuleset) { + *out = *in + if in.Enforcement != nil { + in, out := &in.Enforcement, &out.Enforcement + *out = new(string) + **out = **in + } + if in.Target != nil { + in, out := &in.Target, &out.Target + *out = new(string) + **out = **in + } + if in.BypassActors != nil { + in, out := &in.BypassActors, &out.BypassActors + *out = make([]*RulesetByPassActors, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(RulesetByPassActors) + (*in).DeepCopyInto(*out) + } + } + } + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = new(RulesetConditions) + (*in).DeepCopyInto(*out) + } + if in.Rules != nil { + in, out := &in.Rules, &out.Rules + *out = new(Rules) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RepositoryRuleset. +func (in *RepositoryRuleset) DeepCopy() *RepositoryRuleset { + if in == nil { + return nil + } + out := new(RepositoryRuleset) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *RepositorySpec) DeepCopyInto(out *RepositorySpec) { *out = *in @@ -880,6 +933,252 @@ func (in *RequiredStatusChecks) DeepCopy() *RequiredStatusChecks { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Rules) DeepCopyInto(out *Rules) { + *out = *in + if in.Creation != nil { + in, out := &in.Creation, &out.Creation + *out = new(bool) + **out = **in + } + if in.Deletion != nil { + in, out := &in.Deletion, &out.Deletion + *out = new(bool) + **out = **in + } + if in.Update != nil { + in, out := &in.Update, &out.Update + *out = new(bool) + **out = **in + } + if in.RequiredLinearHistory != nil { + in, out := &in.RequiredLinearHistory, &out.RequiredLinearHistory + *out = new(bool) + **out = **in + } + if in.RequiredDeployments != nil { + in, out := &in.RequiredDeployments, &out.RequiredDeployments + *out = new(RulesRequiredDeployments) + (*in).DeepCopyInto(*out) + } + if in.RequiredSignatures != nil { + in, out := &in.RequiredSignatures, &out.RequiredSignatures + *out = new(bool) + **out = **in + } + if in.PullRequest != nil { + in, out := &in.PullRequest, &out.PullRequest + *out = new(RulesPullRequest) + (*in).DeepCopyInto(*out) + } + if in.RequiredStatusChecks != nil { + in, out := &in.RequiredStatusChecks, &out.RequiredStatusChecks + *out = new(RulesRequiredStatusChecks) + (*in).DeepCopyInto(*out) + } + if in.NonFastForward != nil { + in, out := &in.NonFastForward, &out.NonFastForward + *out = new(bool) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Rules. +func (in *Rules) DeepCopy() *Rules { + if in == nil { + return nil + } + out := new(Rules) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RulesPullRequest) DeepCopyInto(out *RulesPullRequest) { + *out = *in + if in.DismissStaleReviewsOnPush != nil { + in, out := &in.DismissStaleReviewsOnPush, &out.DismissStaleReviewsOnPush + *out = new(bool) + **out = **in + } + if in.RequireCodeOwnerReview != nil { + in, out := &in.RequireCodeOwnerReview, &out.RequireCodeOwnerReview + *out = new(bool) + **out = **in + } + if in.RequireLastPushApproval != nil { + in, out := &in.RequireLastPushApproval, &out.RequireLastPushApproval + *out = new(bool) + **out = **in + } + if in.RequiredApprovingReviewCount != nil { + in, out := &in.RequiredApprovingReviewCount, &out.RequiredApprovingReviewCount + *out = new(int) + **out = **in + } + if in.RequiredReviewThreadResolution != nil { + in, out := &in.RequiredReviewThreadResolution, &out.RequiredReviewThreadResolution + *out = new(bool) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RulesPullRequest. +func (in *RulesPullRequest) DeepCopy() *RulesPullRequest { + if in == nil { + return nil + } + out := new(RulesPullRequest) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RulesRequiredDeployments) DeepCopyInto(out *RulesRequiredDeployments) { + *out = *in + if in.Environments != nil { + in, out := &in.Environments, &out.Environments + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RulesRequiredDeployments. +func (in *RulesRequiredDeployments) DeepCopy() *RulesRequiredDeployments { + if in == nil { + return nil + } + out := new(RulesRequiredDeployments) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RulesRequiredStatusChecks) DeepCopyInto(out *RulesRequiredStatusChecks) { + *out = *in + if in.RequiredStatusChecks != nil { + in, out := &in.RequiredStatusChecks, &out.RequiredStatusChecks + *out = make([]*RulesRequiredStatusChecksParameters, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(RulesRequiredStatusChecksParameters) + (*in).DeepCopyInto(*out) + } + } + } + if in.StrictRequiredStatusChecksPolicy != nil { + in, out := &in.StrictRequiredStatusChecksPolicy, &out.StrictRequiredStatusChecksPolicy + *out = new(bool) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RulesRequiredStatusChecks. +func (in *RulesRequiredStatusChecks) DeepCopy() *RulesRequiredStatusChecks { + if in == nil { + return nil + } + out := new(RulesRequiredStatusChecks) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RulesRequiredStatusChecksParameters) DeepCopyInto(out *RulesRequiredStatusChecksParameters) { + *out = *in + if in.IntegrationId != nil { + in, out := &in.IntegrationId, &out.IntegrationId + *out = new(int64) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RulesRequiredStatusChecksParameters. +func (in *RulesRequiredStatusChecksParameters) DeepCopy() *RulesRequiredStatusChecksParameters { + if in == nil { + return nil + } + out := new(RulesRequiredStatusChecksParameters) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RulesetByPassActors) DeepCopyInto(out *RulesetByPassActors) { + *out = *in + if in.ActorId != nil { + in, out := &in.ActorId, &out.ActorId + *out = new(int64) + **out = **in + } + if in.ActorType != nil { + in, out := &in.ActorType, &out.ActorType + *out = new(string) + **out = **in + } + if in.BypassMode != nil { + in, out := &in.BypassMode, &out.BypassMode + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RulesetByPassActors. +func (in *RulesetByPassActors) DeepCopy() *RulesetByPassActors { + if in == nil { + return nil + } + out := new(RulesetByPassActors) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RulesetConditions) DeepCopyInto(out *RulesetConditions) { + *out = *in + if in.RefName != nil { + in, out := &in.RefName, &out.RefName + *out = new(RulesetRefName) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RulesetConditions. +func (in *RulesetConditions) DeepCopy() *RulesetConditions { + if in == nil { + return nil + } + out := new(RulesetConditions) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RulesetRefName) DeepCopyInto(out *RulesetRefName) { + *out = *in + if in.Include != nil { + in, out := &in.Include, &out.Include + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Exclude != nil { + in, out := &in.Exclude, &out.Exclude + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RulesetRefName. +func (in *RulesetRefName) DeepCopy() *RulesetRefName { + if in == nil { + return nil + } + out := new(RulesetRefName) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Team) DeepCopyInto(out *Team) { *out = *in diff --git a/examples/organizations/repository.yaml b/examples/organizations/repository.yaml index c4c4d9a..ee99df6 100644 --- a/examples/organizations/repository.yaml +++ b/examples/organizations/repository.yaml @@ -47,6 +47,49 @@ spec: - context: terraform_validate - context: deploy appId: 123456 + repositoryRules: + - name: test-ruleset-2 + target: branch + bypassActors: + - actorId: 8743474 + actorType: Team + bypassMode: always + - actorId: 1 + actorType: OrganizationAdmin + bypassMode: always + - actorId: 397599 + actorType: Integration + bypassMode: always + - actorId: 2 + actorType: RepositoryRole + bypassMode: always + conditions: + refName: + include: + [ ] + exclude: + - refs/heads/exclude + - refs/heads/feature/exclude + rules: + creation: false + nonFastForward: true + requiredLinearHistory: true + deletion: true + update: true + requiredSignatures: true + requiredDeployments: + environments: + - production-github + pullRequest: + dismissStaleReviewsOnPush: false + requiredApprovingReviewCount: 4 + requireCodeOwnerReview: true + requireLastPushApproval: true + requiredReviewThreadResolution: true + requiredStatusChecks: + strictRequiredStatusChecksPolicy: true + requiredStatusChecks: + - context: validate --- apiVersion: organizations.github.crossplane.io/v1alpha1 kind: Repository diff --git a/internal/clients/client.go b/internal/clients/client.go index ee93770..aa95855 100644 --- a/internal/clients/client.go +++ b/internal/clients/client.go @@ -87,6 +87,11 @@ type RepositoriesClient interface { RemoveBranchProtection(ctx context.Context, owner, repo, branch string) (*github.Response, error) RequireSignaturesOnProtectedBranch(ctx context.Context, owner, repo, branch string) (*github.SignaturesProtectedBranch, *github.Response, error) OptionalSignaturesOnProtectedBranch(ctx context.Context, owner, repo, branch string) (*github.Response, error) + GetAllRulesets(ctx context.Context, owner, repo string, includesParents bool) ([]*github.Ruleset, *github.Response, error) + GetRuleset(ctx context.Context, owner, repo string, rulesetID int64, includesParents bool) (*github.Ruleset, *github.Response, error) + CreateRuleset(ctx context.Context, owner, repo string, ruleset *github.Ruleset) (*github.Ruleset, *github.Response, error) + UpdateRuleset(ctx context.Context, owner, repo string, rulesetID int64, ruleset *github.Ruleset) (*github.Ruleset, *github.Response, error) + DeleteRuleset(ctx context.Context, owner, repo string, rulesetID int64) (*github.Response, error) } // NewClient creates a new client. diff --git a/internal/clients/fake/client.go b/internal/clients/fake/client.go index 66a1931..74a23ae 100644 --- a/internal/clients/fake/client.go +++ b/internal/clients/fake/client.go @@ -87,6 +87,11 @@ type MockRepositoriesClient struct { MockRemoveBranchProtection func(ctx context.Context, owner, repo, branch string) (*github.Response, error) MockRequireSignaturesOnProtectedBranch func(ctx context.Context, owner, repo, branch string) (*github.SignaturesProtectedBranch, *github.Response, error) MockOptionalSignaturesOnProtectedBranch func(ctx context.Context, owner, repo, branch string) (*github.Response, error) + MockGetAllRulesets func(ctx context.Context, owner, repo string) ([]*github.Ruleset, *github.Response, error) + MockGetRuleset func(ctx context.Context, owner, repo string, rulesetID int64, includesParents bool) (*github.Ruleset, *github.Response, error) + MockCreateRuleset func(ctx context.Context, owner, repo string, ruleset *github.Ruleset) (*github.Ruleset, *github.Response, error) + MockUpdateRuleset func(ctx context.Context, owner, repo string, rulesetID int64, ruleset *github.Ruleset) (*github.Ruleset, *github.Response, error) + MockDeleteRuleset func(ctx context.Context, owner, repo string, rulesetID int64) (*github.Response, error) } func (m *MockRepositoriesClient) Get(ctx context.Context, owner, repo string) (*github.Repository, *github.Response, error) { @@ -169,6 +174,26 @@ func (m *MockRepositoriesClient) OptionalSignaturesOnProtectedBranch(ctx context return m.MockOptionalSignaturesOnProtectedBranch(ctx, owner, repo, branch) } +func (m *MockRepositoriesClient) GetAllRulesets(ctx context.Context, owner, repo string, includesParents bool) ([]*github.Ruleset, *github.Response, error) { + return m.MockGetAllRulesets(ctx, owner, repo) +} + +func (m *MockRepositoriesClient) GetRuleset(ctx context.Context, owner, repo string, rulesetID int64, includesParents bool) (*github.Ruleset, *github.Response, error) { + return m.MockGetRuleset(ctx, owner, repo, rulesetID, includesParents) +} + +func (m *MockRepositoriesClient) CreateRuleset(ctx context.Context, owner, repo string, ruleset *github.Ruleset) (*github.Ruleset, *github.Response, error) { + return m.MockCreateRuleset(ctx, owner, repo, ruleset) +} + +func (m *MockRepositoriesClient) UpdateRuleset(ctx context.Context, owner, repo string, rulesetID int64, ruleset *github.Ruleset) (*github.Ruleset, *github.Response, error) { + return m.MockUpdateRuleset(ctx, owner, repo, rulesetID, ruleset) +} + +func (m *MockRepositoriesClient) DeleteRuleset(ctx context.Context, owner, repo string, rulesetID int64) (*github.Response, error) { + return m.MockDeleteRuleset(ctx, owner, repo, rulesetID) +} + type MockTeamsClient struct { MockGetTeamBySlug func(ctx context.Context, org, slug string) (*github.Team, *github.Response, error) MockListTeamMembersBySlug func(ctx context.Context, org, slug string, opts *github.TeamListTeamMembersOptions) ([]*github.User, *github.Response, error) diff --git a/internal/controller/repository/repository.go b/internal/controller/repository/repository.go index 924ffb8..cebd4eb 100644 --- a/internal/controller/repository/repository.go +++ b/internal/controller/repository/repository.go @@ -18,6 +18,7 @@ package repository import ( "context" + "encoding/json" "fmt" "reflect" "sort" @@ -198,6 +199,20 @@ func (c *external) Observe(ctx context.Context, mg resource.Managed) (managed.Ex } } + if cr.Spec.ForProvider.RepositoryRules != nil { + ghRepositoryRules, _ := getRepositoryRules(ctx, c.github, cr.Spec.ForProvider.Org, name) + + crRepositoryRulesToConfig := getRepositoryRulesMapFromCr(cr.Spec.ForProvider.RepositoryRules) + ghRepositoryRulesToConfig, err := getRepositoryRulesWithConfig(ctx, c.github, cr.Spec.ForProvider.Org, name, ghRepositoryRules) + if err != nil { + return managed.ExternalObservation{}, err + } + + if !cmp.Equal(crRepositoryRulesToConfig, ghRepositoryRulesToConfig) { + return notUpToDate, nil + } + } + archivedCr := pointer.BoolDeref(cr.Spec.ForProvider.Archived, false) if archivedCr != *repo.Archived { return notUpToDate, nil @@ -712,6 +727,18 @@ func (c *external) Create(ctx context.Context, mg resource.Managed) (managed.Ext } } } + if cr.Spec.ForProvider.RepositoryRules != nil { + rulesMap := getRepositoryRulesMapFromCr(cr.Spec.ForProvider.RepositoryRules) + for key := range rulesMap { + // avoid "G601: Implicit memory aliasing in for loop" + rule := rulesMap[key] + _, _, err := c.github.Repositories.CreateRuleset(ctx, cr.Spec.ForProvider.Org, name, crRepoRulesToRulesConfig(rule)) + if err != nil { + return managed.ExternalCreation{}, err + } + } + + } cr.SetConditions(xpv1.Available()) @@ -977,6 +1004,441 @@ func handleBranchProtectionSignature(ctx context.Context, gh *ghclient.Client, o return nil } +// getRepositoryRules retrieves all the rules for a given GitHub repository. +// It uses pagination to handle large numbers of rules, fetching 100 rules per API call. +func getRepositoryRules(ctx context.Context, gh *ghclient.Client, org, repo string) ([]*github.Ruleset, error) { + opt := &github.ListOptions{PerPage: 100} + var allRules []*github.Ruleset + + for { + rules, resp, err := gh.Repositories.GetAllRulesets(ctx, org, repo, true) + if err != nil { + return nil, err + } + + allRules = append(allRules, rules...) + + if resp.NextPage == 0 { + break + } + opt.Page = resp.NextPage + } + + return allRules, nil +} + +// getRepositoryRulesMapFromCr generates a map from the RepositoryRules slice +// in the Crossplane resource. +// +//nolint:gocyclo +func getRepositoryRulesMapFromCr(rules []v1alpha1.RepositoryRuleset) map[string]v1alpha1.RepositoryRuleset { + crRulesToConfig := make(map[string]v1alpha1.RepositoryRuleset, len(rules)) + + for i := range rules { + // Use a copy to avoid changing passed []v1alpha1.RepositoryRules + // This prevents the controller from changing the spec of the live CR + // It can also prevent infinite reconciliation loops when managing the resources with ArgoCD + orig := &rules[i] + rCopy := orig.DeepCopy() + + // handle optional fields + rCopy.Target = util.StringDerefToPointer(rCopy.Target, "branch") + rCopy.Enforcement = util.StringDerefToPointer(rCopy.Enforcement, "active") + + rConditions := rCopy.Conditions + + if rConditions != nil && rConditions.RefName != nil { + if rConditions.RefName.Include != nil { + rConditions.RefName.Include = util.SortAndReturn(rConditions.RefName.Include) + } + if rConditions.RefName.Exclude != nil { + rConditions.RefName.Exclude = util.SortAndReturn(rConditions.RefName.Exclude) + } + } + + if rConditions == nil { + rConditions = &v1alpha1.RulesetConditions{ + RefName: &v1alpha1.RulesetRefName{ + Include: []string{}, + Exclude: []string{}, + }, + } + // Update the rConditions reference in rCopy + rCopy.Conditions = rConditions + } + + rBActors := rCopy.BypassActors + if rBActors != nil { + for a := range rBActors { + actor := rBActors[a] // Make a copy of the actor + + // Set ActorId, ActorType, and BypassMode fields + actor.ActorId = rBActors[a].ActorId + actor.ActorType = rBActors[a].ActorType + actor.BypassMode = rBActors[a].BypassMode + + // Update the actor in the slice + rBActors[a] = actor + } + util.SortRulesBypassActors(rBActors) + } + rRules := rCopy.Rules + if rRules != nil { + rRules.RequiredSignatures = util.BoolDerefToPointer(rRules.RequiredSignatures, false) + rRules.NonFastForward = util.BoolDerefToPointer(rRules.NonFastForward, false) + rRules.Creation = util.BoolDerefToPointer(rRules.Creation, false) + rRules.Deletion = util.BoolDerefToPointer(rRules.Deletion, false) + rRules.RequiredLinearHistory = util.BoolDerefToPointer(rRules.RequiredLinearHistory, false) + rRules.Update = util.BoolDerefToPointer(rRules.Update, false) + + if rRules.RequiredDeployments != nil { + if rRules.RequiredDeployments.Environments != nil { + rRules.RequiredDeployments.Environments = util.SortAndReturn(rRules.RequiredDeployments.Environments) + } + } + if rRules.PullRequest != nil { + rRules.PullRequest.DismissStaleReviewsOnPush = util.BoolDerefToPointer(rRules.PullRequest.DismissStaleReviewsOnPush, false) + rRules.PullRequest.RequireCodeOwnerReview = util.BoolDerefToPointer(rRules.PullRequest.RequireCodeOwnerReview, false) + rRules.PullRequest.RequireLastPushApproval = util.BoolDerefToPointer(rRules.PullRequest.RequireLastPushApproval, false) + rRules.PullRequest.RequiredReviewThreadResolution = util.BoolDerefToPointer(rRules.PullRequest.RequiredReviewThreadResolution, false) + rRules.PullRequest.RequiredApprovingReviewCount = util.IntDerefToPointer(rRules.PullRequest.RequiredApprovingReviewCount, 0) + } + if rRules.RequiredStatusChecks != nil { + if rRules.RequiredStatusChecks.RequiredStatusChecks != nil { + copyOfStatusChecks := make([]*v1alpha1.RulesRequiredStatusChecksParameters, len(rRules.RequiredStatusChecks.RequiredStatusChecks)) + copy(copyOfStatusChecks, rRules.RequiredStatusChecks.RequiredStatusChecks) + util.SortRulesRequiredStatusChecks(copyOfStatusChecks) + rRules.RequiredStatusChecks.RequiredStatusChecks = copyOfStatusChecks + } + rRules.RequiredStatusChecks.StrictRequiredStatusChecksPolicy = util.BoolDerefToPointer(rRules.RequiredStatusChecks.StrictRequiredStatusChecksPolicy, false) + } + } + crRulesToConfig[rCopy.Name] = *rCopy + } + + return crRulesToConfig +} + +// getRepositoryRulesWithConfig creates a map of RepositoryRules based on the +// branch rules fetched from the GitHub API. +// +//nolint:gocyclo +func getRepositoryRulesWithConfig(ctx context.Context, gh *ghclient.Client, owner, repo string, ghRulesets []*github.Ruleset) (map[string]v1alpha1.RepositoryRuleset, error) { + rulesToConfig := make(map[string]v1alpha1.RepositoryRuleset, len(ghRulesets)) + + for _, rule := range ghRulesets { + rRuleset, _, err := gh.Repositories.GetRuleset(ctx, owner, repo, *rule.ID, true) + if err != nil { + return nil, err + } + ruleset := v1alpha1.RepositoryRuleset{ + Target: util.ToStringPtr(rule.GetTarget()), + Enforcement: &rule.Enforcement, + Name: rule.Name, + + Conditions: &v1alpha1.RulesetConditions{ + RefName: &v1alpha1.RulesetRefName{ + Include: []string{}, + Exclude: []string{}, + }, + }, + BypassActors: nil, + Rules: &v1alpha1.Rules{ + Creation: util.ToBoolPtr(false), + Update: util.ToBoolPtr(false), + Deletion: util.ToBoolPtr(false), + RequiredLinearHistory: util.ToBoolPtr(false), + RequiredDeployments: nil, + RequiredSignatures: util.ToBoolPtr(false), + NonFastForward: util.ToBoolPtr(false), + PullRequest: nil, + RequiredStatusChecks: nil, + }, + } + + if rRuleset.Conditions != nil { + if rRuleset.Conditions.RefName != nil { + ruleset.Conditions.RefName = &v1alpha1.RulesetRefName{ + Include: util.SortAndReturn(rRuleset.Conditions.RefName.Include), + Exclude: util.SortAndReturn(rRuleset.Conditions.RefName.Exclude), + } + } + } + + if rRuleset.BypassActors != nil { + if len(rRuleset.BypassActors) > 0 { + ruleset.BypassActors = make([]*v1alpha1.RulesetByPassActors, len(rRuleset.BypassActors)) + for i, actor := range rRuleset.BypassActors { + ruleset.BypassActors[i] = &v1alpha1.RulesetByPassActors{ + ActorType: actor.ActorType, + ActorId: actor.ActorID, + BypassMode: actor.BypassMode, + } + } + util.SortRulesBypassActors(ruleset.BypassActors) + } + + } + if rRuleset != nil { + for _, rule := range rRuleset.Rules { + switch rule.Type { + case "creation": + ruleset.Rules.Creation = util.ToBoolPtr(true) + case "deletion": + ruleset.Rules.Deletion = util.ToBoolPtr(true) + case "required_linear_history": + ruleset.Rules.RequiredLinearHistory = util.ToBoolPtr(true) + case "required_signatures": + ruleset.Rules.RequiredSignatures = util.ToBoolPtr(true) + case "non_fast_forward": + ruleset.Rules.NonFastForward = util.ToBoolPtr(true) + case "update": + ruleset.Rules.Update = util.ToBoolPtr(true) + case "pull_request": + if rule.Parameters != nil { + params := github.PullRequestRuleParameters{} + if err := json.Unmarshal(*rule.Parameters, ¶ms); err != nil { + return nil, err + } + ruleset.Rules.PullRequest = &v1alpha1.RulesPullRequest{ + RequireCodeOwnerReview: util.ToBoolPtr(params.RequireCodeOwnerReview), + RequireLastPushApproval: util.ToBoolPtr(params.RequireLastPushApproval), + RequiredReviewThreadResolution: util.ToBoolPtr(params.RequiredReviewThreadResolution), + RequiredApprovingReviewCount: util.ToIntPtr(params.RequiredApprovingReviewCount), + DismissStaleReviewsOnPush: util.ToBoolPtr(params.DismissStaleReviewsOnPush), + } + } + case "required_deployments": + if rule.Parameters != nil { + params := github.RequiredDeploymentEnvironmentsRuleParameters{} + if err := json.Unmarshal(*rule.Parameters, ¶ms); err != nil { + return nil, err + } + ruleset.Rules.RequiredDeployments = &v1alpha1.RulesRequiredDeployments{ + Environments: util.SortAndReturn(params.RequiredDeploymentEnvironments), + } + } + case "required_status_checks": + if rule.Parameters != nil { + params := github.RequiredStatusChecksRuleParameters{} + if err := json.Unmarshal(*rule.Parameters, ¶ms); err != nil { + return nil, err + } + requiredStatusChecksParameters := make([]*v1alpha1.RulesRequiredStatusChecksParameters, len(params.RequiredStatusChecks)) + for i, statusCheck := range params.RequiredStatusChecks { + requiredStatusChecksParameters[i] = &v1alpha1.RulesRequiredStatusChecksParameters{ + Context: statusCheck.Context, + IntegrationId: statusCheck.IntegrationID, + } + } + util.SortRulesRequiredStatusChecks(requiredStatusChecksParameters) + + ruleset.Rules.RequiredStatusChecks = &v1alpha1.RulesRequiredStatusChecks{ + StrictRequiredStatusChecksPolicy: util.ToBoolPtr(params.StrictRequiredStatusChecksPolicy), + RequiredStatusChecks: requiredStatusChecksParameters, + } + } + } + + } + + } + + rulesToConfig[rule.Name] = ruleset + } + + return rulesToConfig, nil + +} + +// crRepoRulesToRulesConfig transforms a RepositoryRuleset object from the Crossplane resource +// into a Ruleset object that can be used with the GitHub API. +// +//nolint:gocyclo +func crRepoRulesToRulesConfig(rule v1alpha1.RepositoryRuleset) *github.Ruleset { + githubRuleset := &github.Ruleset{ + Name: rule.Name, + Enforcement: *rule.Enforcement, + Target: rule.Target, + } + + // If BypassActors is not nil, transform it into the github rule BypassActors + if rule.BypassActors != nil { + githubBypassActors := make([]*github.BypassActor, len(rule.BypassActors)) + for i, actor := range rule.BypassActors { + githubBypassActors[i] = &github.BypassActor{ + ActorID: actor.ActorId, + ActorType: actor.ActorType, + BypassMode: actor.BypassMode, + } + } + githubRuleset.BypassActors = githubBypassActors + } + + // If Conditions is not nil, transform it into the github rule Conditions + if rule.Conditions != nil { + githubConditions := &github.RulesetConditions{ + RefName: &github.RulesetRefConditionParameters{ + Include: rule.Conditions.RefName.Include, + Exclude: rule.Conditions.RefName.Exclude, + }, + } + githubRuleset.Conditions = githubConditions + } + // If Rules is not nil, transform it into the github rule Rules + if rule.Rules != nil { + githubRules := make([]*github.RepositoryRule, 0) + if rule.Rules.RequiredStatusChecks != nil { + params := github.RequiredStatusChecksRuleParameters{ + StrictRequiredStatusChecksPolicy: *rule.Rules.RequiredStatusChecks.StrictRequiredStatusChecksPolicy, + } + requiredStatusChecks := make([]github.RuleRequiredStatusChecks, len(rule.Rules.RequiredStatusChecks.RequiredStatusChecks)) + for i, statusCheck := range rule.Rules.RequiredStatusChecks.RequiredStatusChecks { + requiredStatusChecks[i] = github.RuleRequiredStatusChecks{ + Context: statusCheck.Context, + IntegrationID: statusCheck.IntegrationId, + } + } + params.RequiredStatusChecks = requiredStatusChecks + paramsBytes, err := json.Marshal(params) + if err != nil { + return nil + } + rawParams := json.RawMessage(paramsBytes) + githubRules = append(githubRules, &github.RepositoryRule{ + Type: "required_status_checks", + Parameters: &rawParams, + }) + } + + if *rule.Rules.Creation { + githubRules = append(githubRules, &github.RepositoryRule{ + Type: "creation", + }) + } + + if *rule.Rules.Deletion { + githubRules = append(githubRules, &github.RepositoryRule{ + Type: "deletion", + }) + } + + if *rule.Rules.RequiredLinearHistory { + githubRules = append(githubRules, &github.RepositoryRule{ + Type: "required_linear_history", + }) + } + + if *rule.Rules.RequiredSignatures { + githubRules = append(githubRules, &github.RepositoryRule{ + Type: "required_signatures", + }) + } + if *rule.Rules.NonFastForward { + githubRules = append(githubRules, &github.RepositoryRule{ + Type: "non_fast_forward", + }) + } + if *rule.Rules.Update { + githubRules = append(githubRules, &github.RepositoryRule{ + Type: "update", + }) + } + if rule.Rules.PullRequest != nil { + params := github.PullRequestRuleParameters{ + DismissStaleReviewsOnPush: *rule.Rules.PullRequest.DismissStaleReviewsOnPush, + RequireCodeOwnerReview: *rule.Rules.PullRequest.RequireCodeOwnerReview, + RequireLastPushApproval: *rule.Rules.PullRequest.RequireLastPushApproval, + RequiredReviewThreadResolution: *rule.Rules.PullRequest.RequiredReviewThreadResolution, + RequiredApprovingReviewCount: *rule.Rules.PullRequest.RequiredApprovingReviewCount, + } + paramsBytes, err := json.Marshal(params) + if err != nil { + return nil + } + rawParams := json.RawMessage(paramsBytes) + githubRules = append(githubRules, &github.RepositoryRule{ + Type: "pull_request", + Parameters: &rawParams, + }) + } + if rule.Rules.RequiredDeployments != nil { + params := github.RequiredDeploymentEnvironmentsRuleParameters{ + RequiredDeploymentEnvironments: rule.Rules.RequiredDeployments.Environments, + } + paramsBytes, err := json.Marshal(params) + if err != nil { + return nil + } + rawParams := json.RawMessage(paramsBytes) + githubRules = append(githubRules, &github.RepositoryRule{ + Type: "required_deployments", + Parameters: &rawParams, + }) + } + githubRuleset.Rules = githubRules + + } + return githubRuleset +} + +// updateRepositoryRules synchronizes the repository rules of a GitHub repository +// to match with those detailed in the repository resource object. +// It performs necessary additions, updates, or deletions based on the difference between +// the actual state on GitHub and the desired state in the resource object. +func updateRepositoryRules(ctx context.Context, cr *v1alpha1.Repository, gh *ghclient.Client, repoName string) error { + // Fetch the current repository rules from GitHub + ghRepoRules, err := getRepositoryRules(ctx, gh, cr.Spec.ForProvider.Org, repoName) + if err != nil { + return err + } + // Generate a map of the repository rules from the Crossplane resource + crRToConfig := getRepositoryRulesMapFromCr(cr.Spec.ForProvider.RepositoryRules) + // Generate a map of the repository rules from GitHub + ghRToConfig, err := getRepositoryRulesWithConfig(ctx, gh, cr.Spec.ForProvider.Org, repoName, ghRepoRules) + if err != nil { + return err + } + // Determine which rules need to be deleted, added, or updated + toDelete, toAdd, toUpdate := util.DiffRepositoryRulesets(ghRToConfig, crRToConfig) + + // Delete the rules that are no longer needed + for name := range toDelete { + rulesetID, _ := findRulesetIDByName(ghRepoRules, name) + _, err = gh.Repositories.DeleteRuleset(ctx, cr.Spec.ForProvider.Org, repoName, rulesetID) + if err != nil { + return err + } + } + // Add the new rules + for _, rule := range toAdd { + _, _, err := gh.Repositories.CreateRuleset(ctx, cr.Spec.ForProvider.Org, repoName, crRepoRulesToRulesConfig(rule)) + if err != nil { + return err + } + } + // Update the existing rules + for name, rule := range toUpdate { + rulesetID, _ := findRulesetIDByName(ghRepoRules, name) + _, _, err := gh.Repositories.UpdateRuleset(ctx, cr.Spec.ForProvider.Org, repoName, rulesetID, crRepoRulesToRulesConfig(rule)) + if err != nil { + return err + } + } + return nil +} + +// findRulesetIDByName iterates over a slice of GitHub Ruleset pointers and returns the ID of the ruleset +// that matches the provided name. If no match is found, it returns an error. +func findRulesetIDByName(rulesets []*github.Ruleset, name string) (int64, error) { + for _, ruleset := range rulesets { + if ruleset.Name == name { + return *ruleset.ID, nil + } + } + return 0, fmt.Errorf("ruleset with name %s not found", name) +} + //nolint:gocyclo func (c *external) Update(ctx context.Context, mg resource.Managed) (managed.ExternalUpdate, error) { cr, ok := mg.(*v1alpha1.Repository) @@ -1036,6 +1498,13 @@ func (c *external) Update(ctx context.Context, mg resource.Managed) (managed.Ext return managed.ExternalUpdate{}, err } } + if cr.Spec.ForProvider.RepositoryRules != nil { + err = updateRepositoryRules(ctx, cr, c.github, name) + if err != nil { + return managed.ExternalUpdate{}, err + } + + } return managed.ExternalUpdate{}, nil } diff --git a/internal/controller/repository/repository_test.go b/internal/controller/repository/repository_test.go index 80b8ea7..89ddcfd 100644 --- a/internal/controller/repository/repository_test.go +++ b/internal/controller/repository/repository_test.go @@ -78,6 +78,22 @@ var ( bpr1allowForkSyncing = false bpr1requireSignedCommits = false bpr1requiredStatusCheck = "terraform_validate" + + rr1Id int64 = 123 + rr1name = "test-ruleset-1" + rr1target = "branch" + rr1enforcement = "active" + rr1actorType = "Team" + rr1bypassMode = "always" + rr1rulesCreation = true + rr1rulesDeletion = true + rr1rulesUpdate = true + rr1rulesRequiredLinearHistory = true + rr1rulesRequiredSignatures = true + rr1rulesNonFastForward = true + rr1actorId int64 = 123 + rr1Include = []string{"include"} + rr1Exclude = []string{"exclude"} ) func withTeamPermission() repositoryModifier { @@ -142,6 +158,34 @@ func repository(m ...repositoryModifier) *v1alpha1.Repository { }, }, } + cr.Spec.ForProvider.RepositoryRules = []v1alpha1.RepositoryRuleset{ + { + Name: rr1name, + Target: &rr1target, + Enforcement: &rr1enforcement, + Conditions: &v1alpha1.RulesetConditions{ + RefName: &v1alpha1.RulesetRefName{ + Include: rr1Include, + Exclude: rr1Exclude, + }, + }, + BypassActors: []*v1alpha1.RulesetByPassActors{ + { + ActorId: &rr1actorId, + ActorType: &rr1actorType, + BypassMode: &rr1bypassMode, + }, + }, + Rules: &v1alpha1.Rules{ + Creation: &rr1rulesCreation, + Deletion: &rr1rulesDeletion, + Update: &rr1rulesUpdate, + RequiredLinearHistory: &rr1rulesRequiredLinearHistory, + RequiredSignatures: &rr1rulesRequiredSignatures, + NonFastForward: &rr1rulesNonFastForward, + }, + }, + } meta.SetExternalName(cr, repo) @@ -213,6 +257,51 @@ func githubProtectedBranch() *github.Protection { } } +func githubRuleset() []*github.Ruleset { + return []*github.Ruleset{ + { + ID: &rr1Id, + Name: rr1name, + Target: &rr1target, + Enforcement: rr1enforcement, + Conditions: &github.RulesetConditions{ + RefName: &github.RulesetRefConditionParameters{ + Include: rr1Include, + Exclude: rr1Exclude, + }, + }, + BypassActors: []*github.BypassActor{ + { + ActorID: &rr1actorId, + ActorType: &rr1actorType, + BypassMode: &rr1bypassMode, + }, + }, + Rules: []*github.RepositoryRule{ + { + Type: "creation", + }, + { + Type: "deletion", + }, + { + Type: "update", + }, + { + Type: "required_linear_history", + }, + { + Type: "required_signatures", + }, + { + Type: "non_fast_forward", + }, + }, + }, + } + +} + func githubCollaborators() []*github.User { return []*github.User{ { @@ -295,6 +384,12 @@ func TestObserve(t *testing.T) { MockListBranches: func(ctx context.Context, owner, repo string, opts *github.BranchListOptions) ([]*github.Branch, *github.Response, error) { return []*github.Branch{}, fake.GenerateEmptyResponse(), nil }, + MockGetAllRulesets: func(ctx context.Context, owner, repo string) ([]*github.Ruleset, *github.Response, error) { + return githubRuleset(), fake.GenerateEmptyResponse(), nil + }, + MockGetRuleset: func(ctx context.Context, owner, repo string, rulesetID int64, includesParents bool) (*github.Ruleset, *github.Response, error) { + return githubRuleset()[0], fake.GenerateEmptyResponse(), nil + }, }, }, }, @@ -334,6 +429,12 @@ func TestObserve(t *testing.T) { MockGetBranchProtection: func(ctx context.Context, owner, repo, branch string) (*github.Protection, *github.Response, error) { return githubProtectedBranch(), fake.GenerateEmptyResponse(), nil }, + MockGetAllRulesets: func(ctx context.Context, owner, repo string) ([]*github.Ruleset, *github.Response, error) { + return githubRuleset(), fake.GenerateEmptyResponse(), nil + }, + MockGetRuleset: func(ctx context.Context, owner, repo string, rulesetID int64, includesParents bool) (*github.Ruleset, *github.Response, error) { + return githubRuleset()[0], fake.GenerateEmptyResponse(), nil + }, }, }, }, diff --git a/internal/util/util.go b/internal/util/util.go index 72434f9..969ca20 100644 --- a/internal/util/util.go +++ b/internal/util/util.go @@ -20,11 +20,9 @@ import ( "reflect" "sort" - "k8s.io/utils/pointer" - - "github.com/google/go-cmp/cmp" - "github.com/crossplane/provider-github/apis/organizations/v1alpha1" + "github.com/google/go-cmp/cmp" + "k8s.io/utils/pointer" ) func SortByKey(m map[string]string) map[string]string { @@ -146,6 +144,38 @@ func DiffProtectedBranches(a, b map[string]v1alpha1.BranchProtectionRule) ( } +// DiffRepositoryRulesets compares two maps of RepositoryRuleset, 'a' and 'b'. +// It returns three maps: +// inANotInB: entities (keys) that are present in 'a' but not in 'b' mapped to their values in 'a' +// inBNotInA: entities (keys) that are present in 'b' but not in 'a' mapped to their values in 'b' +// diffs: entities (keys) that are present in both 'a' and 'b' but have different values, mapped to their values in 'b' +func DiffRepositoryRulesets(a, b map[string]v1alpha1.RepositoryRuleset) ( + map[string]v1alpha1.RepositoryRuleset, + map[string]v1alpha1.RepositoryRuleset, + map[string]v1alpha1.RepositoryRuleset) { + inANotInB := make(map[string]v1alpha1.RepositoryRuleset) + inBNotInA := make(map[string]v1alpha1.RepositoryRuleset) + diffs := make(map[string]v1alpha1.RepositoryRuleset) + + for entity, va := range a { + vb, ok := b[entity] + if !ok { + inANotInB[entity] = va + } else if !reflect.DeepEqual(va, vb) { + diffs[entity] = vb + } + } + + for entity, vb := range b { + _, ok := a[entity] + if !ok { + inBNotInA[entity] = vb + } + } + + return inANotInB, inBNotInA, diffs +} + // DefaultToStringSlice is a helper function that checks if the provided slice is // nil and returns an empty string slice in that case. If the slice is not nil, // the same slice is returned. The purpose of this function is to avoid nil @@ -179,11 +209,46 @@ func SortRequiredStatusChecks(checks []*v1alpha1.RequiredStatusCheck) { }) } +// SortRulesRequiredStatusChecks sorts a slice of RequiredStatusCheck pointers in-place +// by the Context field in ascending order. +func SortRulesRequiredStatusChecks(checks []*v1alpha1.RulesRequiredStatusChecksParameters) { + sort.Slice(checks, func(i, j int) bool { + return checks[i].Context < checks[j].Context + }) +} + +// SortRulesBypassActors sorts a slice of RulesetByPassActors pointers in-place +// by the ActorId field in ascending order. +func SortRulesBypassActors(actors []*v1alpha1.RulesetByPassActors) { + sort.Slice(actors, func(i, j int) bool { + return *actors[i].ActorId < *actors[j].ActorId + }) + +} + // ToBoolPtr converts a boolean value to a pointer to a boolean value. func ToBoolPtr(b bool) *bool { return &b } +// ToIntPtr is a helper function that takes an integer 'i' as input and returns a pointer to 'i'. +// This can be useful when you want to create a pointer to an integer value. +func ToIntPtr(i int) *int { + return &i +} + +// ToInt64Ptr is a helper function that takes an int64 'i' as input and returns a pointer to 'i'. +// This can be useful when you want to create a pointer to an int64 value. +func ToInt64Ptr(i int64) *int64 { + return &i +} + +// ToStringPtr is a helper function that takes a string 's' as input and returns a pointer to 's'. +// This can be useful when you want to create a pointer to a string value. +func ToStringPtr(s string) *string { + return &s +} + // BoolDerefToPointer dereferences the pointer to bool 'ptr', // uses 'def' as a default if 'ptr' is nil, and returns a new pointer to the resulting bool. func BoolDerefToPointer(ptr *bool, def bool) *bool { @@ -191,6 +256,27 @@ func BoolDerefToPointer(ptr *bool, def bool) *bool { return &b } +// StringDerefToPointer is a helper function that dereferences a pointer to a string 'ptr', +// and returns a new pointer to the resulting string. If 'ptr' is nil, it uses 'def' as a default value. +func StringDerefToPointer(ptr *string, def string) *string { + s := pointer.StringDeref(ptr, def) + return &s +} + +// IntDerefToPointer is a helper function that dereferences a pointer to an int 'ptr', +// and returns a new pointer to the resulting int. If 'ptr' is nil, it uses 'def' as a default value. +func IntDerefToPointer(ptr *int, def int) *int { + i := pointer.IntDeref(ptr, def) + return &i +} + +// Int64DerefToPointer is a helper function that dereferences a pointer to an int64 'ptr', +// and returns a new pointer to the resulting int64. If 'ptr' is nil, it uses 'def' as a default value. +func Int64DerefToPointer(ptr *int64, def int64) *int64 { + i := pointer.Int64Deref(ptr, def) + return &i +} + // BoolToInt converts a boolean value to an integer func BoolToInt(b bool) int { if b { diff --git a/package/crds/organizations.github.crossplane.io_repositories.yaml b/package/crds/organizations.github.crossplane.io_repositories.yaml index ec37ed5..2c5821c 100644 --- a/package/crds/organizations.github.crossplane.io_repositories.yaml +++ b/package/crds/organizations.github.crossplane.io_repositories.yaml @@ -581,6 +581,164 @@ spec: description: Private sets the repository to private, if false it will be public type: boolean + repositoryRules: + description: RepositoryRules are the rules for the repository + items: + description: RepositoryRuleset represents the rules for a repository + properties: + bypassActors: + description: BypassActors is the list of actors that can + bypass the ruleset + items: + properties: + actorId: + description: ActorId is the ID of the actor + format: int64 + type: integer + actorType: + description: 'ActorType is the type of the actor, + can be one of: Integration, OrganizationAdmin, RepositoryRole, + Team' + type: string + bypassMode: + description: 'BypassMode is the bypass mode of the + actor, can be one of: "always", "pull_request"' + type: string + type: object + type: array + conditions: + description: Conditions is the conditions for the ruleset, + which branches or tags are included or excluded from the + ruleset + properties: + refName: + properties: + exclude: + description: Exclude is the list of branches or + tags to exclude + items: + type: string + type: array + include: + description: Include is the list of branches or + tags to include + items: + type: string + type: array + required: + - exclude + - include + type: object + type: object + enforcement: + description: 'Enforcement is the enforcement level of the + ruleset, can be one of: "disabled", "active"' + type: string + name: + description: Name is the name of the ruleset + type: string + rules: + description: Rules is the rules for the ruleset + properties: + creation: + description: Creation restricts the creation of matching + branches or tags that are set in Conditions + type: boolean + deletion: + description: Deletion restricts the deletion of matching + branches or tags that are set in Conditions + type: boolean + nonFastForward: + description: NonFastForward restricts force pushes to + matching branches or tags that are set in Conditions + type: boolean + pullRequest: + description: PullRequest is the rules for pull requests + properties: + dismissStaleReviewsOnPush: + description: DismissStaleReviewsOnPush automatically + dismiss approving reviews when someone pushes + a new commit. + type: boolean + requireCodeOwnerReview: + description: RequireCodeOwnerReview requires the + pull request to be approved by a code owner. + type: boolean + requireLastPushApproval: + description: RequireLastPushApproval requires the + most recent push to be approved by someone other + than the person who pushed it. + type: boolean + requiredApprovingReviewCount: + description: RequiredApprovingReviewCount specifies + the number of reviewers required to approve pull + requests. + type: integer + requiredReviewThreadResolution: + description: RequiredReviewThreadResolution requires + all conversations on code to be resolved before + a pull request can be merged. + type: boolean + type: object + requiredDeployments: + description: RequiredDeployments requires that deployment + to specific environments are successful before merging. + properties: + environments: + description: Environments is the list of environments + that are required to be deployed to before merging + items: + type: string + type: array + type: object + requiredLinearHistory: + description: RequiredLinearHistory requires a linear + commit history, which prevents merge commits. + type: boolean + requiredSignatures: + description: RequiredSignatures requires signed commits. + type: boolean + requiredStatusChecks: + description: RequiredStatusChecks requires status checks + to pass before merging. + properties: + requiredStatusChecks: + description: RequiredStatusChecks is the list of + status checks to require in order to merge into + this branch. + items: + properties: + context: + description: Context is the name of the required + check. + type: string + integrationId: + description: IntegrationId is the ID of integration + that must provide this check. + format: int64 + type: integer + required: + - context + type: object + type: array + strictRequiredStatusChecksPolicy: + description: StrictRequiredStatusChecksPolicy requires + branches to be up-to-date before merging. + type: boolean + type: object + update: + description: Update restricts the update of matching + branches or tags that are set in Conditions + type: boolean + type: object + target: + description: 'Target is the target of the ruleset, can be + one of: "branch", "tag"' + type: string + required: + - name + type: object + type: array webhooks: items: description: Repository webhook https://docs.github.com/en/webhooks/types-of-webhooks#repository-webhooks