From 409cd111ed46f548f75f1bdadbb7d186c233a227 Mon Sep 17 00:00:00 2001 From: Sergiu Ghitea <28300158+sergiught@users.noreply.github.com> Date: Tue, 8 Aug 2023 12:56:49 +0200 Subject: [PATCH] Add abstractions to fetch client resource data in tf cmd --- internal/cli/terraform.go | 36 +++++++ internal/cli/terraform_fetcher.go | 79 ++++++++++++++ internal/cli/terraform_fetcher_test.go | 142 +++++++++++++++++++++++++ internal/cli/terraform_test.go | 41 +++++++ 4 files changed, 298 insertions(+) create mode 100644 internal/cli/terraform_fetcher.go create mode 100644 internal/cli/terraform_fetcher_test.go diff --git a/internal/cli/terraform.go b/internal/cli/terraform.go index d11d50b65..7f978b017 100644 --- a/internal/cli/terraform.go +++ b/internal/cli/terraform.go @@ -1,10 +1,13 @@ package cli import ( + "context" "os" "path" "github.com/spf13/cobra" + + "github.com/auth0/auth0-cli/internal/auth0" ) var tfFlags = terraformFlags{ @@ -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", @@ -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 } @@ -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 { diff --git a/internal/cli/terraform_fetcher.go b/internal/cli/terraform_fetcher.go new file mode 100644 index 000000000..55d74af83 --- /dev/null +++ b/internal/cli/terraform_fetcher.go @@ -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-]+" + re = regexp.MustCompile(namePattern) + + sanitizedName = re.ReplaceAllString(sanitizedName, "") + + return sanitizedName +} diff --git a/internal/cli/terraform_fetcher_test.go b/internal/cli/terraform_fetcher_test.go new file mode 100644 index 000000000..c17732df4 --- /dev/null +++ b/internal/cli/terraform_fetcher_test.go @@ -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") + }) +} diff --git a/internal/cli/terraform_test.go b/internal/cli/terraform_test.go index 4f16420b5..ba164a1da 100644 --- a/internal/cli/terraform_test.go +++ b/internal/cli/terraform_test.go @@ -1,6 +1,8 @@ package cli import ( + "context" + "errors" "os" "path" "testing" @@ -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",