如何让一个 Golang 脚本修改我的 Terraform(HCL 格式)文件中的一个值?

huangapple go评论65阅读模式
英文:

How can I have a Golang script modify a value in my Terraform (HCL format) file?

问题

我正在尝试对一个 Terraform 文件进行一小部分自动化处理,该文件定义了一个 Azure 网络安全组。基本上,我有一个网站和 SSH 访问,我只想允许它们访问我的公共 IP 地址,我可以从 icanhazip.com 获取。我希望使用一个 Golang 脚本将我的 IP 写入 .tf 文件的相关部分(即设置 security_rule.source_address_prefixes 的值)。

我尝试使用 Golang 中的 hclsimple 库,还尝试过 gohclhclwrite 等其他库,但是在将 HCL 文件转换为 Golang 结构时遇到了困难。

我的 Terraform 文件(应该是 HCL 格式)如下所示:

resource "azurerm_network_security_group" "my_nsg" {
  name                = "my_nsg"
  location            = "loc"
  resource_group_name = "rgname"

  security_rule       = [
           {
               access                                     = "Deny"
               description                                = "Desc"
               destination_address_prefix                 = "*"
               destination_address_prefixes               = []
               destination_application_security_group_ids = []
               destination_port_range                     = ""
               destination_port_ranges                    = [
                   "123",
                   "456",
                   "789",
                   "1001",
                ]
               direction                                  = "Inbound"
               name                                       = "AllowInboundThing"
               priority                                   = 100
               protocol                                   = "*"
               source_address_prefix                      = "*"
               source_address_prefixes                    = [
                  # obtain from icanhazip.com
                  "1.2.3.4"
               ]
               source_application_security_group_ids      = []
               source_port_range                          = "*"
               source_port_ranges                         = []
            },
           {
               access                                     = "Allow"
               description                                = "Grant acccess to App"
               destination_address_prefix                 = "*"
               destination_address_prefixes               = []
               destination_application_security_group_ids = []
               destination_port_range                     = ""
               destination_port_ranges                    = [
                   "443",
                   "80",
                ]
               direction                                  = "Inbound"
               name                                       = "AllowIPInBound"
               priority                                   = 200
               protocol                                   = "*"
               source_address_prefix                      = ""
               source_address_prefixes                    = [
                # obtain from icanhazip.com
                   "1.2.3.4"
                ]
               source_application_security_group_ids      = []
               source_port_range                          = "*"
               source_port_ranges                         = []
            }
        ]
}

至此,我已经编写了一个 Golang 脚本,试图将上述数据表示为结构体,然后解码 .tf 文件本身(我从 hclsimple 中本地复制了几个方法,以便它可以解码 .tf 文件,正如在其文档中建议的那样)。

package main

import (
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"os"
	"path/filepath"
	"strings"

	"github.com/hashicorp/hcl/v2"
	"github.com/hashicorp/hcl/v2/gohcl"
	"github.com/hashicorp/hcl/v2/hclsimple"
	"github.com/hashicorp/hcl/v2/hclsyntax"
	"github.com/hashicorp/hcl/v2/json"
)

type Config struct {
	NetworkSecurityGroup []NetworkSecurityGroup `hcl:"resource,block"`
}

type NetworkSecurityGroup struct {
	Type              string         `hcl:"azurerm_network_security_group,label"`
	Name              string         `hcl:"mick-linux3-nsg,label"`
	NameAttr          string         `hcl:"name"`
	Location          string         `hcl:"location"`
	ResourceGroupName string         `hcl:"resource_group_name"`
	SecurityRule      []SecurityRule `hcl:"security_rule,block"`
}

