diff --git a/internal/cli/root.go b/internal/cli/root.go index 19f7f7f0a..e4d39d2ff 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -168,6 +168,7 @@ func addSubCommands(rootCmd *cobra.Command, cli *cli) { rootCmd.AddCommand(testCmd(cli)) rootCmd.AddCommand(logsCmd(cli)) rootCmd.AddCommand(apiCmd(cli)) + rootCmd.AddCommand(terraformCmd(cli)) // keep completion at the bottom: rootCmd.AddCommand(completionCmd(cli)) diff --git a/internal/cli/terraform.go b/internal/cli/terraform.go new file mode 100644 index 000000000..0e6bd05a9 --- /dev/null +++ b/internal/cli/terraform.go @@ -0,0 +1,112 @@ +package cli + +import ( + "os" + "path" + + "github.com/spf13/cobra" +) + +var tfFlags = terraformFlags{ + OutputDIR: Flag{ + Name: "Output Dir", + LongForm: "output-dir", + ShortForm: "o", + Help: "Output directory for the generated Terraform config files.", + }, +} + +type ( + terraformFlags struct { + OutputDIR Flag + } + + terraformInputs struct { + OutputDIR string + } +) + +func terraformCmd(cli *cli) *cobra.Command { + cmd := &cobra.Command{ + Use: "terraform", + Aliases: []string{"tf"}, + Short: "Manage terraform configuration for your Auth0 Tenant", + Long: "This command facilitates the integration of Auth0 with [Terraform](https://www.terraform.io/), an " + + "infrastructure as code tool.", + } + + cmd.SetUsageTemplate(resourceUsageTemplate()) + cmd.AddCommand(generateTerraformCmd(cli)) + + return cmd +} + +func generateTerraformCmd(cli *cli) *cobra.Command { + var inputs terraformInputs + + cmd := &cobra.Command{ + Use: "generate", + Aliases: []string{"gen", "export"}, + Short: "Generate terraform configuration for your Auth0 Tenant", + Long: "This command is designed to streamline the process of generating Terraform configuration files for " + + "your Auth0 resources, serving as a bridge between the two.\n\n It automatically scans your Auth0 Tenant " + + "and compiles a set of Terraform configuration files based on the existing resources and configurations." + + "\n\nThe generated Terraform files are written in HashiCorp Configuration Language (HCL).", + RunE: generateTerraformCmdRun(cli, &inputs), + } + + tfFlags.OutputDIR.RegisterString(cmd, &inputs.OutputDIR, "./") + + return cmd +} + +func generateTerraformCmdRun(cli *cli, inputs *terraformInputs) func(cmd *cobra.Command, args []string) error { + return func(cmd *cobra.Command, args []string) error { + if err := generateTerraformConfigFiles(inputs); err != nil { + return err + } + + cli.renderer.Infof("Terraform config files generated successfully.") + cli.renderer.Infof( + "Follow this " + + "[quickstart](https://registry.terraform.io/providers/auth0/auth0/latest/docs/guides/quickstart) " + + "to go through setting up an Auth0 application for the provider to authenticate against and manage " + + "resources.", + ) + + return nil + } +} + +func generateTerraformConfigFiles(inputs *terraformInputs) error { + const readWritePermission = 0755 + if err := os.MkdirAll(inputs.OutputDIR, readWritePermission); err != nil { + if !os.IsExist(err) { + return err + } + } + + mainTerraformConfigFile, err := os.Create(path.Join(inputs.OutputDIR, "main.tf")) + if err != nil { + return err + } + defer mainTerraformConfigFile.Close() + + mainTerraformConfigFileContent := `terraform { + required_version = "~> 1.5.0" + required_providers { + auth0 = { + source = "auth0/auth0" + version = "1.0.0-beta.1" + } + } +} + +provider "auth0" { + debug = true +} +` + + _, err = mainTerraformConfigFile.WriteString(mainTerraformConfigFileContent) + return err +} diff --git a/internal/cli/terraform_test.go b/internal/cli/terraform_test.go new file mode 100644 index 000000000..c171a6949 --- /dev/null +++ b/internal/cli/terraform_test.go @@ -0,0 +1,85 @@ +package cli + +import ( + "os" + "path" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGenerateTerraformConfigFiles(t *testing.T) { + testInputs := terraformInputs{ + OutputDIR: "./terraform/dev", + } + defer os.RemoveAll(testInputs.OutputDIR) + + t.Run("it can correctly generate the terraform main config file", func(t *testing.T) { + assertTerraformConfigFilesWereGeneratedWithCorrectContent(t, &testInputs) + }) + + t.Run("it can correctly generate the terraform main config file even if the dir exists", func(t *testing.T) { + err := os.MkdirAll(testInputs.OutputDIR, 0755) + require.NoError(t, err) + + assertTerraformConfigFilesWereGeneratedWithCorrectContent(t, &testInputs) + }) + + t.Run("it fails to create the directory if path is a read only location", func(t *testing.T) { + testInputs := terraformInputs{ + OutputDIR: "/terraform/dev", + } + + err := generateTerraformConfigFiles(&testInputs) + assert.EqualError(t, err, "mkdir /terraform: read-only file system") + }) + + t.Run("it fails to create the main.tf file if file is already created and read only", func(t *testing.T) { + err := os.MkdirAll(testInputs.OutputDIR, 0755) + require.NoError(t, err) + + mainFilePath := path.Join(testInputs.OutputDIR, "main.tf") + _, err = os.Create(mainFilePath) + require.NoError(t, err) + + err = os.Chmod(mainFilePath, 0444) + require.NoError(t, err) + + err = generateTerraformConfigFiles(&testInputs) + assert.EqualError(t, err, "open terraform/dev/main.tf: permission denied") + }) +} + +func assertTerraformConfigFilesWereGeneratedWithCorrectContent(t *testing.T, testInputs *terraformInputs) { + err := generateTerraformConfigFiles(testInputs) + require.NoError(t, err) + + // Assert that the directory was created. + _, err = os.Stat(testInputs.OutputDIR) + assert.NoError(t, err) + + // Assert that the main.tf file was created with the correct content. + mainTerraformConfigFilePath := path.Join(testInputs.OutputDIR, "main.tf") + _, err = os.Stat(mainTerraformConfigFilePath) + assert.NoError(t, err) + + expectedContent := `terraform { + required_version = "~> 1.5.0" + required_providers { + auth0 = { + source = "auth0/auth0" + version = "1.0.0-beta.1" + } + } +} + +provider "auth0" { + debug = true +} +` + // Read the file content and check if it matches the expected content + content, err := os.ReadFile(mainTerraformConfigFilePath) + assert.NoError(t, err) + assert.Equal(t, expectedContent, string(content)) +}