HCL 是 hashicorp 推出的一个配置语言,在 hashicorp 的产品,如 Consul、Terraform 中用作标准的配置语言,其语法结构有点类似于 nginx 的配置文件,一个简单的例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
table = "filter"

enabled = ["mon","osd"]
disabled = []

rule "ipv4" "mon" {
priority = 100

tcp = [
"-p tcp -m multiport --dport 6789,3300 -m state --state NEW -j ACCEPT",
"-p tcp -m multiport --dport 6789,3300 -m state --state ESTABLISHED -j ACCEPT",
]
}

rule "ipv4" "osd" {
priority = 200

tcp = [
"-p tcp -m multiport --dport 6800:7300 -m state --state NEW -j ACCEPT",
"-p tcp -m multiport --dport 6800:7300 -m state --state ESTABLISHED -j ACCEPT",
]
}

针对 hcl 文档的操作,官方的 hashicorp/hcl2 库提供了非常完善的支持,本文用到的仅是该库关于 hcl 文档解析里非常、非常小的一部分。

hcl 文档结构

hcl 文档由顶层的 attribute(s) 和 block(s) 组成;

block 由 block header 以及内层的 attribute(s) 和 block(s) 组成;

block header 由 type 和 label(s) 组成。

hcl 文档解析上层接口

可以使用上层的接口,以类似 json、yaml 的方式进行解析,直接从 hcl 文档 decode 到 Go 数据结构,可以参考如下的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
package main

import (
"github.com/hashicorp/hcl2/gohcl"
"github.com/hashicorp/hcl2/hclparse"
"github.com/pkg/errors"
"github.com/sanity-io/litter"
"log"
)

const data = `table = "filter"

enabled = ["mon","osd"]
disabled = []

ipv4 "mon" {
priority = 100

tcp = [
"-p tcp -m multiport --dport 6789,3300 -m state --state NEW -j ACCEPT",
"-p tcp -m multiport --dport 6789,3300 -m state --state ESTABLISHED -j ACCEPT",
]
}

ipv6 "osd" {
priority = 200

tcp = [
"-p tcp -m multiport --dport 6800:7300 -m state --state NEW -j ACCEPT",
"-p tcp -m multiport --dport 6800:7300 -m state --state ESTABLISHED -j ACCEPT",
]
}
`

type Config struct {
Table *string `hcl:"table,attr"`

Enabled []string `hcl:"enabled,attr"`
Disabled []string `hcl:"disabled,optional"`

RulesV4 []Rule `hcl:"ipv4,block"`
RulesV6 []Rule `hcl:"ipv6,block"`
}

type Rule struct {
Name string `hcl:"name,label"`
Priority *int `hcl:"priority,optional"`
TCP []string `hcl:"tcp,optional"`
UDP []string `hcl:"udp,optional"`
}

func decode() (Config, error) {
parser := hclparse.NewParser()
file, diags := parser.ParseHCL([]byte(data), "literal-document")
if file == nil || file.Body == nil {
return Config{}, errors.Wrap(diags, "failed to parse hcl document")
}

var config Config
if diags := gohcl.DecodeBody(file.Body, nil, &config); diags != nil {
return Config{}, errors.Wrap(diags, "failed to decode hcl document")
}
return config, nil
}

func main() {
config, err := decode()
if err != nil {
log.Fatal(err)
}
litter.Dump(config)
}

hcl 文档解析底层接口

也可以根据 hcl 文档的结构使用底层接口进行更灵活的解析,可以参考如下的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
package main

import (
"github.com/hashicorp/hcl2/gohcl"
"github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl2/hclparse"
"github.com/pkg/errors"
"github.com/sanity-io/litter"
"log"
)

const data = `table = "filter"

enabled = ["mon","osd"]
disabled = []

ipv4 "mon" {
priority = 100

tcp = [
"-p tcp -m multiport --dport 6789,3300 -m state --state NEW -j ACCEPT",
"-p tcp -m multiport --dport 6789,3300 -m state --state ESTABLISHED -j ACCEPT",
]
}

ipv6 "osd" {
priority = 200

tcp = [
"-p tcp -m multiport --dport 6800:7300 -m state --state NEW -j ACCEPT",
"-p tcp -m multiport --dport 6800:7300 -m state --state ESTABLISHED -j ACCEPT",
]
}
`