type SecurityRule struct {
	Access                                 string   `hcl:"access"`
	Description                            string   `hcl:"description"`
	DestinationAddressPrefix               string   `hcl:"destination_address_prefix"`
	DestinationAddressPrefixes             []string `hcl:"destination_address_prefixes"`
	DestinationApplicationSecurityGroupIds []string `hcl:"destination_application_security_group_ids"`
	DestinationPortRange                   string   `hcl:"destination_port_range"`
	DestinationPortRanges                  []string `hcl:"destination_port_ranges"`
	Direction                              string   `hcl:"direction"`
	Name                                   string   `hcl:"name"`
	Priority                               int      `hcl:"priority"`
	Protocol                               string   `hcl:"protocol"`
	SourceAddressPrefix                    string   `hcl:"source_address_prefix"`
	SourceAddressPrefixes                  []string `hcl:"source_address_prefixes"`
	SourceApplicationSecurityGroupIds      []string `hcl:"source_application_security_group_ids"`
	SourcePortRange                        string   `hcl:"source_port_range"`
	SourcePortRanges                       []string `hcl:"source_port_ranges"`
}

func main() {
	// lets pass this in as a param?
	configFilePath := "nsg.tf"

	// create new Config struct
	var config Config

	// This decodes the TF file into the config struct, and hydrates the values
	err := MyDecodeFile(configFilePath, nil, &config)
	if err != nil {
		log.Fatalf("Failed to load configuration: %s", err)
	}
	log.Printf("Configuration is %#v", config)

	// let's read in the file contents
	file, err := os.Open(configFilePath)
	if err != nil {
		fmt.Printf("Failed to read file: %v\n", err)
		return
	}
	defer file.Close()

	// Read the file and output as a []bytes
	bytes, err := io.ReadAll(file)
	if err != nil {
		fmt.Println("Error reading file:", err)
		return
	}

	// Parse, decode and evaluate the config of the .tf file
	hclsimple.Decode(configFilePath, bytes, nil, &config)

	// iterate through the rules until we find one with
	// Description = "Grant acccess to Flask App"

	// CODE GO HERE
	for _, nsg := range config.NetworkSecurityGroup {
		fmt.Printf("security rule: %s", nsg.SecurityRule)
	}
}

// Basically copied from here https://github.com/hashicorp/hcl/blob/v2.16.2/hclsimple/hclsimple.go#L59
// but modified to handle .tf files too
func MyDecode(filename string, src []byte, ctx *hcl.EvalContext, target interface{}) error {
	var file *hcl.File
	var diags hcl.Diagnostics

	switch suffix := strings.ToLower(filepath.Ext(filename)); suffix {
	case ".tf":
		file, diags = hclsyntax.ParseConfig(src, filename, hcl.Pos{Line: 1, Column: 1})
	case ".hcl":
		file, diags = hclsyntax.ParseConfig(src, filename, hcl.Pos{Line: 1, Column: 1})
	case ".json":
		file, diags = json.Parse(src, filename)
	default:
		diags = diags.Append(&hcl.Diagnostic{
			Severity: hcl.DiagError,
			Summary:  "Unsupported file format",
			Detail:   fmt.Sprintf("Cannot read from %s: unrecognized file format suffix %q.", filename, suffix),
		})
		return diags
	}
	if diags.HasErrors() {
		return diags
	}

	diags = gohcl.DecodeBody(file.Body, ctx, target)
	if diags.HasErrors() {
		return diags
	}
	return nil
}

// Taken from here https://github.com/hashicorp/hcl/blob/v2.16.2/hclsimple/hclsimple.go#L89
func MyDecodeFile(filename string, ctx *hcl.EvalContext, target interface{}) error {
	src, err := ioutil.ReadFile(filename)
	if err != nil {
		if os.IsNotExist(err) {
			return hcl.Diagnostics{
				{
					Severity: hcl.DiagError,
					Summary:  "Configuration file not found",
					Detail:   fmt.Sprintf("The configuration file %s does not exist.", filename),
				},
			}
		}
		return hcl.Diagnostics{
			{
				Severity: hcl.DiagError,
				Summary:  "Failed to read configuration",
				Detail:   fmt.Sprintf("Can't read %s: %s.", filename, err),
			},
		}
	}
	return MyDecode(filename, src, ctx, target)
}

当我运行代码时,我遇到了定义 NetworkSecurityGroup.SecurityRule 的困难,并收到了上述代码中的错误:

2023/05/24 11:42:11 Failed to load configuration: nsg.tf:6,3-16: Unsupported argument; An argument named "security_rule" is not expected here. Did you mean to define a block of type "security_rule"?
exit status 1

非常感谢您的任何建议。

