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-498: Add terraform generate command skeleton #792

Merged
merged 5 commits 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
13 changes: 13 additions & 0 deletions docs/auth0_terraform.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
layout: default
has_toc: false
has_children: true
---
# auth0 terraform

This command facilitates the integration of Auth0 with [Terraform](https://www.terraform.io/), an Infrastructure as Code tool.

## Commands

- [auth0 terraform generate](auth0_terraform_generate.md) - Generate terraform configuration for your Auth0 Tenant

47 changes: 47 additions & 0 deletions docs/auth0_terraform_generate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
---
layout: default
parent: auth0 terraform
has_toc: false
---
# auth0 terraform generate

This command is designed to streamline the process of generating Terraform configuration files for your Auth0 resources, serving as a bridge between the two.

It automatically scans your Auth0 Tenant and compiles a set of Terraform configuration files based on the existing resources and configurations.

The generated Terraform files are written in HashiCorp Configuration Language (HCL).

## Usage
```
auth0 terraform generate [flags]
```

## Examples

```

```


## Flags

```
-o, --output-dir string Output directory for the generated Terraform config files. If not provided, the files will be saved in the current working directory. (default "./")
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I considered naming this just --dir, however the extra verbosity adds clarity in the full form for this flag and considering there's also the short form option of -o, I deemed unnecessary to have the flag shorter.

```


## Inherited Flags

```
--debug Enable debug mode.
--no-color Disable colors.
--no-input Disable interactivity.
--tenant string Specific tenant to use.
```


## Related Commands

- [auth0 terraform generate](auth0_terraform_generate.md) - Generate terraform configuration for your Auth0 Tenant


1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ Authenticating as a user is not supported for **private cloud** tenants. Instead
- [auth0 roles](auth0_roles.md) - Manage resources for roles
- [auth0 rules](auth0_rules.md) - Manage resources for rules
- [auth0 tenants](auth0_tenants.md) - Manage configured tenants
- [auth0 terraform](auth0_terraform.md) - Manage terraform configuration for your Auth0 Tenant
- [auth0 test](auth0_test.md) - Try your Universal Login box or get a token
- [auth0 universal-login](auth0_universal-login.md) - Manage the Universal Login experience
- [auth0 users](auth0_users.md) - Manage resources for users
Expand Down
1 change: 1 addition & 0 deletions internal/cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
113 changes: 113 additions & 0 deletions internal/cli/terraform.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
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. If not provided, the files will be " +
"saved in the current working directory.",
},
}

type (
terraformFlags struct {
OutputDIR Flag
}

terraformInputs struct {
OutputDIR string
}
)

func terraformCmd(cli *cli) *cobra.Command {
cmd := &cobra.Command{
Use: "terraform",
Aliases: []string{"tf"},
Copy link
Contributor

Choose a reason for hiding this comment

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

I like the tf alias 👍

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"}, // Reconsider aliases and command name before releasing.
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\nIt 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).",
Copy link
Contributor

Choose a reason for hiding this comment

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

Reminder for us to include instructions and/or link to a guide once one is available.

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) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Does there need to be some nuance here? There's no certainty that an existing main.tf will be compatible with the command.

Some ideas on how to manage:

  • Require absence of main.tf
  • If main.tf exists, print warning explaining a potential incompatibility

Copy link
Contributor Author

@sergiught sergiught Aug 8, 2023

Choose a reason for hiding this comment

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

Good callout, however considering we will have the same issue with the import.tf file and the generated files, let's focus on this as a separate PR after those are added as well.

Currently, if the file exists it gets truncated and overwritten.

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"
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a way to set this value programmatically?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Can't think of any. Any suggestions?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Once v1 GA goes live, we can consider omitting the version, so it always fetches the latest stable.

Copy link
Contributor

Choose a reason for hiding this comment

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

Once v1 GA goes live, we can consider omitting the version, so it always fetches the latest stable.

Once we eventually release v2 (e.g. in a couple of years), would this mean it will automatically fetch the new major (along with its breaking changes)?

Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

All good things to consider, but at the moment we don't have any stable v1 so we can use "~> 1.0", and we need to specify the version explicitly for the beta so we can do some rapid testing as well. We'll adjust that value accordingly in the future.

}
}
}

provider "auth0" {
debug = true
}
`

_, err = mainTerraformConfigFile.WriteString(mainTerraformConfigFileContent)
return err
}
85 changes: 85 additions & 0 deletions internal/cli/terraform_test.go
Original file line number Diff line number Diff line change
@@ -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("./terraform")

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 empty", func(t *testing.T) {
testInputs := terraformInputs{
OutputDIR: "",
}

err := generateTerraformConfigFiles(&testInputs)
assert.EqualError(t, err, "mkdir : no such file or directory")
})

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))
}