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

posted on 2020-07-06 13:11  荣锋亮  阅读(578)  评论(0编辑  收藏  举报

导航