英文:

I am attempting to do a small bit of automation on a Terraform file I have, that defines an Azure Network Security Group. Essentially I have a website & SSH access that I only want to allow to my public IP address, which I can obtain from icanhazip.com. I am hoping to write my IP into the relevant part of the .tf file with a Golang script (essentially set the value for security_rule.source_address_prefixes).

I am attempting to use hclsimple library in Golang, as well as having tried gohcl, hclwrite and others, but I'm not getting anywhere with converting the HCL file into Golang structs, essentially.

My Terraform file (HCL format I believe) is as follows:

resource "azurerm_network_security_group" "my_nsg" {
name                = "my_nsg"
location            = "loc"
resource_group_name = "rgname"
security_rule       = [
{
access                                     = "Deny"
description                                = "Desc"
destination_address_prefix                 = "*"
destination_address_prefixes               = []
destination_application_security_group_ids = []
destination_port_range                     = ""
destination_port_ranges                    = [
"123",
"456",
"789",
"1001",
]
direction                                  = "Inbound"
name                                       = "AllowInboundThing"
priority                                   = 100
protocol                                   = "*"
source_address_prefix                      = "*"
source_address_prefixes                    = [
# obtain from icanhazip.com
"1.2.3.4"
]
source_application_security_group_ids      = []
source_port_range                          = "*"
source_port_ranges                         = []
},
{
access                                     = "Allow"
description                                = "Grant acccess to App"
destination_address_prefix                 = "*"
destination_address_prefixes               = []
destination_application_security_group_ids = []
destination_port_range                     = ""
destination_port_ranges                    = [
"443",
"80",
]
direction                                  = "Inbound"
name                                       = "AllowIPInBound"
priority                                   = 200
protocol                                   = "*"
source_address_prefix                      = ""
source_address_prefixes                    = [
# obtain from icanhazip.com
"1.2.3.4"
]
source_application_security_group_ids      = []
source_port_range                          = "*"
source_port_ranges                         = []
}
]
}

