Merge branch 'main' into res/kms_key

main
Elie 2021-02-25 10:54:51 +01:00 committed by GitHub
commit e76a8e432b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 20657 additions and 177 deletions

View File

@ -14,6 +14,7 @@ Below you can find the minimal scope required for driftctl to be able to scan ev
```shell
repo # Required to enumerate public and private repos
read:org # Used to list your organization teams
```
**⚠️ Beware that if you don't set correct permissions for your token, you won't see any errors and all resources will appear as deleted from remote**
@ -21,3 +22,4 @@ repo # Required to enumerate public and private repos
## Supported resources
- [x] github_repository
- [x] github_team

View File

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

View File

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

View File

@ -0,0 +1,41 @@
[
{
"CreateDefaultMaintainer": false,
"Description": "test",
"Etag": "W/\"04b608322c60381373485f1154b7670f1daeb6bdfa9062f7eba05739171fcac0\"",
"Id": "4556715",
"LdapDn": "",
"MembersCount": 1,
"Name": "team1",
"NodeId": "MDQ6VGVhbTQ1NTY3MTU=",
"ParentTeamId": null,
"Privacy": "closed",
"Slug": "team1"
},
{
"CreateDefaultMaintainer": false,
"Description": "test 2",
"Etag": "W/\"1af373c3f173859e7f06690e8e353c21a9a1a90224963e8f162546f1022af224\"",
"Id": "4556719",
"LdapDn": "",
"MembersCount": 1,
"Name": "team2",
"NodeId": "MDQ6VGVhbTQ1NTY3MTk=",
"ParentTeamId": null,
"Privacy": "secret",
"Slug": "team2"
},
{
"CreateDefaultMaintainer": false,
"Description": "test parent team",
"Etag": "W/\"d6fded1b23237d988a0914455547a2e66789bb625fc0a519babe993429facd37\"",
"Id": "4556747",
"LdapDn": "",
"MembersCount": 1,
"Name": "new team with parent",
"NodeId": "MDQ6VGVhbTQ1NTY3NDc=",
"ParentTeamId": 4556715,
"Privacy": "closed",
"Slug": "new-team-with-parent"
}
]

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,24 @@
terraform {
required_version = ">= 0.14.4"
required_providers {
github = "=4.4.0"
}
}
resource "github_team" "team1" {
name = "team1"
description = "test"
privacy = "closed"
}
resource "github_team" "team2" {
name = "team2"
description = "test 2"
}
resource "github_team" "with_parent" {
name = "new team with parent"
description = "test parent team"
parent_team_id = github_team.team1.id
privacy = "closed"
}

View File

@ -0,0 +1,87 @@
{
"version": 4,
"terraform_version": "0.14.4",
"serial": 3,
"lineage": "9fb78851-b86b-b53a-f625-c5b3407eb935",
"outputs": {},
"resources": [
{
"mode": "managed",
"type": "github_team",
"name": "team1",
"provider": "provider[\"registry.terraform.io/hashicorp/github\"]",
"instances": [
{
"schema_version": 0,
"attributes": {
"create_default_maintainer": null,
"description": "test",
"etag": "W/\"04b608322c60381373485f1154b7670f1daeb6bdfa9062f7eba05739171fcac0\"",
"id": "4556715",
"ldap_dn": "",
"members_count": 1,
"name": "team1",
"node_id": "MDQ6VGVhbTQ1NTY3MTU=",
"parent_team_id": null,
"privacy": "closed",
"slug": "team1"
},
"sensitive_attributes": [],
"private": "eyJzY2hlbWFfdmVyc2lvbiI6IjAifQ=="
}
]
},
{
"mode": "managed",
"type": "github_team",
"name": "team2",
"provider": "provider[\"registry.terraform.io/hashicorp/github\"]",
"instances": [
{
"schema_version": 0,
"attributes": {
"create_default_maintainer": null,
"description": "test 2",
"etag": "W/\"1af373c3f173859e7f06690e8e353c21a9a1a90224963e8f162546f1022af224\"",
"id": "4556719",
"ldap_dn": "",
"members_count": 1,
"name": "team2",
"node_id": "MDQ6VGVhbTQ1NTY3MTk=",
"parent_team_id": null,
"privacy": "secret",
"slug": "team2"
},
"sensitive_attributes": [],
"private": "eyJzY2hlbWFfdmVyc2lvbiI6IjAifQ=="
}
]
},
{
"mode": "managed",
"type": "github_team",
"name": "with_parent",
"provider": "provider[\"registry.terraform.io/hashicorp/github\"]",
"instances": [
{
"schema_version": 0,
"attributes": {
"create_default_maintainer": null,
"description": "test parent team",
"etag": "W/\"d6fded1b23237d988a0914455547a2e66789bb625fc0a519babe993429facd37\"",
"id": "4556747",
"ldap_dn": "",
"members_count": 1,
"name": "new team with parent",
"node_id": "MDQ6VGVhbTQ1NTY3NDc=",
"parent_team_id": 4556715,
"privacy": "closed",
"slug": "new-team-with-parent"
},
"sensitive_attributes": [],
"private": "eyJzY2hlbWFfdmVyc2lvbiI6IjAifQ=="
}
]
}
]
}

View File

@ -1,6 +1,7 @@
package aws
import (
"github.com/cloudskiff/driftctl/pkg/remote/aws/repository"
"github.com/cloudskiff/driftctl/pkg/remote/deserializer"
remoteerror "github.com/cloudskiff/driftctl/pkg/remote/error"
"github.com/cloudskiff/driftctl/pkg/resource"
@ -8,8 +9,6 @@ import (
awsdeserializer "github.com/cloudskiff/driftctl/pkg/resource/aws/deserializer"
"github.com/cloudskiff/driftctl/pkg/terraform"
"github.com/aws/aws-sdk-go/service/rds"
"github.com/aws/aws-sdk-go/service/rds/rdsiface"
"github.com/sirupsen/logrus"
"github.com/zclconf/go-cty/cty"
)
@ -17,7 +16,7 @@ import (
type DBInstanceSupplier struct {
reader terraform.ResourceReader
deserializer deserializer.CTYDeserializer
client rdsiface.RDSAPI
client repository.RDSRepository
runner *terraform.ParallelResourceReader
}
@ -25,27 +24,14 @@ func NewDBInstanceSupplier(provider *AWSTerraformProvider) *DBInstanceSupplier {
return &DBInstanceSupplier{
provider,
awsdeserializer.NewDBInstanceDeserializer(),
rds.New(provider.session),
repository.NewRDSRepository(provider.session),
terraform.NewParallelResourceReader(provider.Runner().SubRunner()),
}
}
func listAwsDBInstances(client rdsiface.RDSAPI) ([]*rds.DBInstance, error) {
var result []*rds.DBInstance
input := &rds.DescribeDBInstancesInput{}
err := client.DescribeDBInstancesPages(input, func(res *rds.DescribeDBInstancesOutput, lastPage bool) bool {
result = append(result, res.DBInstances...)
return !lastPage
})
if err != nil {
return nil, err
}
return result, nil
}
func (s DBInstanceSupplier) Resources() ([]resource.Resource, error) {
resourceList, err := listAwsDBInstances(s.client)
resourceList, err := s.client.ListAllDBInstances()
if err != nil {
return nil, remoteerror.NewResourceEnumerationError(err, resourceaws.AwsDbInstanceResourceType)

View File

@ -4,6 +4,8 @@ import (
"context"
"testing"
"github.com/cloudskiff/driftctl/pkg/remote/aws/repository"
remoteerror "github.com/cloudskiff/driftctl/pkg/remote/error"
resourceaws "github.com/cloudskiff/driftctl/pkg/resource/aws"
@ -30,85 +32,68 @@ import (
func TestDBInstanceSupplier_Resources(t *testing.T) {
tests := []struct {
test string
dirName string
instancesPages mocks.DescribeDBInstancesPagesOutput
instancesPagesError error
err error
test string
dirName string
mocks func(client *repository.MockRDSRepository)
err error
}{
{
test: "no dbs",
dirName: "db_instance_empty",
instancesPages: mocks.DescribeDBInstancesPagesOutput{
{
true,
&rds.DescribeDBInstancesOutput{},
},
mocks: func(client *repository.MockRDSRepository) {
client.On("ListAllDBInstances").Return([]*rds.DBInstance{}, nil)
},
err: nil,
},
{
test: "single db",
dirName: "db_instance_single",
instancesPages: mocks.DescribeDBInstancesPagesOutput{
{
true,
&rds.DescribeDBInstancesOutput{
DBInstances: []*rds.DBInstance{
{
DBInstanceIdentifier: awssdk.String("terraform-20201015115018309600000001"),
},
},
mocks: func(client *repository.MockRDSRepository) {
client.On("ListAllDBInstances").Return([]*rds.DBInstance{
{
DBInstanceIdentifier: awssdk.String("terraform-20201015115018309600000001"),
},
},
}, nil)
},
err: nil,
},
{
test: "multiples mixed db",
dirName: "db_instance_multiple",
instancesPages: mocks.DescribeDBInstancesPagesOutput{
{
true,
&rds.DescribeDBInstancesOutput{
DBInstances: []*rds.DBInstance{
{
DBInstanceIdentifier: awssdk.String("terraform-20201015115018309600000001"),
},
{
DBInstanceIdentifier: awssdk.String("database-1"),
},
},
mocks: func(client *repository.MockRDSRepository) {
client.On("ListAllDBInstances").Return([]*rds.DBInstance{
{
DBInstanceIdentifier: awssdk.String("terraform-20201015115018309600000001"),
},
},
{
DBInstanceIdentifier: awssdk.String("database-1"),
},
}, nil)
},
err: nil,
},
{
test: "multiples mixed db",
dirName: "db_instance_multiple",
instancesPages: mocks.DescribeDBInstancesPagesOutput{
{
true,
&rds.DescribeDBInstancesOutput{
DBInstances: []*rds.DBInstance{
{
DBInstanceIdentifier: awssdk.String("terraform-20201015115018309600000001"),
},
{
DBInstanceIdentifier: awssdk.String("database-1"),
},
},
mocks: func(client *repository.MockRDSRepository) {
client.On("ListAllDBInstances").Return([]*rds.DBInstance{
{
DBInstanceIdentifier: awssdk.String("terraform-20201015115018309600000001"),
},
},
{
DBInstanceIdentifier: awssdk.String("database-1"),
},
}, nil)
},
err: nil,
},
{
test: "Cannot list db instances",
dirName: "db_instance_empty",
instancesPagesError: awserr.NewRequestFailure(nil, 403, ""),
err: remoteerror.NewResourceEnumerationError(awserr.NewRequestFailure(nil, 403, ""), resourceaws.AwsDbInstanceResourceType),
test: "Cannot list db instances",
dirName: "db_instance_empty",
mocks: func(client *repository.MockRDSRepository) {
client.On("ListAllDBInstances").Return([]*rds.DBInstance{}, awserr.NewRequestFailure(nil, 403, ""))
},
err: remoteerror.NewResourceEnumerationError(awserr.NewRequestFailure(nil, 403, ""), resourceaws.AwsDbInstanceResourceType),
},
}
for _, tt := range tests {
@ -129,11 +114,8 @@ func TestDBInstanceSupplier_Resources(t *testing.T) {
provider := mocks.NewMockedGoldenTFProvider(tt.dirName, providerLibrary.Provider(terraform.AWS), shouldUpdate)
deserializer := awsdeserializer.NewDBInstanceDeserializer()
client := mocks.NewMockAWSRDSClient(tt.instancesPages)
if tt.instancesPagesError != nil {
client = mocks.NewMockAWSRDSErrorClient(tt.instancesPagesError)
}
client := &repository.MockRDSRepository{}
tt.mocks(client)
s := &DBInstanceSupplier{
provider,
deserializer,

View File

@ -1,6 +1,7 @@
package aws
import (
"github.com/cloudskiff/driftctl/pkg/remote/aws/repository"
"github.com/cloudskiff/driftctl/pkg/remote/deserializer"
remoteerror "github.com/cloudskiff/driftctl/pkg/remote/error"
"github.com/cloudskiff/driftctl/pkg/resource/aws"
@ -9,8 +10,6 @@ import (
"github.com/aws/aws-sdk-go/service/rds"
"github.com/aws/aws-sdk-go/service/rds/rdsiface"
"github.com/cloudskiff/driftctl/pkg/resource"
"github.com/cloudskiff/driftctl/pkg/terraform"
@ -20,7 +19,7 @@ import (
type DBSubnetGroupSupplier struct {
reader terraform.ResourceReader
deserializer deserializer.CTYDeserializer
client rdsiface.RDSAPI
client repository.RDSRepository
runner *terraform.ParallelResourceReader
}
@ -28,21 +27,14 @@ func NewDBSubnetGroupSupplier(provider *AWSTerraformProvider) *DBSubnetGroupSupp
return &DBSubnetGroupSupplier{
provider,
awsdeserializer.NewDBSubnetGroupDeserializer(),
rds.New(provider.session),
repository.NewRDSRepository(provider.session),
terraform.NewParallelResourceReader(provider.Runner().SubRunner()),
}
}
func (s DBSubnetGroupSupplier) Resources() ([]resource.Resource, error) {
input := rds.DescribeDBSubnetGroupsInput{}
var subnetGroups []*rds.DBSubnetGroup
err := s.client.DescribeDBSubnetGroupsPages(&input,
func(resp *rds.DescribeDBSubnetGroupsOutput, lastPage bool) bool {
subnetGroups = append(subnetGroups, resp.DBSubnetGroups...)
return !lastPage
},
)
subnetGroups, err := s.client.ListAllDbSubnetGroups()
if err != nil {
return nil, remoteerror.NewResourceEnumerationError(err, aws.AwsDbSubnetGroupResourceType)

View File

@ -4,6 +4,8 @@ import (
"context"
"testing"
"github.com/cloudskiff/driftctl/pkg/remote/aws/repository"
remoteerror "github.com/cloudskiff/driftctl/pkg/remote/error"
resourceaws "github.com/cloudskiff/driftctl/pkg/resource/aws"
@ -29,55 +31,41 @@ import (
func TestDBSubnetGroupSupplier_Resources(t *testing.T) {
tests := []struct {
test string
dirName string
subnets mocks.DescribeSubnetGroupResponse
subnetsListError error
err error
test string
dirName string
mocks func(client *repository.MockRDSRepository)
err error
}{
{
test: "no subnets",
dirName: "db_subnet_empty",
subnets: mocks.DescribeSubnetGroupResponse{
{
true,
&rds.DescribeDBSubnetGroupsOutput{},
},
mocks: func(client *repository.MockRDSRepository) {
client.On("ListAllDbSubnetGroups").Return([]*rds.DBSubnetGroup{}, nil)
},
err: nil,
},
{
test: "multiples db subnets",
dirName: "db_subnet_multiples",
subnets: mocks.DescribeSubnetGroupResponse{
{
false,
&rds.DescribeDBSubnetGroupsOutput{
DBSubnetGroups: []*rds.DBSubnetGroup{
&rds.DBSubnetGroup{
DBSubnetGroupName: aws.String("foo"),
},
},
mocks: func(client *repository.MockRDSRepository) {
client.On("ListAllDbSubnetGroups").Return([]*rds.DBSubnetGroup{
{
DBSubnetGroupName: aws.String("foo"),
},
},
{
true,
&rds.DescribeDBSubnetGroupsOutput{
DBSubnetGroups: []*rds.DBSubnetGroup{
&rds.DBSubnetGroup{
DBSubnetGroupName: aws.String("bar"),
},
},
{
DBSubnetGroupName: aws.String("bar"),
},
},
}, nil)
},
err: nil,
},
{
test: "Cannot list subnet",
dirName: "db_subnet_empty",
subnetsListError: awserr.NewRequestFailure(nil, 403, ""),
err: remoteerror.NewResourceEnumerationError(awserr.NewRequestFailure(nil, 403, ""), resourceaws.AwsDbSubnetGroupResourceType),
test: "Cannot list subnet",
dirName: "db_subnet_empty",
mocks: func(client *repository.MockRDSRepository) {
client.On("ListAllDbSubnetGroups").Return([]*rds.DBSubnetGroup{}, awserr.NewRequestFailure(nil, 403, ""))
},
err: remoteerror.NewResourceEnumerationError(awserr.NewRequestFailure(nil, 403, ""), resourceaws.AwsDbSubnetGroupResourceType),
},
}
for _, tt := range tests {
@ -98,10 +86,8 @@ func TestDBSubnetGroupSupplier_Resources(t *testing.T) {
t.Run(tt.test, func(t *testing.T) {
provider := mocks.NewMockedGoldenTFProvider(tt.dirName, providerLibrary.Provider(terraform.AWS), shouldUpdate)
deserializer := awsdeserializer.NewDBSubnetGroupDeserializer()
client := mocks.NewMockAWSRDSSubnetGroupClient(tt.subnets)
if tt.subnetsListError != nil {
client = mocks.NewMockAWSRDSErrorClient(tt.subnetsListError)
}
client := &repository.MockRDSRepository{}
tt.mocks(client)
s := &DBSubnetGroupSupplier{
provider,
deserializer,

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,59 @@
// Code generated by mockery v0.0.0-dev. DO NOT EDIT.
package repository
import (
rds "github.com/aws/aws-sdk-go/service/rds"
mock "github.com/stretchr/testify/mock"
)
// MockRDSRepository is an autogenerated mock type for the RDSRepository type
type MockRDSRepository struct {
mock.Mock
}
// ListAllDBInstances provides a mock function with given fields:
func (_m *MockRDSRepository) ListAllDBInstances() ([]*rds.DBInstance, error) {
ret := _m.Called()
var r0 []*rds.DBInstance
if rf, ok := ret.Get(0).(func() []*rds.DBInstance); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*rds.DBInstance)
}
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// ListAllDbSubnetGroups provides a mock function with given fields:
func (_m *MockRDSRepository) ListAllDbSubnetGroups() ([]*rds.DBSubnetGroup, error) {
ret := _m.Called()
var r0 []*rds.DBSubnetGroup
if rf, ok := ret.Get(0).(func() []*rds.DBSubnetGroup); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*rds.DBSubnetGroup)
}
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}

View File

@ -0,0 +1,51 @@
package repository
import (
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/rds"
"github.com/aws/aws-sdk-go/service/rds/rdsiface"
)
type RDSClient interface {
rdsiface.RDSAPI
}
type RDSRepository interface {
ListAllDBInstances() ([]*rds.DBInstance, error)
ListAllDbSubnetGroups() ([]*rds.DBSubnetGroup, error)
}
type rdsRepository struct {
client rdsiface.RDSAPI
}
func NewRDSRepository(session *session.Session) *rdsRepository {
return &rdsRepository{
rds.New(session),
}
}
func (r *rdsRepository) ListAllDBInstances() ([]*rds.DBInstance, error) {
var result []*rds.DBInstance
input := &rds.DescribeDBInstancesInput{}
err := r.client.DescribeDBInstancesPages(input, func(res *rds.DescribeDBInstancesOutput, lastPage bool) bool {
result = append(result, res.DBInstances...)
return !lastPage
})
if err != nil {
return nil, err
}
return result, nil
}
func (r *rdsRepository) ListAllDbSubnetGroups() ([]*rds.DBSubnetGroup, error) {
var subnetGroups []*rds.DBSubnetGroup
input := rds.DescribeDBSubnetGroupsInput{}
err := r.client.DescribeDBSubnetGroupsPages(&input,
func(resp *rds.DescribeDBSubnetGroupsOutput, lastPage bool) bool {
subnetGroups = append(subnetGroups, resp.DBSubnetGroups...)
return !lastPage
},
)
return subnetGroups, err
}

View File

@ -0,0 +1,134 @@
package repository
import (
"strings"
"testing"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/rds"
"github.com/r3labs/diff/v2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
func Test_rdsRepository_ListAllDBInstances(t *testing.T) {
tests := []struct {
name string
mocks func(client *MockRDSClient)
want []*rds.DBInstance
wantErr error
}{
{
name: "List with 2 pages",
mocks: func(client *MockRDSClient) {
client.On("DescribeDBInstancesPages",
&rds.DescribeDBInstancesInput{},
mock.MatchedBy(func(callback func(res *rds.DescribeDBInstancesOutput, lastPage bool) bool) bool {
callback(&rds.DescribeDBInstancesOutput{
DBInstances: []*rds.DBInstance{
{DBInstanceIdentifier: aws.String("1")},
{DBInstanceIdentifier: aws.String("2")},
{DBInstanceIdentifier: aws.String("3")},
},
}, false)
callback(&rds.DescribeDBInstancesOutput{
DBInstances: []*rds.DBInstance{
{DBInstanceIdentifier: aws.String("4")},
{DBInstanceIdentifier: aws.String("5")},
{DBInstanceIdentifier: aws.String("6")},
},
}, true)
return true
})).Return(nil)
},
want: []*rds.DBInstance{
{DBInstanceIdentifier: aws.String("1")},
{DBInstanceIdentifier: aws.String("2")},
{DBInstanceIdentifier: aws.String("3")},
{DBInstanceIdentifier: aws.String("4")},
{DBInstanceIdentifier: aws.String("5")},
{DBInstanceIdentifier: aws.String("6")},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client := &MockRDSClient{}
tt.mocks(client)
r := &rdsRepository{
client: client,
}
got, err := r.ListAllDBInstances()
assert.Equal(t, tt.wantErr, err)
changelog, err := diff.Diff(got, tt.want)
assert.Nil(t, err)
if len(changelog) > 0 {
for _, change := range changelog {
t.Errorf("%s: %s -> %s", strings.Join(change.Path, "."), change.From, change.To)
}
t.Fail()
}
})
}
}
func Test_rdsRepository_ListAllDbSubnetGroups(t *testing.T) {
tests := []struct {
name string
mocks func(client *MockRDSClient)
want []*rds.DBSubnetGroup
wantErr error
}{
{
name: "List with 2 pages",
mocks: func(client *MockRDSClient) {
client.On("DescribeDBSubnetGroupsPages",
&rds.DescribeDBSubnetGroupsInput{},
mock.MatchedBy(func(callback func(res *rds.DescribeDBSubnetGroupsOutput, lastPage bool) bool) bool {
callback(&rds.DescribeDBSubnetGroupsOutput{
DBSubnetGroups: []*rds.DBSubnetGroup{
{DBSubnetGroupName: aws.String("1")},
{DBSubnetGroupName: aws.String("2")},
{DBSubnetGroupName: aws.String("3")},
},
}, false)
callback(&rds.DescribeDBSubnetGroupsOutput{
DBSubnetGroups: []*rds.DBSubnetGroup{
{DBSubnetGroupName: aws.String("4")},
{DBSubnetGroupName: aws.String("5")},
{DBSubnetGroupName: aws.String("6")},
},
}, true)
return true
})).Return(nil)
},
want: []*rds.DBSubnetGroup{
{DBSubnetGroupName: aws.String("1")},
{DBSubnetGroupName: aws.String("2")},
{DBSubnetGroupName: aws.String("3")},
{DBSubnetGroupName: aws.String("4")},
{DBSubnetGroupName: aws.String("5")},
{DBSubnetGroupName: aws.String("6")},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client := &MockRDSClient{}
tt.mocks(client)
r := &rdsRepository{
client: client,
}
got, err := r.ListAllDbSubnetGroups()
assert.Equal(t, tt.wantErr, err)
changelog, err := diff.Diff(got, tt.want)
assert.Nil(t, err)
if len(changelog) > 0 {
for _, change := range changelog {
t.Errorf("%s: %s -> %s", strings.Join(change.Path, "."), change.From, change.To)
}
t.Fail()
}
})
}
}

View File

@ -0,0 +1,61 @@
package github
import (
"fmt"
"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 GithubTeamSupplier struct {
reader terraform.ResourceReader
deserializer deserializer.CTYDeserializer
repository GithubRepository
runner *terraform.ParallelResourceReader
}
func NewGithubTeamSupplier(provider *GithubTerraformProvider, repository GithubRepository) *GithubTeamSupplier {
return &GithubTeamSupplier{
provider,
ghdeserializer.NewGithubTeamDeserializer(),
repository,
terraform.NewParallelResourceReader(provider.Runner().SubRunner()),
}
}
func (s GithubTeamSupplier) Resources() ([]resource.Resource, error) {
resourceList, err := s.repository.ListTeams()
if err != nil {
return nil, remoteerror.NewResourceEnumerationError(err, resourcegithub.GithubTeamResourceType)
}
for _, id := range resourceList {
id := id
s.runner.Run(func() (cty.Value, error) {
completeResource, err := s.reader.ReadResource(terraform.ReadResourceArgs{
Ty: resourcegithub.GithubTeamResourceType,
ID: fmt.Sprintf("%d", id),
})
if err != nil {
logrus.Warnf("Error reading %d[%s]: %+v", id, resourcegithub.GithubTeamResourceType, 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,80 @@
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 TestGithubTeamSupplier_Resources(t *testing.T) {
cases := []struct {
test string
dirName string
mocks func(client *MockGithubRepository)
err error
}{
{
test: "no github teams",
dirName: "github_teams_empty",
mocks: func(client *MockGithubRepository) {
client.On("ListTeams").Return([]int{}, nil)
},
err: nil,
},
{
test: "Multiple github teams with parent",
dirName: "github_teams_multiple",
mocks: func(client *MockGithubRepository) {
client.On("ListTeams").Return([]int{
4556811, // github_team.team1
4556812, // github_team.team2
4556814, // github_team.with_parent
}, 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(NewGithubTeamSupplier(provider, &mockedRepo))
}
t.Run(c.test, func(tt *testing.T) {
provider := dritftctlmocks.NewMockedGoldenTFProvider(c.dirName, providerLibrary.Provider(terraform.GITHUB), shouldUpdate)
GithubTeamDeserializer := ghdeserializer.NewGithubTeamDeserializer()
s := &GithubTeamSupplier{
provider,
GithubTeamDeserializer,
&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, GithubTeamDeserializer, shouldUpdate, tt)
})
}
}

View File

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

View File

@ -31,3 +31,26 @@ func (_m *MockGithubRepository) ListRepositories() ([]string, error) {
return r0, r1
}
// ListTeams provides a mock function with given fields:
func (_m *MockGithubRepository) ListTeams() ([]int, error) {
ret := _m.Called()
var r0 []int
if rf, ok := ret.Get(0).(func() []int); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]int)
}
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}

View File

@ -9,6 +9,7 @@ import (
type GithubRepository interface {
ListRepositories() ([]string, error)
ListTeams() ([]int, error)
}
type GithubGraphQLClient interface {
@ -118,3 +119,43 @@ func (r githubRepository) listRepoForOwner() ([]string, error) {
}
return results, nil
}
type listTeamsQuery struct {
Organization struct {
Teams struct {
Nodes []struct {
DatabaseId int
}
PageInfo struct {
EndCursor githubv4.String
HasNextPage bool
}
} `graphql:"teams(first: 100, after: $cursor)"`
} `graphql:"organization(login: $login)"`
}
func (r githubRepository) ListTeams() ([]int, error) {
query := listTeamsQuery{}
results := make([]int, 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 _, team := range query.Organization.Teams.Nodes {
results = append(results, team.DatabaseId)
}
if !query.Organization.Teams.PageInfo.HasNextPage {
break
}
variables["cursor"] = githubv4.NewString(query.Organization.Teams.PageInfo.EndCursor)
}
return results, nil
}

View File

@ -197,3 +197,106 @@ func TestListRepositoriesForOrganization(t *testing.T) {
"repo4",
}, repos)
}
func TestListTeams_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.ListTeams()
assert.Equal(expectedError, err)
}
func TestListTeams_WithoutOrganization(t *testing.T) {
assert := assert.New(t)
r := githubRepository{}
teams, err := r.ListTeams()
assert.Nil(err)
assert.Equal([]int{}, teams)
}
func TestListTeams(t *testing.T) {
assert := assert.New(t)
mockedClient := mocks.GithubGraphQLClient{}
mockedClient.On("Query",
mock.Anything,
mock.MatchedBy(func(query interface{}) bool {
q, ok := query.(*listTeamsQuery)
if !ok {
return false
}
q.Organization.Teams.Nodes = []struct {
DatabaseId int
}{
{
DatabaseId: 1,
},
{
DatabaseId: 2,
},
}
q.Organization.Teams.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.(*listTeamsQuery)
if !ok {
return false
}
q.Organization.Teams.Nodes = []struct {
DatabaseId int
}{
{
DatabaseId: 3,
},
{
DatabaseId: 4,
},
}
q.Organization.Teams.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.ListTeams()
if err != nil {
t.Fatal(err)
}
assert.Equal([]int{1, 2, 3, 4}, teams)
}

View File

@ -0,0 +1 @@
[]

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,5 @@
{
"Typ": "WyJvYmplY3QiLHsiY3JlYXRlX2RlZmF1bHRfbWFpbnRhaW5lciI6ImJvb2wiLCJkZXNjcmlwdGlvbiI6InN0cmluZyIsImV0YWciOiJzdHJpbmciLCJpZCI6InN0cmluZyIsImxkYXBfZG4iOiJzdHJpbmciLCJtZW1iZXJzX2NvdW50IjoibnVtYmVyIiwibmFtZSI6InN0cmluZyIsIm5vZGVfaWQiOiJzdHJpbmciLCJwYXJlbnRfdGVhbV9pZCI6Im51bWJlciIsInByaXZhY3kiOiJzdHJpbmciLCJzbHVnIjoic3RyaW5nIn1d",
"Val": "eyJjcmVhdGVfZGVmYXVsdF9tYWludGFpbmVyIjpudWxsLCJkZXNjcmlwdGlvbiI6InRlc3QiLCJldGFnIjoiVy9cIjc5YTg2NWI1MjFjOTcwZTRjYmYyZGMwMDU1MmYxMzQ1MGQyYzVhMjM5MjczZTMxMTdlZTg3MWIzZGY1ZmQwNWVcIiIsImlkIjoiNDU1NjgxMSIsImxkYXBfZG4iOiIiLCJtZW1iZXJzX2NvdW50IjowLCJuYW1lIjoidGVhbTEiLCJub2RlX2lkIjoiTURRNlZHVmhiVFExTlRZNE1URT0iLCJwYXJlbnRfdGVhbV9pZCI6bnVsbCwicHJpdmFjeSI6ImNsb3NlZCIsInNsdWciOiJ0ZWFtMSJ9",
"Err": null
}

View File

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

View File

@ -0,0 +1,5 @@
{
"Typ": "WyJvYmplY3QiLHsiY3JlYXRlX2RlZmF1bHRfbWFpbnRhaW5lciI6ImJvb2wiLCJkZXNjcmlwdGlvbiI6InN0cmluZyIsImV0YWciOiJzdHJpbmciLCJpZCI6InN0cmluZyIsImxkYXBfZG4iOiJzdHJpbmciLCJtZW1iZXJzX2NvdW50IjoibnVtYmVyIiwibmFtZSI6InN0cmluZyIsIm5vZGVfaWQiOiJzdHJpbmciLCJwYXJlbnRfdGVhbV9pZCI6Im51bWJlciIsInByaXZhY3kiOiJzdHJpbmciLCJzbHVnIjoic3RyaW5nIn1d",
"Val": "eyJjcmVhdGVfZGVmYXVsdF9tYWludGFpbmVyIjpudWxsLCJkZXNjcmlwdGlvbiI6InRlc3QgcGFyZW50IHRlYW0iLCJldGFnIjoiVy9cIjI1OTg1NGQ5NWEyYTkwODU1Yjk0ZTY3ZjU2NGVhZGU3NTM3YzMzNGU5NDViMmI1YTUzMWU4MGIyMWIzZjZiNGJcIiIsImlkIjoiNDU1NjgxNCIsImxkYXBfZG4iOiIiLCJtZW1iZXJzX2NvdW50IjowLCJuYW1lIjoibmV3IHRlYW0gd2l0aCBwYXJlbnQiLCJub2RlX2lkIjoiTURRNlZHVmhiVFExTlRZNE1UUT0iLCJwYXJlbnRfdGVhbV9pZCI6NDU1NjgxMSwicHJpdmFjeSI6ImNsb3NlZCIsInNsdWciOiJuZXctdGVhbS13aXRoLXBhcmVudCJ9",
"Err": null
}

View File

@ -0,0 +1,41 @@
[
{
"create_default_maintainer": null,
"description": "test 2",
"etag": "W/\"c7192e5edb122a28f3635512f14fc80a1f8c376fd27a7fb10b8b6b4bfddaef66\"",
"id": "4556812",
"ldap_dn": "",
"members_count": 0,
"name": "team2",
"node_id": "MDQ6VGVhbTQ1NTY4MTI=",
"parent_team_id": null,
"privacy": "secret",
"slug": "team2"
},
{
"create_default_maintainer": null,
"description": "test parent team",
"etag": "W/\"259854d95a2a90855b94e67f564eade7537c334e945b2b5a531e80b21b3f6b4b\"",
"id": "4556814",
"ldap_dn": "",
"members_count": 0,
"name": "new team with parent",
"node_id": "MDQ6VGVhbTQ1NTY4MTQ=",
"parent_team_id": 4556811,
"privacy": "closed",
"slug": "new-team-with-parent"
},
{
"create_default_maintainer": null,
"description": "test",
"etag": "W/\"79a865b521c970e4cbf2dc00552f13450d2c5a239273e3117ee871b3df5fd05e\"",
"id": "4556811",
"ldap_dn": "",
"members_count": 0,
"name": "team1",
"node_id": "MDQ6VGVhbTQ1NTY4MTE=",
"parent_team_id": null,
"privacy": "closed",
"slug": "team1"
}
]

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 GithubTeamDeserializer struct {
}
func NewGithubTeamDeserializer() *GithubTeamDeserializer {
return &GithubTeamDeserializer{}
}
func (s GithubTeamDeserializer) HandledType() resource.ResourceType {
return github.GithubTeamResourceType
}
func (s GithubTeamDeserializer) Deserialize(rawResourceList []cty.Value) ([]resource.Resource, error) {
resources := make([]resource.Resource, 0)
for _, raw := range rawResourceList {
raw := raw
res, err := decodeTeam(&raw)
if err != nil {
return nil, errors.Wrapf(err, "error when deserializing github_team %+v : %+v", raw, err)
}
resources = append(resources, res)
}
return resources, nil
}
func decodeTeam(res *cty.Value) (resource.Resource, error) {
var decoded github.GithubTeam
if err := gocty.FromCtyValue(*res, &decoded); err != nil {
return nil, err
}
return &decoded, nil
}

View File

@ -0,0 +1,26 @@
// GENERATED, DO NOT EDIT THIS FILE
package github
const GithubTeamResourceType = "github_team"
type GithubTeam struct {
CreateDefaultMaintainer *bool `cty:"create_default_maintainer"`
Description *string `cty:"description"`
Etag *string `cty:"etag" computed:"true" diff:"-"`
Id string `cty:"id" computed:"true"`
LdapDn *string `cty:"ldap_dn"`
MembersCount *int `cty:"members_count" computed:"true"`
Name *string `cty:"name"`
NodeId *string `cty:"node_id" computed:"true"`
ParentTeamId *int `cty:"parent_team_id"`
Privacy *string `cty:"privacy"`
Slug *string `cty:"slug" computed:"true"`
}
func (r *GithubTeam) TerraformId() string {
return r.Id
}
func (r *GithubTeam) TerraformType() string {
return GithubTeamResourceType
}

View File

@ -0,0 +1,29 @@
package github
import (
"fmt"
awssdk "github.com/aws/aws-sdk-go/aws"
"github.com/cloudskiff/driftctl/pkg/resource"
)
func (r *GithubTeam) String() string {
if r.Name != nil {
return fmt.Sprintf("%s (Id: %s)", *r.Name, r.Id)
}
return r.Id
}
func (r *GithubTeam) NormalizeForState() (resource.Resource, error) {
if r.CreateDefaultMaintainer == nil {
r.CreateDefaultMaintainer = awssdk.Bool(false)
}
return r, nil
}
func (r *GithubTeam) NormalizeForProvider() (resource.Resource, error) {
if r.CreateDefaultMaintainer == nil {
r.CreateDefaultMaintainer = awssdk.Bool(false)
}
return r, nil
}

View File

@ -0,0 +1,38 @@
package github
import (
"testing"
awssdk "github.com/aws/aws-sdk-go/aws"
)
func TestGithubTeam_String(t *testing.T) {
tests := []struct {
name string
team GithubTeam
want string
}{
{
name: "test with name",
team: GithubTeam{
Id: "1234",
Name: awssdk.String("my-org-name"),
},
want: "my-org-name (Id: 1234)",
},
{
name: "test without name",
team: GithubTeam{
Id: "1234",
},
want: "1234",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.team.String(); got != tt.want {
t.Errorf("String() = %v, want %v", got, tt.want)
}
})
}
}

View File

@ -0,0 +1,29 @@
package github_test
import (
"testing"
"github.com/cloudskiff/driftctl/test/acceptance"
)
func TestAcc_Github_Team(t *testing.T) {
acceptance.Run(t, acceptance.AccTestCase{
Paths: []string{"./testdata/acc/github_team"},
Args: []string{
"scan",
"--to", "github+tf",
"--filter", "Type=='github_team'",
},
Checks: []acceptance.AccCheck{
{
Check: func(result *acceptance.ScanResult, stdout string, err error) {
if err != nil {
t.Fatal(err)
}
result.AssertInfrastructureIsInSync()
result.AssertManagedCount(3)
},
},
},
})
}

View File

@ -0,0 +1,21 @@
# This file is maintained automatically by "terraform init".
# Manual edits may be lost in future updates.
provider "registry.terraform.io/hashicorp/github" {
version = "4.4.0"
constraints = "4.4.0"
hashes = [
"h1:eKArqtLcYoYUFf4dgNzVemqu2GsoEf7K0ZLEXjSoPBo=",
"zh:0ebb07c4971ca7d60fce8614270d056328a121fd4ffbda4b29a06d4a1e90e939",
"zh:178b333f2f285c1a59b9335320f584bd01304179c2d6a1919366945b55cfb293",
"zh:2c9087e987a5e1af2aad803a79fa5bd847ac060d4c766b5a187b9aabb3f734a4",
"zh:419597d8d284616ed93a2c13b3833b129aaba7af7a057d2f48aeb7bc3610cefc",
"zh:61686d578880ad76cb8e9c2cc72ad14ef2896fde973cca18b8f7c8848781c71d",
"zh:662e125ac42a0c113d811afd2e7a0f81ba061f00cc62ba7435bd685f889290b9",
"zh:87fb3d97070cae7f0b623d1a9b59e8cfad0dfece4a27ee964c4c77f228592a80",
"zh:e9dcc85ef2f2e09d298f3bbbb9b0d673596d62c1d7d480b2999b4badb2f4aeff",
"zh:f052c377a0630a6881c183ac5de0dbef4e5627638a23434a6aa7fce8977b43de",
"zh:f7456ec2a6a31caa5d2c85f4da660689f8bac5541c70324803de0c26a14586e1",
"zh:fbf6bfddde6f209dc65052ca27e0c83e96bae5fde6940edf029ca42e7e4f1110",
]
}

View File

@ -0,0 +1,24 @@
terraform {
required_version = ">= 0.14.4"
required_providers {
github = "=4.4.0"
}
}
resource "github_team" "team1" {
name = "team1"
description = "test"
privacy = "closed"
}
resource "github_team" "team2" {
name = "team2"
description = "test 2"
}
resource "github_team" "with_parent" {
name = "new team with parent"
description = "test parent team"
parent_team_id = github_team.team1.id
privacy = "closed"
}

View File

@ -1,55 +0,0 @@
package mocks
import (
"github.com/aws/aws-sdk-go/service/rds"
"github.com/aws/aws-sdk-go/service/rds/rdsiface"
)
type DescribeSubnetGroupResponse []struct {
LastPage bool
Response *rds.DescribeDBSubnetGroupsOutput
}
type DescribeDBInstancesPagesOutput []struct {
LastPage bool
Response *rds.DescribeDBInstancesOutput
}
type MockAWSRDSClient struct {
rdsiface.RDSAPI
dbInstancesPages DescribeDBInstancesPagesOutput
describeSubnetGroupResponse DescribeSubnetGroupResponse
err error
}
func NewMockAWSRDSErrorClient(err error) *MockAWSRDSClient {
return &MockAWSRDSClient{err: err}
}
func NewMockAWSRDSClient(dbInstancesPages DescribeDBInstancesPagesOutput) *MockAWSRDSClient {
return &MockAWSRDSClient{dbInstancesPages: dbInstancesPages}
}
func NewMockAWSRDSSubnetGroupClient(describeSubnetGroupResponse DescribeSubnetGroupResponse) *MockAWSRDSClient {
return &MockAWSRDSClient{describeSubnetGroupResponse: describeSubnetGroupResponse}
}
func (m *MockAWSRDSClient) DescribeDBInstancesPages(_ *rds.DescribeDBInstancesInput, cb func(*rds.DescribeDBInstancesOutput, bool) bool) error {
if m.err != nil {
return m.err
}
for _, dbInstancesPage := range m.dbInstancesPages {
cb(dbInstancesPage.Response, dbInstancesPage.LastPage)
}
return nil
}
func (m *MockAWSRDSClient) DescribeDBSubnetGroupsPages(input *rds.DescribeDBSubnetGroupsInput, callback func(*rds.DescribeDBSubnetGroupsOutput, bool) bool) error {
if m.err != nil {
return m.err
}
for _, response := range m.describeSubnetGroupResponse {
callback(response.Response, response.LastPage)
}
return nil
}