type Config struct {
Table *string

Enabled []string
Disabled []string

RulesV4 []Rule
RulesV6 []Rule
}

type Rule struct {
Name string
Priority *int
TCP []string
UDP []string
}

var fileSchema = &hcl.BodySchema{
Attributes: []hcl.AttributeSchema{
{
Name: "table",
},
{
Name: "enabled",
},
{
Name: "disabled",
Required: false,
},
},
Blocks: []hcl.BlockHeaderSchema{
{
Type: "ipv4",
LabelNames: []string{"name"},
},
{
Type: "ipv6",
LabelNames: []string{"name"},
},
},
}

var ruleBlockSchema = &hcl.BodySchema{
Attributes: []hcl.AttributeSchema{
{
Name: "priority",
Required: false,
},
{
Name: "tcp",
Required: false,
},
{
Name: "udp",
Required: false,
},
},
}

func decode() (Config, error) {
parser := hclparse.NewParser()
file, diags := parser.ParseHCL([]byte(data), "literal-document")
if file == nil || file.Body == nil {
return Config{}, errors.Wrap(diags, "failed to parse hcl document")
}

content, diags := file.Body.Content(fileSchema)
if content == nil {
return Config{}, errors.Wrap(diags, "failed to decode hcl document")
}

var config Config
for _, attr := range content.Attributes {
switch attr.Name {
case "table":
attrDiags := gohcl.DecodeExpression(attr.Expr, nil, &config.Table)
diags = append(diags, attrDiags...)
case "enabled":
attrDiags := gohcl.DecodeExpression(attr.Expr, nil, &config.Enabled)
diags = append(diags, attrDiags...)
case "disabled":
attrDiags := gohcl.DecodeExpression(attr.Expr, nil, &config.Disabled)
diags = append(diags, attrDiags...)
default:
// Should never happen because the above cases should be exhaustive
// for all attribute type names in our schema.
continue
}
}

for _, block := range content.Blocks {
switch block.Type {
case "ipv4":
rule, blockDiags := decodeRuleBlock(block)
config.RulesV4 = append(config.RulesV4, rule)
diags = append(diags, blockDiags...)
case "ipv6":
rule, blockDiags := decodeRuleBlock(block)
config.RulesV6 = append(config.RulesV6, rule)
diags = append(diags, blockDiags...)
default:
// Should never happen because the above cases should be exhaustive
// for all block type names in our schema.
continue
}
}

if diags.HasErrors() {
return Config{}, errors.Wrap(diags, "failed to decode hcl content")
}
return config, nil
}

func decodeRuleBlock(block *hcl.Block) (Rule, hcl.Diagnostics) {
content, diags := block.Body.Content(ruleBlockSchema)
if content == nil {
return Rule{}, diags
}

rule := Rule{
Name: block.Labels[0],
}
if attr, exists := content.Attributes["priority"]; exists {
attrDiags := gohcl.DecodeExpression(attr.Expr, nil, &rule.Priority)
diags = append(diags, attrDiags...)
}
if attr, exists := content.Attributes["tcp"]; exists {
attrDiags := gohcl.DecodeExpression(attr.Expr, nil, &rule.TCP)
diags = append(diags, attrDiags...)
}
if attr, exists := content.Attributes["udp"]; exists {
attrDiags := gohcl.DecodeExpression(attr.Expr, nil, &rule.UDP)
diags = append(diags, attrDiags...)
}
return rule, diags
}

func main() {
config, err := decode()
if err != nil {
log.Fatal(err)
}
litter.Dump(config)
}

参考资料

A small repo of sample HCL2 apps

https://github.com/sean-/hcl2tests

HCL Config Language Toolkit

https://hcl.readthedocs.io/en/latest/

Terraform is a tool for building, changing, and combining infrastructure safely and efficiently.

https://github.com/hashicorp/terraform/tree/master/configs