Skip to content

Commit

Permalink
chore: new scaffold command for creating resources/data sources (#1739)
Browse files Browse the repository at this point in the history
* feat: new scaffold command for guiding new development

* include template for acceptance test file
  • Loading branch information
AgustinBettati committed Dec 14, 2023
1 parent c0ae26a commit b9e795b
Show file tree
Hide file tree
Showing 10 changed files with 666 additions and 22 deletions.
72 changes: 50 additions & 22 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
# Contributing

Thanks for your interest in contributing to MongoDB Atlas Terraform Provider, this document describes some guidelines necessary to participate in the community.

## Table of Contents

- [Development Setup](#development-setup)
- [Prerequisite Tools](#prerequisite-tools)
- [Environment](#prerequisite-tools)
- [Open a Pull Request](#open-a-pull-request)
- [Testing the Provider](#testing-the-provider)
- [Running Acceptance Tests](#running-acceptance-tests)
- [Code and Test Best Practices](#code-and-test-best-practices)
- [Creating New Resource and Data Sources](#creating-new-resources-and-data-sources)
- [Documentation Best Practices](#documentation-best-practices)
- [Discovering New API features](#discovering-new-api-features)


## Development Setup
### Prerequisite Tools
Expand Down Expand Up @@ -47,6 +62,26 @@ For more explained information about plugin override check [Development Override
- Commit and push your changes to your branch then submit a pull request against the `master` branch.
- A repo maintainer will review the your pull request, and may either request additional changes or merge the pull request.

#### PR Title Format
We use [*Conventional Commits*](https://www.conventionalcommits.org/):
- `fix: description of the PR`: a commit of the type fix patches a bug in your codebase (this correlates with PATCH in Semantic Versioning).
- `chore: description of the PR`: the commit includes a technical or preventative maintenance task that is necessary for managing the product or the repository, but it is not tied to any specific feature or user story (this correlates with PATCH in Semantic Versioning).
- `doc: description of the PR`: The commit adds, updates, or revises documentation that is stored in the repository (this correlates with PATCH in Semantic Versioning).
- `test: description of the PR`: The commit enhances, adds to, revised, or otherwise changes the suite of automated tests for the product (this correlates with PATCH in Semantic Versioning).
- `security: description of the PR`: The commit improves the security of the product or resolves a security issue that has been reported (this correlates with PATCH in Semantic Versioning).
- `refactor: description of the PR`: The commit refactors existing code in the product, but does not alter or change existing behavior in the product (this correlates with Minor in Semantic Versioning).
- `perf: description of the PR`: The commit improves the performance of algorithms or general execution time of the product, but does not fundamentally change an existing feature (this correlates with Minor in Semantic Versioning).
- `ci: description of the PR`: The commit makes changes to continuous integration or continuous delivery scripts or configuration files (this correlates with Minor in Semantic Versioning).
- `revert: description of the PR`: The commit reverts one or more commits that were previously included in the product, but were accidentally merged or serious issues were discovered that required their removal from the main branch (this correlates with Minor in Semantic Versioning).
- `style: description of the PR`: The commit updates or reformats the style of the source code, but does not otherwise change the product implementation (this correlates with Minor in Semantic Versioning).
- `feat: description of the PR`: a commit of the type feat introduces a new feature to the codebase (this correlates with MINOR in Semantic Versioning).
- `deprecate: description of the PR`: The commit deprecates existing functionality, but does not remove it from the product (this correlates with MINOR in Semantic Versioning).
- `BREAKING CHANGE`: a commit that has a footer BREAKING CHANGE:, or appends a ! after the type/scope, introduces a breaking API change (correlating with MAJOR in Semantic Versioning). A BREAKING CHANGE can be part of commits of any type.
Examples:
- `fix!: description of the ticket`
- If the PR has `BREAKING CHANGE`: in its description is a breaking change
- `remove!: description of the PR`: The commit removes a feature from the product. Typically features are deprecated first for a period of time before being removed. Removing a feature is a breaking change (correlating with MAJOR in Semantic Versioning).

### Testing the Provider

In order to test the provider, you can run `make test`. You can use [meta-arguments](https://www.terraform.io/docs/configuration/providers.html) such as `alias` and `version`. The following arguments are supported in the MongoDB Atlas `provider` block:
Expand All @@ -61,7 +96,7 @@ In order to test the provider, you can run `make test`. You can use [meta-argume

~> **Notice:** If you do not have a `public_key` and `private_key` you must create a programmatic API key to configure the provider (see [Creating Programmatic API key](#Programmatic-API-key)). If you already have one, you can continue with [Configuring environment variables](#Configuring-environment-variables)

### Running the acceptance test
### Running Acceptance Tests

#### Programmatic API key

Expand Down Expand Up @@ -269,29 +304,22 @@ To do this you can:
- `internal/testutils/acc` contains helper test methods for Acceptance and Migration tests.
- Tests that need the provider binary like End-to-End tests don’t belong to the source code packages and go in `test/e2e`.

## Documentation Best Practises
### Creating New Resource and Data Sources

- In our documentation, when a resource field allows a maximum of only one item, we do not format that field as an array. Instead, we create a subsection specifically for this field. Within this new subsection, we enumerate all the attributes of the field. Let's illustrate this with an example: [cloud_backup_schedule.html.markdown](https://github.com/mongodb/terraform-provider-mongodbatlas/blob/master/website/docs/r/cloud_backup_schedule.html.markdown?plain=1#L207)
A scaffolding command was defined with the intention of speeding up development process, while also preserving common conventions throughout our codebase.

## PR Title Format
We use [*Conventional Commits*](https://www.conventionalcommits.org/):
- `fix: description of the PR`: a commit of the type fix patches a bug in your codebase (this correlates with PATCH in Semantic Versioning).
- `chore: description of the PR`: the commit includes a technical or preventative maintenance task that is necessary for managing the product or the repository, but it is not tied to any specific feature or user story (this correlates with PATCH in Semantic Versioning).
- `doc: description of the PR`: The commit adds, updates, or revises documentation that is stored in the repository (this correlates with PATCH in Semantic Versioning).
- `test: description of the PR`: The commit enhances, adds to, revised, or otherwise changes the suite of automated tests for the product (this correlates with PATCH in Semantic Versioning).
- `security: description of the PR`: The commit improves the security of the product or resolves a security issue that has been reported (this correlates with PATCH in Semantic Versioning).
- `refactor: description of the PR`: The commit refactors existing code in the product, but does not alter or change existing behavior in the product (this correlates with Minor in Semantic Versioning).
- `perf: description of the PR`: The commit improves the performance of algorithms or general execution time of the product, but does not fundamentally change an existing feature (this correlates with Minor in Semantic Versioning).
- `ci: description of the PR`: The commit makes changes to continuous integration or continuous delivery scripts or configuration files (this correlates with Minor in Semantic Versioning).
- `revert: description of the PR`: The commit reverts one or more commits that were previously included in the product, but were accidentally merged or serious issues were discovered that required their removal from the main branch (this correlates with Minor in Semantic Versioning).
- `style: description of the PR`: The commit updates or reformats the style of the source code, but does not otherwise change the product implementation (this correlates with Minor in Semantic Versioning).
- `feat: description of the PR`: a commit of the type feat introduces a new feature to the codebase (this correlates with MINOR in Semantic Versioning).
- `deprecate: description of the PR`: The commit deprecates existing functionality, but does not remove it from the product (this correlates with MINOR in Semantic Versioning).
- `BREAKING CHANGE`: a commit that has a footer BREAKING CHANGE:, or appends a ! after the type/scope, introduces a breaking API change (correlating with MAJOR in Semantic Versioning). A BREAKING CHANGE can be part of commits of any type.
Examples:
- `fix!: description of the ticket`
- If the PR has `BREAKING CHANGE`: in its description is a breaking change
- `remove!: description of the PR`: The commit removes a feature from the product. Typically features are deprecated first for a period of time before being removed. Removing a feature is a breaking change (correlating with MAJOR in Semantic Versioning).
This command can be used the following way:
```bash
make scaffold name=streamInstance type=resource
```
- **name**: The name of the resource, which must be defined in camel case.
- **type**: Describes the type of resource being created. There are 3 different types: `resource`, `data-source`, `plural-data-source`.

This will generate resource/data source files and accompanying test files needed for starting the development, and will contain multiple comments with `TODO:` statements which give guidance for the development.

## Documentation Best Practices

- In our documentation, when a resource field allows a maximum of only one item, we do not format that field as an array. Instead, we create a subsection specifically for this field. Within this new subsection, we enumerate all the attributes of the field. Let's illustrate this with an example: [cloud_backup_schedule.html.markdown](https://github.com/mongodb/terraform-provider-mongodbatlas/blob/master/website/docs/r/cloud_backup_schedule.html.markdown?plain=1#L207)

## Discovering New API features

Expand Down
6 changes: 6 additions & 0 deletions GNUmakefile
Original file line number Diff line number Diff line change
Expand Up @@ -110,3 +110,9 @@ link-git-hooks: ## Install git hooks
update-atlas-sdk: ## Update the atlas-sdk dependency
./scripts/update-sdk.sh

# details on usage can be found in CONTRIBUTING.md under "Creating New Resource and Data Sources"
.PHONY: scaffold
scaffold:
@go run ./tools/scaffold/*.go $(name) $(type)
@echo "Reminder: configure the new $(type) in provider.go"

153 changes: 153 additions & 0 deletions tools/scaffold/scaffold.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package main

import (
"errors"
"fmt"
"log"
"os"
"path/filepath"
"strings"
"text/template"
)

const (
ResourceCmd = "resource"
DataSourceCmd = "data-source"
PluralDataSourceCmd = "plural-data-source"
)

// struct which is applied to go template files
type ScaffoldParams struct {
GenerationType string
NamePascalCase string
NameCamelCase string
NameSnakeCase string
NameLowerNoSpaces string
}

type FileGeneration struct {
TemplatePath string
OutputPath string
}

func main() {
nameCamelCase := os.Args[1]
generationType := os.Args[2]

params := ScaffoldParams{
GenerationType: generationType,
NamePascalCase: ToPascalCase(nameCamelCase),
NameCamelCase: nameCamelCase,
NameSnakeCase: ToSnakeCase(nameCamelCase),
NameLowerNoSpaces: strings.ToLower(nameCamelCase),
}

files, err := filesToGenerate(&params)
if err != nil {
panic(err)
}
for _, file := range files {
if err := generateFileFromTemplate(file, &params); err != nil {
panic(err)
}
}
}

func filesToGenerate(params *ScaffoldParams) ([]FileGeneration, error) {
folderPath := fmt.Sprintf("internal/service/%s", params.NameLowerNoSpaces)

switch params.GenerationType {
case ResourceCmd:
return []FileGeneration{
{
TemplatePath: "tools/scaffold/template/resource.tmpl",
OutputPath: fmt.Sprintf("%s/resource_%s.go", folderPath, params.NameSnakeCase),
},
{
TemplatePath: "tools/scaffold/template/acc_test.tmpl",
OutputPath: fmt.Sprintf("%s/resource_%s_test.go", folderPath, params.NameSnakeCase),
},
{
TemplatePath: "tools/scaffold/template/model.tmpl",
OutputPath: fmt.Sprintf("%s/model_%s.go", folderPath, params.NameSnakeCase),
},
{
TemplatePath: "tools/scaffold/template/model_test.tmpl",
OutputPath: fmt.Sprintf("%s/model_%s_test.go", folderPath, params.NameSnakeCase),
},
}, nil
case DataSourceCmd:
return []FileGeneration{
{
TemplatePath: "tools/scaffold/template/datasource.tmpl",
OutputPath: fmt.Sprintf("%s/data_source_%s.go", folderPath, params.NameSnakeCase),
},
{
TemplatePath: "tools/scaffold/template/acc_test.tmpl",
OutputPath: fmt.Sprintf("%s/data_source_%s_test.go", folderPath, params.NameSnakeCase),
},
{
TemplatePath: "tools/scaffold/template/model.tmpl",
OutputPath: fmt.Sprintf("%s/model_%s.go", folderPath, params.NameSnakeCase),
},
{
TemplatePath: "tools/scaffold/template/model_test.tmpl",
OutputPath: fmt.Sprintf("%s/model_%s_test.go", folderPath, params.NameSnakeCase),
},
}, nil
case PluralDataSourceCmd:
return []FileGeneration{
{
TemplatePath: "tools/scaffold/template/pluraldatasource.tmpl",
OutputPath: fmt.Sprintf("%s/data_source_%ss.go", folderPath, params.NameSnakeCase),
},
{
TemplatePath: "tools/scaffold/template/acc_test.tmpl",
OutputPath: fmt.Sprintf("%s/data_source_%ss_test.go", folderPath, params.NameSnakeCase),
},
{
TemplatePath: "tools/scaffold/template/model.tmpl",
OutputPath: fmt.Sprintf("%s/model_%s.go", folderPath, params.NameSnakeCase),
},
{
TemplatePath: "tools/scaffold/template/model_test.tmpl",
OutputPath: fmt.Sprintf("%s/model_%s_test.go", folderPath, params.NameSnakeCase),
},
}, nil
default:
return nil, errors.New("unknown generation type provided")
}
}

func generateFileFromTemplate(generation FileGeneration, params *ScaffoldParams) error {
tmpl, err := template.ParseFiles(generation.TemplatePath)
if err != nil {
return err
}

// ensure content of existing files is not overwritten
if _, err := os.Stat(generation.OutputPath); err == nil {
log.Printf("File already exists: %s", generation.OutputPath)
return nil
}
file := createDirsAndFile(generation.OutputPath)

if err := tmpl.Execute(file, params); err != nil {
return err
}
file.Close()
return nil
}

func createDirsAndFile(path string) *os.File {
dirPath := filepath.Dir(path)
if err := os.MkdirAll(dirPath, os.ModePerm); err != nil {
log.Fatalf("Failed to create directories: %s", err)
}

file, err := os.Create(path)
if err != nil {
log.Fatalf("Failed to create file: %s", err)
}
return file
}
23 changes: 23 additions & 0 deletions tools/scaffold/string_cases.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package main

import (
"strings"
"unicode"
)

// toPascalCase converts camel case to pascal case.
func ToPascalCase(input string) string {
return strings.ToUpper(input[:1]) + input[1:]
}

// toSnakeCase converts camel case to snake case.
func ToSnakeCase(input string) string {
var result []rune
for i, r := range input {
if unicode.IsUpper(r) && i > 0 {
result = append(result, '_')
}
result = append(result, unicode.ToLower(r))
}
return string(result)
}
36 changes: 36 additions & 0 deletions tools/scaffold/template/acc_test.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package {{.NameLowerNoSpaces}}_test

import (
"testing"

"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"github.com/mongodb/terraform-provider-mongodbatlas/internal/testutil/acc"
)

// TODO: if acceptance test will be run in an existing CI group of resources, the name should include the group in the prefix followed by the name of the resource e.i. TestAccStreamRSStreamInstance_basic
// In addition, if acceptance test contains testing of both resource and data sources, the RS/DS can be omitted.
func TestAcc{{.NamePascalCase}}{{if eq .GenerationType "resource"}}RS{{else}}DS{{end}}_basic(t *testing.T) {

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { acc.PreCheckBasic(t) },
ProtoV6ProviderFactories: acc.TestAccProviderV6Factories,
// CheckDestroy: checkDestroy{{.NamePascalCase}},
Steps: []resource.TestStep{ // TODO: verify updates and import in case of resources
// {
// Config: {{.NameCamelCase}}Config(),
// Check: {{.NameCamelCase}}AttributeChecks(),
// },
// {
// Config: {{.NameCamelCase}}Config(),
// Check: {{.NameCamelCase}}AttributeChecks(),
// },
// {
// Config: {{.NameCamelCase}}Config(),
// ResourceName: resourceName,
// ImportStateIdFunc: check{{.NamePascalCase}}ImportStateIDFunc,
// ImportState: true,
// ImportStateVerify: true,
},
},
)
}
68 changes: 68 additions & 0 deletions tools/scaffold/template/datasource.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package {{.NameLowerNoSpaces}}

import (
"context"

"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/mongodb/terraform-provider-mongodbatlas/internal/config"
)

const {{.NameCamelCase}}Name = "{{.NameSnakeCase}}" // TODO: if resource exists this can be deleted

var _ datasource.DataSource = &{{.NameCamelCase}}DS{}
var _ datasource.DataSourceWithConfigure = &{{.NameCamelCase}}DS{}

func DataSource() datasource.DataSource {
return &{{.NameCamelCase}}DS{
DSCommon: config.DSCommon{
DataSourceName: {{.NameCamelCase}}Name,
},
}
}

type {{.NameCamelCase}}DS struct {
config.DSCommon
}

// TODO: if resource exists and TF{{.NamePascalCase}}Model is identical to data source attributes the existing model should be reutilized
// type TF{{.NamePascalCase}}DSModel struct {
// ID types.String `tfsdk:"id"`
// TODO: add attribute definitions
//}

func (d *{{.NameCamelCase}}DS) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Computed: true,
},
// TODO: add attribute definitions
},
}
}

func (d *{{.NameCamelCase}}DS) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var {{.NameCamelCase}}Config TF{{.NamePascalCase}}Model
resp.Diagnostics.Append(req.Config.Get(ctx, &{{.NameCamelCase}}Config)...)
if resp.Diagnostics.HasError() {
return
}

// TODO: make get request to resource

// connV2 := r.Client.AtlasV2
//if err != nil {
// resp.Diagnostics.AddError("error fetching resource", err.Error())
// return
//}

// TODO: process response into new terraform state
new{{.NamePascalCase}}Model, diags := NewTF{{.NamePascalCase}}(ctx, apiResp)
if diags.HasError() {
resp.Diagnostics.Append(diags...)
return
}
resp.Diagnostics.Append(resp.State.Set(ctx, new{{.NamePascalCase}}Model)...)
}
Loading

0 comments on commit b9e795b

Please sign in to comment.