diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4df474c..f0df951 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 diff --git a/apis/organizations/v1alpha1/organization_types.go b/apis/organizations/v1alpha1/organization_types.go index 3080c70..a47d09b 100644 --- a/apis/organizations/v1alpha1/organization_types.go +++ b/apis/organizations/v1alpha1/organization_types.go @@ -25,9 +25,29 @@ import ( xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" ) +// ActionsConfiguration are the configurable fields of an Organization Actions. +type ActionsConfiguration struct { + EnabledRepos []ActionEnabledRepo `json:"enabledRepos,omitempty"` +} + +type ActionEnabledRepo struct { + // Name of the repository + // +crossplane:generate:reference:type=Repository + Repo string `json:"repo,omitempty"` + + // RepoRef is a reference to the Repositories + // +optional + RepoRef *xpv1.Reference `json:"repoRef,omitempty"` + + // RepoSelector selects a reference to an Repositories + // +optional + RepoSelector *xpv1.Selector `json:"repoSelector,omitempty"` +} + // OrganizationParameters are the configurable fields of a Organization. type OrganizationParameters struct { - Description string `json:"description"` + Description string `json:"description"` + 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 4d3db9d..856ef7f 100644 --- a/apis/organizations/v1alpha1/zz_generated.deepcopy.go +++ b/apis/organizations/v1alpha1/zz_generated.deepcopy.go @@ -26,6 +26,53 @@ 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 *ActionEnabledRepo) DeepCopyInto(out *ActionEnabledRepo) { + *out = *in + if in.RepoRef != nil { + in, out := &in.RepoRef, &out.RepoRef + *out = new(v1.Reference) + (*in).DeepCopyInto(*out) + } + 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 { + 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 +283,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 +300,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..2360ae9 100644 --- a/apis/organizations/v1alpha1/zz_generated.resolvers.go +++ b/apis/organizations/v1alpha1/zz_generated.resolvers.go @@ -50,6 +50,35 @@ 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 + + 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 + + } + + 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/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 750e472..8440cb6 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 8d03c9a..3a33f5a 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= diff --git a/internal/clients/client.go b/internal/clients/client.go index 5e022ee..881768a 100644 --- a/internal/clients/client.go +++ b/internal/clients/client.go @@ -28,12 +28,19 @@ 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) + 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 { 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 +105,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/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.go b/internal/controller/organization/organization.go index 2bbd86d..4fb23f0 100644 --- a/internal/controller/organization/organization.go +++ b/internal/controller/organization/organization.go @@ -18,13 +18,8 @@ package organization import ( "context" - - "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" @@ -34,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" @@ -142,11 +143,31 @@ 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 + } + + notUpToDate := managed.ExternalObservation{ + ResourceExists: true, + ResourceUpToDate: false, + } + + aRepos := getSortedRepoNames(aResp.Repositories) + + crARepos := getSortedEnabledReposFromCr(cr.Spec.ForProvider.Actions.EnabledRepos) + + 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()) @@ -183,6 +204,42 @@ func (c *external) Update(ctx context.Context, mg resource.Managed) (managed.Ext return managed.ExternalUpdate{}, err } + 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 := gh.Actions.ListEnabledReposInOrg(ctx, name, &github.ListOptions{PerPage: 100}) + if err != nil { + return managed.ExternalUpdate{}, err + } + + // Extract repository names from the list + aRepos := getSortedRepoNames(aResp.Repositories) + + missingReposIds, err := getUpdateRepoIds(ctx, gh, name, crARepos, aRepos) + if err != nil { + return managed.ExternalUpdate{}, err + } + + for _, missingRepo := range missingReposIds { + _, err := gh.Actions.AddEnabledReposInOrg(ctx, name, missingRepo) + 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 := gh.Actions.RemoveEnabledRepoInOrg(ctx, name, toDeleteRepo) + if err != nil { + return managed.ExternalUpdate{}, err + } + } + return managed.ExternalUpdate{}, nil } @@ -195,3 +252,41 @@ func (c *external) Delete(ctx context.Context, mg resource.Managed) error { return nil } + +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 +} 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{ 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 35af585..a4505b3 100644 --- a/package/crds/organizations.github.crossplane.io_organizations.yaml +++ b/package/crds/organizations.github.crossplane.io_organizations.yaml @@ -68,6 +68,98 @@ spec: description: OrganizationParameters are the configurable fields of a Organization. properties: + actions: + description: ActionsConfiguration are the configurable fields + of an Organization Actions. + properties: + enabledRepos: + items: + properties: + repo: + description: Name of the repository + type: string + 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: