Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DXCDT-499: Add abstractions to fetch client resource data in tf cmd #794

Merged
merged 1 commit into from
Aug 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions internal/cli/terraform.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package cli

import (
"context"
"os"
"path"

"github.com/spf13/cobra"

"github.com/auth0/auth0-cli/internal/auth0"
)

var tfFlags = terraformFlags{
Expand All @@ -27,6 +30,15 @@ type (
}
)

func (i *terraformInputs) parseResourceFetchers(api *auth0.API) []resourceDataFetcher {
// Hard coding this for now until we add support for the `--resources` flag.
return []resourceDataFetcher{
&clientResourceFetcher{
api: api,
},
}
}

func terraformCmd(cli *cli) *cobra.Command {
cmd := &cobra.Command{
Use: "terraform",
Expand Down Expand Up @@ -63,6 +75,15 @@ func generateTerraformCmd(cli *cli) *cobra.Command {

func generateTerraformCmdRun(cli *cli, inputs *terraformInputs) func(cmd *cobra.Command, args []string) error {
return func(cmd *cobra.Command, args []string) error {
data, err := fetchImportData(cmd.Context(), inputs.parseResourceFetchers(cli.api)...)
if err != nil {
return err
}

// Just temporarily. Remove this once import file generation is in place.
cli.renderer.JSONResult(data)
cli.renderer.Newline()

if err := generateTerraformConfigFiles(inputs); err != nil {
return err
}
Expand All @@ -79,6 +100,21 @@ func generateTerraformCmdRun(cli *cli, inputs *terraformInputs) func(cmd *cobra.
}
}

func fetchImportData(ctx context.Context, fetchers ...resourceDataFetcher) (importDataList, error) {
var importData importDataList

for _, fetcher := range fetchers {
data, err := fetcher.FetchData(ctx)
if err != nil {
return nil, err
}

importData = append(importData, data...)
}

return importData, nil
}

func generateTerraformConfigFiles(inputs *terraformInputs) error {
const readWritePermission = 0755
if err := os.MkdirAll(inputs.OutputDIR, readWritePermission); err != nil {
Expand Down
79 changes: 79 additions & 0 deletions internal/cli/terraform_fetcher.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package cli

import (
"context"
"regexp"

"github.com/auth0/go-auth0/management"

"github.com/auth0/auth0-cli/internal/auth0"
)

type (
importDataList []importDataItem

importDataItem struct {
ResourceName string
ImportID string
}

resourceDataFetcher interface {
FetchData(ctx context.Context) (importDataList, error)
}

clientResourceFetcher struct {
api *auth0.API
}
)

func (f *clientResourceFetcher) FetchData(ctx context.Context) (importDataList, error) {
var data importDataList

var page int
for {
clients, err := f.api.Client.List(
ctx,
management.Page(page),
management.Parameter("is_global", "false"),
management.IncludeFields("client_id", "name"),
)
if err != nil {
return nil, err
}

for _, client := range clients.Clients {
data = append(data, importDataItem{
ResourceName: "auth0_client." + sanitizeResourceName(client.GetName()),
ImportID: client.GetClientID(),
})
}

if !clients.HasNext() {
break
}

page++
}

return data, nil
}

// sanitizeResourceName will return a valid terraform resource name.
//
// A name must start with a letter or underscore and may
// contain only letters, digits, underscores, and dashes.
func sanitizeResourceName(name string) string {
// Regular expression pattern to remove invalid characters.
namePattern := "[^a-zA-Z0-9_-]+"
re := regexp.MustCompile(namePattern)

sanitizedName := re.ReplaceAllString(name, "")

// Regular expression pattern to remove leading digits or dashes.
namePattern = "^[0-9-]+"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be preferable to match any leading character that is not a letter or underscore?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How the sanitize works is that it matches invalid characters and removes them, so it's doing the opposite.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But it's only matching a subset of invalid characters, not all possible invalid characters.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, you're already removing all the other possible chars in the step above.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logic is split in 2 through 2 regexes, the first one removes all invalid characters, and the second one removes digits or - from the start of the string.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the explanation. My bad.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No worries, happy to clarify 👍🏻

re = regexp.MustCompile(namePattern)

sanitizedName = re.ReplaceAllString(sanitizedName, "")

return sanitizedName
}
142 changes: 142 additions & 0 deletions internal/cli/terraform_fetcher_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package cli

import (
"context"
"fmt"
"testing"

"github.com/auth0/go-auth0/management"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"

"github.com/auth0/auth0-cli/internal/auth0"
"github.com/auth0/auth0-cli/internal/auth0/mock"
)

func TestSanitizeResourceName(t *testing.T) {
testCases := []struct {
input string
expected string
}{
// Test cases with valid names
{"ValidName123", "ValidName123"},
{"_Another_Valid-Name", "_Another_Valid-Name"},
{"name_with_123", "name_with_123"},
{"_start_with_underscore", "_start_with_underscore"},

// Test cases with invalid names to be sanitized
{"Invalid@Name", "InvalidName"},
{"Invalid Name", "InvalidName"},
{"123StartWithNumber", "StartWithNumber"},
{"-StartWithDash", "StartWithDash"},
{"", ""},
}

for _, testCase := range testCases {
t.Run(testCase.input, func(t *testing.T) {
sanitized := sanitizeResourceName(testCase.input)
assert.Equal(t, testCase.expected, sanitized)
})
}
}

func TestClientResourceFetcher_FetchData(t *testing.T) {
t.Run("it successfully retrieves client data", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()

clientAPI := mock.NewMockClientAPI(ctrl)
clientAPI.EXPECT().
List(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
Return(
&management.ClientList{
List: management.List{
Start: 0,
Limit: 2,
Total: 4,
},
Clients: []*management.Client{
{
ClientID: auth0.String("clientID_1"),
Name: auth0.String("My Test Client 1"),
},
{
ClientID: auth0.String("clientID_2"),
Name: auth0.String("My Test Client 2"),
},
},
},
nil,
)
clientAPI.EXPECT().
List(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
Return(
&management.ClientList{
List: management.List{
Start: 2,
Limit: 4,
Total: 4,
},
Clients: []*management.Client{
{
ClientID: auth0.String("clientID_3"),
Name: auth0.String("My Test Client 3"),
},
{
ClientID: auth0.String("clientID_4"),
Name: auth0.String("My Test Client 4"),
},
},
},
nil,
)

fetcher := clientResourceFetcher{
api: &auth0.API{
Client: clientAPI,
},
}

expectedData := importDataList{
{
ResourceName: "auth0_client.MyTestClient1",
ImportID: "clientID_1",
},
{
ResourceName: "auth0_client.MyTestClient2",
ImportID: "clientID_2",
},
{
ResourceName: "auth0_client.MyTestClient3",
ImportID: "clientID_3",
},
{
ResourceName: "auth0_client.MyTestClient4",
ImportID: "clientID_4",
},
}

data, err := fetcher.FetchData(context.Background())
assert.NoError(t, err)
assert.Equal(t, expectedData, data)
})

t.Run("it returns an error if api call fails", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()

clientAPI := mock.NewMockClientAPI(ctrl)
clientAPI.EXPECT().
List(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
Return(nil, fmt.Errorf("failed to list clients"))

fetcher := clientResourceFetcher{
api: &auth0.API{
Client: clientAPI,
},
}

_, err := fetcher.FetchData(context.Background())
assert.EqualError(t, err, "failed to list clients")
})
}
41 changes: 41 additions & 0 deletions internal/cli/terraform_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package cli

import (
"context"
"errors"
"os"
"path"
"testing"
Expand All @@ -9,6 +11,45 @@ import (
"github.com/stretchr/testify/require"
)

type mockFetcher struct {
mockData importDataList
mockErr error
}

func (m *mockFetcher) FetchData(context.Context) (importDataList, error) {
return m.mockData, m.mockErr
}

func TestFetchImportData(t *testing.T) {
t.Run("it can successfully fetch import data for multiple resources", func(t *testing.T) {
mockData1 := importDataList{{ResourceName: "Resource1", ImportID: "123"}}
mockData2 := importDataList{{ResourceName: "Resource2", ImportID: "456"}}
mockFetchers := []resourceDataFetcher{
&mockFetcher{mockData: mockData1},
&mockFetcher{mockData: mockData2},
}

expectedData := importDataList{
{ResourceName: "Resource1", ImportID: "123"},
{ResourceName: "Resource2", ImportID: "456"},
}

data, err := fetchImportData(context.Background(), mockFetchers...)
assert.NoError(t, err)
assert.Equal(t, expectedData, data)
})

t.Run("it returns an error when a data fetcher fails", func(t *testing.T) {
expectedErr := errors.New("failed to list clients")
mockFetchers := []resourceDataFetcher{
&mockFetcher{mockErr: expectedErr},
}

_, err := fetchImportData(context.Background(), mockFetchers...)
assert.EqualError(t, err, "failed to list clients")
})
}

func TestGenerateTerraformConfigFiles(t *testing.T) {
testInputs := terraformInputs{
OutputDIR: "./terraform/dev",
Expand Down
Loading