From bd6f038eb759c409dbfc13fef36f0366b6545340 Mon Sep 17 00:00:00 2001 From: Lado Golijashvili Date: Mon, 13 Nov 2023 12:32:21 +0000 Subject: [PATCH 1/6] added functionality to list actions enabled repositories in org --- .../v1alpha1/organization_types.go | 18 ++++- .../v1alpha1/zz_generated.deepcopy.go | 28 ++++++- .../v1alpha1/zz_generated.resolvers.go | 26 ++++++ internal/clients/client.go | 6 ++ .../controller/organization/organization.go | 8 ++ ...ns.github.crossplane.io_organizations.yaml | 81 +++++++++++++++++++ 6 files changed, 165 insertions(+), 2 deletions(-) diff --git a/apis/organizations/v1alpha1/organization_types.go b/apis/organizations/v1alpha1/organization_types.go index 3080c70..fd3ab21 100644 --- a/apis/organizations/v1alpha1/organization_types.go +++ b/apis/organizations/v1alpha1/organization_types.go @@ -25,9 +25,25 @@ import ( xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" ) +type ActionsConfiguration struct { + // Org is the Organization for the Membership + // +immutable + // +crossplane:generate:reference:type=Organization + Name string `json:"name,omitempty"` + + // OrgRef is a reference to an Organization + // +optional + NameRef *xpv1.Reference `json:"nameRef,omitempty"` + + // OrgSlector selects a reference to an Organization + // +optional + NameSelector *xpv1.Selector `json:"nameSelector,omitempty"` +} + // OrganizationParameters are the configurable fields of a Organization. type OrganizationParameters struct { - Description string `json:"description"` + Description string `json:"description"` + Actions ActionsConfiguration `json:"actions"` } // OrganizationObservation are the observable fields of a Organization. diff --git a/apis/organizations/v1alpha1/zz_generated.deepcopy.go b/apis/organizations/v1alpha1/zz_generated.deepcopy.go index 4d3db9d..accb712 100644 --- a/apis/organizations/v1alpha1/zz_generated.deepcopy.go +++ b/apis/organizations/v1alpha1/zz_generated.deepcopy.go @@ -26,6 +26,31 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ActionsConfiguration) DeepCopyInto(out *ActionsConfiguration) { + *out = *in + if in.NameRef != nil { + in, out := &in.NameRef, &out.NameRef + *out = new(v1.Reference) + (*in).DeepCopyInto(*out) + } + if in.NameSelector != nil { + in, out := &in.NameSelector, &out.NameSelector + *out = new(v1.Selector) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ActionsConfiguration. +func (in *ActionsConfiguration) DeepCopy() *ActionsConfiguration { + if in == nil { + return nil + } + out := new(ActionsConfiguration) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Membership) DeepCopyInto(out *Membership) { *out = *in @@ -236,6 +261,7 @@ func (in *OrganizationObservation) DeepCopy() *OrganizationObservation { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OrganizationParameters) DeepCopyInto(out *OrganizationParameters) { *out = *in + in.Actions.DeepCopyInto(&out.Actions) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OrganizationParameters. @@ -252,7 +278,7 @@ func (in *OrganizationParameters) DeepCopy() *OrganizationParameters { func (in *OrganizationSpec) DeepCopyInto(out *OrganizationSpec) { *out = *in in.ResourceSpec.DeepCopyInto(&out.ResourceSpec) - out.ForProvider = in.ForProvider + in.ForProvider.DeepCopyInto(&out.ForProvider) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OrganizationSpec. diff --git a/apis/organizations/v1alpha1/zz_generated.resolvers.go b/apis/organizations/v1alpha1/zz_generated.resolvers.go index 982fcf9..a4c947e 100644 --- a/apis/organizations/v1alpha1/zz_generated.resolvers.go +++ b/apis/organizations/v1alpha1/zz_generated.resolvers.go @@ -50,6 +50,32 @@ func (mg *Membership) ResolveReferences(ctx context.Context, c client.Reader) er return nil } +// ResolveReferences of this Organization. +func (mg *Organization) ResolveReferences(ctx context.Context, c client.Reader) error { + r := reference.NewAPIResolver(c, mg) + + var rsp reference.ResolutionResponse + var err error + + rsp, err = r.Resolve(ctx, reference.ResolutionRequest{ + CurrentValue: mg.Spec.ForProvider.Actions.Name, + Extract: reference.ExternalName(), + Reference: mg.Spec.ForProvider.Actions.NameRef, + Selector: mg.Spec.ForProvider.Actions.NameSelector, + To: reference.To{ + List: &OrganizationList{}, + Managed: &Organization{}, + }, + }) + if err != nil { + return errors.Wrap(err, "mg.Spec.ForProvider.Actions.Name") + } + mg.Spec.ForProvider.Actions.Name = rsp.ResolvedValue + mg.Spec.ForProvider.Actions.NameRef = rsp.ResolvedReference + + return nil +} + // ResolveReferences of this Repository. func (mg *Repository) ResolveReferences(ctx context.Context, c client.Reader) error { r := reference.NewAPIResolver(c, mg) diff --git a/internal/clients/client.go b/internal/clients/client.go index 5e022ee..607b01d 100644 --- a/internal/clients/client.go +++ b/internal/clients/client.go @@ -28,12 +28,17 @@ import ( ) type Client struct { + Actions ActionsClient Organizations OrganizationsClient Users UsersClient Teams TeamsClient Repositories RepositoriesClient } +type ActionsClient interface { + ListEnabledReposInOrg(ctx context.Context, owner string, opts *github.ListOptions) (*github.ActionsEnabledOnOrgRepos, *github.Response, error) +} + type OrganizationsClient interface { Get(ctx context.Context, org string) (*github.Organization, *github.Response, error) Edit(ctx context.Context, name string, org *github.Organization) (*github.Organization, *github.Response, error) @@ -98,6 +103,7 @@ func NewClient(creds string) (*Client, error) { } return &Client{ + Actions: ghclient.Actions, Organizations: ghclient.Organizations, Users: ghclient.Users, Teams: ghclient.Teams, diff --git a/internal/controller/organization/organization.go b/internal/controller/organization/organization.go index 2bbd86d..75d9f63 100644 --- a/internal/controller/organization/organization.go +++ b/internal/controller/organization/organization.go @@ -18,6 +18,7 @@ package organization import ( "context" + "fmt" "k8s.io/utils/pointer" @@ -142,6 +143,13 @@ func (c *external) Observe(ctx context.Context, mg resource.Managed) (managed.Ex return managed.ExternalObservation{}, err } + // To use this function, the organization permission policy for enabled_repositories must be configured to selected, otherwise you get error 409 Conflict + aResp, _, err := c.github.Actions.ListEnabledReposInOrg(ctx, name, &github.ListOptions{PerPage: 100}) + if err != nil { + return managed.ExternalObservation{}, err + } + fmt.Print(aResp) + if cr.Spec.ForProvider.Description != pointer.StringDeref(org.Description, "") { return managed.ExternalObservation{ ResourceExists: true, diff --git a/package/crds/organizations.github.crossplane.io_organizations.yaml b/package/crds/organizations.github.crossplane.io_organizations.yaml index 35af585..a6dfe34 100644 --- a/package/crds/organizations.github.crossplane.io_organizations.yaml +++ b/package/crds/organizations.github.crossplane.io_organizations.yaml @@ -68,9 +68,90 @@ spec: description: OrganizationParameters are the configurable fields of a Organization. properties: + actions: + properties: + name: + description: Org is the Organization for the Membership + type: string + nameRef: + description: OrgRef is a reference to an Organization + properties: + name: + description: Name of the referenced object. + type: string + policy: + description: Policies for referencing. + properties: + resolution: + default: Required + description: Resolution specifies whether resolution + of this reference is required. The default is 'Required', + which means the reconcile will fail if the reference + cannot be resolved. 'Optional' means this reference + will be a no-op if it cannot be resolved. + enum: + - Required + - Optional + type: string + resolve: + description: Resolve specifies when this reference + should be resolved. The default is 'IfNotPresent', + which will attempt to resolve the reference only + when the corresponding field is not present. Use + 'Always' to resolve the reference on every reconcile. + enum: + - Always + - IfNotPresent + type: string + type: object + required: + - name + type: object + nameSelector: + description: OrgSlector selects a reference to an Organization + properties: + matchControllerRef: + description: MatchControllerRef ensures an object with + the same controller reference as the selecting object + is selected. + type: boolean + matchLabels: + additionalProperties: + type: string + description: MatchLabels ensures an object with matching + labels is selected. + type: object + policy: + description: Policies for selection. + properties: + resolution: + default: Required + description: Resolution specifies whether resolution + of this reference is required. The default is 'Required', + which means the reconcile will fail if the reference + cannot be resolved. 'Optional' means this reference + will be a no-op if it cannot be resolved. + enum: + - Required + - Optional + type: string + resolve: + description: Resolve specifies when this reference + should be resolved. The default is 'IfNotPresent', + which will attempt to resolve the reference only + when the corresponding field is not present. Use + 'Always' to resolve the reference on every reconcile. + enum: + - Always + - IfNotPresent + type: string + type: object + type: object + type: object description: type: string required: + - actions - description type: object managementPolicies: From d8fc7818001d33eb56fd12bb07c30755afbba53d Mon Sep 17 00:00:00 2001 From: Lado Golijashvili Date: Tue, 21 Nov 2023 14:11:57 +0400 Subject: [PATCH 2/6] implement observe, update function logic --- .../v1alpha1/organization_types.go | 22 ++- .../v1alpha1/zz_generated.deepcopy.go | 32 +++- .../v1alpha1/zz_generated.resolvers.go | 31 ++-- internal/clients/client.go | 2 + .../controller/organization/organization.go | 123 +++++++++++-- internal/util/util.go | 10 ++ ...ns.github.crossplane.io_organizations.yaml | 165 ++++++++++-------- 7 files changed, 267 insertions(+), 118 deletions(-) diff --git a/apis/organizations/v1alpha1/organization_types.go b/apis/organizations/v1alpha1/organization_types.go index fd3ab21..a47d09b 100644 --- a/apis/organizations/v1alpha1/organization_types.go +++ b/apis/organizations/v1alpha1/organization_types.go @@ -25,25 +25,29 @@ import ( xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" ) +// ActionsConfiguration are the configurable fields of an Organization Actions. type ActionsConfiguration struct { - // Org is the Organization for the Membership - // +immutable - // +crossplane:generate:reference:type=Organization - Name string `json:"name,omitempty"` + EnabledRepos []ActionEnabledRepo `json:"enabledRepos,omitempty"` +} + +type ActionEnabledRepo struct { + // Name of the repository + // +crossplane:generate:reference:type=Repository + Repo string `json:"repo,omitempty"` - // OrgRef is a reference to an Organization + // RepoRef is a reference to the Repositories // +optional - NameRef *xpv1.Reference `json:"nameRef,omitempty"` + RepoRef *xpv1.Reference `json:"repoRef,omitempty"` - // OrgSlector selects a reference to an Organization + // RepoSelector selects a reference to an Repositories // +optional - NameSelector *xpv1.Selector `json:"nameSelector,omitempty"` + RepoSelector *xpv1.Selector `json:"repoSelector,omitempty"` } // OrganizationParameters are the configurable fields of a Organization. type OrganizationParameters struct { Description string `json:"description"` - Actions ActionsConfiguration `json:"actions"` + Actions ActionsConfiguration `json:"actions,omitempty"` } // OrganizationObservation are the observable fields of a Organization. diff --git a/apis/organizations/v1alpha1/zz_generated.deepcopy.go b/apis/organizations/v1alpha1/zz_generated.deepcopy.go index accb712..856ef7f 100644 --- a/apis/organizations/v1alpha1/zz_generated.deepcopy.go +++ b/apis/organizations/v1alpha1/zz_generated.deepcopy.go @@ -27,20 +27,42 @@ import ( ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ActionsConfiguration) DeepCopyInto(out *ActionsConfiguration) { +func (in *ActionEnabledRepo) DeepCopyInto(out *ActionEnabledRepo) { *out = *in - if in.NameRef != nil { - in, out := &in.NameRef, &out.NameRef + if in.RepoRef != nil { + in, out := &in.RepoRef, &out.RepoRef *out = new(v1.Reference) (*in).DeepCopyInto(*out) } - if in.NameSelector != nil { - in, out := &in.NameSelector, &out.NameSelector + if in.RepoSelector != nil { + in, out := &in.RepoSelector, &out.RepoSelector *out = new(v1.Selector) (*in).DeepCopyInto(*out) } } +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ActionEnabledRepo. +func (in *ActionEnabledRepo) DeepCopy() *ActionEnabledRepo { + if in == nil { + return nil + } + out := new(ActionEnabledRepo) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ActionsConfiguration) DeepCopyInto(out *ActionsConfiguration) { + *out = *in + if in.EnabledRepos != nil { + in, out := &in.EnabledRepos, &out.EnabledRepos + *out = make([]ActionEnabledRepo, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ActionsConfiguration. func (in *ActionsConfiguration) DeepCopy() *ActionsConfiguration { if in == nil { diff --git a/apis/organizations/v1alpha1/zz_generated.resolvers.go b/apis/organizations/v1alpha1/zz_generated.resolvers.go index a4c947e..2360ae9 100644 --- a/apis/organizations/v1alpha1/zz_generated.resolvers.go +++ b/apis/organizations/v1alpha1/zz_generated.resolvers.go @@ -57,21 +57,24 @@ func (mg *Organization) ResolveReferences(ctx context.Context, c client.Reader) var rsp reference.ResolutionResponse var err error - rsp, err = r.Resolve(ctx, reference.ResolutionRequest{ - CurrentValue: mg.Spec.ForProvider.Actions.Name, - Extract: reference.ExternalName(), - Reference: mg.Spec.ForProvider.Actions.NameRef, - Selector: mg.Spec.ForProvider.Actions.NameSelector, - To: reference.To{ - List: &OrganizationList{}, - Managed: &Organization{}, - }, - }) - if err != nil { - return errors.Wrap(err, "mg.Spec.ForProvider.Actions.Name") + for i4 := 0; i4 < len(mg.Spec.ForProvider.Actions.EnabledRepos); i4++ { + rsp, err = r.Resolve(ctx, reference.ResolutionRequest{ + CurrentValue: mg.Spec.ForProvider.Actions.EnabledRepos[i4].Repo, + Extract: reference.ExternalName(), + Reference: mg.Spec.ForProvider.Actions.EnabledRepos[i4].RepoRef, + Selector: mg.Spec.ForProvider.Actions.EnabledRepos[i4].RepoSelector, + To: reference.To{ + List: &RepositoryList{}, + Managed: &Repository{}, + }, + }) + if err != nil { + return errors.Wrap(err, "mg.Spec.ForProvider.Actions.EnabledRepos[i4].Repo") + } + mg.Spec.ForProvider.Actions.EnabledRepos[i4].Repo = rsp.ResolvedValue + mg.Spec.ForProvider.Actions.EnabledRepos[i4].RepoRef = rsp.ResolvedReference + } - mg.Spec.ForProvider.Actions.Name = rsp.ResolvedValue - mg.Spec.ForProvider.Actions.NameRef = rsp.ResolvedReference return nil } diff --git a/internal/clients/client.go b/internal/clients/client.go index 607b01d..881768a 100644 --- a/internal/clients/client.go +++ b/internal/clients/client.go @@ -37,6 +37,8 @@ type Client struct { type ActionsClient interface { ListEnabledReposInOrg(ctx context.Context, owner string, opts *github.ListOptions) (*github.ActionsEnabledOnOrgRepos, *github.Response, error) + AddEnabledReposInOrg(ctx context.Context, owner string, repositoryID int64) (*github.Response, error) + RemoveEnabledRepoInOrg(ctx context.Context, owner string, repositoryID int64) (*github.Response, error) } type OrganizationsClient interface { diff --git a/internal/controller/organization/organization.go b/internal/controller/organization/organization.go index 75d9f63..5126d3a 100644 --- a/internal/controller/organization/organization.go +++ b/internal/controller/organization/organization.go @@ -18,14 +18,8 @@ package organization import ( "context" - "fmt" - - "k8s.io/utils/pointer" - - "github.com/pkg/errors" - "k8s.io/apimachinery/pkg/types" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" + "reflect" + "slices" xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" "github.com/crossplane/crossplane-runtime/pkg/connection" @@ -35,6 +29,12 @@ import ( "github.com/crossplane/crossplane-runtime/pkg/ratelimiter" "github.com/crossplane/crossplane-runtime/pkg/reconciler/managed" "github.com/crossplane/crossplane-runtime/pkg/resource" + "github.com/crossplane/provider-github/internal/util" + "github.com/pkg/errors" + "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/pointer" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" "github.com/crossplane/provider-github/apis/organizations/v1alpha1" apisv1alpha1 "github.com/crossplane/provider-github/apis/v1alpha1" @@ -145,16 +145,35 @@ func (c *external) Observe(ctx context.Context, mg resource.Managed) (managed.Ex // To use this function, the organization permission policy for enabled_repositories must be configured to selected, otherwise you get error 409 Conflict aResp, _, err := c.github.Actions.ListEnabledReposInOrg(ctx, name, &github.ListOptions{PerPage: 100}) + if err != nil { return managed.ExternalObservation{}, err } - fmt.Print(aResp) + + notUpToDate := managed.ExternalObservation{ + ResourceExists: true, + ResourceUpToDate: false, + } + + // Extract repository names from the list + aRepos := make([]string, 0, len(aResp.Repositories)) + for _, repo := range aResp.Repositories { + aRepos = append(aRepos, repo.GetName()) + } + slices.Sort(aRepos) + + crARepos := getEnabledReposFromCr(cr.Spec.ForProvider.Actions.EnabledRepos) + slices.Sort(crARepos) + + if err != nil { + return managed.ExternalObservation{}, err + } + if !reflect.DeepEqual(aRepos, crARepos) { + return notUpToDate, nil + } if cr.Spec.ForProvider.Description != pointer.StringDeref(org.Description, "") { - return managed.ExternalObservation{ - ResourceExists: true, - ResourceUpToDate: false, - }, nil + return notUpToDate, nil } cr.SetConditions(xpv1.Available()) @@ -191,6 +210,76 @@ func (c *external) Update(ctx context.Context, mg resource.Managed) (managed.Ext return managed.ExternalUpdate{}, err } + crARepos := getEnabledReposFromCr(cr.Spec.ForProvider.Actions.EnabledRepos) + if err != nil { + return managed.ExternalUpdate{}, err + } + slices.Sort(crARepos) + + // To use this function, the organization permission policy for enabled_repositories must be configured to selected, otherwise you get error 409 Conflict + aResp, _, err := c.github.Actions.ListEnabledReposInOrg(ctx, name, &github.ListOptions{PerPage: 100}) + if err != nil { + return managed.ExternalUpdate{}, err + } + + // Extract repository names from the list + aRepos := make([]string, 0, len(aResp.Repositories)) + for _, repo := range aResp.Repositories { + aRepos = append(aRepos, repo.GetName()) + } + slices.Sort(aRepos) + + // Identify repositories that should be enabled + var missingRepos []string + for _, repo := range crARepos { + // Check if the repository from CRD is not in GitHub + if !util.Contains(aRepos, repo) { + missingRepos = append(missingRepos, repo) + } + } + missingReposIds := make([]int64, 0, len(missingRepos)) + for _, missingRepo := range missingRepos { + repo, _, err := c.github.Repositories.Get(ctx, name, missingRepo) + repoID := repo.GetID() + missingReposIds = append(missingReposIds, repoID) + if err != nil { + return managed.ExternalUpdate{}, err + } + } + // Enable actions for missing repositories + for _, missingRepo := range missingReposIds { + _, err := c.github.Actions.AddEnabledReposInOrg(ctx, name, missingRepo) + if err != nil { + return managed.ExternalUpdate{}, err + } + } + + // Identify repositories that should be disabled + toDeleteRepos := make([]string, 0, len(aRepos)) + for _, repo := range aRepos { + // Check if the repository from CRD is not in GitHub + if !util.Contains(crARepos, repo) { + toDeleteRepos = append(toDeleteRepos, repo) + } + } + toDeleteReposIds := make([]int64, 0, len(toDeleteRepos)) + for _, toDeleteRepo := range toDeleteRepos { + repo, _, err := c.github.Repositories.Get(ctx, name, toDeleteRepo) + repoID := repo.GetID() + toDeleteReposIds = append(toDeleteReposIds, repoID) + if err != nil { + return managed.ExternalUpdate{}, err + } + } + + // Disable actions for missing repositories + for _, toDeleteRepo := range toDeleteReposIds { + _, err := c.github.Actions.RemoveEnabledRepoInOrg(ctx, name, toDeleteRepo) + if err != nil { + return managed.ExternalUpdate{}, err + } + } + return managed.ExternalUpdate{}, nil } @@ -203,3 +292,11 @@ func (c *external) Delete(ctx context.Context, mg resource.Managed) error { return nil } + +func getEnabledReposFromCr(repos []v1alpha1.ActionEnabledRepo) []string { + crAEnabledRepos := make([]string, 0, len(repos)) + for _, repo := range repos { + crAEnabledRepos = append(crAEnabledRepos, repo.Repo) + } + return crAEnabledRepos +} diff --git a/internal/util/util.go b/internal/util/util.go index 1ec550d..fedda8b 100644 --- a/internal/util/util.go +++ b/internal/util/util.go @@ -70,3 +70,13 @@ func MergeMaps(m1 map[string]string, m2 map[string]string) map[string]string { } return merged } + +// Contains function to check if a string slice contains a specific string +func Contains(slice []string, str string) bool { + for _, s := range slice { + if s == str { + return true + } + } + return false +} diff --git a/package/crds/organizations.github.crossplane.io_organizations.yaml b/package/crds/organizations.github.crossplane.io_organizations.yaml index a6dfe34..a4505b3 100644 --- a/package/crds/organizations.github.crossplane.io_organizations.yaml +++ b/package/crds/organizations.github.crossplane.io_organizations.yaml @@ -69,89 +69,100 @@ spec: a Organization. properties: actions: + description: ActionsConfiguration are the configurable fields + of an Organization Actions. properties: - name: - description: Org is the Organization for the Membership - type: string - nameRef: - description: OrgRef is a reference to an Organization - properties: - name: - description: Name of the referenced object. - type: string - policy: - description: Policies for referencing. - properties: - resolution: - default: Required - description: Resolution specifies whether resolution - of this reference is required. The default is 'Required', - which means the reconcile will fail if the reference - cannot be resolved. 'Optional' means this reference - will be a no-op if it cannot be resolved. - enum: - - Required - - Optional - type: string - resolve: - description: Resolve specifies when this reference - should be resolved. The default is 'IfNotPresent', - which will attempt to resolve the reference only - when the corresponding field is not present. Use - 'Always' to resolve the reference on every reconcile. - enum: - - Always - - IfNotPresent - type: string - type: object - required: - - name - type: object - nameSelector: - description: OrgSlector selects a reference to an Organization - properties: - matchControllerRef: - description: MatchControllerRef ensures an object with - the same controller reference as the selecting object - is selected. - type: boolean - matchLabels: - additionalProperties: + enabledRepos: + items: + properties: + repo: + description: Name of the repository type: string - description: MatchLabels ensures an object with matching - labels is selected. - type: object - policy: - description: Policies for selection. - properties: - resolution: - default: Required - description: Resolution specifies whether resolution - of this reference is required. The default is 'Required', - which means the reconcile will fail if the reference - cannot be resolved. 'Optional' means this reference - will be a no-op if it cannot be resolved. - enum: - - Required - - Optional - type: string - resolve: - description: Resolve specifies when this reference - should be resolved. The default is 'IfNotPresent', - which will attempt to resolve the reference only - when the corresponding field is not present. Use - 'Always' to resolve the reference on every reconcile. - enum: - - Always - - IfNotPresent - type: string - type: object - type: object + repoRef: + description: RepoRef is a reference to the Repositories + properties: + name: + description: Name of the referenced object. + type: string + policy: + description: Policies for referencing. + properties: + resolution: + default: Required + description: Resolution specifies whether resolution + of this reference is required. The default + is 'Required', which means the reconcile will + fail if the reference cannot be resolved. + 'Optional' means this reference will be a + no-op if it cannot be resolved. + enum: + - Required + - Optional + type: string + resolve: + description: Resolve specifies when this reference + should be resolved. The default is 'IfNotPresent', + which will attempt to resolve the reference + only when the corresponding field is not present. + Use 'Always' to resolve the reference on every + reconcile. + enum: + - Always + - IfNotPresent + type: string + type: object + required: + - name + type: object + repoSelector: + description: RepoSelector selects a reference to an + Repositories + properties: + matchControllerRef: + description: MatchControllerRef ensures an object + with the same controller reference as the selecting + object is selected. + type: boolean + matchLabels: + additionalProperties: + type: string + description: MatchLabels ensures an object with + matching labels is selected. + type: object + policy: + description: Policies for selection. + properties: + resolution: + default: Required + description: Resolution specifies whether resolution + of this reference is required. The default + is 'Required', which means the reconcile will + fail if the reference cannot be resolved. + 'Optional' means this reference will be a + no-op if it cannot be resolved. + enum: + - Required + - Optional + type: string + resolve: + description: Resolve specifies when this reference + should be resolved. The default is 'IfNotPresent', + which will attempt to resolve the reference + only when the corresponding field is not present. + Use 'Always' to resolve the reference on every + reconcile. + enum: + - Always + - IfNotPresent + type: string + type: object + type: object + type: object + type: array type: object description: type: string required: - - actions - description type: object managementPolicies: From 713acf7dce353e3fe4ed8b32806da44afe980452 Mon Sep 17 00:00:00 2001 From: Lado Golijashvili Date: Tue, 21 Nov 2023 21:13:33 +0400 Subject: [PATCH 3/6] change update function logic to fix go-lint cyclomatic complexity issue --- .../controller/organization/organization.go | 100 ++++++++---------- 1 file changed, 45 insertions(+), 55 deletions(-) diff --git a/internal/controller/organization/organization.go b/internal/controller/organization/organization.go index 5126d3a..4fb23f0 100644 --- a/internal/controller/organization/organization.go +++ b/internal/controller/organization/organization.go @@ -155,15 +155,9 @@ func (c *external) Observe(ctx context.Context, mg resource.Managed) (managed.Ex ResourceUpToDate: false, } - // Extract repository names from the list - aRepos := make([]string, 0, len(aResp.Repositories)) - for _, repo := range aResp.Repositories { - aRepos = append(aRepos, repo.GetName()) - } - slices.Sort(aRepos) + aRepos := getSortedRepoNames(aResp.Repositories) - crARepos := getEnabledReposFromCr(cr.Spec.ForProvider.Actions.EnabledRepos) - slices.Sort(crARepos) + crARepos := getSortedEnabledReposFromCr(cr.Spec.ForProvider.Actions.EnabledRepos) if err != nil { return managed.ExternalObservation{}, err @@ -210,71 +204,37 @@ func (c *external) Update(ctx context.Context, mg resource.Managed) (managed.Ext return managed.ExternalUpdate{}, err } - crARepos := getEnabledReposFromCr(cr.Spec.ForProvider.Actions.EnabledRepos) - if err != nil { - return managed.ExternalUpdate{}, err - } - slices.Sort(crARepos) + crARepos := getSortedEnabledReposFromCr(cr.Spec.ForProvider.Actions.EnabledRepos) // To use this function, the organization permission policy for enabled_repositories must be configured to selected, otherwise you get error 409 Conflict - aResp, _, err := c.github.Actions.ListEnabledReposInOrg(ctx, name, &github.ListOptions{PerPage: 100}) + aResp, _, err := gh.Actions.ListEnabledReposInOrg(ctx, name, &github.ListOptions{PerPage: 100}) if err != nil { return managed.ExternalUpdate{}, err } // Extract repository names from the list - aRepos := make([]string, 0, len(aResp.Repositories)) - for _, repo := range aResp.Repositories { - aRepos = append(aRepos, repo.GetName()) - } - slices.Sort(aRepos) + aRepos := getSortedRepoNames(aResp.Repositories) - // Identify repositories that should be enabled - var missingRepos []string - for _, repo := range crARepos { - // Check if the repository from CRD is not in GitHub - if !util.Contains(aRepos, repo) { - missingRepos = append(missingRepos, repo) - } - } - missingReposIds := make([]int64, 0, len(missingRepos)) - for _, missingRepo := range missingRepos { - repo, _, err := c.github.Repositories.Get(ctx, name, missingRepo) - repoID := repo.GetID() - missingReposIds = append(missingReposIds, repoID) - if err != nil { - return managed.ExternalUpdate{}, err - } + missingReposIds, err := getUpdateRepoIds(ctx, gh, name, crARepos, aRepos) + if err != nil { + return managed.ExternalUpdate{}, err } - // Enable actions for missing repositories + for _, missingRepo := range missingReposIds { - _, err := c.github.Actions.AddEnabledReposInOrg(ctx, name, missingRepo) + _, err := gh.Actions.AddEnabledReposInOrg(ctx, name, missingRepo) if err != nil { return managed.ExternalUpdate{}, err } } - // Identify repositories that should be disabled - toDeleteRepos := make([]string, 0, len(aRepos)) - for _, repo := range aRepos { - // Check if the repository from CRD is not in GitHub - if !util.Contains(crARepos, repo) { - toDeleteRepos = append(toDeleteRepos, repo) - } - } - toDeleteReposIds := make([]int64, 0, len(toDeleteRepos)) - for _, toDeleteRepo := range toDeleteRepos { - repo, _, err := c.github.Repositories.Get(ctx, name, toDeleteRepo) - repoID := repo.GetID() - toDeleteReposIds = append(toDeleteReposIds, repoID) - if err != nil { - return managed.ExternalUpdate{}, err - } + toDeleteReposIds, err := getUpdateRepoIds(ctx, gh, name, aRepos, crARepos) + if err != nil { + return managed.ExternalUpdate{}, err } // Disable actions for missing repositories for _, toDeleteRepo := range toDeleteReposIds { - _, err := c.github.Actions.RemoveEnabledRepoInOrg(ctx, name, toDeleteRepo) + _, err := gh.Actions.RemoveEnabledRepoInOrg(ctx, name, toDeleteRepo) if err != nil { return managed.ExternalUpdate{}, err } @@ -293,10 +253,40 @@ func (c *external) Delete(ctx context.Context, mg resource.Managed) error { return nil } -func getEnabledReposFromCr(repos []v1alpha1.ActionEnabledRepo) []string { +func getSortedEnabledReposFromCr(repos []v1alpha1.ActionEnabledRepo) []string { crAEnabledRepos := make([]string, 0, len(repos)) for _, repo := range repos { crAEnabledRepos = append(crAEnabledRepos, repo.Repo) } + slices.Sort(crAEnabledRepos) return crAEnabledRepos } + +func getSortedRepoNames(repos []*github.Repository) []string { + repoNames := make([]string, 0, len(repos)) + for _, repo := range repos { + repoNames = append(repoNames, repo.GetName()) + } + slices.Sort(repoNames) + return repoNames +} + +func getUpdateRepoIds(ctx context.Context, gh *ghclient.Client, org string, crRepos []string, aRepos []string) ([]int64, error) { + var updateRepos []string + for _, repo := range crRepos { + // Check if the repository from CRD is not in GitHub + if !util.Contains(aRepos, repo) { + updateRepos = append(updateRepos, repo) + } + } + reposIds := make([]int64, 0, len(updateRepos)) + for _, repo := range updateRepos { + repo, _, err := gh.Repositories.Get(ctx, org, repo) + repoID := repo.GetID() + reposIds = append(reposIds, repoID) + if err != nil { + return nil, err + } + } + return reposIds, nil +} From 74eabf7a84a9971b54dec8db1604809b5f996c1d Mon Sep 17 00:00:00 2001 From: Lado Golijashvili Date: Wed, 22 Nov 2023 16:48:33 +0400 Subject: [PATCH 4/6] upgrade go version to 1.21 --- cluster/Dockerfile | 2 +- go.mod | 2 +- go.sum | 12 ++++++++++++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/cluster/Dockerfile b/cluster/Dockerfile index 6486c7b..b678668 100644 --- a/cluster/Dockerfile +++ b/cluster/Dockerfile @@ -1,5 +1,5 @@ # Build the manager binary -FROM golang:1.16 as builder +FROM golang:1.21 as builder WORKDIR /workspace # Copy the Go Modules manifests diff --git a/go.mod b/go.mod index 6af5cbb..dfbc023 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/crossplane/provider-github -go 1.20 +go 1.21 require ( github.com/bradleyfalzon/ghinstallation/v2 v2.6.0 diff --git a/go.sum b/go.sum index af81f4b..c7f9416 100644 --- a/go.sum +++ b/go.sum @@ -95,6 +95,7 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.m github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= +github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww= github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= @@ -118,6 +119,7 @@ github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= github.com/gobuffalo/flect v1.0.2 h1:eqjPGSo2WmjgY2XlpGwo2NXgL3RucAKo4k4qQMNA5sA= github.com/gobuffalo/flect v1.0.2/go.mod h1:A5msMlrHtLqh9umBSnvabjsMrCcCpAyzglnDvkbYKHs= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= @@ -195,6 +197,7 @@ github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20230705174524-200ffdc848b8 h1:n6vlPhxsA+BW/XsS5+uqi7GyzaLa5MH7qlSLBZtRdiA= +github.com/google/pprof v0.0.0-20230705174524-200ffdc848b8/go.mod h1:Jh3hGz2jkYak8qXPD19ryItVnUgpgeqzdkY/D0EaeuA= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= @@ -229,6 +232,7 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -250,9 +254,13 @@ github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjY github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q= +github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k= github.com/onsi/gomega v1.27.7 h1:fVih9JD6ogIiHUN6ePK7HJidyEDpWGVB5mzM7cWNXoU= +github.com/onsi/gomega v1.27.7/go.mod h1:1p8OOlwo2iUUDsHnOrjE5UKYJ+e3W8eQ3qSlRahPmr4= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -271,6 +279,7 @@ github.com/prometheus/procfs v0.10.0/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPH github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM= @@ -312,6 +321,7 @@ go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= +go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= @@ -434,6 +444,7 @@ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -686,6 +697,7 @@ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= From 2f5a8a1ec213038e49175779532b5c3514f8c61c Mon Sep 17 00:00:00 2001 From: Lado Golijashvili Date: Wed, 22 Nov 2023 16:54:20 +0400 Subject: [PATCH 5/6] upgrade go version to 1.21 in ci file --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ab00f54..0ba3711 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,7 @@ on: env: # Common versions - GO_VERSION: '1.20' + GO_VERSION: '1.21' GOLANGCI_VERSION: 'v1.54.0' DOCKER_BUILDX_VERSION: 'v0.9.1' # Common users. We can't run a step 'if secrets.XXX != ""' but we can run a From 6a9a196e311c03585b39aaa0f1900172acc56436 Mon Sep 17 00:00:00 2001 From: Lado Golijashvili Date: Fri, 24 Nov 2023 17:14:12 +0400 Subject: [PATCH 6/6] implement tests for organization actions enabled repos --- internal/clients/fake/client.go | 18 ++++++++ .../organization/organization_test.go | 45 ++++++++++++++++--- 2 files changed, 57 insertions(+), 6 deletions(-) diff --git a/internal/clients/fake/client.go b/internal/clients/fake/client.go index 576210e..929ce4b 100644 --- a/internal/clients/fake/client.go +++ b/internal/clients/fake/client.go @@ -7,6 +7,24 @@ import ( "github.com/google/go-github/v54/github" ) +type MockActionsClient struct { + MockListEnabledReposInOrg func(ctx context.Context, owner string, opts *github.ListOptions) (*github.ActionsEnabledOnOrgRepos, *github.Response, error) + MockAddEnabledReposInOrg func(ctx context.Context, owner string, repositoryID int64) (*github.Response, error) + MockRemoveEnabledRepoInOrg func(ctx context.Context, owner string, repositoryID int64) (*github.Response, error) +} + +func (m *MockActionsClient) ListEnabledReposInOrg(ctx context.Context, owner string, opts *github.ListOptions) (*github.ActionsEnabledOnOrgRepos, *github.Response, error) { + return m.MockListEnabledReposInOrg(ctx, owner, opts) +} + +func (m *MockActionsClient) AddEnabledReposInOrg(ctx context.Context, owner string, repositoryID int64) (*github.Response, error) { + return m.MockAddEnabledReposInOrg(ctx, owner, repositoryID) +} + +func (m *MockActionsClient) RemoveEnabledRepoInOrg(ctx context.Context, owner string, repositoryID int64) (*github.Response, error) { + return m.MockRemoveEnabledRepoInOrg(ctx, owner, repositoryID) +} + type MockOrganizationsClient struct { MockGet func(ctx context.Context, org string) (*github.Organization, *github.Response, error) MockEdit func(ctx context.Context, name string, org *github.Organization) (*github.Organization, *github.Response, error) diff --git a/internal/controller/organization/organization_test.go b/internal/controller/organization/organization_test.go index 39d202f..4d2e349 100644 --- a/internal/controller/organization/organization_test.go +++ b/internal/controller/organization/organization_test.go @@ -20,11 +20,10 @@ import ( "context" "testing" - "github.com/google/go-cmp/cmp" - "github.com/crossplane/provider-github/apis/organizations/v1alpha1" ghclient "github.com/crossplane/provider-github/internal/clients" "github.com/crossplane/provider-github/internal/clients/fake" + "github.com/google/go-cmp/cmp" "github.com/crossplane/crossplane-runtime/pkg/meta" "github.com/crossplane/crossplane-runtime/pkg/reconciler/managed" @@ -45,6 +44,8 @@ var ( org = "test-org" description = "test description" otherDescription = "other description" + repo = "test-repo" + repo2 = "test-repo2" ) type organizationModifier func(*v1alpha1.Organization) @@ -55,10 +56,19 @@ func withDescription() organizationModifier { } } -func organization(m ...organizationModifier) *v1alpha1.Organization { +func organization(repos []string, m ...organizationModifier) *v1alpha1.Organization { cr := &v1alpha1.Organization{} cr.Spec.ForProvider.Description = description + cr.Spec.ForProvider.Actions = v1alpha1.ActionsConfiguration{ + EnabledRepos: make([]v1alpha1.ActionEnabledRepo, len(repos)), + } + for i, repo := range repos { + cr.Spec.ForProvider.Actions.EnabledRepos[i] = v1alpha1.ActionEnabledRepo{ + Repo: repo, + } + } + meta.SetExternalName(cr, org) for _, f := range m { @@ -74,6 +84,14 @@ func githubOrganization() *github.Organization { } } +func githubOrgRepoActions() *github.ActionsEnabledOnOrgRepos { + repos := []*github.Repository{ + {Name: &repo}, + {Name: &repo2}, + } + return &github.ActionsEnabledOnOrgRepos{Repositories: repos} +} + func TestObserve(t *testing.T) { type fields struct { github *ghclient.Client @@ -103,10 +121,15 @@ func TestObserve(t *testing.T) { return githubOrganization(), nil, nil }, }, + Actions: &fake.MockActionsClient{ + MockListEnabledReposInOrg: func(ctx context.Context, owner string, opts *github.ListOptions) (*github.ActionsEnabledOnOrgRepos, *github.Response, error) { + return githubOrgRepoActions(), nil, nil + }, + }, }, }, args: args{ - mg: organization(withDescription()), + mg: organization([]string{repo, repo2}, withDescription()), }, want: want{ o: managed.ExternalObservation{ @@ -124,10 +147,15 @@ func TestObserve(t *testing.T) { return githubOrganization(), nil, nil }, }, + Actions: &fake.MockActionsClient{ + MockListEnabledReposInOrg: func(ctx context.Context, owner string, opts *github.ListOptions) (*github.ActionsEnabledOnOrgRepos, *github.Response, error) { + return githubOrgRepoActions(), nil, nil + }, + }, }, }, args: args{ - mg: organization(), + mg: organization([]string{repo, repo2}), }, want: want{ o: managed.ExternalObservation{ @@ -145,10 +173,15 @@ func TestObserve(t *testing.T) { return nil, nil, fake.Generate404Response() }, }, + Actions: &fake.MockActionsClient{ + MockListEnabledReposInOrg: func(ctx context.Context, owner string, opts *github.ListOptions) (*github.ActionsEnabledOnOrgRepos, *github.Response, error) { + return nil, nil, fake.Generate404Response() + }, + }, }, }, args: args{ - mg: organization(), + mg: organization([]string{repo, repo2}), }, want: want{ o: managed.ExternalObservation{