internal/tfengine/config.go (104 lines of code) (raw):
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package tfengine
import (
"encoding/json"
"fmt"
"io/ioutil"
"path/filepath"
"github.com/GoogleCloudPlatform/healthcare-data-protection-suite/internal/hcl"
"github.com/GoogleCloudPlatform/healthcare-data-protection-suite/internal/jsonschema"
"github.com/GoogleCloudPlatform/healthcare-data-protection-suite/internal/template"
"github.com/ghodss/yaml"
"github.com/hashicorp/hcl/v2/hclsimple"
"github.com/zclconf/go-cty/cty"
)
// Config is the user supplied config for the engine.
// HCL struct tags are documented at https://pkg.go.dev/github.com/hashicorp/hcl2/gohcl.
type Config struct {
// Optional constraint on the binary version required for this config.
// Syntax: https://www.terraform.io/docs/configuration/version-constraints.html
Version string `hcl:"version,optional" json:"version,omitempty"`
// HCL decoder can't unmarshal into map[string]interface{},
// so make it unmarshal to a cty.Value and manually convert to map.
// TODO(https://github.com/hashicorp/hcl/issues/291): Remove the need for DataCty.
DataCty *cty.Value `hcl:"data,optional" json:"-"`
Data map[string]interface{} `json:"data,omitempty"`
SchemaCty *cty.Value `hcl:"schema,optional" json:"-"`
Schema map[string]interface{} `json:"schema,omitempty"`
Templates []*templateInfo `hcl:"template,block" json:"template,omitempty"`
}
type templateInfo struct {
Name string `hcl:",label" json:"name"`
Source string `hcl:"source,optional" json:"source,omitempty"`
ComponentPath string `hcl:"component_path,optional" json:"component_path,omitempty"`
RecipePath string `hcl:"recipe_path,optional" json:"recipe_path,omitempty"`
OutputPath string `hcl:"output_path,optional" json:"output_path,omitempty"`
Flatten []*template.FlattenInfo `hcl:"flatten,block" json:"flatten,omitempty"`
Passthrough []string `hcl:"passthrough,optional" json:"passthrough,omitempty"`
DataCty *cty.Value `hcl:"data,optional" json:"-"`
Data map[string]interface{} `json:"data,omitempty"`
}
// Init initializes and validates the config.
func (c *Config) Init() error {
var err error
if c.DataCty != nil {
c.Data, err = hcl.CtyValueToMap(c.DataCty)
if err != nil {
return fmt.Errorf("failed to convert %v to map: %v", c.DataCty, err)
}
}
if c.Data == nil {
c.Data = make(map[string]interface{})
}
if c.SchemaCty != nil {
c.Schema, err = hcl.CtyValueToMap(c.SchemaCty)
if err != nil {
return fmt.Errorf("failed to convert schema %v to map: %v", c.SchemaCty, err)
}
// Add output_path to be a valid field in schema. It is set for each template below.
props := c.Schema["properties"].(map[string]interface{})
props["output_path"] = make(map[string]interface{})
}
for _, t := range c.Templates {
if t.DataCty != nil {
t.Data, err = hcl.CtyValueToMap(t.DataCty)
if err != nil {
return fmt.Errorf("failed to convert data %v to map: %v", t.DataCty, err)
}
}
if t.Data == nil {
t.Data = make(map[string]interface{})
}
if t.OutputPath != "" {
t.Data["output_path"] = filepath.Clean(t.OutputPath)
}
}
return c.validate()
}
func (c *Config) validate() error {
sj, err := hcl.ToJSON([]byte(Schema))
if err != nil {
return err
}
cj, err := json.Marshal(c)
if err != nil {
return err
}
return jsonschema.ValidateJSONBytes(sj, cj)
}
func loadConfig(path string, data map[string]interface{}) (*Config, error) {
b, err := ioutil.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("read file %v: %v", path, err)
}
buf, err := template.WriteBuffer(string(b), data)
if err != nil {
return nil, fmt.Errorf("parsing template %v: %v", string(b), err)
}
// Convert yaml to json so hcl decoder can parse it.
cj := buf.Bytes()
if filepath.Ext(path) == ".yaml" {
cj, err = yaml.YAMLToJSON(cj)
if err != nil {
return nil, fmt.Errorf("converting yaml to json %v: %v", string(cj), err)
}
// hclsimple.Decode doesn't actually use the path for anything other
// than its extension, so just pass in any file name ending with json so
// the library knows to treat these bytes as json and not yaml.
path = "file.json"
}
c := new(Config)
if err := hclsimple.Decode(path, cj, nil, c); err != nil {
return nil, fmt.Errorf("decode as hcl %v: %v", string(cj), err)
}
if err := c.Init(); err != nil {
return nil, fmt.Errorf("init config %v: %v", c, err)
}
return c, nil
}