-
Notifications
You must be signed in to change notification settings - Fork 192
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
support for mocking client (UI test)
- Loading branch information
Showing
8 changed files
with
428 additions
and
338 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,320 +1,20 @@ | ||
package meta | ||
|
||
import ( | ||
"bytes" | ||
"context" | ||
"encoding/json" | ||
"fmt" | ||
"os" | ||
"path" | ||
"path/filepath" | ||
"strings" | ||
import "github.com/magodo/aztfy/internal/config" | ||
|
||
"github.com/magodo/aztfy/internal/armtemplate" | ||
"github.com/magodo/aztfy/schema" | ||
|
||
"github.com/Azure/azure-sdk-for-go/services/resources/mgmt/2020-06-01/resources" | ||
"github.com/hashicorp/go-version" | ||
"github.com/hashicorp/hcl/v2" | ||
"github.com/hashicorp/hcl/v2/hclwrite" | ||
"github.com/hashicorp/terraform-exec/tfexec" | ||
) | ||
|
||
// The minimun required terraform version that has the `terraform add` command. | ||
var minRequiredTFVersion = version.Must(version.NewSemver("1.1.0-alpha20210811")) | ||
|
||
type Meta struct { | ||
subscriptionId string | ||
resourceGroup string | ||
workspace string | ||
tf *tfexec.Terraform | ||
auth *Authorizer | ||
armTemplate armtemplate.Template | ||
} | ||
|
||
func NewMeta(ctx context.Context, rg string) (*Meta, error) { | ||
// Initialize the workspace | ||
cachedir, err := os.UserCacheDir() | ||
if err != nil { | ||
return nil, fmt.Errorf("error finding the user cache directory: %w", err) | ||
} | ||
|
||
// Initialize the workspace | ||
rootDir := filepath.Join(cachedir, "aztfy") | ||
if err := os.MkdirAll(rootDir, 0755); err != nil { | ||
return nil, fmt.Errorf("creating workspace root %q: %w", rootDir, err) | ||
} | ||
|
||
tfDir := filepath.Join(rootDir, "terraform") | ||
if err := os.MkdirAll(tfDir, 0755); err != nil { | ||
return nil, fmt.Errorf("creating terraform cache dir %q: %w", tfDir, err) | ||
} | ||
|
||
wsp := filepath.Join(rootDir, rg) | ||
if err := os.RemoveAll(wsp); err != nil { | ||
return nil, fmt.Errorf("removing existing workspace %q: %w", wsp, err) | ||
} | ||
if err := os.MkdirAll(wsp, 0755); err != nil { | ||
return nil, fmt.Errorf("creating workspace %q: %w", wsp, err) | ||
} | ||
|
||
// Authentication | ||
auth, err := NewAuthorizer() | ||
if err != nil { | ||
return nil, fmt.Errorf("building authorizer: %w", err) | ||
} | ||
|
||
// Initialize the Terraform | ||
execPath, err := FindTerraform(ctx, tfDir, minRequiredTFVersion) | ||
if err != nil { | ||
return nil, fmt.Errorf("error finding a terraform exectuable: %w", err) | ||
} | ||
|
||
tf, err := tfexec.NewTerraform(wsp, execPath) | ||
if err != nil { | ||
return nil, fmt.Errorf("error running NewTerraform: %w", err) | ||
} | ||
|
||
return &Meta{ | ||
subscriptionId: auth.Config.SubscriptionID, | ||
resourceGroup: rg, | ||
workspace: wsp, | ||
tf: tf, | ||
auth: auth, | ||
}, nil | ||
} | ||
|
||
func providerConfig() string { | ||
return fmt.Sprintf(`terraform { | ||
required_providers { | ||
azurerm = { | ||
source = "hashicorp/azurerm" | ||
version = "%s" | ||
} | ||
} | ||
} | ||
provider "azurerm" { | ||
features {} | ||
} | ||
`, schema.ProviderVersion) | ||
} | ||
|
||
func (meta Meta) ResourceGroupName() string { | ||
return meta.resourceGroup | ||
} | ||
|
||
func (meta Meta) Workspace() string { | ||
return meta.workspace | ||
type Meta interface { | ||
Init() error | ||
ResourceGroupName() string | ||
Workspace() string | ||
ListResource() ImportList | ||
CleanTFState() | ||
Import(item ImportItem) error | ||
GenerateCfg(l ImportList) error | ||
} | ||
|
||
func (meta *Meta) Init(ctx context.Context) error { | ||
if err := meta.initProvider(ctx); err != nil { | ||
return err | ||
} | ||
if err := meta.exportArmTemplate(ctx); err != nil { | ||
return err | ||
} | ||
return nil | ||
} | ||
|
||
func (meta Meta) ListResource() ImportList { | ||
var ids []string | ||
for _, res := range meta.armTemplate.Resources { | ||
ids = append(ids, res.ID(meta.subscriptionId, meta.resourceGroup)) | ||
} | ||
ids = append(ids, armtemplate.ResourceGroupId.ID(meta.subscriptionId, meta.resourceGroup)) | ||
|
||
l := make(ImportList, 0, len(ids)) | ||
for _, id := range ids { | ||
l = append(l, ImportItem{ | ||
ResourceID: id, | ||
}) | ||
} | ||
return l | ||
} | ||
|
||
func (meta *Meta) CleanTFState() { | ||
os.Remove(path.Join(meta.Workspace(), "terraform.tfstate")) | ||
} | ||
|
||
func (meta *Meta) Import(ctx context.Context, item ImportItem) error { | ||
// Generate a temp Terraform config to include the empty template for each resource. | ||
// This is required for the following importing. | ||
cfgFile := filepath.Join(meta.workspace, "main.tf") | ||
tpl, err := meta.tf.Add(ctx, item.TFAddr()) | ||
if err != nil { | ||
return fmt.Errorf("generating resource template for %s: %w", item.TFAddr(), err) | ||
} | ||
if err := os.WriteFile(cfgFile, []byte(tpl), 0644); err != nil { | ||
return fmt.Errorf("generating resource template file: %w", err) | ||
} | ||
defer os.Remove(cfgFile) | ||
|
||
// Import resources | ||
return meta.tf.Import(ctx, item.TFAddr(), item.ResourceID) | ||
} | ||
|
||
func (meta Meta) GenerateCfg(ctx context.Context, l ImportList) error { | ||
cfginfos, err := meta.stateToConfig(ctx, l) | ||
if err != nil { | ||
return fmt.Errorf("converting from state to configurations: %w", err) | ||
} | ||
cfginfos, err = meta.resolveDependency(ctx, cfginfos) | ||
if err != nil { | ||
return fmt.Errorf("resolving cross resource dependencies: %w", err) | ||
func NewMeta(cfg config.Config) (Meta, error) { | ||
if cfg.MockClient { | ||
return newMetaDummy(cfg.ResourceGroupName) | ||
} | ||
return meta.generateConfig(cfginfos) | ||
} | ||
|
||
func (meta *Meta) initProvider(ctx context.Context) error { | ||
cfgFile := filepath.Join(meta.workspace, "provider.tf") | ||
|
||
// Always use the latest provider version here, as this is a one shot tool, which should guarantees to work with the latest version. | ||
if err := os.WriteFile(cfgFile, []byte(providerConfig()), 0644); err != nil { | ||
return fmt.Errorf("error creating provider config: %w", err) | ||
} | ||
|
||
if err := meta.tf.Init(ctx); err != nil { | ||
return fmt.Errorf("error running terraform init: %s", err) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func (meta *Meta) exportArmTemplate(ctx context.Context) error { | ||
client := meta.auth.NewResourceGroupClient() | ||
|
||
exportOpt := "SkipAllParameterization" | ||
future, err := client.ExportTemplate(ctx, meta.resourceGroup, resources.ExportTemplateRequest{ | ||
ResourcesProperty: &[]string{"*"}, | ||
Options: &exportOpt, | ||
}) | ||
if err != nil { | ||
return fmt.Errorf("exporting arm template of resource group %s: %w", meta.resourceGroup, err) | ||
} | ||
|
||
if err := future.WaitForCompletionRef(ctx, client.Client); err != nil { | ||
return fmt.Errorf("waiting for exporting arm template of resource group %s: %w", meta.resourceGroup, err) | ||
} | ||
|
||
result, err := future.Result(client) | ||
if err != nil { | ||
return fmt.Errorf("getting the arm template of resource group %s: %w", meta.resourceGroup, err) | ||
} | ||
|
||
// The response has been read into the ".Template" field as an interface, and the reader has been drained. | ||
// As we have defined some (useful) types for the arm template, so we will do a json marshal then unmarshal here | ||
// to convert the ".Template" (interface{}) into that artificial type. | ||
raw, err := json.Marshal(result.Template) | ||
if err != nil { | ||
return fmt.Errorf("marshalling the template: %w", err) | ||
} | ||
if err := json.Unmarshal(raw, &meta.armTemplate); err != nil { | ||
return fmt.Errorf("unmarshalling the template: %w", err) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func (meta Meta) stateToConfig(ctx context.Context, list ImportList) (ConfigInfos, error) { | ||
out := ConfigInfos{} | ||
|
||
for _, item := range list.Imported() { | ||
tpl, err := meta.tf.Add(ctx, item.TFAddr(), tfexec.FromState(true)) | ||
if err != nil { | ||
return nil, fmt.Errorf("converting terraform state to config for resource %s: %w", item.TFAddr(), err) | ||
} | ||
f, diag := hclwrite.ParseConfig([]byte(tpl), "", hcl.InitialPos) | ||
if diag.HasErrors() { | ||
return nil, fmt.Errorf("parsing the HCL generated by \"terraform add\" of %s: %s", item.TFAddr(), diag.Error()) | ||
} | ||
|
||
rb := f.Body().Blocks()[0].Body() | ||
sch := schema.ProviderSchemaInfo.ResourceSchemas[item.TFResourceType] | ||
if err := tuneHCLSchemaForResource(rb, sch); err != nil { | ||
return nil, err | ||
} | ||
|
||
out = append(out, ConfigInfo{ | ||
ImportItem: item, | ||
hcl: f, | ||
}) | ||
} | ||
|
||
return out, nil | ||
} | ||
|
||
func (meta Meta) resolveDependency(ctx context.Context, configs ConfigInfos) (ConfigInfos, error) { | ||
depInfo := meta.armTemplate.DependencyInfo() | ||
|
||
configSet := map[armtemplate.ResourceId]ConfigInfo{} | ||
for _, cfg := range configs { | ||
armId, err := armtemplate.NewResourceId(cfg.ResourceID) | ||
if err != nil { | ||
return nil, fmt.Errorf("new arm tempalte resource id from azure resource id: %w", err) | ||
} | ||
configSet[*armId] = cfg | ||
} | ||
|
||
// Iterate each config to add dependency by querying the dependency info from arm template. | ||
var out ConfigInfos | ||
for armId, cfg := range configSet { | ||
if armId == armtemplate.ResourceGroupId { | ||
out = append(out, cfg) | ||
continue | ||
} | ||
// This should never happen as we always ensure there is at least one implicit dependency on the resource group for each resource. | ||
if _, ok := depInfo[armId]; !ok { | ||
return nil, fmt.Errorf("can't find resource %q in the arm template", armId.ID(meta.subscriptionId, meta.resourceGroup)) | ||
} | ||
|
||
if err := meta.hclBlockAppendDependency(cfg.hcl.Body().Blocks()[0].Body(), depInfo[armId], configSet); err != nil { | ||
return nil, err | ||
} | ||
out = append(out, cfg) | ||
} | ||
|
||
return out, nil | ||
} | ||
|
||
func (meta Meta) hclBlockAppendDependency(body *hclwrite.Body, armIds []armtemplate.ResourceId, cfgset map[armtemplate.ResourceId]ConfigInfo) error { | ||
dependencies := []string{} | ||
for _, armid := range armIds { | ||
cfg, ok := cfgset[armid] | ||
if !ok { | ||
dependencies = append(dependencies, fmt.Sprintf("# Depending on %q, which is not imported by Terraform.", armid.ID(meta.subscriptionId, meta.resourceGroup))) | ||
continue | ||
} | ||
dependencies = append(dependencies, cfg.TFAddr()+",") | ||
} | ||
if len(dependencies) > 0 { | ||
src := []byte("depends_on = [\n" + strings.Join(dependencies, "\n") + "\n]") | ||
expr, diags := hclwrite.ParseConfig(src, "generate_depends_on", hcl.InitialPos) | ||
if diags.HasErrors() { | ||
return fmt.Errorf(`building "depends_on" attribute: %s`, diags.Error()) | ||
} | ||
|
||
body.SetAttributeRaw("depends_on", expr.Body().GetAttribute("depends_on").Expr().BuildTokens(nil)) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func (meta Meta) generateConfig(cfgs ConfigInfos) error { | ||
cfgFile := filepath.Join(meta.workspace, "main.tf") | ||
buf := bytes.NewBuffer([]byte{}) | ||
for i, cfg := range cfgs { | ||
if _, err := cfg.DumpHCL(buf); err != nil { | ||
return err | ||
} | ||
if i != len(cfgs)-1 { | ||
buf.Write([]byte("\n")) | ||
} | ||
} | ||
if err := os.WriteFile(cfgFile, buf.Bytes(), 0644); err != nil { | ||
return fmt.Errorf("generating main configuration file: %w", err) | ||
} | ||
|
||
return nil | ||
return newMetaImpl(cfg.ResourceGroupName) | ||
} |
Oops, something went wrong.