pkg/config/path.go (156 lines of code) (raw):
// Copyright 2023 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 config
import (
"fmt"
"reflect"
"github.com/pkg/errors"
"github.com/zclconf/go-cty/cty"
)
// Path is unique identifier of a piece of configuration.
type Path interface {
String() string
Parent() Path
}
type basePath struct {
InternalPrev Path
InternalPiece string
}
func (p basePath) Parent() Path { return p.InternalPrev }
func (p basePath) String() string {
pref := ""
if p.Parent() != nil {
pref = p.Parent().String()
}
return fmt.Sprintf("%s%s", pref, p.InternalPiece)
}
type arrayPath[E any] struct{ basePath }
func (p arrayPath[E]) At(i int) E {
var e E
initPath(&e, &p, fmt.Sprintf("[%d]", i))
return e
}
type mapPath[E any] struct{ basePath }
func (p mapPath[E]) Dot(k string) E {
var e E
initPath(&e, &p, fmt.Sprintf(".%s", k))
return e
}
// ctyPath is a specialization of Path that can be extended with cty.Path
type ctyPath struct{ basePath }
// Cty builds a path chain that starts with p and each link corresponds to a step in cty.Path
// If any step in cty.Path is not supported, the path chain will be built up to that point.
// E.g.
// Root.Vars.Dot("alpha").Cty(cty.Path{}.IndexInt(6)) == "vars.alpha[6]"
func (p ctyPath) Cty(cp cty.Path) basePath {
cur := p.basePath
for _, s := range cp {
prev := cur
var nxt basePath
piece, err := ctyStepToString(s)
if err != nil {
return cur // fall back to longest path build up to this point
}
initPath(&nxt, &prev, piece)
cur = nxt
}
return cur
}
func ctyStepToString(s cty.PathStep) (string, error) {
switch s := s.(type) {
case cty.GetAttrStep:
return fmt.Sprintf(".%s", s.Name), nil // equivalent to mapPath.Dot
case cty.IndexStep:
switch s.Key.Type() {
case cty.Number:
return fmt.Sprintf("[%s]", s.Key.AsBigFloat().String()), nil // equivalent to arrayPath.At
case cty.String:
return fmt.Sprintf(".%s", s.Key.AsString()), nil // equivalent to mapPath.Dot
default:
return "", errors.New("key value not number or string")
}
default:
return "", errors.Errorf("unknown cty.PathStep type: %#v", s)
}
}
// initPath walks through all child paths of p and initializes them. E.g.
// initPath(&Root, nil, "") will trigger
// -> initPath(&Root.BlueprintName, &Root, "blueprint_name")
func initPath(p any, prev any, piece string) {
r := reflect.Indirect(reflect.ValueOf(p))
ty := reflect.TypeOf(p).Elem()
if !r.FieldByName("InternalPiece").IsValid() || !r.FieldByName("InternalPrev").IsValid() {
panic(fmt.Sprintf("%s does not embed basePath", ty.Name()))
}
if _, ok := prev.(Path); prev != nil && !ok {
panic(fmt.Sprintf("prev is not a Path: %#v", p))
}
r.FieldByName("InternalPiece").SetString(piece)
if prev != nil {
r.FieldByName("InternalPrev").Set(reflect.ValueOf(prev))
}
for i := 0; i < ty.NumField(); i++ {
tag, ok := ty.Field(i).Tag.Lookup("path")
if !ok {
continue
}
initPath(r.Field(i).Addr().Interface(), p, tag)
}
}
type rootPath struct {
basePath
BlueprintName basePath `path:"blueprint_name"`
GhpcVersion basePath `path:"ghpc_version"`
Validators arrayPath[validatorCfgPath] `path:"validators"`
ValidationLevel basePath `path:"validation_level"`
Vars dictPath `path:"vars"`
Groups arrayPath[groupPath] `path:"deployment_groups"`
Backend backendPath `path:"terraform_backend_defaults"`
Provider mapPath[providerPath] `path:"terraform_providers"`
ToolkitModulesURL basePath `path:"toolkit_modules_url"`
ToolkitModulesVersion basePath `path:"toolkit_modules_version"`
}
type validatorCfgPath struct {
basePath
Validator basePath `path:".validator"`
Inputs dictPath `path:".inputs"`
Skip basePath `path:".skip"`
}
type dictPath struct{ mapPath[ctyPath] }
type backendPath struct {
basePath
Type basePath `path:".type"`
Configuration dictPath `path:".configuration"`
}
type providerPath struct {
basePath
Source basePath `path:".source"`
Version basePath `path:".version"`
Configuration dictPath `path:".configuration"`
}
type groupPath struct {
basePath
Name basePath `path:".group"`
Backend backendPath `path:".terraform_backend"`
Provider mapPath[providerPath] `path:".terraform_provider"`
Modules arrayPath[ModulePath] `path:".modules"`
}
type ModulePath struct {
basePath
Source basePath `path:".source"`
Kind basePath `path:".kind"`
ID basePath `path:".id"`
Use arrayPath[basePath] `path:".use"`
Outputs arrayPath[outputPath] `path:".outputs"`
Settings dictPath `path:".settings"`
}
type outputPath struct {
basePath
Name basePath `path:".name"`
Description basePath `path:".description"`
Sensitive basePath `path:".sensitive"`
}
// Root is a starting point for creating a Blueprint Path
var Root rootPath
func init() {
initPath(&Root, nil, "")
}
func IsModuleSettingsPath(p Path) bool {
parent := p.Parent()
if parent == nil {
return false
}
mp, ok := parent.(*ModulePath)
if !ok {
return false
}
return p == mp.Settings
}