-
Notifications
You must be signed in to change notification settings - Fork 1
chore: Convert free clusters with count #13
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
Changes from all commits
6667df0
ea632ba
2e9de57
7345e36
ad4b065
9657eb2
bb76598
9a9c906
4c3fc75
8e129d8
5c0c264
203e5f2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,22 +2,38 @@ package hcl | |
|
||
import ( | ||
"fmt" | ||
"strconv" | ||
|
||
"github.com/hashicorp/hcl/v2" | ||
"github.com/hashicorp/hcl/v2/hclsyntax" | ||
"github.com/hashicorp/hcl/v2/hclwrite" | ||
"github.com/zclconf/go-cty/cty" | ||
) | ||
|
||
const ( | ||
resourceType = "resource" | ||
cluster = "mongodbatlas_cluster" | ||
advCluster = "mongodbatlas_advanced_cluster" | ||
resourceType = "resource" | ||
cluster = "mongodbatlas_cluster" | ||
advCluster = "mongodbatlas_advanced_cluster" | ||
nameReplicationSpecs = "replication_specs" | ||
nameRegionConfigs = "region_configs" | ||
nameElectableSpecs = "electable_specs" | ||
nameProviderRegionName = "provider_region_name" | ||
nameRegionName = "region_name" | ||
nameProviderName = "provider_name" | ||
nameBackingProviderName = "backing_provider_name" | ||
nameProviderInstanceSizeName = "provider_instance_size_name" | ||
nameInstanceSize = "instance_size" | ||
nameClusterType = "cluster_type" | ||
namePriority = "priority" | ||
|
||
errFreeCluster = "free cluster (because no " + nameReplicationSpecs + ")" | ||
) | ||
|
||
// ClusterToAdvancedCluster transforms all mongodbatlas_cluster definitions in a | ||
// Terraform configuration file into mongodbatlas_advanced_cluster schema v2 definitions. | ||
// All other resources and data sources are left untouched. | ||
// TODO: at the moment it just changes the resource type. | ||
// Note: hclwrite.Tokens are used instead of cty.Value so expressions like var.region can be preserved. | ||
// cty.Value only supports resolved values. | ||
func ClusterToAdvancedCluster(config []byte) ([]byte, error) { | ||
parser, err := getParser(config) | ||
if err != nil { | ||
|
@@ -30,31 +46,105 @@ func ClusterToAdvancedCluster(config []byte) ([]byte, error) { | |
continue | ||
} | ||
resourceBody := resource.Body() | ||
|
||
// TODO: Do the full transformation | ||
labels[0] = advCluster | ||
resource.SetLabels(labels) | ||
|
||
if isFreeTier(resourceBody) { | ||
if err := fillFreeTier(resourceBody); err != nil { | ||
return nil, err | ||
} | ||
} | ||
|
||
resourceBody.AppendNewline() | ||
appendComment(resourceBody, "Generated by atlas-cli-plugin-terraform.") | ||
EspenAlbert marked this conversation as resolved.
Show resolved
Hide resolved
|
||
appendComment(resourceBody, "Please confirm that all references to this resource are updated.") | ||
} | ||
return parser.Bytes(), nil | ||
} | ||
|
||
func getParser(config []byte) (*hclwrite.File, error) { | ||
parser, diags := hclwrite.ParseConfig(config, "", hcl.Pos{Line: 1, Column: 1}) | ||
if diags.HasErrors() { | ||
return nil, fmt.Errorf("failed to parse Terraform config file: %s", diags.Error()) | ||
func isFreeTier(body *hclwrite.Body) bool { | ||
return body.FirstMatchingBlock(nameReplicationSpecs, nil) == nil | ||
} | ||
|
||
func fillFreeTier(body *hclwrite.Body) error { | ||
const ( | ||
valClusterType = "REPLICASET" | ||
valPriority = 7 | ||
) | ||
body.SetAttributeValue(nameClusterType, cty.StringVal(valClusterType)) | ||
regionConfig := hclwrite.NewEmptyFile() | ||
regionConfigBody := regionConfig.Body() | ||
setAttrInt(regionConfigBody, "priority", valPriority) | ||
if err := moveAttribute(nameProviderRegionName, nameRegionName, body, regionConfigBody, errFreeCluster); err != nil { | ||
return err | ||
} | ||
return parser, nil | ||
if err := moveAttribute(nameProviderName, nameProviderName, body, regionConfigBody, errFreeCluster); err != nil { | ||
return err | ||
} | ||
if err := moveAttribute(nameBackingProviderName, nameBackingProviderName, body, regionConfigBody, errFreeCluster); err != nil { | ||
return err | ||
} | ||
electableSpec := hclwrite.NewEmptyFile() | ||
if err := moveAttribute(nameProviderInstanceSizeName, nameInstanceSize, body, electableSpec.Body(), errFreeCluster); err != nil { | ||
return err | ||
} | ||
regionConfigBody.SetAttributeRaw(nameElectableSpecs, tokensObject(electableSpec)) | ||
|
||
replicationSpec := hclwrite.NewEmptyFile() | ||
replicationSpec.Body().SetAttributeRaw(nameRegionConfigs, tokensArrayObject(regionConfig)) | ||
body.SetAttributeRaw(nameReplicationSpecs, tokensArrayObject(replicationSpec)) | ||
return nil | ||
} | ||
|
||
func moveAttribute(fromAttrName, toAttrName string, fromBody, toBody *hclwrite.Body, errPrefix string) error { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I like the abstraction signature and you don't need to worry about the type of the attribute! |
||
attr := fromBody.GetAttribute(fromAttrName) | ||
if attr == nil { | ||
return fmt.Errorf("%s: attribute %s not found", errPrefix, fromAttrName) | ||
} | ||
fromBody.RemoveAttribute(fromAttrName) | ||
toBody.SetAttributeRaw(toAttrName, attr.Expr().BuildTokens(nil)) | ||
return nil | ||
} | ||
|
||
func setAttrInt(body *hclwrite.Body, attrName string, number int) { | ||
tokens := hclwrite.Tokens{ | ||
{Type: hclsyntax.TokenNumberLit, Bytes: []byte(strconv.Itoa(number))}, | ||
} | ||
body.SetAttributeRaw(attrName, tokens) | ||
} | ||
|
||
func tokensArrayObject(file *hclwrite.File) hclwrite.Tokens { | ||
ret := hclwrite.Tokens{ | ||
{Type: hclsyntax.TokenOBrack, Bytes: []byte("[")}, | ||
} | ||
ret = append(ret, tokensObject(file)...) | ||
ret = append(ret, | ||
&hclwrite.Token{Type: hclsyntax.TokenCBrack, Bytes: []byte("]")}) | ||
return ret | ||
} | ||
|
||
func tokensObject(file *hclwrite.File) hclwrite.Tokens { | ||
ret := hclwrite.Tokens{ | ||
{Type: hclsyntax.TokenOBrack, Bytes: []byte("{")}, | ||
{Type: hclsyntax.TokenNewline, Bytes: []byte("\n")}, | ||
} | ||
ret = append(ret, file.BuildTokens(nil)...) | ||
ret = append(ret, | ||
&hclwrite.Token{Type: hclsyntax.TokenCBrack, Bytes: []byte("}")}) | ||
return ret | ||
} | ||
|
||
func appendComment(body *hclwrite.Body, comment string) { | ||
tokens := hclwrite.Tokens{ | ||
&hclwrite.Token{ | ||
Type: hclsyntax.TokenComment, | ||
Bytes: []byte("# " + comment + "\n"), | ||
}, | ||
&hclwrite.Token{Type: hclsyntax.TokenComment, Bytes: []byte("# " + comment + "\n")}, | ||
} | ||
body.AppendUnstructuredTokens(tokens) | ||
} | ||
|
||
func getParser(config []byte) (*hclwrite.File, error) { | ||
parser, diags := hclwrite.ParseConfig(config, "", hcl.Pos{Line: 1, Column: 1}) | ||
if diags.HasErrors() { | ||
return nil, fmt.Errorf("failed to parse Terraform config file: %s", diags.Error()) | ||
} | ||
return parser, nil | ||
} |
This file was deleted.
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
resource this is an invalid HCL configuration file | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
{ | ||
"configuration_file_error": "failed to parse Terraform config file", | ||
"free_cluster_missing_attribute": "free cluster (because no replication_specs): attribute backing_provider_name not found" | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. errors file instead of having individual out files with just the error message, so it's easier to have multiple input files with errors and don't have many files, and have a place to see all error messages. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
resource "resource1" "res1" { | ||
name = "name1" | ||
} | ||
|
||
resource "mongodbatlas_cluster" "free_cluster" { # comment in the resource | ||
# comment in own line in the beginning | ||
count = local.use_free_cluster ? 1 : 0 | ||
project_id = var.project_id # inline comment kept | ||
name = var.cluster_name | ||
# comment in own line in the middle is deleted | ||
provider_name = "TENANT" # inline comment for attribute moved is not kept | ||
provider_region_name = var.region | ||
provider_instance_size_name = "M0" | ||
# comment in own line at the end happens before replication_specs | ||
} | ||
|
||
data "mongodbatlas_cluster" "cluster2" { | ||
name = "name4" | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
resource "resource1" "res1" { | ||
name = "name1" | ||
} | ||
|
||
resource "mongodbatlas_cluster" "free_cluster" { # comment in the resource | ||
# comment in own line in the beginning | ||
count = local.use_free_cluster ? 1 : 0 | ||
project_id = var.project_id # inline comment kept | ||
name = var.cluster_name | ||
# comment in own line in the middle is deleted | ||
provider_name = "TENANT" # inline comment for attribute moved is not kept | ||
backing_provider_name = "AWS" | ||
provider_region_name = var.region | ||
provider_instance_size_name = "M0" | ||
# comment in own line at the end happens before replication_specs | ||
} | ||
|
||
data "mongodbatlas_cluster" "cluster2" { | ||
name = "name4" | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
resource "resource1" "res1" { | ||
name = "name1" | ||
} | ||
|
||
resource "mongodbatlas_advanced_cluster" "free_cluster" { # comment in the resource | ||
# comment in own line in the beginning | ||
count = local.use_free_cluster ? 1 : 0 | ||
project_id = var.project_id # inline comment kept | ||
name = var.cluster_name | ||
# comment in own line at the end happens before replication_specs | ||
cluster_type = "REPLICASET" | ||
replication_specs = [{ | ||
region_configs = [{ | ||
priority = 7 | ||
region_name = var.region | ||
provider_name = "TENANT" | ||
backing_provider_name = "AWS" | ||
electable_specs = { | ||
instance_size = "M0" | ||
} | ||
}] | ||
}] | ||
|
||
# Generated by atlas-cli-plugin-terraform. | ||
# Please confirm that all references to this resource are updated. | ||
} | ||
|
||
data "mongodbatlas_cluster" "cluster2" { | ||
name = "name4" | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cool approach of starting with one specific case.
I guess some operations might be more generally applicable, like moving instance_size, but we'll find out once we add more cases!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, I'll go now with one with num_shards