Skip to content

Commit

Permalink
support for mocking client (UI test)
Browse files Browse the repository at this point in the history
  • Loading branch information
magodo committed Sep 20, 2021
1 parent 765e979 commit 58bc613
Show file tree
Hide file tree
Showing 8 changed files with 428 additions and 338 deletions.
1 change: 1 addition & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ type Config struct {
ResourceGroupName string // specified via CLI
Logfile string `env:"AZTFY_LOGFILE" default:""`
Debug bool `env:"AZTFY_DEBUG" default:"false"`
MockClient bool `env:"AZTFY_MOCK_CLIENT" default:"false"`
}

func NewConfig(rg string) (*Config, error) {
Expand Down
326 changes: 13 additions & 313 deletions internal/meta/meta.go
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)
}
Loading

0 comments on commit 58bc613

Please sign in to comment.