Skip to content

Commit

Permalink
feature: add support for exporting to azapi provider (#462)
Browse files Browse the repository at this point in the history
* add azapi support

* remove azurerm config when exporting to azapi

* code format and remove GenTFResources

* set only corresponding provider in provider config file

* format
  • Loading branch information
wuxu92 authored Oct 20, 2023
1 parent eb789a4 commit a24308b
Show file tree
Hide file tree
Showing 10 changed files with 312 additions and 306 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
.vscode
dist/
.idea
aztfexport
189 changes: 189 additions & 0 deletions flag.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,21 @@ package main

import (
"fmt"
"os"
"os/exec"
"strings"

"github.com/Azure/aztfexport/internal/cfgfile"
"github.com/Azure/aztfexport/pkg/config"
"github.com/Azure/aztfexport/pkg/telemetry"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/arm"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
"github.com/gofrs/uuid"
"github.com/hashicorp/go-hclog"
"github.com/magodo/terraform-client-go/tfclient"
"github.com/urfave/cli/v2"
)

Expand All @@ -18,6 +31,7 @@ type FlagSet struct {
flagAppend bool
flagDevProvider bool
flagProviderVersion string
flagProviderName string
flagBackendType string
flagBackendConfig cli.StringSlice
flagFullConfig bool
Expand Down Expand Up @@ -172,3 +186,178 @@ func (flag FlagSet) DescribeCLI(mode string) string {
}
return "aztfexport " + strings.Join(args, " ")
}

func initTelemetryClient(subscriptionId string) telemetry.Client {
cfg, err := cfgfile.GetConfig()
if err != nil {
return telemetry.NewNullClient()
}
enabled, installId := cfg.TelemetryEnabled, cfg.InstallationId
if !enabled {
return telemetry.NewNullClient()
}
if installId == "" {
uuid, err := uuid.NewV4()
if err == nil {
installId = uuid.String()
} else {
installId = "undefined"
}
}

sessionId := "undefined"
if uuid, err := uuid.NewV4(); err == nil {
sessionId = uuid.String()
}
return telemetry.NewAppInsight(subscriptionId, installId, sessionId)
}

// buildAzureSDKCredAndClientOpt builds the Azure SDK credential and client option from multiple sources (i.e. environment variables, MSI, Azure CLI).
func buildAzureSDKCredAndClientOpt(fset FlagSet) (azcore.TokenCredential, *arm.ClientOptions, error) {
var cloudCfg cloud.Configuration
switch env := fset.flagEnv; strings.ToLower(env) {
case "public":
cloudCfg = cloud.AzurePublic
case "usgovernment":
cloudCfg = cloud.AzureGovernment
case "china":
cloudCfg = cloud.AzureChina
default:
return nil, nil, fmt.Errorf("unknown environment specified: %q", env)
}

// Maps the auth related environment variables used in the provider to what azidentity honors
if v, ok := os.LookupEnv("ARM_TENANT_ID"); ok {
// #nosec G104
os.Setenv("AZURE_TENANT_ID", v)
}
if v, ok := os.LookupEnv("ARM_CLIENT_ID"); ok {
// #nosec G104
os.Setenv("AZURE_CLIENT_ID", v)
}
if v, ok := os.LookupEnv("ARM_CLIENT_SECRET"); ok {
// #nosec G104
os.Setenv("AZURE_CLIENT_SECRET", v)
}
if v, ok := os.LookupEnv("ARM_CLIENT_CERTIFICATE_PATH"); ok {
// #nosec G104
os.Setenv("AZURE_CLIENT_CERTIFICATE_PATH", v)
}

clientOpt := &arm.ClientOptions{
ClientOptions: policy.ClientOptions{
Cloud: cloudCfg,
Telemetry: policy.TelemetryOptions{
ApplicationID: "aztfexport",
Disabled: false,
},
Logging: policy.LogOptions{
IncludeBody: true,
},
},
}

tenantId := os.Getenv("ARM_TENANT_ID")
var (
cred azcore.TokenCredential
err error
)
switch {
case fset.flagUseEnvironmentCred:
cred, err = azidentity.NewEnvironmentCredential(&azidentity.EnvironmentCredentialOptions{
ClientOptions: clientOpt.ClientOptions,
})
if err != nil {
return nil, nil, fmt.Errorf("failed to new Environment credential: %v", err)
}
return cred, clientOpt, nil
case fset.flagUseManagedIdentityCred:
cred, err = azidentity.NewManagedIdentityCredential(&azidentity.ManagedIdentityCredentialOptions{
ClientOptions: clientOpt.ClientOptions,
})
if err != nil {
return nil, nil, fmt.Errorf("failed to new Managed Identity credential: %v", err)
}
return cred, clientOpt, nil
case fset.flagUseAzureCLICred:
cred, err = azidentity.NewAzureCLICredential(&azidentity.AzureCLICredentialOptions{
TenantID: tenantId,
})
if err != nil {
return nil, nil, fmt.Errorf("failed to new Azure CLI credential: %v", err)
}
return cred, clientOpt, nil
case fset.flagUseOIDCCred:
cred, err = NewOidcCredential(&OidcCredentialOptions{
ClientOptions: clientOpt.ClientOptions,
TenantID: tenantId,
ClientID: os.Getenv("ARM_CLIENT_ID"),
RequestToken: fset.flagOIDCRequestToken,
RequestUrl: fset.flagOIDCRequestURL,
Token: fset.flagOIDCToken,
TokenFilePath: fset.flagOIDCTokenFilePath,
})
if err != nil {
return nil, nil, fmt.Errorf("failed to new OIDC credential: %v", err)
}
return cred, clientOpt, nil
default:
opt := &azidentity.DefaultAzureCredentialOptions{
ClientOptions: clientOpt.ClientOptions,
TenantID: tenantId,
}
cred, err := azidentity.NewDefaultAzureCredential(opt)
if err != nil {
return nil, nil, fmt.Errorf("failed to new Default credential: %v", err)
}
return cred, clientOpt, nil
}
}

func (f FlagSet) BuildCommonConfig() (config.CommonConfig, error) {
cred, clientOpt, err := buildAzureSDKCredAndClientOpt(f)
if err != nil {
return config.CommonConfig{}, err
}

cfg := config.CommonConfig{
SubscriptionId: f.flagSubscriptionId,
AzureSDKCredential: cred,
AzureSDKClientOption: *clientOpt,
OutputDir: f.flagOutputDir,
ProviderVersion: f.flagProviderVersion,
ProviderName: f.flagProviderName,
DevProvider: f.flagDevProvider,
ContinueOnError: f.flagContinue,
BackendType: f.flagBackendType,
BackendConfig: f.flagBackendConfig.Value(),
FullConfig: f.flagFullConfig,
Parallelism: f.flagParallelism,
HCLOnly: f.flagHCLOnly,
ModulePath: f.flagModulePath,
TelemetryClient: initTelemetryClient(f.flagSubscriptionId),
}

if f.flagAppend {
cfg.OutputFileNames = config.OutputFileNames{
TerraformFileName: "terraform.aztfexport.tf",
ProviderFileName: "provider.aztfexport.tf",
MainFileName: "main.aztfexport.tf",
ImportBlockFileName: "import.aztfexport.tf",
}
}

if f.hflagTFClientPluginPath != "" {
// #nosec G204
tfc, err := tfclient.New(tfclient.Option{
Cmd: exec.Command(flagset.hflagTFClientPluginPath),
Logger: hclog.NewNullLogger(),
})
if err != nil {
return cfg, err
}
cfg.TFClient = tfc
}

return cfg, nil
}
43 changes: 39 additions & 4 deletions internal/meta/base_meta.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ type baseMeta struct {
resourceClient *armresources.Client
providerVersion string
devProvider bool
providerName string
backendType string
backendConfig []string
providerConfig map[string]cty.Value
Expand Down Expand Up @@ -204,6 +205,7 @@ func NewBaseMeta(cfg config.CommonConfig) (*baseMeta, error) {
backendType: cfg.BackendType,
backendConfig: cfg.BackendConfig,
providerConfig: cfg.ProviderConfig,
providerName: cfg.ProviderName,
fullConfig: cfg.FullConfig,
parallelism: cfg.Parallelism,
hclOnly: cfg.HCLOnly,
Expand Down Expand Up @@ -257,6 +259,7 @@ func (meta *baseMeta) CleanTFState(ctx context.Context, addr string) {
func (meta *baseMeta) ParallelImport(ctx context.Context, items []*ImportItem) error {
meta.tc.Trace(telemetry.Info, "ParallelImport Enter")
defer meta.tc.Trace(telemetry.Info, "ParallelImport Leave")

itemsCh := make(chan *ImportItem, len(items))
for _, item := range items {
itemsCh <- item
Expand Down Expand Up @@ -512,11 +515,26 @@ func (meta baseMeta) generateCfg(ctx context.Context, l ImportList, cfgTrans ...
return meta.generateConfig(cfginfos)
}

func (meta *baseMeta) useAzAPI() bool {
return meta.providerName == "azapi"
}

func (meta *baseMeta) buildTerraformConfigForImportDir() string {
if meta.devProvider {
return "terraform {}"
}

if meta.useAzAPI() {
return `terraform {
required_providers {
azapi = {
source = "azure/azapi"
}
}
}
`
}

return fmt.Sprintf(`terraform {
required_providers {
azurerm = {
Expand All @@ -536,6 +554,18 @@ func (meta *baseMeta) buildTerraformConfig(backendType string) string {
`, backendType)
}

if meta.useAzAPI() {
return fmt.Sprintf(`terraform {
backend %q {}
required_providers {
azapi = {
source = "azure/azapi"
}
}
}
`, backendType)
}

return fmt.Sprintf(`terraform {
backend %q {}
required_providers {
Expand All @@ -550,10 +580,15 @@ func (meta *baseMeta) buildTerraformConfig(backendType string) string {

func (meta *baseMeta) buildProviderConfig() string {
f := hclwrite.NewEmptyFile()
body := f.Body().AppendNewBlock("provider", []string{"azurerm"}).Body()
body.AppendNewBlock("features", nil)
for k, v := range meta.providerConfig {
body.SetAttributeValue(k, v)

if meta.useAzAPI() {
f.Body().AppendNewBlock("provider", []string{"azapi"}).Body()
} else {
body := f.Body().AppendNewBlock("provider", []string{"azurerm"}).Body()
body.AppendNewBlock("features", nil)
for k, v := range meta.providerConfig {
body.SetAttributeValue(k, v)
}
}
return string(f.Bytes())
}
Expand Down
13 changes: 9 additions & 4 deletions internal/meta/config_info.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/hashicorp/hcl/v2/hclwrite"
"github.com/magodo/armid"
"github.com/zclconf/go-cty/cty"
)

Expand Down Expand Up @@ -141,9 +142,9 @@ func (cfgs ConfigInfos) addReferenceDependency() error {
// TF resource id to Azure resource ids.
// Typically, one TF resource id maps to one Azure resource id. However, there are cases that one one TF resource id maps to multiple Azure resource ids.
// E.g. A parent and child resources have the same TF id. Or the association resource's TF id is the same as the master resource's.
m := map[string][]string{}
m := map[string][]armid.ResourceId{}
for _, cfg := range cfgs {
m[cfg.TFResourceId] = append(m[cfg.TFResourceId], cfg.AzureResourceID.String())
m[cfg.TFResourceId] = append(m[cfg.TFResourceId], cfg.AzureResourceID)
}

for i, cfg := range cfgs {
Expand All @@ -170,10 +171,14 @@ func (cfgs ConfigInfos) addReferenceDependency() error {

var dependingResourceIdsWithoutSelf []string
for _, id := range dependingResourceIds[:] {
if id == cfg.AzureResourceID.String() {
if id.String() == cfg.AzureResourceID.String() {
continue
}
dependingResourceIdsWithoutSelf = append(dependingResourceIdsWithoutSelf, id)
// if cfg is parent of `id` resource, we should skip, or it will cause circular dependency, so skip parent depends on sub resources
if cfg.AzureResourceID.Equal(id.Parent()) {
continue
}
dependingResourceIdsWithoutSelf = append(dependingResourceIdsWithoutSelf, id.String())
}
if len(dependingResourceIdsWithoutSelf) != 0 {
cfg.DependsOn = append(cfg.DependsOn, Dependency{Candidates: dependingResourceIdsWithoutSelf})
Expand Down
26 changes: 16 additions & 10 deletions internal/meta/meta_query.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,17 +50,23 @@ func (meta *MetaQuery) ListResource(ctx context.Context) (ImportList, error) {
if err != nil {
return nil, err
}
log.Printf("[DEBUG] Populate resource set")
if err := rset.PopulateResource(); err != nil {
return nil, fmt.Errorf("tweaking single resources in the azure resource set: %v", err)
}
log.Printf("[DEBUG] Reduce resource set")
if err := rset.ReduceResource(); err != nil {
return nil, fmt.Errorf("tweaking across resources in the azure resource set: %v", err)
}
var rl []resourceset.TFResource
if meta.useAzAPI() {
log.Printf("[DEBUG] Azure Resource set map to TF resource set")
rl = rset.ToTFAzAPIResources()
} else {
log.Printf("[DEBUG] Populate resource set")
if err := rset.PopulateResource(); err != nil {
return nil, fmt.Errorf("tweaking single resources in the azure resource set: %v", err)
}
log.Printf("[DEBUG] Reduce resource set")
if err := rset.ReduceResource(); err != nil {
return nil, fmt.Errorf("tweaking across resources in the azure resource set: %v", err)
}

log.Printf("[DEBUG] Azure Resource set map to TF resource set")
rl := rset.ToTFResources(meta.parallelism, meta.azureSDKCred, meta.azureSDKClientOpt)
log.Printf("[DEBUG] Azure Resource set map to TF resource set")
rl = rset.ToTFAzureRMResources(meta.parallelism, meta.azureSDKCred, meta.azureSDKClientOpt)
}

var l ImportList
for i, res := range rl {
Expand Down
Loading

0 comments on commit a24308b

Please sign in to comment.