mirror of
https://github.com/turbot/steampipe.git
synced 2025-12-19 18:12:43 -05:00
477 lines
13 KiB
Go
477 lines
13 KiB
Go
package modconfig
|
|
|
|
import (
|
|
"fmt"
|
|
"github.com/spf13/viper"
|
|
typehelpers "github.com/turbot/go-kit/types"
|
|
"github.com/turbot/steampipe/pkg/constants"
|
|
"github.com/zclconf/go-cty/cty"
|
|
"strings"
|
|
|
|
"github.com/hashicorp/hcl/v2"
|
|
"github.com/stevenle/topsort"
|
|
"github.com/turbot/steampipe/pkg/utils"
|
|
)
|
|
|
|
const rootRuntimeDependencyNode = "rootRuntimeDependencyNode"
|
|
const RuntimeDependencyDashboardScope = "self"
|
|
|
|
// Dashboard is a struct representing the Dashboard resource
|
|
type Dashboard struct {
|
|
ResourceWithMetadataImpl
|
|
ModTreeItemImpl
|
|
WithProviderImpl
|
|
|
|
// required to allow partial decoding
|
|
Remain hcl.Body `hcl:",remain" json:"-"`
|
|
|
|
Width *int `cty:"width" hcl:"width" column:"width,text"`
|
|
Display *string `cty:"display" hcl:"display" column:"display,text"`
|
|
Inputs []*DashboardInput `cty:"inputs" column:"inputs,jsonb"`
|
|
UrlPath string `cty:"url_path" column:"url_path,jsonb"`
|
|
Base *Dashboard `hcl:"base"`
|
|
// store children in a way which can be serialised via cty
|
|
ChildNames []string `cty:"children" column:"children,jsonb"`
|
|
// map of all inputs in our resource tree
|
|
selfInputsMap map[string]*DashboardInput
|
|
runtimeDependencyGraph *topsort.Graph
|
|
}
|
|
|
|
func NewDashboard(block *hcl.Block, mod *Mod, shortName string) HclResource {
|
|
fullName := fmt.Sprintf("%s.%s.%s", mod.ShortName, block.Type, shortName)
|
|
|
|
c := &Dashboard{
|
|
ModTreeItemImpl: ModTreeItemImpl{
|
|
HclResourceImpl: HclResourceImpl{
|
|
ShortName: shortName,
|
|
FullName: fullName,
|
|
UnqualifiedName: fmt.Sprintf("%s.%s", block.Type, shortName),
|
|
DeclRange: block.DefRange,
|
|
blockType: block.Type,
|
|
},
|
|
Mod: mod,
|
|
},
|
|
}
|
|
c.SetAnonymous(block)
|
|
c.setUrlPath()
|
|
|
|
return c
|
|
}
|
|
|
|
// NewQueryDashboard creates a dashboard to wrap a query/control
|
|
// this is used for snapshot generation
|
|
func NewQueryDashboard(qp QueryProvider) (*Dashboard, error) {
|
|
parsedName, title, err := getQueryDashboardName(qp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// for query dashboard use generated title, for control use original title
|
|
if qp.BlockType() != BlockTypeQuery {
|
|
title = qp.GetTitle()
|
|
}
|
|
|
|
var dashboard = &Dashboard{
|
|
ResourceWithMetadataImpl: ResourceWithMetadataImpl{
|
|
metadata: &ResourceMetadata{},
|
|
},
|
|
ModTreeItemImpl: ModTreeItemImpl{
|
|
HclResourceImpl: HclResourceImpl{
|
|
ShortName: parsedName.Name,
|
|
FullName: parsedName.ToFullName(),
|
|
UnqualifiedName: parsedName.ToResourceName(),
|
|
Title: utils.ToStringPointer(title),
|
|
Description: utils.ToStringPointer(qp.GetDescription()),
|
|
Documentation: utils.ToStringPointer(qp.GetDocumentation()),
|
|
Tags: qp.GetTags(),
|
|
blockType: BlockTypeDashboard,
|
|
},
|
|
Mod: qp.GetMod(),
|
|
},
|
|
}
|
|
|
|
dashboard.setUrlPath()
|
|
|
|
table, err := NewQueryDashboardTable(qp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
dashboard.children = []ModTreeItem{table}
|
|
|
|
return dashboard, nil
|
|
}
|
|
|
|
func getQueryDashboardName(qp QueryProvider) (*ParsedResourceName, string, error) {
|
|
var sql string
|
|
if q := qp.GetQuery(); q != nil {
|
|
sql = typehelpers.SafeString(q.GetSQL())
|
|
} else {
|
|
sql = typehelpers.SafeString(qp.GetSQL())
|
|
}
|
|
hash, err := utils.Base36Hash(sql, 8)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
dashboardName := fmt.Sprintf("custom.dashboard.sql_%s", hash)
|
|
|
|
parsed, err := ParseResourceName(dashboardName)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
title := getQueryDashboardTitle(hash)
|
|
return parsed, title, nil
|
|
}
|
|
|
|
func getQueryDashboardTitle(queryHash string) string {
|
|
if titleArg := viper.GetString(constants.ArgSnapshotTitle); titleArg != "" {
|
|
return titleArg
|
|
}
|
|
return fmt.Sprintf("Custom query [%s]", queryHash)
|
|
}
|
|
|
|
func (d *Dashboard) setUrlPath() {
|
|
d.UrlPath = fmt.Sprintf("/%s", d.FullName)
|
|
}
|
|
|
|
func (d *Dashboard) Equals(other *Dashboard) bool {
|
|
diff := d.Diff(other)
|
|
return !diff.HasChanges()
|
|
}
|
|
|
|
// OnDecoded implements HclResource
|
|
func (d *Dashboard) OnDecoded(block *hcl.Block, _ ResourceMapsProvider) hcl.Diagnostics {
|
|
d.setBaseProperties()
|
|
|
|
d.ChildNames = make([]string, len(d.children))
|
|
for i, child := range d.children {
|
|
d.ChildNames[i] = child.Name()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetWidth implements DashboardLeafNode
|
|
func (d *Dashboard) GetWidth() int {
|
|
if d.Width == nil {
|
|
return 0
|
|
}
|
|
return *d.Width
|
|
}
|
|
|
|
// GetDisplay implements DashboardLeafNode
|
|
func (d *Dashboard) GetDisplay() string {
|
|
return typehelpers.SafeString(d.Display)
|
|
}
|
|
|
|
// GetType implements DashboardLeafNode
|
|
func (d *Dashboard) GetType() string {
|
|
return ""
|
|
}
|
|
|
|
func (d *Dashboard) Diff(other *Dashboard) *DashboardTreeItemDiffs {
|
|
res := &DashboardTreeItemDiffs{
|
|
Item: d,
|
|
Name: d.Name(),
|
|
}
|
|
|
|
if !utils.SafeStringsEqual(d.FullName, other.FullName) {
|
|
res.AddPropertyDiff("Name")
|
|
}
|
|
|
|
if !utils.SafeStringsEqual(d.Title, other.Title) {
|
|
res.AddPropertyDiff("Title")
|
|
}
|
|
|
|
if !utils.SafeIntEqual(d.Width, other.Width) {
|
|
res.AddPropertyDiff("Width")
|
|
}
|
|
|
|
if len(d.Tags) != len(other.Tags) {
|
|
res.AddPropertyDiff("Tags")
|
|
} else {
|
|
for k, v := range d.Tags {
|
|
if otherVal := other.Tags[k]; v != otherVal {
|
|
res.AddPropertyDiff("Tags")
|
|
}
|
|
}
|
|
}
|
|
|
|
if !utils.SafeStringsEqual(d.Description, other.Description) {
|
|
res.AddPropertyDiff("Description")
|
|
}
|
|
|
|
if !utils.SafeStringsEqual(d.Documentation, other.Documentation) {
|
|
res.AddPropertyDiff("Documentation")
|
|
}
|
|
|
|
res.populateChildDiffs(d, other)
|
|
return res
|
|
}
|
|
|
|
func (d *Dashboard) SetChildren(children []ModTreeItem) {
|
|
d.children = children
|
|
}
|
|
|
|
func (d *Dashboard) AddChild(child ModTreeItem) {
|
|
d.children = append(d.children, child)
|
|
|
|
switch c := child.(type) {
|
|
case *DashboardInput:
|
|
d.Inputs = append(d.Inputs, c)
|
|
case *DashboardWith:
|
|
d.AddWith(c)
|
|
}
|
|
}
|
|
|
|
func (d *Dashboard) WalkResources(resourceFunc func(resource HclResource) (bool, error)) error {
|
|
for _, child := range d.children {
|
|
continueWalking, err := resourceFunc(child.(HclResource))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !continueWalking {
|
|
break
|
|
}
|
|
|
|
if container, ok := child.(*DashboardContainer); ok {
|
|
if err := container.WalkResources(resourceFunc); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (d *Dashboard) ValidateRuntimeDependencies(workspace ResourceMapsProvider) error {
|
|
d.runtimeDependencyGraph = topsort.NewGraph()
|
|
// add root node - this will depend on all other nodes
|
|
d.runtimeDependencyGraph.AddNode(rootRuntimeDependencyNode)
|
|
|
|
// define a walk function which determines whether the resource has runtime dependencies and if so,
|
|
// add to the graph
|
|
resourceFunc := func(resource HclResource) (bool, error) {
|
|
wp, ok := resource.(WithProvider)
|
|
if !ok {
|
|
// continue walking
|
|
return true, nil
|
|
}
|
|
|
|
if err := d.validateRuntimeDependenciesForResource(resource, workspace); err != nil {
|
|
return false, err
|
|
}
|
|
|
|
// if the query provider has any 'with' blocks, add these dependencies as well
|
|
for _, with := range wp.GetWiths() {
|
|
if err := d.validateRuntimeDependenciesForResource(with, workspace); err != nil {
|
|
return false, err
|
|
}
|
|
}
|
|
|
|
// continue walking
|
|
return true, nil
|
|
}
|
|
if err := d.WalkResources(resourceFunc); err != nil {
|
|
return err
|
|
}
|
|
|
|
// ensure that dependencies can be resolved
|
|
if _, err := d.runtimeDependencyGraph.TopSort(rootRuntimeDependencyNode); err != nil {
|
|
return fmt.Errorf("runtime depedencies cannot be resolved: %s", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (d *Dashboard) validateRuntimeDependenciesForResource(resource HclResource, workspace ResourceMapsProvider) error {
|
|
// TODO [node_reuse] re-add parse time validation https://github.com/turbot/steampipe/issues/2925
|
|
return nil
|
|
//rdp := resource.(RuntimeDependencyProvider)
|
|
//// WHAT ABOUT CHILDREN
|
|
//if len(runtimeDependencies) == 0 {
|
|
// return nil
|
|
//}
|
|
//name := resource.Name()
|
|
//if !d.runtimeDependencyGraph.ContainsNode(name) {
|
|
// d.runtimeDependencyGraph.AddNode(name)
|
|
//}
|
|
//
|
|
//for _, dependency := range runtimeDependencies {
|
|
// // try to resolve the dependency source resource
|
|
// if err := dependency.ValidateSource(d, workspace); err != nil {
|
|
// return err
|
|
// }
|
|
// if err := d.runtimeDependencyGraph.AddEdge(rootRuntimeDependencyNode, name); err != nil {
|
|
// return err
|
|
// }
|
|
// depString := dependency.String()
|
|
// if !d.runtimeDependencyGraph.ContainsNode(depString) {
|
|
// d.runtimeDependencyGraph.AddNode(depString)
|
|
// }
|
|
// if err := d.runtimeDependencyGraph.AddEdge(name, dependency.String()); err != nil {
|
|
// return err
|
|
// }
|
|
//}
|
|
//return nil
|
|
}
|
|
|
|
func (d *Dashboard) GetInput(name string) (*DashboardInput, bool) {
|
|
input, found := d.selfInputsMap[name]
|
|
return input, found
|
|
}
|
|
|
|
func (d *Dashboard) GetInputs() map[string]*DashboardInput {
|
|
return d.selfInputsMap
|
|
}
|
|
|
|
func (d *Dashboard) InitInputs() hcl.Diagnostics {
|
|
// add all our direct child inputs to a map
|
|
// (we must do this before adding child container inputs to detect dupes)
|
|
duplicates := d.setInputMap()
|
|
|
|
// add child containers and dashboard inputs
|
|
resourceFunc := func(resource HclResource) (bool, error) {
|
|
if container, ok := resource.(*DashboardContainer); ok {
|
|
for _, i := range container.Inputs {
|
|
// check we do not already have this input
|
|
if _, ok := d.selfInputsMap[i.UnqualifiedName]; ok {
|
|
duplicates = append(duplicates, i.Name())
|
|
|
|
}
|
|
d.Inputs = append(d.Inputs, i)
|
|
d.selfInputsMap[i.UnqualifiedName] = i
|
|
}
|
|
}
|
|
// continue walking
|
|
return true, nil
|
|
}
|
|
if err := d.WalkResources(resourceFunc); err != nil {
|
|
return hcl.Diagnostics{&hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: fmt.Sprintf("Dashboard '%s' WalkResources failed", d.Name()),
|
|
Detail: err.Error(),
|
|
Subject: &d.DeclRange,
|
|
}}
|
|
}
|
|
|
|
if len(duplicates) > 0 {
|
|
return hcl.Diagnostics{&hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: fmt.Sprintf("Dashboard '%s' contains duplicate input names for: %s", d.Name(), strings.Join(duplicates, ",")),
|
|
Subject: &d.DeclRange,
|
|
}}
|
|
}
|
|
|
|
var diags hcl.Diagnostics
|
|
// ensure they inputs not have cyclical dependencies
|
|
if err := d.validateInputDependencies(d.Inputs); err != nil {
|
|
return hcl.Diagnostics{&hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: fmt.Sprintf("Failed to resolve input dependency order for dashboard '%s'", d.Name()),
|
|
Detail: err.Error(),
|
|
Subject: &d.DeclRange,
|
|
}}
|
|
}
|
|
// now 'claim' all inputs and add to mod
|
|
for _, input := range d.Inputs {
|
|
input.SetDashboard(d)
|
|
moreDiags := d.Mod.AddResource(input)
|
|
diags = append(diags, moreDiags...)
|
|
}
|
|
|
|
return diags
|
|
}
|
|
|
|
// populate our input map
|
|
func (d *Dashboard) setInputMap() []string {
|
|
var duplicates []string
|
|
d.selfInputsMap = make(map[string]*DashboardInput)
|
|
for _, i := range d.Inputs {
|
|
if _, ok := d.selfInputsMap[i.UnqualifiedName]; ok {
|
|
duplicates = append(duplicates, i.UnqualifiedName)
|
|
} else {
|
|
d.selfInputsMap[i.UnqualifiedName] = i
|
|
}
|
|
}
|
|
return duplicates
|
|
}
|
|
|
|
// CtyValue implements CtyValueProvider
|
|
func (d *Dashboard) CtyValue() (cty.Value, error) {
|
|
return GetCtyValue(d)
|
|
}
|
|
|
|
func (d *Dashboard) setBaseProperties() {
|
|
if d.Base == nil {
|
|
return
|
|
}
|
|
// copy base into the HclResourceImpl 'base' property so it is accessible to all nested structs
|
|
d.base = d.Base
|
|
// call into parent nested struct setBaseProperties
|
|
d.ModTreeItemImpl.setBaseProperties()
|
|
|
|
if d.Width == nil {
|
|
d.Width = d.Base.Width
|
|
}
|
|
|
|
if len(d.children) == 0 {
|
|
d.children = d.Base.children
|
|
d.ChildNames = d.Base.ChildNames
|
|
}
|
|
|
|
d.addBaseInputs(d.Base.Inputs)
|
|
}
|
|
|
|
func (d *Dashboard) addBaseInputs(baseInputs []*DashboardInput) {
|
|
if len(baseInputs) == 0 {
|
|
return
|
|
}
|
|
// rebuild Inputs and children
|
|
inheritedInputs := make([]*DashboardInput, len(baseInputs))
|
|
inheritedChildren := make([]ModTreeItem, len(baseInputs))
|
|
|
|
for i, baseInput := range baseInputs {
|
|
input := baseInput.Clone()
|
|
input.SetDashboard(d)
|
|
// add to mod
|
|
d.Mod.AddResource(input)
|
|
// add to our inputs
|
|
inheritedInputs[i] = input
|
|
inheritedChildren[i] = input
|
|
|
|
}
|
|
// add inputs to beginning of our existing inputs (if any)
|
|
d.Inputs = append(inheritedInputs, d.Inputs...)
|
|
// add inputs to beginning of our children
|
|
d.children = append(inheritedChildren, d.children...)
|
|
d.setInputMap()
|
|
}
|
|
|
|
// ensure that dependencies between inputs are resolveable
|
|
func (d *Dashboard) validateInputDependencies(inputs []*DashboardInput) error {
|
|
dependencyGraph := topsort.NewGraph()
|
|
rootDependencyNode := "dashboard"
|
|
dependencyGraph.AddNode(rootDependencyNode)
|
|
for _, i := range inputs {
|
|
for _, runtimeDep := range i.GetRuntimeDependencies() {
|
|
depName := runtimeDep.PropertyPath.ToResourceName()
|
|
to := depName
|
|
from := i.UnqualifiedName
|
|
if !dependencyGraph.ContainsNode(from) {
|
|
dependencyGraph.AddNode(from)
|
|
}
|
|
if !dependencyGraph.ContainsNode(to) {
|
|
dependencyGraph.AddNode(to)
|
|
}
|
|
if err := dependencyGraph.AddEdge(from, to); err != nil {
|
|
return err
|
|
}
|
|
if err := dependencyGraph.AddEdge(rootDependencyNode, i.UnqualifiedName); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
// now verify we can get a dependency order
|
|
_, err := dependencyGraph.TopSort(rootDependencyNode)
|
|
return err
|
|
}
|