And this is as far as I have got with my Golang script, in attempting to represent the above data as structs, and then decoding the .tf file itself (I copied a couple of methods locally from hclsimple in order to have it decode a .tf file, as suggested in their docs.

package main
import (
"fmt"
"io"
"io/ioutil"
"log"
"os"
"path/filepath"
"strings"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/gohcl"
"github.com/hashicorp/hcl/v2/hclsimple"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/hashicorp/hcl/v2/json"
)
type Config struct {
NetworkSecurityGroup []NetworkSecurityGroup `hcl:"resource,block"`
}
type NetworkSecurityGroup struct {
Type              string         `hcl:"azurerm_network_security_group,label"`
Name              string         `hcl:"mick-linux3-nsg,label"`
NameAttr          string         `hcl:"name"`
Location          string         `hcl:"location"`
ResourceGroupName string         `hcl:"resource_group_name"`
SecurityRule      []SecurityRule `hcl:"security_rule,block"`
}
type SecurityRule struct {
Access                                 string   `hcl:"access"`
Description                            string   `hcl:"description"`
DestinationAddressPrefix               string   `hcl:"destination_address_prefix"`
DestinationAddressPrefixes             []string `hcl:"destination_address_prefixes"`
DestinationApplicationSecurityGroupIds []string `hcl:"destination_application_security_group_ids"`
DestinationPortRange                   string   `hcl:"destination_port_range"`
DestinationPortRanges                  []string `hcl:"destination_port_ranges"`
Direction                              string   `hcl:"direction"`
Name                                   string   `hcl:"name"`
Priority                               int      `hcl:"priority"`
Protocol                               string   `hcl:"protocol"`
SourceAddressPrefix                    string   `hcl:"source_address_prefix"`
SourceAddressPrefixes                  []string `hcl:"source_address_prefixes"`
SourceApplicationSecurityGroupIds      []string `hcl:"source_application_security_group_ids"`
SourcePortRange                        string   `hcl:"source_port_range"`
SourcePortRanges                       []string `hcl:"source_port_ranges"`
}
func main() {
// lets pass this in as a param?
configFilePath := "nsg.tf"
// create new Config struct
var config Config
// This decodes the TF file into the config struct, and hydrates the values
err := MyDecodeFile(configFilePath, nil, &config)
if err != nil {
log.Fatalf("Failed to load configuration: %s", err)
}
log.Printf("Configuration is %#v", config)
// let's read in the file contents
file, err := os.Open(configFilePath)
if err != nil {
fmt.Printf("Failed to read file: %v\n", err)
return
}
defer file.Close()
// Read the file and output as a []bytes
bytes, err := io.ReadAll(file)
if err != nil {
fmt.Println("Error reading file:", err)
return
}
// Parse, decode and evaluate the config of the .tf file
hclsimple.Decode(configFilePath, bytes, nil, &config)
// iterate through the rules until we find one with
// Description = "Grant acccess to Flask App"
// CODE GO HERE
for _, nsg := range config.NetworkSecurityGroup {
fmt.Printf("security rule: %s", nsg.SecurityRule)
}
}
// Basically copied from here https://github.com/hashicorp/hcl/blob/v2.16.2/hclsimple/hclsimple.go#L59
// but modified to handle .tf files too
func MyDecode(filename string, src []byte, ctx *hcl.EvalContext, target interface{}) error {
var file *hcl.File
var diags hcl.Diagnostics
switch suffix := strings.ToLower(filepath.Ext(filename)); suffix {
case ".tf":
file, diags = hclsyntax.ParseConfig(src, filename, hcl.Pos{Line: 1, Column: 1})
case ".hcl":
file, diags = hclsyntax.ParseConfig(src, filename, hcl.Pos{Line: 1, Column: 1})
case ".json":
file, diags = json.Parse(src, filename)
default:
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary:  "Unsupported file format",
Detail:   fmt.Sprintf("Cannot read from %s: unrecognized file format suffix %q.", filename, suffix),
})
return diags
}
if diags.HasErrors() {
return diags
}
diags = gohcl.DecodeBody(file.Body, ctx, target)
if diags.HasErrors() {
return diags
}
return nil
}
// Taken from here https://github.com/hashicorp/hcl/blob/v2.16.2/hclsimple/hclsimple.go#L89
func MyDecodeFile(filename string, ctx *hcl.EvalContext, target interface{}) error {
src, err := ioutil.ReadFile(filename)
if err != nil {
if os.IsNotExist(err) {
return hcl.Diagnostics{
{
Severity: hcl.DiagError,
Summary:  "Configuration file not found",
Detail:   fmt.Sprintf("The configuration file %s does not exist.", filename),
},
}
}
return hcl.Diagnostics{
{
Severity: hcl.DiagError,
Summary:  "Failed to read configuration",
Detail:   fmt.Sprintf("Can't read %s: %s.", filename, err),
},
}
}
return MyDecode(filename, src, ctx, target)
}

When I run the code, essentially I am struggling to define NetworkSecurityGroup.SecurityRule, and receive the following error with the above code:

2023/05/24 11:42:11 Failed to load configuration: nsg.tf:6,3-16: Unsupported argument; An argument named "security_rule" is not expected here. Did you mean to define a block of type "security_rule"?
exit status 1

Any advice much appreciated

答案1

得分: 0

所以目前使用https://github.com/minamijoyo/hcledit是不可能的(请参见https://github.com/minamijoyo/hcledit/issues/50 - 这表明hclwrite本身需要进行更改以便实现)。

所以我按照@Martin Atkins的建议进行了解决:

我创建了一个名为locals.tf的文件,其中包含一个本地变量,然后在NSG安全规则中引用该变量:

locals {
    my_ip = "1.2.3.4"
}

现在,我只需获取我的IP并使用sed命令更新locals.tf文件中的值:

my_ip=$(curl -s -4 icanhazip.com)
sed -i "s|my_ip = \".*\"|my_ip = \"$my_ip\"|" locals.tf
英文:

So it's not currently possible with https://github.com/minamijoyo/hcledit (see here https://github.com/minamijoyo/hcledit/issues/50 - this suggests that hclwrite itself would need a change to facilitate)

So I've worked around as suggested by @Martin Atkins:

I've created a single locals.tf file containing a locals variable, which I then reference in the NSG security rules:

