Add github membership

main
Elie 2021-02-26 16:12:27 +01:00
parent d9b4ca5104
commit ba8f950aad
No known key found for this signature in database
GPG Key ID: 399AF69092C727B6
20 changed files with 8519 additions and 0 deletions

View File

@ -23,3 +23,4 @@ read:org # Used to list your organization teams
- [x] github_repository
- [x] github_team
- [x] github_membership

View File

@ -61,5 +61,6 @@ func Deserializers() []deserializer.CTYDeserializer {
ghdeserializer.NewGithubRepositoryDeserializer(),
ghdeserializer.NewGithubTeamDeserializer(),
ghdeserializer.NewGithubMembershipDeserializer(),
}
}

View File

@ -157,6 +157,7 @@ func TestTerraformStateReader_Github_Resources(t *testing.T) {
}{
{name: "github repository", dirName: "github_repository", wantErr: false},
{name: "github team", dirName: "github_team", wantErr: false},
{name: "github membership", dirName: "github_membership", wantErr: false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

View File

@ -0,0 +1,14 @@
[
{
"Etag": "W/\"2d1cae5b1a58be39bd09bad91fb225b3b589ddc47ff101cf4b8a86d3d8b6f65d\"",
"Id": "driftctl-test:driftctl-acceptance-tester",
"Role": "admin",
"Username": "driftctl-acceptance-tester"
},
{
"Etag": "W/\"d2a4eb901334f41629f5ead98a4b6e02489ae2f13b2152630f00e4dfa6488586\"",
"Id": "driftctl-test:eliecharra",
"Role": "admin",
"Username": "eliecharra"
}
]

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,47 @@
{
"version": 4,
"terraform_version": "0.14.4",
"serial": 180,
"lineage": "9fb78851-b86b-b53a-f625-c5b3407eb935",
"outputs": {},
"resources": [
{
"mode": "managed",
"type": "github_membership",
"name": "driftctl-acceptance-tester",
"provider": "provider[\"registry.terraform.io/hashicorp/github\"]",
"instances": [
{
"schema_version": 0,
"attributes": {
"etag": "W/\"2d1cae5b1a58be39bd09bad91fb225b3b589ddc47ff101cf4b8a86d3d8b6f65d\"",
"id": "driftctl-test:driftctl-acceptance-tester",
"role": "admin",
"username": "driftctl-acceptance-tester"
},
"sensitive_attributes": [],
"private": "eyJzY2hlbWFfdmVyc2lvbiI6IjAifQ=="
}
]
},
{
"mode": "managed",
"type": "github_membership",
"name": "eliecharra",
"provider": "provider[\"registry.terraform.io/hashicorp/github\"]",
"instances": [
{
"schema_version": 0,
"attributes": {
"etag": "W/\"d2a4eb901334f41629f5ead98a4b6e02489ae2f13b2152630f00e4dfa6488586\"",
"id": "driftctl-test:eliecharra",
"role": "admin",
"username": "eliecharra"
},
"sensitive_attributes": [],
"private": "eyJzY2hlbWFfdmVyc2lvbiI6IjAifQ=="
}
]
}
]
}

View File

@ -0,0 +1,59 @@
package github
import (
"github.com/cloudskiff/driftctl/pkg/remote/deserializer"
remoteerror "github.com/cloudskiff/driftctl/pkg/remote/error"
"github.com/cloudskiff/driftctl/pkg/resource"
resourcegithub "github.com/cloudskiff/driftctl/pkg/resource/github"
ghdeserializer "github.com/cloudskiff/driftctl/pkg/resource/github/deserializer"
"github.com/cloudskiff/driftctl/pkg/terraform"
"github.com/sirupsen/logrus"
"github.com/zclconf/go-cty/cty"
)
type GithubMembershipSupplier struct {
reader terraform.ResourceReader
deserializer deserializer.CTYDeserializer
repository GithubRepository
runner *terraform.ParallelResourceReader
}
func NewGithubMembershipSupplier(provider *GithubTerraformProvider, repository GithubRepository) *GithubMembershipSupplier {
return &GithubMembershipSupplier{
provider,
ghdeserializer.NewGithubMembershipDeserializer(),
repository,
terraform.NewParallelResourceReader(provider.Runner().SubRunner()),
}
}
func (s GithubMembershipSupplier) Resources() ([]resource.Resource, error) {
resourceList, err := s.repository.ListMembership()
if err != nil {
return nil, remoteerror.NewResourceEnumerationError(err, resourcegithub.GithubMembershipResourceType)
}
for _, id := range resourceList {
id := id
s.runner.Run(func() (cty.Value, error) {
completeResource, err := s.reader.ReadResource(terraform.ReadResourceArgs{
Ty: resourcegithub.GithubMembershipResourceType,
ID: id,
})
if err != nil {
logrus.Warnf("Error reading %s[%s]: %+v", id, resourcegithub.GithubMembershipResourceType, err)
return cty.NilVal, err
}
return *completeResource, nil
})
}
results, err := s.runner.Wait()
if err != nil {
return nil, err
}
return s.deserializer.Deserialize(results)
}

View File

@ -0,0 +1,79 @@
package github
import (
"context"
"testing"
"github.com/cloudskiff/driftctl/pkg/parallel"
"github.com/cloudskiff/driftctl/pkg/resource"
ghdeserializer "github.com/cloudskiff/driftctl/pkg/resource/github/deserializer"
"github.com/cloudskiff/driftctl/pkg/terraform"
"github.com/cloudskiff/driftctl/test"
"github.com/cloudskiff/driftctl/test/goldenfile"
dritftctlmocks "github.com/cloudskiff/driftctl/test/mocks"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
func TestGithubMembershipupplier_Resources(t *testing.T) {
cases := []struct {
test string
dirName string
mocks func(client *MockGithubRepository)
err error
}{
{
test: "no members",
dirName: "github_membership_empty",
mocks: func(client *MockGithubRepository) {
client.On("ListMembership").Return([]string{}, nil)
},
err: nil,
},
{
test: "Multiple membership with admin and member roles",
dirName: "github_membership_multiple",
mocks: func(client *MockGithubRepository) {
client.On("ListMembership").Return([]string{
"driftctl-test:driftctl-acceptance-tester",
"driftctl-test:eliecharra",
}, nil)
},
err: nil,
},
}
for _, c := range cases {
shouldUpdate := c.dirName == *goldenfile.Update
providerLibrary := terraform.NewProviderLibrary()
supplierLibrary := resource.NewSupplierLibrary()
mockedRepo := MockGithubRepository{}
c.mocks(&mockedRepo)
if shouldUpdate {
provider, err := InitTestGithubProvider(providerLibrary)
if err != nil {
t.Fatal(err)
}
supplierLibrary.AddSupplier(NewGithubMembershipSupplier(provider, &mockedRepo))
}
t.Run(c.test, func(tt *testing.T) {
provider := dritftctlmocks.NewMockedGoldenTFProvider(c.dirName, providerLibrary.Provider(terraform.GITHUB), shouldUpdate)
GithubMembershipDeserializer := ghdeserializer.NewGithubMembershipDeserializer()
s := &GithubMembershipSupplier{
provider,
GithubMembershipDeserializer,
&mockedRepo,
terraform.NewParallelResourceReader(parallel.NewParallelRunner(context.TODO(), 10)),
}
got, err := s.Resources()
assert.Equal(tt, c.err, err)
mock.AssertExpectationsForObjects(tt)
test.CtyTestDiff(got, c.dirName, provider, GithubMembershipDeserializer, shouldUpdate, tt)
})
}
}

View File

@ -28,6 +28,7 @@ func Init(alerter *alerter.Alerter, providerLibrary *terraform.ProviderLibrary,
supplierLibrary.AddSupplier(NewGithubRepositorySupplier(provider, repository))
supplierLibrary.AddSupplier(NewGithubTeamSupplier(provider, repository))
supplierLibrary.AddSupplier(NewGithubMembershipSupplier(provider, repository))
return nil
}

View File

@ -9,6 +9,29 @@ type MockGithubRepository struct {
mock.Mock
}
// ListMembership provides a mock function with given fields:
func (_m *MockGithubRepository) ListMembership() ([]string, error) {
ret := _m.Called()
var r0 []string
if rf, ok := ret.Get(0).(func() []string); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]string)
}
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// ListRepositories provides a mock function with given fields:
func (_m *MockGithubRepository) ListRepositories() ([]string, error) {
ret := _m.Called()

View File

@ -2,6 +2,7 @@ package github
import (
"context"
"fmt"
"github.com/shurcooL/githubv4"
"golang.org/x/oauth2"
@ -10,6 +11,7 @@ import (
type GithubRepository interface {
ListRepositories() ([]string, error)
ListTeams() ([]int, error)
ListMembership() ([]string, error)
}
type GithubGraphQLClient interface {
@ -159,3 +161,43 @@ func (r githubRepository) ListTeams() ([]int, error) {
}
return results, nil
}
type listMembership struct {
Organization struct {
MembersWithRole struct {
Nodes []struct {
Login string
}
PageInfo struct {
EndCursor githubv4.String
HasNextPage bool
}
} `graphql:"membersWithRole(first: 100, after: $cursor)"`
} `graphql:"organization(login: $login)"`
}
func (r *githubRepository) ListMembership() ([]string, error) {
query := listMembership{}
results := make([]string, 0)
if r.config.Organization == "" {
return results, nil
}
variables := map[string]interface{}{
"cursor": (*githubv4.String)(nil),
"login": (githubv4.String)(r.config.Organization),
}
for {
err := r.client.Query(r.ctx, &query, variables)
if err != nil {
return nil, err
}
for _, membership := range query.Organization.MembersWithRole.Nodes {
results = append(results, fmt.Sprintf("%s:%s", r.config.Organization, membership.Login))
}
if !query.Organization.MembersWithRole.PageInfo.HasNextPage {
break
}
variables["cursor"] = githubv4.NewString(query.Organization.MembersWithRole.PageInfo.EndCursor)
}
return results, nil
}

View File

@ -300,3 +300,111 @@ func TestListTeams(t *testing.T) {
assert.Equal([]int{1, 2, 3, 4}, teams)
}
func TestListMembership_WithError(t *testing.T) {
assert := assert.New(t)
mockedClient := mocks.GithubGraphQLClient{}
expectedError := errors.New("test error from graphql")
mockedClient.On("Query", mock.Anything, mock.Anything, mock.Anything).Return(expectedError)
r := githubRepository{
client: &mockedClient,
config: githubConfig{
Organization: "testorg",
},
}
_, err := r.ListMembership()
assert.Equal(expectedError, err)
}
func TestListMembership_WithoutOrganization(t *testing.T) {
assert := assert.New(t)
r := githubRepository{}
teams, err := r.ListMembership()
assert.Nil(err)
assert.Equal([]string{}, teams)
}
func TestListMembership(t *testing.T) {
assert := assert.New(t)
mockedClient := mocks.GithubGraphQLClient{}
mockedClient.On("Query",
mock.Anything,
mock.MatchedBy(func(query interface{}) bool {
q, ok := query.(*listMembership)
if !ok {
return false
}
q.Organization.MembersWithRole.Nodes = []struct {
Login string
}{
{
Login: "user-admin",
},
{
Login: "user-non-admin-1",
},
}
q.Organization.MembersWithRole.PageInfo = pageInfo{
EndCursor: "next",
HasNextPage: true,
}
return true
}),
map[string]interface{}{
"login": (githubv4.String)("testorg"),
"cursor": (*githubv4.String)(nil),
}).Return(nil)
mockedClient.On("Query",
mock.Anything,
mock.MatchedBy(func(query interface{}) bool {
q, ok := query.(*listMembership)
if !ok {
return false
}
q.Organization.MembersWithRole.Nodes = []struct {
Login string
}{
{
Login: "user-non-admin-2",
},
{
Login: "user-non-admin-3",
},
}
q.Organization.MembersWithRole.PageInfo = pageInfo{
HasNextPage: false,
}
return true
}),
map[string]interface{}{
"login": (githubv4.String)("testorg"),
"cursor": githubv4.NewString("next"),
}).Return(nil)
r := githubRepository{
client: &mockedClient,
ctx: context.TODO(),
config: githubConfig{
Organization: "testorg",
},
}
teams, err := r.ListMembership()
if err != nil {
t.Fatal(err)
}
assert.Equal([]string{
"testorg:user-admin",
"testorg:user-non-admin-1",
"testorg:user-non-admin-2",
"testorg:user-non-admin-3",
}, teams)
}

View File

@ -0,0 +1 @@
[]

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,5 @@
{
"Typ": "WyJvYmplY3QiLHsiZXRhZyI6InN0cmluZyIsImlkIjoic3RyaW5nIiwicm9sZSI6InN0cmluZyIsInVzZXJuYW1lIjoic3RyaW5nIn1d",
"Val": "eyJldGFnIjoiVy9cImVhZGY4NzgyYjA2ZmRjYzNmY2NjNjM4NTYzOWFlMDY3YWRkMzE2ZWFkMTc0MjMzNjU4OTU1NWUzNjI3YzcwZDlcIiIsImlkIjoiZHJpZnRjdGwtdGVzdDpkcmlmdGN0bC1hY2NlcHRhbmNlLXRlc3RlciIsInJvbGUiOiJtZW1iZXIiLCJ1c2VybmFtZSI6ImRyaWZ0Y3RsLWFjY2VwdGFuY2UtdGVzdGVyIn0=",
"Err": null
}

View File

@ -0,0 +1,5 @@
{
"Typ": "WyJvYmplY3QiLHsiZXRhZyI6InN0cmluZyIsImlkIjoic3RyaW5nIiwicm9sZSI6InN0cmluZyIsInVzZXJuYW1lIjoic3RyaW5nIn1d",
"Val": "eyJldGFnIjoiVy9cImQyYTRlYjkwMTMzNGY0MTYyOWY1ZWFkOThhNGI2ZTAyNDg5YWUyZjEzYjIxNTI2MzBmMDBlNGRmYTY0ODg1ODZcIiIsImlkIjoiZHJpZnRjdGwtdGVzdDplbGllY2hhcnJhIiwicm9sZSI6ImFkbWluIiwidXNlcm5hbWUiOiJlbGllY2hhcnJhIn0=",
"Err": null
}

View File

@ -0,0 +1,14 @@
[
{
"etag": "W/\"d2a4eb901334f41629f5ead98a4b6e02489ae2f13b2152630f00e4dfa6488586\"",
"id": "driftctl-test:eliecharra",
"role": "admin",
"username": "eliecharra"
},
{
"etag": "W/\"eadf8782b06fdcc3fccc6385639ae067add316ead1742336589555e3627c70d9\"",
"id": "driftctl-test:driftctl-acceptance-tester",
"role": "member",
"username": "driftctl-acceptance-tester"
}
]

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,41 @@
package deserializer
import (
"github.com/cloudskiff/driftctl/pkg/resource"
"github.com/cloudskiff/driftctl/pkg/resource/github"
"github.com/pkg/errors"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/gocty"
)
type GithubMembershipDeserializer struct {
}
func NewGithubMembershipDeserializer() *GithubMembershipDeserializer {
return &GithubMembershipDeserializer{}
}
func (s GithubMembershipDeserializer) HandledType() resource.ResourceType {
return github.GithubMembershipResourceType
}
func (s GithubMembershipDeserializer) Deserialize(rawResourceList []cty.Value) ([]resource.Resource, error) {
resources := make([]resource.Resource, 0)
for _, raw := range rawResourceList {
raw := raw
res, err := decodeMembership(&raw)
if err != nil {
return nil, errors.Wrapf(err, "error when deserializing github_membership %+v : %+v", raw, err)
}
resources = append(resources, res)
}
return resources, nil
}
func decodeMembership(res *cty.Value) (resource.Resource, error) {
var decoded github.GithubMembership
if err := gocty.FromCtyValue(*res, &decoded); err != nil {
return nil, err
}
return &decoded, nil
}

View File

@ -0,0 +1,19 @@
// GENERATED, DO NOT EDIT THIS FILE
package github
const GithubMembershipResourceType = "github_membership"
type GithubMembership struct {
Etag *string `cty:"etag" computed:"true" diff:"-"`
Id string `cty:"id" computed:"true"`
Role *string `cty:"role"`
Username *string `cty:"username"`
}
func (r *GithubMembership) TerraformId() string {
return r.Id
}
func (r *GithubMembership) TerraformType() string {
return GithubMembershipResourceType
}