mirror of
https://github.com/turbot/steampipe.git
synced 2025-12-19 18:12:43 -05:00
207 lines
6.7 KiB
Go
207 lines
6.7 KiB
Go
package parse
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
|
|
"github.com/hashicorp/hcl/v2"
|
|
"github.com/hashicorp/hcl/v2/gohcl"
|
|
filehelpers "github.com/turbot/go-kit/files"
|
|
"github.com/turbot/go-kit/helpers"
|
|
"github.com/turbot/steampipe-plugin-sdk/v5/plugin"
|
|
"github.com/turbot/steampipe/pkg/constants"
|
|
"github.com/turbot/steampipe/pkg/steampipeconfig/modconfig"
|
|
"github.com/turbot/steampipe/pkg/steampipeconfig/options"
|
|
)
|
|
|
|
func LoadWorkspaceProfiles(workspaceProfilePath string) (profileMap map[string]*modconfig.WorkspaceProfile, err error) {
|
|
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
err = helpers.ToError(r)
|
|
}
|
|
// be sure to return the default
|
|
if profileMap != nil && profileMap["default"] == nil {
|
|
profileMap["default"] = &modconfig.WorkspaceProfile{ProfileName: "default"}
|
|
}
|
|
}()
|
|
|
|
// create profile map to populate
|
|
profileMap = map[string]*modconfig.WorkspaceProfile{}
|
|
|
|
configPaths, err := filehelpers.ListFiles(workspaceProfilePath, &filehelpers.ListOptions{
|
|
Flags: filehelpers.FilesFlat,
|
|
Include: filehelpers.InclusionsFromExtensions([]string{constants.ConfigExtension}),
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(configPaths) == 0 {
|
|
return profileMap, nil
|
|
}
|
|
|
|
fileData, diags := LoadFileData(configPaths...)
|
|
if diags.HasErrors() {
|
|
return nil, plugin.DiagsToError("Failed to load workspace profiles", diags)
|
|
}
|
|
|
|
body, diags := ParseHclFiles(fileData)
|
|
if diags.HasErrors() {
|
|
return nil, plugin.DiagsToError("Failed to load workspace profiles", diags)
|
|
}
|
|
|
|
// do a partial decode
|
|
content, diags := body.Content(ConfigBlockSchema)
|
|
if diags.HasErrors() {
|
|
return nil, plugin.DiagsToError("Failed to load workspace profiles", diags)
|
|
}
|
|
|
|
parseCtx := NewWorkspaceProfileParseContext(workspaceProfilePath)
|
|
parseCtx.SetDecodeContent(content, fileData)
|
|
|
|
// build parse context
|
|
return parseWorkspaceProfiles(parseCtx)
|
|
|
|
}
|
|
func parseWorkspaceProfiles(parseCtx *WorkspaceProfileParseContext) (map[string]*modconfig.WorkspaceProfile, error) {
|
|
// we may need to decode more than once as we gather dependencies as we go
|
|
// continue decoding as long as the number of unresolved blocks decreases
|
|
prevUnresolvedBlocks := 0
|
|
for attempts := 0; ; attempts++ {
|
|
_, diags := decodeWorkspaceProfiles(parseCtx)
|
|
if diags.HasErrors() {
|
|
return nil, plugin.DiagsToError("Failed to decode all workspace profile files", diags)
|
|
}
|
|
|
|
// if there are no unresolved blocks, we are done
|
|
unresolvedBlocks := len(parseCtx.UnresolvedBlocks)
|
|
if unresolvedBlocks == 0 {
|
|
log.Printf("[TRACE] parse complete after %d decode passes", attempts+1)
|
|
break
|
|
}
|
|
// if the number of unresolved blocks has NOT reduced, fail
|
|
if prevUnresolvedBlocks != 0 && unresolvedBlocks >= prevUnresolvedBlocks {
|
|
str := parseCtx.FormatDependencies()
|
|
return nil, fmt.Errorf("failed to resolve workspace profile dependencies after %d attempts\nDependencies:\n%s", attempts+1, str)
|
|
}
|
|
// update prevUnresolvedBlocks
|
|
prevUnresolvedBlocks = unresolvedBlocks
|
|
}
|
|
|
|
return parseCtx.workspaceProfiles, nil
|
|
|
|
}
|
|
|
|
func decodeWorkspaceProfiles(parseCtx *WorkspaceProfileParseContext) (map[string]*modconfig.WorkspaceProfile, hcl.Diagnostics) {
|
|
profileMap := map[string]*modconfig.WorkspaceProfile{}
|
|
|
|
var diags hcl.Diagnostics
|
|
blocksToDecode, err := parseCtx.BlocksToDecode()
|
|
// build list of blocks to decode
|
|
if err != nil {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "failed to determine required dependency order",
|
|
Detail: err.Error()})
|
|
return nil, diags
|
|
}
|
|
|
|
// now clear dependencies from run context - they will be rebuilt
|
|
parseCtx.ClearDependencies()
|
|
|
|
for _, block := range blocksToDecode {
|
|
if block.Type == modconfig.BlockTypeWorkspaceProfile {
|
|
workspaceProfile, res := decodeWorkspaceProfile(block, parseCtx)
|
|
|
|
if res.Success() {
|
|
// success - add to map
|
|
profileMap[workspaceProfile.ProfileName] = workspaceProfile
|
|
}
|
|
diags = append(diags, res.Diags...)
|
|
}
|
|
}
|
|
return profileMap, diags
|
|
}
|
|
|
|
// decodeWorkspaceProfileOption decodes an options block as a workspace profile property
|
|
// setting the necessary overrides for special handling of the "dashboard" option which is different
|
|
// from the global "dashboard" option
|
|
func decodeWorkspaceProfileOption(block *hcl.Block) (options.Options, hcl.Diagnostics) {
|
|
return DecodeOptions(block, WithOverride(constants.CmdNameDashboard, &options.WorkspaceProfileDashboard{}))
|
|
}
|
|
|
|
func decodeWorkspaceProfile(block *hcl.Block, parseCtx *WorkspaceProfileParseContext) (*modconfig.WorkspaceProfile, *DecodeResult) {
|
|
res := newDecodeResult()
|
|
// get shell resource
|
|
resource := modconfig.NewWorkspaceProfile(block)
|
|
|
|
// do a partial decode to get options blocks into workspaceProfileOptions, with all other attributes in rest
|
|
workspaceProfileOptions, rest, diags := block.Body.PartialContent(WorkspaceProfileBlockSchema)
|
|
if diags.HasErrors() {
|
|
res.handleDecodeDiags(diags)
|
|
return nil, res
|
|
}
|
|
|
|
diags = gohcl.DecodeBody(rest, parseCtx.EvalCtx, resource)
|
|
if len(diags) > 0 {
|
|
res.handleDecodeDiags(diags)
|
|
}
|
|
// use a map keyed by a string for fast lookup
|
|
// we use an empty struct as the value type, so that
|
|
// we don't use up unnecessary memory
|
|
foundOptions := map[string]struct{}{}
|
|
for _, block := range workspaceProfileOptions.Blocks {
|
|
switch block.Type {
|
|
case "options":
|
|
optionsBlockType := block.Labels[0]
|
|
if _, found := foundOptions[optionsBlockType]; found {
|
|
// fail
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Subject: &block.DefRange,
|
|
Summary: fmt.Sprintf("Duplicate options type '%s'", optionsBlockType),
|
|
})
|
|
}
|
|
opts, moreDiags := decodeWorkspaceProfileOption(block)
|
|
if moreDiags.HasErrors() {
|
|
diags = append(diags, moreDiags...)
|
|
break
|
|
}
|
|
moreDiags = resource.SetOptions(opts, block)
|
|
if moreDiags.HasErrors() {
|
|
diags = append(diags, moreDiags...)
|
|
}
|
|
foundOptions[optionsBlockType] = struct{}{}
|
|
default:
|
|
// this should never happen
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: fmt.Sprintf("invalid block type '%s' - only 'options' blocks are supported for workspace profiles", block.Type),
|
|
Subject: &block.DefRange,
|
|
})
|
|
}
|
|
}
|
|
|
|
handleWorkspaceProfileDecodeResult(resource, res, block, parseCtx)
|
|
return resource, res
|
|
}
|
|
|
|
func handleWorkspaceProfileDecodeResult(resource *modconfig.WorkspaceProfile, res *DecodeResult, block *hcl.Block, parseCtx *WorkspaceProfileParseContext) {
|
|
if res.Success() {
|
|
// call post decode hook
|
|
// NOTE: must do this BEFORE adding resource to run context to ensure we respect the base property
|
|
moreDiags := resource.OnDecoded()
|
|
res.addDiags(moreDiags)
|
|
|
|
moreDiags = parseCtx.AddResource(resource)
|
|
res.addDiags(moreDiags)
|
|
return
|
|
}
|
|
|
|
// failure :(
|
|
if len(res.Depends) > 0 {
|
|
moreDiags := parseCtx.AddDependencies(block, resource.Name(), res.Depends)
|
|
res.addDiags(moreDiags)
|
|
}
|
|
}
|