locals {
my_ip = "1.2.3.4"
}

and now I simply obtain my IP and use sed to update the value in the locals.tf file

my_ip=$(curl -s -4 icanhazip.com)
sed -i "s|my_ip = \".*\"|my_ip = \"$my_ip\"|" locals.tf

答案2

得分: 0

slice应该表示为

security_rule { item1 }
security_rule { item2 }

而不是

security_rule = [{ item1 }, { item2 }]

在HCL中。

还有一个新的HCL解组器:https://github.com/genelet/determined/。

"determined"的结果是:

&main.Config{NetworkSecurityGroup:[]main.NetworkSecurityGroup{main.NetworkSecurityGroup{Type:"azurerm_network_security_group", Name:"my_nsg", NameAttr:"my_nsg", Location:"loc", ResourceGroupName:"rgname", SecurityRule:[]main.SecurityRule{main.SecurityRule{Access:"Deny", Description:"Desc", DestinationAddressPrefix:"*", DestinationAddressPrefixes:[]string{}, DestinationApplicationSecurityGroupIds:[]string{}, DestinationPortRange:"", DestinationPortRanges:[]string{"123", "456", "789", "1001"}, Direction:"Inbound", Name:"AllowInboundThing", Priority:100, Protocol:"*", SourceAddressPrefix:"*", SourceAddressPrefixes:[]string{"1.2.3.4"}, SourceApplicationSecurityGroupIds:[]string{}, SourcePortRange:"*", SourcePortRanges:[]string{}}, main.SecurityRule{Access:"Allow", Description:"Grant acccess to App", DestinationAddressPrefix:"*", DestinationAddressPrefixes:[]string{}, DestinationApplicationSecurityGroupIds:[]string{}, DestinationPortRange:"", DestinationPortRanges:[]string{"443", "80"}, Direction:"Inbound", Name:"AllowIPInBound", Priority:200, Protocol:"*", SourceAddressPrefix:"", SourceAddressPrefixes:[]string{"1.2.3.4"}, SourceApplicationSecurityGroupIds:[]string{}, SourcePortRange:"*", SourcePortRanges:[]string{}}}}}}
英文:

slice should be represented as

security_rule { item1 }
security_rule { item2 }

instead of

security_rule = [{ item1 }, { item2 }]

in HCL.

There is also a new HCL unmarshaler: https://github.com/genelet/determined/.

The result from "determined" is:

&main.Config{NetworkSecurityGroup:[]main.NetworkSecurityGroup{main.NetworkSecurityGroup{Type:"azurerm_network_security_group", Name:"my_nsg", NameAttr:"my_nsg", Location:"loc", ResourceGroupName:"rgname", SecurityRule:[]main.SecurityRule{main.SecurityRule{Access:"Deny", Description:"Desc", DestinationAddressPrefix:"*", DestinationAddressPrefixes:[]string{}, DestinationApplicationSecurityGroupIds:[]string{}, DestinationPortRange:"", DestinationPortRanges:[]string{"123", "456", "789", "1001"}, Direction:"Inbound", Name:"AllowInboundThing", Priority:100, Protocol:"*", SourceAddressPrefix:"*", SourceAddressPrefixes:[]string{"1.2.3.4"}, SourceApplicationSecurityGroupIds:[]string{}, SourcePortRange:"*", SourcePortRanges:[]string{}}, main.SecurityRule{Access:"Allow", Description:"Grant acccess to App", DestinationAddressPrefix:"*", DestinationAddressPrefixes:[]string{}, DestinationApplicationSecurityGroupIds:[]string{}, DestinationPortRange:"", DestinationPortRanges:[]string{"443", "80"}, Direction:"Inbound", Name:"AllowIPInBound", Priority:200, Protocol:"*", SourceAddressPrefix:"", SourceAddressPrefixes:[]string{"1.2.3.4"}, SourceApplicationSecurityGroupIds:[]string{}, SourcePortRange:"*", SourcePortRanges:[]string{}}}}}}

huangapple
  • 本文由 发表于 2023年5月24日 18:48:34
  • 转载请务必保留本文链接:https://go.coder-hub.com/76322692.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定