hcl v2 golang 使用的一个参考demo
代码内容来自hashcorp 公司的一个分享,此demo 里边的一些实践很不错,很值得参考(实际上consul,vault,packer,terroform。。。都是值得参考的)
代码结构
├── README.md
├── go.mod
├── go.sum
├── ink.jpg
├── main.go
├── pet.go
├── pet_test.go
├── pets.hcl
└── testdata
├── basic.hcl
├── function.hcl
└── variables.hcl
一个参考配置
pet "Ink" {
type = "cat"
characteristics {
sound = "${env.CAT_SOUND}s ${random("evilly", "lazily", "sleepily", "gracefully")}"
}
}
pet "Swinney" {
type = "dog"
characteristics {
breed = "Dachshund"
}
}
pet.go 核心处理代码
简单说明,pet 对于hcl 文件的处理,不是全家桶模式的,而是进行了拆分,基于hcl 实现了一个类似面向对象的处理
pet 氛围cat 以及dog,所以定义了一个interface pet cat 以及dog 实现了say 以及act 方法,因为每个类型会有自己的
字段,所以每个字段有自己的hcl golang tag 处理,demo 中没有像我们平时直接定义一个通用struct 的模式,而且定义
了一个通用的对于不同的地方直接使用hcl 的remain,然后各自的type 处理自己的解析,好处是简化了类型的判断到一个
复杂而且大的struct 中
hcl 使用内部函数提供解析,参考
// 创建解析器
parser := hclparse.NewParser()
// 基于文件内容,解析hcl content
srcHCL, diag := parser.ParseHCL(src, filename)
// 解析部分定义的hcl 内容,可以灵活扩展
gohcl.DecodeBody(srcHCL.Body, evalContext, petsHCL)
pet hcl dsl 定义
拆分为了PetHCLBodies 以及CharacteristicsHCL 好处是基于业务模型拆分,每部分拥有自己的实现
type PetsHCL struct {
PetHCLBodies []*struct {
Name string `hcl:",label"`
Type string `hcl:"type"`
CharacteristicsHCL *struct {
HCL hcl.Body `hcl:",remain"`
} `hcl:"characteristics,block"`
} `hcl:"pet,block"`
}
核心读取部分
实现了env 以及function 的功能
// ReadConfig decodes the HCL file at filename into a slice of Pets and returns
// it.
func ReadConfig(filename string) ([]Pet, error) {
// First, open a file handle to the input filename.
input, err := os.Open(filename)
if err != nil {
return []Pet{}, fmt.Errorf(
"error in ReadConfig openin pet config file: %w", err,
)
}
defer input.Close()
// Next, read that file into a byte slice for use as a buffer. Because HCL
// decoding must happen in the context of a whole file, it does not take an
// io.Reader as an input, instead relying on byte slices.
src, err := ioutil.ReadAll(input)
if err != nil {
return []Pet{}, fmt.Errorf(
"error in ReadConfig reading input `%s`: %w", filename, err,
)
}
// Instantiate an HCL parser with the source byte slice.
parser := hclparse.NewParser()
srcHCL, diag := parser.ParseHCL(src, filename)
if diag.HasErrors() {
return []Pet{}, fmt.Errorf(
"error in ReadConfig parsing HCL: %w", diag,
)
}
// Call a helper function which creates an HCL context for use in
// decoding the parsed HCL.
evalContext, err := createContext()
if err != nil {
return []Pet{}, fmt.Errorf(
"error in ReadConfig creating HCL evaluation context: %w", err,
)
}
// Start the first pass of decoding. This decodes all pet blocks into
// a generic form, with a Type field for use in determining whether they
// are cats or dogs. The configuration in the characteristics will be left
// undecoded in an hcl.Body. This Body will be decoded into different pet
// types later, once the context of the Type is known.
petsHCL := &PetsHCL{}
if diag := gohcl.DecodeBody(srcHCL.Body, evalContext, petsHCL); diag.HasErrors() {
return []Pet{}, fmt.Errorf(
"error in ReadConfig decoding HCL configuration: %w", diag,
)
}
// Iterate through the generic pets, switch on type, then decode the
// hcl.Body into the correct pet type. This allows "polymorphism" in the
// pet blocks.
pets := []Pet{}
for _, p := range petsHCL.PetHCLBodies {
switch petType := p.Type; petType {
case "cat":
cat := &Cat{Name: p.Name, Sound: defaultCatSound}
if p.CharacteristicsHCL != nil {
if diag := gohcl.DecodeBody(p.CharacteristicsHCL.HCL, evalContext, cat); diag.HasErrors() {
return []Pet{}, fmt.Errorf(
"error in ReadConfig decoding cat HCL configuration: %w", diag,
)
}
}
pets = append(pets, cat)
case "dog":
dog := &Dog{Name: p.Name, Breed: defaultDogBreed}
if p.CharacteristicsHCL != nil {
if diag := gohcl.DecodeBody(p.CharacteristicsHCL.HCL, evalContext, dog); diag.HasErrors() {
return []Pet{}, fmt.Errorf(
"error in ReadConfig decoding dog HCL configuration: %w", diag,
)
}
}
pets = append(pets, dog)
default:
// Error in the case of an unknown type. In the future, more types
// could be added to the switch to support, for example, fish
// owners.
return []Pet{}, fmt.Errorf("error in ReadConfig: unknown pet type `%s`", petType)
}
}
return pets, nil
}
// createContext is a helper function that creates an *hcl.EvalContext to be
// used in decoding HCL. It creates a set of variables at env.KEY
// (namely, CAT_SOUND). It also creates a function "random(...string)" that can
// be used to assign a random value in an HCL config.
func createContext() (*hcl.EvalContext, error) {
// Extract the sound cats make from the environment, with a default.
catSound := defaultCatSound
if os.Getenv(catSoundKey) != "" {
catSound = os.Getenv(catSoundKey)
}
// variables is a list of cty.Value for use in Decoding HCL. These will
// be nested by using ObjectVal as a value. For istance:
// env.CAT_SOUND => "meow"
variables := map[string]cty.Value{
environmentKey: cty.ObjectVal(map[string]cty.Value{
catSoundKey: cty.StringVal(catSound),
}),
}
// functions is a list of cty.Functions for use in Decoding HCL.
functions := map[string]function.Function{
"random": function.New(&function.Spec{
// Params represents required positional arguments, of which random
// has none.
Params: []function.Parameter{},
// VarParam allows a "VarArgs" type input, in this case, of
// strings.
VarParam: &function.Parameter{Type: cty.String},
// Type is used to determine the output type from the inputs. In
// the case of Random it only accepts strings and only returns
// strings.
Type: function.StaticReturnType(cty.String),
// Impl is the actual function. A "VarArgs" number of cty.String
// will be passed in and a random one returned, also as a
// cty.String.
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
resp := args[rand.Intn(len(args))]
return cty.StringVal(resp.AsString()), nil
},
}),
}
// Return the constructed hcl.EvalContext.
return &hcl.EvalContext{
Variables: variables,
Functions: functions,
}, nil
}
说明
以上是官方hcl v2 golang 的一个demo 学习还是很值得看看的
参考资料
https://github.com/hashicorp/hcl/tree/hcl2
https://github.com/RussellRollins/pet-sounds