From 936dc6d717acb1ecf3dd47869632981d30419810 Mon Sep 17 00:00:00 2001 From: Iulian Mandache <25257851+iul1an@users.noreply.github.com> Date: Tue, 4 Jun 2024 00:04:19 +0300 Subject: [PATCH] Actions and Dependabot ORG secrets access management (#21) --- README.md | 5 +- .../v1alpha1/organization_types.go | 36 +++ .../v1alpha1/zz_generated.deepcopy.go | 81 +++++++ .../v1alpha1/zz_generated.resolvers.go | 44 ++++ examples/organizations/organization.yaml | 9 + internal/clients/client.go | 11 + internal/clients/fake/client.go | 39 +++- .../controller/organization/organization.go | 153 +++++++++++++ .../organization/organization_test.go | 110 +++++++++ ...ns.github.crossplane.io_organizations.yaml | 212 ++++++++++++++++++ 10 files changed, 695 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 849ab30..e02bd3d 100644 --- a/README.md +++ b/README.md @@ -7,9 +7,10 @@ The project is in a prototyping phase but it's already functional and implements follwing objects with partial functionality: * Organization - * actions enabled repositories + * actions enabled repositories + * actions and dependabot secrets repository access * description - * creation and deleteion not supported + * creation and deletion not supported * Team * visibility * description diff --git a/apis/organizations/v1alpha1/organization_types.go b/apis/organizations/v1alpha1/organization_types.go index a47d09b..964cff6 100644 --- a/apis/organizations/v1alpha1/organization_types.go +++ b/apis/organizations/v1alpha1/organization_types.go @@ -44,10 +44,46 @@ type ActionEnabledRepo struct { RepoSelector *xpv1.Selector `json:"repoSelector,omitempty"` } +type SecretSelectedRepo 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 a Repository + // +optional + RepoSelector *xpv1.Selector `json:"repoSelector,omitempty"` +} + +type OrgSecret struct { + // Name of the GitHub secret + Name string `json:"name"` + + // List of repositories that have access to the secret. + RepositoryAccessList []SecretSelectedRepo `json:"repositoryAccessList,omitempty"` +} + +type SecretConfiguration struct { + // List of GitHub Actions secrets + // +optional + ActionsSecrets []OrgSecret `json:"actionsSecrets,omitempty"` + + // List of Dependabot secrets + // +optional + DependabotSecrets []OrgSecret `json:"dependabotSecrets,omitempty"` +} + // OrganizationParameters are the configurable fields of a Organization. type OrganizationParameters struct { Description string `json:"description"` Actions ActionsConfiguration `json:"actions,omitempty"` + + // Configuration for Organization Secrets. + // +optional + Secrets *SecretConfiguration `json:"secrets,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 d21171d..42e2aff 100644 --- a/apis/organizations/v1alpha1/zz_generated.deepcopy.go +++ b/apis/organizations/v1alpha1/zz_generated.deepcopy.go @@ -378,6 +378,28 @@ func (in *MembershipStatus) DeepCopy() *MembershipStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OrgSecret) DeepCopyInto(out *OrgSecret) { + *out = *in + if in.RepositoryAccessList != nil { + in, out := &in.RepositoryAccessList, &out.RepositoryAccessList + *out = make([]SecretSelectedRepo, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OrgSecret. +func (in *OrgSecret) DeepCopy() *OrgSecret { + if in == nil { + return nil + } + out := new(OrgSecret) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Organization) DeepCopyInto(out *Organization) { *out = *in @@ -456,6 +478,11 @@ func (in *OrganizationObservation) DeepCopy() *OrganizationObservation { func (in *OrganizationParameters) DeepCopyInto(out *OrganizationParameters) { *out = *in in.Actions.DeepCopyInto(&out.Actions) + if in.Secrets != nil { + in, out := &in.Secrets, &out.Secrets + *out = new(SecretConfiguration) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OrganizationParameters. @@ -1179,6 +1206,60 @@ func (in *RulesetRefName) DeepCopy() *RulesetRefName { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SecretConfiguration) DeepCopyInto(out *SecretConfiguration) { + *out = *in + if in.ActionsSecrets != nil { + in, out := &in.ActionsSecrets, &out.ActionsSecrets + *out = make([]OrgSecret, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.DependabotSecrets != nil { + in, out := &in.DependabotSecrets, &out.DependabotSecrets + *out = make([]OrgSecret, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretConfiguration. +func (in *SecretConfiguration) DeepCopy() *SecretConfiguration { + if in == nil { + return nil + } + out := new(SecretConfiguration) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SecretSelectedRepo) DeepCopyInto(out *SecretSelectedRepo) { + *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 SecretSelectedRepo. +func (in *SecretSelectedRepo) DeepCopy() *SecretSelectedRepo { + if in == nil { + return nil + } + out := new(SecretSelectedRepo) + 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/apis/organizations/v1alpha1/zz_generated.resolvers.go b/apis/organizations/v1alpha1/zz_generated.resolvers.go index 2360ae9..414a25f 100644 --- a/apis/organizations/v1alpha1/zz_generated.resolvers.go +++ b/apis/organizations/v1alpha1/zz_generated.resolvers.go @@ -75,6 +75,50 @@ func (mg *Organization) ResolveReferences(ctx context.Context, c client.Reader) mg.Spec.ForProvider.Actions.EnabledRepos[i4].RepoRef = rsp.ResolvedReference } + if mg.Spec.ForProvider.Secrets != nil { + for i4 := 0; i4 < len(mg.Spec.ForProvider.Secrets.ActionsSecrets); i4++ { + for i5 := 0; i5 < len(mg.Spec.ForProvider.Secrets.ActionsSecrets[i4].RepositoryAccessList); i5++ { + rsp, err = r.Resolve(ctx, reference.ResolutionRequest{ + CurrentValue: mg.Spec.ForProvider.Secrets.ActionsSecrets[i4].RepositoryAccessList[i5].Repo, + Extract: reference.ExternalName(), + Reference: mg.Spec.ForProvider.Secrets.ActionsSecrets[i4].RepositoryAccessList[i5].RepoRef, + Selector: mg.Spec.ForProvider.Secrets.ActionsSecrets[i4].RepositoryAccessList[i5].RepoSelector, + To: reference.To{ + List: &RepositoryList{}, + Managed: &Repository{}, + }, + }) + if err != nil { + return errors.Wrap(err, "mg.Spec.ForProvider.Secrets.ActionsSecrets[i4].RepositoryAccessList[i5].Repo") + } + mg.Spec.ForProvider.Secrets.ActionsSecrets[i4].RepositoryAccessList[i5].Repo = rsp.ResolvedValue + mg.Spec.ForProvider.Secrets.ActionsSecrets[i4].RepositoryAccessList[i5].RepoRef = rsp.ResolvedReference + + } + } + } + if mg.Spec.ForProvider.Secrets != nil { + for i4 := 0; i4 < len(mg.Spec.ForProvider.Secrets.DependabotSecrets); i4++ { + for i5 := 0; i5 < len(mg.Spec.ForProvider.Secrets.DependabotSecrets[i4].RepositoryAccessList); i5++ { + rsp, err = r.Resolve(ctx, reference.ResolutionRequest{ + CurrentValue: mg.Spec.ForProvider.Secrets.DependabotSecrets[i4].RepositoryAccessList[i5].Repo, + Extract: reference.ExternalName(), + Reference: mg.Spec.ForProvider.Secrets.DependabotSecrets[i4].RepositoryAccessList[i5].RepoRef, + Selector: mg.Spec.ForProvider.Secrets.DependabotSecrets[i4].RepositoryAccessList[i5].RepoSelector, + To: reference.To{ + List: &RepositoryList{}, + Managed: &Repository{}, + }, + }) + if err != nil { + return errors.Wrap(err, "mg.Spec.ForProvider.Secrets.DependabotSecrets[i4].RepositoryAccessList[i5].Repo") + } + mg.Spec.ForProvider.Secrets.DependabotSecrets[i4].RepositoryAccessList[i5].Repo = rsp.ResolvedValue + mg.Spec.ForProvider.Secrets.DependabotSecrets[i4].RepositoryAccessList[i5].RepoRef = rsp.ResolvedReference + + } + } + } return nil } diff --git a/examples/organizations/organization.yaml b/examples/organizations/organization.yaml index 51e1546..2e2168a 100644 --- a/examples/organizations/organization.yaml +++ b/examples/organizations/organization.yaml @@ -6,3 +6,12 @@ spec: deletionPolicy: "Orphan" forProvider: description: this is a sample organization + secrets: + actionsSecrets: + - name: foo-secret + repositoryAccessList: + - repo: my-awesome-repo + dependabotSecrets: + - name: dependabot-token + repositoryAccessList: + - repo: my-awesome-repo diff --git a/internal/clients/client.go b/internal/clients/client.go index 088ebab..f857767 100644 --- a/internal/clients/client.go +++ b/internal/clients/client.go @@ -29,6 +29,7 @@ import ( type Client struct { Actions ActionsClient + Dependabot DependabotClient Organizations OrganizationsClient Users UsersClient Teams TeamsClient @@ -39,6 +40,15 @@ 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) RemoveEnabledReposInOrg(ctx context.Context, owner string, repositoryID int64) (*github.Response, error) + GetOrgSecret(ctx context.Context, org, name string) (*github.Secret, *github.Response, error) + ListSelectedReposForOrgSecret(ctx context.Context, org, name string, opts *github.ListOptions) (*github.SelectedReposList, *github.Response, error) + SetSelectedReposForOrgSecret(ctx context.Context, org, name string, ids github.SelectedRepoIDs) (*github.Response, error) +} + +type DependabotClient interface { + GetOrgSecret(ctx context.Context, org, name string) (*github.Secret, *github.Response, error) + ListSelectedReposForOrgSecret(ctx context.Context, org, name string, opts *github.ListOptions) (*github.SelectedReposList, *github.Response, error) + SetSelectedReposForOrgSecret(ctx context.Context, org, name string, ids github.DependabotSecretsSelectedRepoIDs) (*github.Response, error) } type OrganizationsClient interface { @@ -123,6 +133,7 @@ func NewClient(creds string) (*Client, error) { return &Client{ Actions: ghclient.Actions, + Dependabot: ghclient.Dependabot, Organizations: ghclient.Organizations, Users: ghclient.Users, Teams: ghclient.Teams, diff --git a/internal/clients/fake/client.go b/internal/clients/fake/client.go index fda4ad6..f2eaa70 100644 --- a/internal/clients/fake/client.go +++ b/internal/clients/fake/client.go @@ -8,9 +8,12 @@ import ( ) 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) - MockRemoveEnabledReposInOrg func(ctx context.Context, owner string, repositoryID int64) (*github.Response, error) + 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) + MockRemoveEnabledReposInOrg func(ctx context.Context, owner string, repositoryID int64) (*github.Response, error) + MockGetOrgSecret func(ctx context.Context, org, name string) (*github.Secret, *github.Response, error) + MockListSelectedReposForOrgSecret func(ctx context.Context, org, name string, opts *github.ListOptions) (*github.SelectedReposList, *github.Response, error) + MockSetSelectedReposForOrgSecret func(ctx context.Context, org, name string, ids github.SelectedRepoIDs) (*github.Response, error) } func (m *MockActionsClient) ListEnabledReposInOrg(ctx context.Context, owner string, opts *github.ListOptions) (*github.ActionsEnabledOnOrgRepos, *github.Response, error) { @@ -25,6 +28,36 @@ func (m *MockActionsClient) RemoveEnabledReposInOrg(ctx context.Context, owner s return m.MockRemoveEnabledReposInOrg(ctx, owner, repositoryID) } +func (m *MockActionsClient) GetOrgSecret(ctx context.Context, org, name string) (*github.Secret, *github.Response, error) { + return m.MockGetOrgSecret(ctx, org, name) +} + +func (m *MockActionsClient) ListSelectedReposForOrgSecret(ctx context.Context, org, name string, opts *github.ListOptions) (*github.SelectedReposList, *github.Response, error) { + return m.MockListSelectedReposForOrgSecret(ctx, org, name, opts) +} + +func (m *MockActionsClient) SetSelectedReposForOrgSecret(ctx context.Context, org, name string, ids github.SelectedRepoIDs) (*github.Response, error) { + return m.MockSetSelectedReposForOrgSecret(ctx, org, name, ids) +} + +type MockDependabotClient struct { + MockGetOrgSecret func(ctx context.Context, org, name string) (*github.Secret, *github.Response, error) + MockListSelectedReposForOrgSecret func(ctx context.Context, org, name string, opts *github.ListOptions) (*github.SelectedReposList, *github.Response, error) + MockSetSelectedReposForOrgSecret func(ctx context.Context, org, name string, ids github.DependabotSecretsSelectedRepoIDs) (*github.Response, error) +} + +func (m *MockDependabotClient) GetOrgSecret(ctx context.Context, org, name string) (*github.Secret, *github.Response, error) { + return m.MockGetOrgSecret(ctx, org, name) +} + +func (m *MockDependabotClient) ListSelectedReposForOrgSecret(ctx context.Context, org, name string, opts *github.ListOptions) (*github.SelectedReposList, *github.Response, error) { + return m.MockListSelectedReposForOrgSecret(ctx, org, name, opts) +} + +func (m *MockDependabotClient) SetSelectedReposForOrgSecret(ctx context.Context, org, name string, ids github.DependabotSecretsSelectedRepoIDs) (*github.Response, error) { + return m.MockSetSelectedReposForOrgSecret(ctx, org, name, ids) +} + 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 0105310..c3301b2 100644 --- a/internal/controller/organization/organization.go +++ b/internal/controller/organization/organization.go @@ -20,6 +20,9 @@ import ( "context" "reflect" "slices" + "sort" + + "github.com/google/go-cmp/cmp" xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" "github.com/crossplane/crossplane-runtime/pkg/connection" @@ -123,6 +126,7 @@ type external struct { github *ghclient.Client } +//nolint:gocyclo func (c *external) Observe(ctx context.Context, mg resource.Managed) (managed.ExternalObservation, error) { cr, ok := mg.(*v1alpha1.Organization) if !ok { @@ -167,6 +171,35 @@ func (c *external) Observe(ctx context.Context, mg resource.Managed) (managed.Ex } } + if cr.Spec.ForProvider.Secrets != nil { + if cr.Spec.ForProvider.Secrets.ActionsSecrets != nil { + crActionsSecretsToConfig, err := getOrgSecretsMapFromCr(ctx, c.github, name, cr.Spec.ForProvider.Secrets.ActionsSecrets) + if err != nil { + return managed.ExternalObservation{}, err + } + ghActionsSecretsToConfig, err := getOrgSecretsWithConfig(ctx, c.github.Actions, name, cr.Spec.ForProvider.Secrets.ActionsSecrets) + if err != nil { + return managed.ExternalObservation{}, err + } + if !cmp.Equal(crActionsSecretsToConfig, ghActionsSecretsToConfig) { + return notUpToDate, nil + } + } + if cr.Spec.ForProvider.Secrets.DependabotSecrets != nil { + crDependabotSecretsToConfig, err := getOrgSecretsMapFromCr(ctx, c.github, name, cr.Spec.ForProvider.Secrets.DependabotSecrets) + if err != nil { + return managed.ExternalObservation{}, err + } + ghDependabotSecretsToConfig, err := getOrgSecretsWithConfig(ctx, c.github.Dependabot, name, cr.Spec.ForProvider.Secrets.DependabotSecrets) + if err != nil { + return managed.ExternalObservation{}, err + } + if !cmp.Equal(crDependabotSecretsToConfig, ghDependabotSecretsToConfig) { + return notUpToDate, nil + } + } + } + if cr.Spec.ForProvider.Description != pointer.StringDeref(org.Description, "") { return notUpToDate, nil } @@ -188,6 +221,7 @@ func (c *external) Create(ctx context.Context, mg resource.Managed) (managed.Ext return managed.ExternalCreation{}, errors.New("Creation of organizations not supported!") } +//nolint:gocyclo func (c *external) Update(ctx context.Context, mg resource.Managed) (managed.ExternalUpdate, error) { cr, ok := mg.(*v1alpha1.Organization) if !ok { @@ -215,6 +249,23 @@ func (c *external) Update(ctx context.Context, mg resource.Managed) (managed.Ext return managed.ExternalUpdate{}, err } } + + secrets := cr.Spec.ForProvider.Secrets + if secrets != nil { + if secrets.ActionsSecrets != nil { + err = updateOrgSecrets(ctx, gh, name, cr.Spec.ForProvider.Secrets.ActionsSecrets, &ActionsSecretSetter{client: gh}) + if err != nil { + return managed.ExternalUpdate{}, err + } + } + if secrets.DependabotSecrets != nil { + err = updateOrgSecrets(ctx, gh, name, cr.Spec.ForProvider.Secrets.DependabotSecrets, &DependabotSecretSetter{client: gh}) + if err != nil { + return managed.ExternalUpdate{}, err + } + } + } + return managed.ExternalUpdate{}, nil } @@ -312,3 +363,105 @@ func updateRepos(ctx context.Context, gh *ghclient.Client, name string, missingR return nil } + +func getOrgSecretsMapFromCr(ctx context.Context, gh *ghclient.Client, org string, secrets []v1alpha1.OrgSecret) (map[string][]int64, error) { + crOrgSecretsToConfig := make(map[string][]int64, len(secrets)) + for _, secret := range secrets { + repoIds := make([]int64, 0, len(secret.RepositoryAccessList)) + for _, selectedRepo := range secret.RepositoryAccessList { + ghRepo, _, err := gh.Repositories.Get(ctx, org, selectedRepo.Repo) + if err != nil { + return nil, err + } + repoIds = append(repoIds, ghRepo.GetID()) + } + sort.Slice(repoIds, func(i, j int) bool { + return repoIds[i] < repoIds[j] + }) + crOrgSecretsToConfig[secret.Name] = repoIds + } + return crOrgSecretsToConfig, nil +} + +type OrgSecretGetter interface { + GetOrgSecret(ctx context.Context, owner, secretName string) (*github.Secret, *github.Response, error) + ListSelectedReposForOrgSecret(ctx context.Context, owner, secretName string, opts *github.ListOptions) (*github.SelectedReposList, *github.Response, error) +} + +func getOrgSecretsWithConfig(ctx context.Context, c OrgSecretGetter, owner string, secrets []v1alpha1.OrgSecret) (map[string][]int64, error) { + orgSecretsToConfig := make(map[string][]int64, len(secrets)) + for _, secret := range secrets { + ghSecret, _, err := c.GetOrgSecret(ctx, owner, secret.Name) + if err != nil { + return nil, err + } + repoIds := make([]int64, 0) + if ghSecret != nil && ghSecret.Visibility == "selected" { + opts := &github.ListOptions{PerPage: 100} + for { + ghRepo, resp, err := c.ListSelectedReposForOrgSecret(ctx, owner, secret.Name, opts) + if err != nil { + return nil, err + } + for _, selectedRepo := range ghRepo.Repositories { + repoIds = append(repoIds, selectedRepo.GetID()) + } + if resp.NextPage == 0 { + break + } + opts.Page = resp.NextPage + } + sort.Slice(repoIds, func(i, j int) bool { + return repoIds[i] < repoIds[j] + }) + } + orgSecretsToConfig[secret.Name] = repoIds + } + return orgSecretsToConfig, nil +} + +type OrgSecretSetter interface { + SetSelectedReposForOrgSecret(ctx context.Context, org string, name string, ids []int64) error +} + +type ActionsSecretSetter struct { + client *ghclient.Client +} + +type DependabotSecretSetter struct { + client *ghclient.Client +} + +func (a *ActionsSecretSetter) SetSelectedReposForOrgSecret(ctx context.Context, org string, name string, ids []int64) error { + _, err := a.client.Actions.SetSelectedReposForOrgSecret(ctx, org, name, ids) + if err != nil { + return err + } + return nil +} + +func (d *DependabotSecretSetter) SetSelectedReposForOrgSecret(ctx context.Context, org string, name string, ids []int64) error { + _, err := d.client.Dependabot.SetSelectedReposForOrgSecret(ctx, org, name, ids) + if err != nil { + return err + } + return nil +} + +func updateOrgSecrets(ctx context.Context, gh *ghclient.Client, owner string, secrets []v1alpha1.OrgSecret, setter OrgSecretSetter) error { + for _, secret := range secrets { + repoIds := make([]int64, 0, len(secret.RepositoryAccessList)) + for _, repo := range secret.RepositoryAccessList { + ghRepo, _, err := gh.Repositories.Get(ctx, owner, repo.Repo) + if err != nil { + return err + } + repoIds = append(repoIds, ghRepo.GetID()) + } + err := setter.SetSelectedReposForOrgSecret(ctx, owner, secret.Name, repoIds) + if err != nil { + return err + } + } + return nil +} diff --git a/internal/controller/organization/organization_test.go b/internal/controller/organization/organization_test.go index 911383f..9d9afc7 100644 --- a/internal/controller/organization/organization_test.go +++ b/internal/controller/organization/organization_test.go @@ -46,6 +46,9 @@ var ( otherDescription = "other description" repo = "test-repo" repo2 = "test-repo2" + orgSecret1 = "org-secret1" + orgSecretRepo1 = "org-secret-repo1" + orgSecretRepo1ID = 123456 ) type organizationModifier func(*v1alpha1.Organization) @@ -69,6 +72,29 @@ func organization(repos []string, m ...organizationModifier) *v1alpha1.Organizat } } + cr.Spec.ForProvider.Secrets = &v1alpha1.SecretConfiguration{ + ActionsSecrets: []v1alpha1.OrgSecret{ + { + Name: orgSecret1, + RepositoryAccessList: []v1alpha1.SecretSelectedRepo{ + { + Repo: orgSecretRepo1, + }, + }, + }, + }, + DependabotSecrets: []v1alpha1.OrgSecret{ + { + Name: orgSecret1, + RepositoryAccessList: []v1alpha1.SecretSelectedRepo{ + { + Repo: orgSecretRepo1, + }, + }, + }, + }, + } + meta.SetExternalName(cr, org) for _, f := range m { @@ -92,6 +118,33 @@ func githubOrgRepoActions() *github.ActionsEnabledOnOrgRepos { return &github.ActionsEnabledOnOrgRepos{Repositories: repos} } +func githubOrgSecret() *github.Secret { + return &github.Secret{ + Name: orgSecret1, + Visibility: "selected", + } +} + +func githubSelectedReposForOrgSecret() *github.SelectedReposList { + id := int64(orgSecretRepo1ID) + return &github.SelectedReposList{ + Repositories: []*github.Repository{ + { + Name: &orgSecretRepo1, + ID: &id, + }, + }, + } +} + +func githubOrgSecretRepo() *github.Repository { + id := int64(orgSecretRepo1ID) + return &github.Repository{ + Name: &orgSecretRepo1, + ID: &id, + } +} + func TestObserve(t *testing.T) { type fields struct { github *ghclient.Client @@ -125,6 +178,25 @@ func TestObserve(t *testing.T) { MockListEnabledReposInOrg: func(ctx context.Context, owner string, opts *github.ListOptions) (*github.ActionsEnabledOnOrgRepos, *github.Response, error) { return githubOrgRepoActions(), nil, nil }, + MockGetOrgSecret: func(ctx context.Context, org, name string) (*github.Secret, *github.Response, error) { + return nil, fake.GenerateEmptyResponse(), nil + }, + MockListSelectedReposForOrgSecret: func(ctx context.Context, org, name string, opts *github.ListOptions) (*github.SelectedReposList, *github.Response, error) { + return nil, fake.GenerateEmptyResponse(), nil + }, + }, + Dependabot: &fake.MockDependabotClient{ + MockGetOrgSecret: func(ctx context.Context, org, name string) (*github.Secret, *github.Response, error) { + return nil, fake.GenerateEmptyResponse(), nil + }, + MockListSelectedReposForOrgSecret: func(ctx context.Context, org, name string, opts *github.ListOptions) (*github.SelectedReposList, *github.Response, error) { + return nil, fake.GenerateEmptyResponse(), nil + }, + }, + Repositories: &fake.MockRepositoriesClient{ + MockGet: func(ctx context.Context, owner, repo string) (*github.Repository, *github.Response, error) { + return nil, fake.GenerateEmptyResponse(), nil + }, }, }, }, @@ -151,6 +223,25 @@ func TestObserve(t *testing.T) { MockListEnabledReposInOrg: func(ctx context.Context, owner string, opts *github.ListOptions) (*github.ActionsEnabledOnOrgRepos, *github.Response, error) { return githubOrgRepoActions(), nil, nil }, + MockGetOrgSecret: func(ctx context.Context, org, name string) (*github.Secret, *github.Response, error) { + return githubOrgSecret(), fake.GenerateEmptyResponse(), nil + }, + MockListSelectedReposForOrgSecret: func(ctx context.Context, org, name string, opts *github.ListOptions) (*github.SelectedReposList, *github.Response, error) { + return githubSelectedReposForOrgSecret(), fake.GenerateEmptyResponse(), nil + }, + }, + Dependabot: &fake.MockDependabotClient{ + MockGetOrgSecret: func(ctx context.Context, org, name string) (*github.Secret, *github.Response, error) { + return githubOrgSecret(), fake.GenerateEmptyResponse(), nil + }, + MockListSelectedReposForOrgSecret: func(ctx context.Context, org, name string, opts *github.ListOptions) (*github.SelectedReposList, *github.Response, error) { + return githubSelectedReposForOrgSecret(), fake.GenerateEmptyResponse(), nil + }, + }, + Repositories: &fake.MockRepositoriesClient{ + MockGet: func(ctx context.Context, owner, repo string) (*github.Repository, *github.Response, error) { + return githubOrgSecretRepo(), fake.GenerateEmptyResponse(), nil + }, }, }, }, @@ -177,6 +268,25 @@ func TestObserve(t *testing.T) { MockListEnabledReposInOrg: func(ctx context.Context, owner string, opts *github.ListOptions) (*github.ActionsEnabledOnOrgRepos, *github.Response, error) { return nil, nil, fake.Generate404Response() }, + MockGetOrgSecret: func(ctx context.Context, org, name string) (*github.Secret, *github.Response, error) { + return nil, nil, fake.Generate404Response() + }, + MockListSelectedReposForOrgSecret: func(ctx context.Context, org, name string, opts *github.ListOptions) (*github.SelectedReposList, *github.Response, error) { + return nil, nil, fake.Generate404Response() + }, + }, + Dependabot: &fake.MockDependabotClient{ + MockGetOrgSecret: func(ctx context.Context, org, name string) (*github.Secret, *github.Response, error) { + return nil, nil, fake.Generate404Response() + }, + MockListSelectedReposForOrgSecret: func(ctx context.Context, org, name string, opts *github.ListOptions) (*github.SelectedReposList, *github.Response, error) { + return nil, nil, fake.Generate404Response() + }, + }, + Repositories: &fake.MockRepositoriesClient{ + MockGet: func(ctx context.Context, owner, repo string) (*github.Repository, *github.Response, error) { + return nil, nil, fake.Generate404Response() + }, }, }, }, diff --git a/package/crds/organizations.github.crossplane.io_organizations.yaml b/package/crds/organizations.github.crossplane.io_organizations.yaml index a4505b3..c7920f0 100644 --- a/package/crds/organizations.github.crossplane.io_organizations.yaml +++ b/package/crds/organizations.github.crossplane.io_organizations.yaml @@ -162,6 +162,218 @@ spec: type: object description: type: string + secrets: + description: Configuration for Organization Secrets. + properties: + actionsSecrets: + description: List of GitHub Actions secrets + items: + properties: + name: + description: Name of the GitHub secret + type: string + repositoryAccessList: + description: List of repositories that have access to + the secret. + 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 a Repository + 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 + required: + - name + type: object + type: array + dependabotSecrets: + description: List of Dependabot secrets + items: + properties: + name: + description: Name of the GitHub secret + type: string + repositoryAccessList: + description: List of repositories that have access to + the secret. + 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 a Repository + 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 + required: + - name + type: object + type: array + type: object required: - description type: object