diff --git a/.github/workflows/01-powerpipe-pre-release.yaml b/.github/workflows/01-powerpipe-pre-release.yaml index de045d4c..f9d9b09c 100644 --- a/.github/workflows/01-powerpipe-pre-release.yaml +++ b/.github/workflows/01-powerpipe-pre-release.yaml @@ -42,7 +42,7 @@ jobs: # this is required, check golangci-lint-action docs - uses: actions/setup-go@v5 with: - go-version: '1.22' + go-version: '1.23' cache: false # setup-go v4 caches by default, do not change this parameter, check golangci-lint-action doc: https://github.com/golangci/golangci-lint-action/pull/704 - name: Build diff --git a/.github/workflows/02-powerpipe-release.yaml b/.github/workflows/02-powerpipe-release.yaml index ba0676d6..13776369 100644 --- a/.github/workflows/02-powerpipe-release.yaml +++ b/.github/workflows/02-powerpipe-release.yaml @@ -61,7 +61,7 @@ jobs: # this is required, check golangci-lint-action docs - uses: actions/setup-go@v5 with: - go-version: '1.22' + go-version: '1.23' cache: false # setup-go v4 caches by default, do not change this parameter, check golangci-lint-action doc: https://github.com/golangci/golangci-lint-action/pull/704 - name: Build diff --git a/.github/workflows/10-test-lint.yaml b/.github/workflows/10-test-lint.yaml index 2ab2f960..9975aa9b 100644 --- a/.github/workflows/10-test-lint.yaml +++ b/.github/workflows/10-test-lint.yaml @@ -24,12 +24,12 @@ jobs: with: repository: turbot/pipe-fittings path: pipe-fittings - ref: v1.6.x + ref: develop # this is required, check golangci-lint-action docs - uses: actions/setup-go@v5 with: - go-version: '1.22' + go-version: '1.23' cache: false # setup-go v4 caches by default, do not change this parameter, check golangci-lint-action doc: https://github.com/golangci/golangci-lint-action/pull/704 - name: golangci-lint @@ -38,3 +38,4 @@ jobs: version: v1.61.0 args: --timeout=10m working-directory: powerpipe + skip-cache: true diff --git a/.github/workflows/11-test-acceptance.yaml b/.github/workflows/11-test-acceptance.yaml index 0b0c09a5..90e1efde 100644 --- a/.github/workflows/11-test-acceptance.yaml +++ b/.github/workflows/11-test-acceptance.yaml @@ -29,12 +29,12 @@ jobs: with: repository: turbot/pipe-fittings path: pipe-fittings - ref: v1.6.x + ref: develop # this is required, check golangci-lint-action docs - uses: actions/setup-go@v5 with: - go-version: '1.22' + go-version: '1.23' cache: false # setup-go v4 caches by default, do not change this parameter, check golangci-lint-action doc: https://github.com/golangci/golangci-lint-action/pull/704 - name: Build diff --git a/go.mod b/go.mod index 40431137..9489085a 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,8 @@ module github.com/turbot/powerpipe -go 1.22.4 +go 1.23.2 -// replace github.com/turbot/pipe-fittings => ../pipe-fittings +replace github.com/turbot/pipe-fittings => ../pipe-fittings require ( github.com/Masterminds/semver/v3 v3.3.0 @@ -21,13 +21,14 @@ require ( github.com/spf13/cobra v1.8.1 github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/viper v1.19.0 - github.com/stevenle/topsort v0.2.0 // indirect + github.com/stevenle/topsort v0.2.0 github.com/turbot/go-kit v0.10.0-rc.0 - github.com/turbot/pipe-fittings v1.6.2 - github.com/turbot/steampipe-plugin-sdk/v5 v5.11.0 + // develop branch d7decaf1e82d8538970cc7d7821d5a8f1c3994d4 (spp specific resource refactor) + github.com/turbot/pipe-fittings v1.6.6-0.20241030182756-d7decaf1e82d + github.com/turbot/steampipe-plugin-sdk/v5 v5.11.0 github.com/turbot/terraform-components v0.0.0-20231213122222-1f3526cab7a7 // indirect github.com/xlab/treeprint v1.2.0 // indirect - github.com/zclconf/go-cty v1.14.4 // indirect + github.com/zclconf/go-cty v1.14.4 github.com/zclconf/go-cty-yaml v1.0.3 // indirect golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 sigs.k8s.io/yaml v1.4.0 // indirect @@ -193,7 +194,6 @@ require ( github.com/prometheus/common v0.39.0 // indirect github.com/prometheus/procfs v0.9.0 // indirect github.com/rivo/uniseg v0.4.3 // indirect - github.com/robfig/cron/v3 v3.0.1 // indirect github.com/rs/xid v1.5.0 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect diff --git a/go.sum b/go.sum index ad6c9aa8..aeddcd6e 100644 --- a/go.sum +++ b/go.sum @@ -739,8 +739,6 @@ github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.3 h1:utMvzDsuh3suAEnhH0RdHmoPbU648o6CvXxTx4SBMOw= github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= -github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= @@ -818,8 +816,8 @@ github.com/tkrajina/go-reflector v0.5.6 h1:hKQ0gyocG7vgMD2M3dRlYN6WBBOmdoOzJ6njQ github.com/tkrajina/go-reflector v0.5.6/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4= github.com/turbot/go-kit v0.10.0-rc.0 h1:kd+jp2ibbIV33Hc8SsMAN410Dl9Pz6SJ40axbKUlSoA= github.com/turbot/go-kit v0.10.0-rc.0/go.mod h1:fFQqR59I5z5JeeBLfK1PjSifn4Oprs3NiQx0CxeSJxs= -github.com/turbot/pipe-fittings v1.6.2 h1:VwAdZ2W42AwR1ZrUjIVI4VlQt1g8BNhsheoWcLVFZPY= -github.com/turbot/pipe-fittings v1.6.2/go.mod h1:1nlRVh18QkYy9eq5pW9gpnoE2VgnQW0Y2zKzrH8Q4kI= +github.com/turbot/pipe-fittings v1.6.6-0.20241030175007-281024164508 h1:qxAyYA755bK7lNICi+5eKIZSPZYhxtMC4jweFyHXIBI= +github.com/turbot/pipe-fittings v1.6.6-0.20241030175007-281024164508/go.mod h1:zqS8HCdNa515xuNohnIOY1uu5WQpUNARmN87Ellrk9A= github.com/turbot/pipes-sdk-go v0.9.1 h1:2yRojY2wymvJn6NQyE6A0EDFV267MNe+yDLxPVvsBwM= github.com/turbot/pipes-sdk-go v0.9.1/go.mod h1:Mb+KhvqqEdRbz/6TSZc2QWDrMa5BN3E4Xw+gPt2TRkc= github.com/turbot/steampipe-plugin-code v0.7.0 h1:SROYIo/TI/Q/YNfXK+sAIS71umypUFm1Uz851TmoJkM= diff --git a/internal/cmd/check.go b/internal/cmd/check.go index bca35756..6813d7eb 100644 --- a/internal/cmd/check.go +++ b/internal/cmd/check.go @@ -16,7 +16,6 @@ import ( "github.com/turbot/pipe-fittings/constants" "github.com/turbot/pipe-fittings/contexthelpers" "github.com/turbot/pipe-fittings/error_helpers" - "github.com/turbot/pipe-fittings/modconfig" "github.com/turbot/pipe-fittings/statushooks" "github.com/turbot/pipe-fittings/utils" localcmdconfig "github.com/turbot/powerpipe/internal/cmdconfig" @@ -27,6 +26,7 @@ import ( "github.com/turbot/powerpipe/internal/controlstatus" "github.com/turbot/powerpipe/internal/display" localqueryresult "github.com/turbot/powerpipe/internal/queryresult" + "github.com/turbot/powerpipe/internal/resources" "github.com/turbot/steampipe-plugin-sdk/v5/sperr" ) @@ -35,7 +35,7 @@ var checkOutputMode = localconstants.CheckOutputModeText // generic command to handle benchmark and control execution func checkCmd[T controlinit.CheckTarget]() *cobra.Command { - typeName := modconfig.GenericTypeToBlockType[T]() + typeName := resources.GenericTypeToBlockType[T]() argsSupported := cobra.ExactArgs(1) if typeName == "benchmark" { argsSupported = cobra.MinimumNArgs(1) diff --git a/internal/cmd/dashboard.go b/internal/cmd/dashboard.go index 333013a0..204dcdd6 100644 --- a/internal/cmd/dashboard.go +++ b/internal/cmd/dashboard.go @@ -28,6 +28,7 @@ import ( "github.com/turbot/powerpipe/internal/controlstatus" "github.com/turbot/powerpipe/internal/dashboardexecute" "github.com/turbot/powerpipe/internal/initialisation" + "github.com/turbot/powerpipe/internal/resources" "github.com/turbot/steampipe-plugin-sdk/v5/logging" ) @@ -119,7 +120,7 @@ func dashboardRun(cmd *cobra.Command, args []string) { ctx = createSnapshotContext(ctx, dashboardName) statushooks.SetStatus(ctx, "Initializing…") - initData := initialisation.NewInitData[*modconfig.Dashboard](ctx, cmd, dashboardName) + initData := initialisation.NewInitData[*resources.Dashboard](ctx, cmd, dashboardName) if len(viper.GetStringSlice(constants.ArgExport)) > 0 { err := initData.RegisterExporters(dashboardExporters()...) @@ -142,7 +143,7 @@ func dashboardRun(cmd *cobra.Command, args []string) { // so a dashboard name was specified - just call GenerateSnapshot target, err := initData.GetSingleTarget() error_helpers.FailOnError(err) - snap, err := dashboardexecute.GenerateSnapshot(ctx, initData.WorkspaceEvents, target, inputs) + snap, err := dashboardexecute.GenerateSnapshot(ctx, initData.Workspace, target, inputs) error_helpers.FailOnError(err) // display the snapshot result (if needed) displaySnapshot(snap) diff --git a/internal/cmd/query.go b/internal/cmd/query.go index b03b77e1..5f94f972 100644 --- a/internal/cmd/query.go +++ b/internal/cmd/query.go @@ -17,7 +17,6 @@ import ( "github.com/turbot/pipe-fittings/constants" "github.com/turbot/pipe-fittings/error_helpers" "github.com/turbot/pipe-fittings/export" - "github.com/turbot/pipe-fittings/modconfig" "github.com/turbot/pipe-fittings/steampipeconfig" "github.com/turbot/pipe-fittings/workspace" localcmdconfig "github.com/turbot/powerpipe/internal/cmdconfig" @@ -26,6 +25,7 @@ import ( "github.com/turbot/powerpipe/internal/display" "github.com/turbot/powerpipe/internal/initialisation" "github.com/turbot/powerpipe/internal/queryresult" + "github.com/turbot/powerpipe/internal/resources" "github.com/turbot/steampipe-plugin-sdk/v5/logging" "github.com/turbot/steampipe-plugin-sdk/v5/sperr" ) @@ -105,7 +105,7 @@ func queryRun(cmd *cobra.Command, args []string) { return } - initData := initialisation.NewInitData[*modconfig.Query](ctx, cmd, args...) + initData := initialisation.NewInitData[*resources.Query](ctx, cmd, args...) // shutdown the service on exit defer initData.Cleanup(ctx) error_helpers.FailOnError(initData.Result.Error) @@ -134,7 +134,7 @@ func queryRun(cmd *cobra.Command, args []string) { exitCode = constants.ExitCodeInitializationFailed error_helpers.FailOnError(err) } - snap, err := dashboardexecute.GenerateSnapshot(ctx, initData.WorkspaceEvents, target, nil) + snap, err := dashboardexecute.GenerateSnapshot(ctx, initData.Workspace, target, nil) if err != nil { exitCode = constants.ExitCodeSnapshotCreationFailed error_helpers.FailOnError(err) @@ -218,7 +218,7 @@ func setExitCodeForQueryError(err error) { func snapshotToQueryResult(snap *steampipeconfig.SteampipeSnapshot, startTime time.Time) (*queryresult.Result, error) { // the table of a snapshot query has a fixed name - tablePanel, ok := snap.Panels[modconfig.SnapshotQueryTableName] + tablePanel, ok := snap.Panels[resources.SnapshotQueryTableName] if !ok { return nil, sperr.New("dashboard does not contain table result for query") } diff --git a/internal/cmd/resource_cmd.go b/internal/cmd/resource_cmd.go index 25ba4129..1b8e3b7a 100644 --- a/internal/cmd/resource_cmd.go +++ b/internal/cmd/resource_cmd.go @@ -13,6 +13,7 @@ import ( "github.com/turbot/pipe-fittings/schema" localconstants "github.com/turbot/powerpipe/internal/constants" "github.com/turbot/powerpipe/internal/display" + "github.com/turbot/powerpipe/internal/resources" ) // variable used to assign the output mode flag @@ -25,7 +26,7 @@ type ResourceCommandConfig struct { } func newResourceCommandConfig[T modconfig.ModTreeItem]() *ResourceCommandConfig { - typeName := modconfig.GenericTypeToBlockType[T]() + typeName := resources.GenericTypeToBlockType[T]() return &ResourceCommandConfig{ cmd: typeName, } @@ -61,7 +62,7 @@ func resourceCmd[T modconfig.ModTreeItem](opts ...ResourceCommandOption) *cobra. } func listCmd[T modconfig.ModTreeItem]() *cobra.Command { - typeName := modconfig.GenericTypeToBlockType[T]() + typeName := resources.GenericTypeToBlockType[T]() var cmd = &cobra.Command{ Use: "list", Args: cobra.NoArgs, @@ -78,7 +79,7 @@ func listCmd[T modconfig.ModTreeItem]() *cobra.Command { } func showCmd[T modconfig.ModTreeItem]() *cobra.Command { - typeName := modconfig.GenericTypeToBlockType[T]() + typeName := resources.GenericTypeToBlockType[T]() var cmd = &cobra.Command{ Use: showCommandUse(typeName), @@ -98,7 +99,7 @@ func showCmd[T modconfig.ModTreeItem]() *cobra.Command { // determine which resource commands apply to this resource func getResourceCommands[T modconfig.ModTreeItem]() []*cobra.Command { - typeName := modconfig.GenericTypeToBlockType[T]() + typeName := resources.GenericTypeToBlockType[T]() var res = []*cobra.Command{listCmd[T](), showCmd[T]()} @@ -117,15 +118,15 @@ func getResourceCommands[T modconfig.ModTreeItem]() []*cobra.Command { func dashboardChildCommands() []*cobra.Command { res := []*cobra.Command{ - resourceCmd[*modconfig.DashboardCard](withCmdName("card")), - resourceCmd[*modconfig.DashboardChart](withCmdName("chart")), - resourceCmd[*modconfig.DashboardContainer](withCmdName("container")), - resourceCmd[*modconfig.DashboardFlow](withCmdName("flow")), - resourceCmd[*modconfig.DashboardGraph](withCmdName("graph")), - resourceCmd[*modconfig.DashboardHierarchy](withCmdName("hierarchy")), - resourceCmd[*modconfig.DashboardImage](withCmdName("image")), - resourceCmd[*modconfig.DashboardTable](withCmdName("table")), - resourceCmd[*modconfig.DashboardText](withCmdName("text")), + resourceCmd[*resources.DashboardCard](withCmdName("card")), + resourceCmd[*resources.DashboardChart](withCmdName("chart")), + resourceCmd[*resources.DashboardContainer](withCmdName("container")), + resourceCmd[*resources.DashboardFlow](withCmdName("flow")), + resourceCmd[*resources.DashboardGraph](withCmdName("graph")), + resourceCmd[*resources.DashboardHierarchy](withCmdName("hierarchy")), + resourceCmd[*resources.DashboardImage](withCmdName("image")), + resourceCmd[*resources.DashboardTable](withCmdName("table")), + resourceCmd[*resources.DashboardText](withCmdName("text")), } // set all to hidden @@ -139,14 +140,14 @@ func runCmd[T modconfig.HclResource]() *cobra.Command { var empty T switch any(empty).(type) { - case *modconfig.Query: + case *resources.Query: return queryRunCmd() - case *modconfig.Dashboard: + case *resources.Dashboard: return dashboardRunCmd() - case *modconfig.Benchmark: - return checkCmd[*modconfig.Benchmark]() - case *modconfig.Control: - return checkCmd[*modconfig.Control]() + case *resources.Benchmark: + return checkCmd[*resources.Benchmark]() + case *resources.Control: + return checkCmd[*resources.Control]() } return nil diff --git a/internal/cmd/root.go b/internal/cmd/root.go index 85fa43e9..fc797ebe 100644 --- a/internal/cmd/root.go +++ b/internal/cmd/root.go @@ -15,6 +15,7 @@ import ( "github.com/turbot/pipe-fittings/statushooks" "github.com/turbot/pipe-fittings/utils" localconstants "github.com/turbot/powerpipe/internal/constants" + "github.com/turbot/powerpipe/internal/resources" ) var exitCode int @@ -53,10 +54,10 @@ func rootCommand() *cobra.Command { serverCmd(), modCmd(), loginCmd(), - resourceCmd[*modconfig.Benchmark](), - resourceCmd[*modconfig.Control](), - resourceCmd[*modconfig.Dashboard](), - resourceCmd[*modconfig.Query](), + resourceCmd[*resources.Benchmark](), + resourceCmd[*resources.Control](), + resourceCmd[*resources.Dashboard](), + resourceCmd[*resources.Query](), resourceCmd[*modconfig.Variable](), ) diff --git a/internal/cmd/server.go b/internal/cmd/server.go index a059d7b2..2a37e461 100644 --- a/internal/cmd/server.go +++ b/internal/cmd/server.go @@ -11,13 +11,13 @@ import ( "github.com/turbot/pipe-fittings/cmdconfig" "github.com/turbot/pipe-fittings/constants" "github.com/turbot/pipe-fittings/error_helpers" - "github.com/turbot/pipe-fittings/modconfig" "github.com/turbot/pipe-fittings/utils" localcmdconfig "github.com/turbot/powerpipe/internal/cmdconfig" localconstants "github.com/turbot/powerpipe/internal/constants" "github.com/turbot/powerpipe/internal/dashboardassets" "github.com/turbot/powerpipe/internal/dashboardserver" "github.com/turbot/powerpipe/internal/initialisation" + "github.com/turbot/powerpipe/internal/resources" "github.com/turbot/powerpipe/internal/service/api" "github.com/turbot/steampipe-plugin-sdk/v5/sperr" "gopkg.in/olahol/melody.v1" @@ -75,7 +75,7 @@ func runServerCmd(cmd *cobra.Command, _ []string) { } // initialise the workspace - modInitData := initialisation.NewInitData[*modconfig.Dashboard](ctx, cmd) + modInitData := initialisation.NewInitData[*resources.Dashboard](ctx, cmd) error_helpers.FailOnError(modInitData.Result.Error) // ensure dashboard assets @@ -85,7 +85,7 @@ func runServerCmd(cmd *cobra.Command, _ []string) { // setup a new webSocket service webSocket := melody.New() // create the dashboardServer - dashboardServer, err := dashboardserver.NewServer(ctx, modInitData.WorkspaceEvents, webSocket) + dashboardServer, err := dashboardserver.NewServer(ctx, modInitData.Workspace, webSocket) error_helpers.FailOnError(err) // send it over to the powerpipe API Server diff --git a/internal/cmdconfig/app_specific.go b/internal/cmdconfig/app_specific.go index 7146ae14..2327e871 100644 --- a/internal/cmdconfig/app_specific.go +++ b/internal/cmdconfig/app_specific.go @@ -14,6 +14,10 @@ import ( "github.com/turbot/pipe-fittings/connection" "github.com/turbot/pipe-fittings/error_helpers" "github.com/turbot/pipe-fittings/filepaths" + "github.com/turbot/pipe-fittings/modconfig" + "github.com/turbot/pipe-fittings/parse" + pparse "github.com/turbot/powerpipe/internal/parse" + "github.com/turbot/powerpipe/internal/resources" ) // SetAppSpecificConstants sets app specific constants defined in pipe-fittings @@ -23,7 +27,7 @@ func SetAppSpecificConstants() { // set all app specific env var keys app_specific.SetAppSpecificEnvVarKeys("POWERPIPE_") - // version + // version string versionString := viper.GetString("main.version") app_specific.AppVersion = semver.MustParse(versionString) @@ -71,6 +75,11 @@ func SetAppSpecificConstants() { app_specific.VersionCheckPath = "api/cli/version/latest" app_specific.EnvProfile = "POWERPIPE_PROFILE" + // set app specific parse related constants + modconfig.AppSpecificNewModResourcesFunc = resources.NewModResources + parse.ModDecoderFunc = pparse.NewPowerpipeModDecoder + parse.AppSpecificGetResourceSchemaFunc = pparse.GetResourceSchema + // register supported connection types registerConnections() } diff --git a/internal/cmdconfig/cmd_targets.go b/internal/cmdconfig/cmd_targets.go index 66e0c461..87988323 100644 --- a/internal/cmdconfig/cmd_targets.go +++ b/internal/cmdconfig/cmd_targets.go @@ -10,10 +10,12 @@ import ( "github.com/turbot/pipe-fittings/constants" "github.com/turbot/pipe-fittings/modconfig" "github.com/turbot/pipe-fittings/workspace" + "github.com/turbot/powerpipe/internal/resources" + pworkspace "github.com/turbot/powerpipe/internal/workspace" "github.com/turbot/steampipe-plugin-sdk/v5/sperr" ) -func ResolveTargets[T modconfig.ModTreeItem](cmdArgs []string, w *workspace.Workspace) ([]modconfig.ModTreeItem, error) { +func ResolveTargets[T modconfig.ModTreeItem](cmdArgs []string, w *pworkspace.PowerpipeWorkspace) ([]modconfig.ModTreeItem, error) { if len(cmdArgs) == 0 { return nil, nil } @@ -39,26 +41,24 @@ func ResolveTargets[T modconfig.ModTreeItem](cmdArgs []string, w *workspace.Work // - verify the resource exists in the workspace // - if the command type is 'query', the target may be a query string rather than a resource name // in this case, convert into a query and add to workspace (to allow for simple snapshot generation) -func resolveSingleTarget[T modconfig.ModTreeItem](cmdArg string, w *workspace.Workspace) ([]modconfig.ModTreeItem, error) { +// +// TODO K add unit test +func resolveSingleTarget[T modconfig.ModTreeItem](cmdArg string, w *pworkspace.PowerpipeWorkspace) ([]modconfig.ModTreeItem, error) { var target modconfig.ModTreeItem - var queryArgs *modconfig.QueryArgs + var queryArgs *resources.QueryArgs var err error - // so there are multiple targets - this must be the benchmark command, so we do not expect any args - // now try to resolve targets - // NOTE: we only expect multiple targets for benchmarks which do not support query args - // however for resilience (and in case this changes), collect query args into a map - - target, queryArgs, err = workspace.ResolveResourceAndArgsFromSQLString[T](cmdArg, w) + target, queryArgs, err = pworkspace.ResolveResourceAndArgsFromSQLString[T](cmdArg, &w.Workspace) if err != nil { return nil, err } if helpers.IsNil(target) { return nil, fmt.Errorf("'%s' not found in %s (%s)", cmdArg, w.Mod.Name(), w.Path) } - if queryArgs != nil { - return nil, sperr.New("benchmarks do not support query args") - } + // TODO KAI CHECK QUERY ARGS LOGIC HERE + //if queryArgs != nil { + // return nil, sperr.New("benchmarks do not support query args") + //} // ok we managed to resolve @@ -81,14 +81,14 @@ func resolveSingleTarget[T modconfig.ModTreeItem](cmdArg string, w *workspace.Wo // if the target is a query provider set the args // (if the target is a dashboard, which is not a query provider, // we read the args from viper separately and use to populate the inputs) - if qp, ok := any(target).(modconfig.QueryProvider); ok { + if qp, ok := any(target).(resources.QueryProvider); ok { qp.SetArgs(queryArgs) } } // now set the command line args if !commandLineQueryArgs.Empty() { // if the target is a query provider set the args - if qp, ok := any(target).(modconfig.QueryProvider); ok { + if qp, ok := any(target).(resources.QueryProvider); ok { qp.SetArgs(commandLineQueryArgs) } } @@ -96,18 +96,18 @@ func resolveSingleTarget[T modconfig.ModTreeItem](cmdArg string, w *workspace.Wo } -func resolveBenchmarkTargets[T modconfig.ModTreeItem](cmdArgs []string, w *workspace.Workspace) ([]modconfig.ModTreeItem, error) { +func resolveBenchmarkTargets[T modconfig.ModTreeItem](cmdArgs []string, w *pworkspace.PowerpipeWorkspace) ([]modconfig.ModTreeItem, error) { var targets []modconfig.ModTreeItem // so there are multiple targets - this must be the benchmark command, so we do not expect any args // verify T is Benchmark (should be enforced by Cobra) var empty T - if _, isBenchmark := (any(empty)).(*modconfig.Benchmark); !isBenchmark { + if _, isBenchmark := (any(empty)).(*resources.Benchmark); !isBenchmark { return nil, sperr.New("multiple targets are only supported for benchmarks") } // now try to resolve targets for _, cmdArg := range cmdArgs { - target, queryArgs, err := workspace.ResolveResourceAndArgsFromSQLString[T](cmdArg, w) + target, queryArgs, err := pworkspace.ResolveResourceAndArgsFromSQLString[T](cmdArg, &w.Workspace) if err != nil { return nil, err } @@ -123,7 +123,7 @@ func resolveBenchmarkTargets[T modconfig.ModTreeItem](cmdArgs []string, w *works return targets, nil } -func handleAllArg[T modconfig.ModTreeItem](args []string, w *workspace.Workspace) ([]modconfig.ModTreeItem, error) { +func handleAllArg[T modconfig.ModTreeItem](args []string, w *pworkspace.PowerpipeWorkspace) ([]modconfig.ModTreeItem, error) { // if there is more than 1 arg, "all" is not valid if len(args) > 1 { // verify that no other benchmarks/controls are given with an all @@ -137,7 +137,7 @@ func handleAllArg[T modconfig.ModTreeItem](args []string, w *workspace.Workspace return nil, nil } var empty T - if _, isBenchmark := (any(empty)).(*modconfig.Benchmark); !isBenchmark { + if _, isBenchmark := (any(empty)).(*resources.Benchmark); !isBenchmark { return nil, nil } @@ -145,14 +145,15 @@ func handleAllArg[T modconfig.ModTreeItem](args []string, w *workspace.Workspace // but NOT children which come from dependency mods filter := workspace.ResourceFilter{ WherePredicate: func(item modconfig.HclResource) bool { - mti, ok := item.(modconfig.ModTreeItem) + mti, ok := item.(modconfig.ModItem) if !ok { return false } - return mti.GetMod().ShortName == w.Mod.ShortName + return mti.GetMod().GetShortName() == w.Mod.ShortName }, } - targetsMap, err := workspace.FilterWorkspaceResourcesOfType[T](w, filter) + // TODO K pass workspace interface instead + targetsMap, err := workspace.FilterWorkspaceResourcesOfType[T](&w.Workspace, filter) if err != nil { return nil, err } @@ -160,16 +161,16 @@ func handleAllArg[T modconfig.ModTreeItem](args []string, w *workspace.Workspace targets := ToModTreeItemSlice(maps.Values(targetsMap)) // make a root item to hold the benchmarks - resolvedItem := modconfig.NewRootBenchmarkWithChildren(w.Mod, targets).(modconfig.ModTreeItem) + resolvedItem := resources.NewRootBenchmarkWithChildren(w.Mod, targets).(modconfig.ModTreeItem) return []modconfig.ModTreeItem{resolvedItem}, nil } // build a QueryArgs from any args passed using the --args flag -func getCommandLineQueryArgs() (*modconfig.QueryArgs, error) { +func getCommandLineQueryArgs() (*resources.QueryArgs, error) { argTuples := viper.GetStringSlice(constants.ArgArg) - var res = modconfig.NewQueryArgs() + var res = resources.NewQueryArgs() if argTuples == nil { return res, nil diff --git a/internal/controldisplay/group.go b/internal/controldisplay/group.go index 0fccd88d..29f4a04a 100644 --- a/internal/controldisplay/group.go +++ b/internal/controldisplay/group.go @@ -5,8 +5,8 @@ import ( "log/slog" "strings" - "github.com/turbot/pipe-fittings/modconfig" "github.com/turbot/powerpipe/internal/controlexecute" + "github.com/turbot/powerpipe/internal/resources" ) type GroupRenderer struct { @@ -43,7 +43,7 @@ func (r GroupRenderer) isLastChild(group *controlexecute.ResultGroup) bool { // get the name of the last sibling which has controls (or is a control) var finalSiblingName string for _, s := range siblings { - if b, ok := s.(*modconfig.Benchmark); ok { + if b, ok := s.(*resources.Benchmark); ok { // find the result group for this benchmark and see if it has controls resultGroup := r.resultTree.Root.GetChildGroupByName(b.Name()) // if the result group has not controls, we will not find it in the result tree @@ -160,7 +160,7 @@ func (r GroupRenderer) renderChildren() []string { var childStrings []string for _, child := range children { - if control, ok := child.(*modconfig.Control); ok { + if control, ok := child.(*resources.Control); ok { // get Result group with a matching name if run := r.group.GetControlRunByName(control.Name()); run != nil { controlRenderer := NewControlRenderer(run, &r) diff --git a/internal/controldisplay/snapshot.go b/internal/controldisplay/snapshot.go index e7f5a0c8..5ce92ef7 100644 --- a/internal/controldisplay/snapshot.go +++ b/internal/controldisplay/snapshot.go @@ -4,17 +4,16 @@ import ( "context" "fmt" - "github.com/turbot/pipe-fittings/modconfig" "github.com/turbot/pipe-fittings/pipes" "github.com/turbot/pipe-fittings/statushooks" "github.com/turbot/pipe-fittings/steampipeconfig" "github.com/turbot/powerpipe/internal/controlexecute" "github.com/turbot/powerpipe/internal/dashboardexecute" - "github.com/turbot/powerpipe/internal/dashboardworkspace" + "github.com/turbot/powerpipe/internal/resources" ) func executionTreeToSnapshot(e *controlexecute.ExecutionTree) (*steampipeconfig.SteampipeSnapshot, error) { - var dashboardNode modconfig.DashboardLeafNode + var dashboardNode resources.DashboardLeafNode var panels map[string]steampipeconfig.SnapshotPanel var checkRun *dashboardexecute.CheckRun @@ -22,7 +21,7 @@ func executionTreeToSnapshot(e *controlexecute.ExecutionTree) (*steampipeconfig. switch root := e.Root.Children[0].(type) { case *controlexecute.ResultGroup: var ok bool - dashboardNode, ok = root.GroupItem.(modconfig.DashboardLeafNode) + dashboardNode, ok = root.GroupItem.(resources.DashboardLeafNode) if !ok { return nil, fmt.Errorf("invalid node found in control execution tree - cannot cast '%s' to a DashboardLeafNode", root.GroupItem.Name()) } @@ -40,7 +39,7 @@ func executionTreeToSnapshot(e *controlexecute.ExecutionTree) (*steampipeconfig. // populate the panels panels = checkRun.BuildSnapshotPanels(make(map[string]steampipeconfig.SnapshotPanel)) - vars, err := dashboardexecute.GetReferencedVariables(checkRun, dashboardworkspace.NewWorkspaceEvents(e.Workspace)) + vars, err := dashboardexecute.GetReferencedVariables(checkRun, e.Workspace) if err != nil { return nil, err } diff --git a/internal/controlexecute/control_run.go b/internal/controlexecute/control_run.go index 30e35402..a8dc2c8b 100644 --- a/internal/controlexecute/control_run.go +++ b/internal/controlexecute/control_run.go @@ -10,7 +10,6 @@ import ( typehelpers "github.com/turbot/go-kit/types" "github.com/turbot/pipe-fittings/constants" "github.com/turbot/pipe-fittings/error_helpers" - "github.com/turbot/pipe-fittings/modconfig" "github.com/turbot/pipe-fittings/queryresult" "github.com/turbot/pipe-fittings/schema" "github.com/turbot/pipe-fittings/statushooks" @@ -20,6 +19,7 @@ import ( "github.com/turbot/powerpipe/internal/dashboardtypes" "github.com/turbot/powerpipe/internal/db_client" localqueryresult "github.com/turbot/powerpipe/internal/queryresult" + "github.com/turbot/powerpipe/internal/resources" "github.com/turbot/powerpipe/internal/snapshot" "github.com/turbot/steampipe-plugin-sdk/v5/grpc" ) @@ -43,7 +43,7 @@ type ControlRun struct { NodeType string `json:"panel_type"` // the control being run - Control *modconfig.Control `json:"-"` + Control *resources.Control `json:"-"` // this is populated by retrieving Control properties with the snapshot tag Properties map[string]any `json:"properties,omitempty"` @@ -105,7 +105,7 @@ func NewControlRunInstance(cr *ControlRun, parent *ResultGroup) ControlRunInstan return res //nolint:govet // we want the struct } -func NewControlRun(control *modconfig.Control, group *ResultGroup, executionTree *ExecutionTree) (*ControlRun, error) { +func NewControlRun(control *resources.Control, group *ResultGroup, executionTree *ExecutionTree) (*ControlRun, error) { controlId := control.Name() // only show qualified control names for controls from dependent mods @@ -327,7 +327,7 @@ func (r *ControlRun) getControlQueryContext(ctx context.Context) context.Context return newCtx } -func (r *ControlRun) resolveControlQuery(control *modconfig.Control) (*modconfig.ResolvedQuery, error) { +func (r *ControlRun) resolveControlQuery(control *resources.Control) (*resources.ResolvedQuery, error) { resolvedQuery, err := r.Tree.Workspace.ResolveQueryFromQueryProvider(control, nil) if err != nil { return nil, fmt.Errorf(`cannot run %s - failed to resolve query "%s": %s`, control.Name(), typehelpers.SafeString(control.SQL), err.Error()) diff --git a/internal/controlexecute/execution_tree.go b/internal/controlexecute/execution_tree.go index 8ff36241..32f5f6c4 100644 --- a/internal/controlexecute/execution_tree.go +++ b/internal/controlexecute/execution_tree.go @@ -10,9 +10,11 @@ import ( "github.com/turbot/pipe-fittings/backend" "github.com/turbot/pipe-fittings/constants" "github.com/turbot/pipe-fittings/modconfig" - "github.com/turbot/pipe-fittings/workspace" + pworkspace "github.com/turbot/pipe-fittings/workspace" "github.com/turbot/powerpipe/internal/controlstatus" "github.com/turbot/powerpipe/internal/db_client" + "github.com/turbot/powerpipe/internal/resources" + "github.com/turbot/powerpipe/internal/workspace" "golang.org/x/sync/semaphore" ) @@ -27,8 +29,8 @@ type ExecutionTree struct { // map of dimension property name to property value to color map DimensionColorGenerator *DimensionColorGenerator `json:"-"` // the current session search path - SearchPath []string `json:"-"` - Workspace *workspace.Workspace `json:"-"` + SearchPath []string `json:"-"` + Workspace *workspace.PowerpipeWorkspace `json:"-"` // ControlRunInstances is a list of control runs for each parent. ControlRunInstances []*ControlRunInstance `json:"-"` client *db_client.DbClient @@ -36,10 +38,10 @@ type ExecutionTree struct { controlNameFilterMap map[string]struct{} } -func NewExecutionTree(ctx context.Context, workspace *workspace.Workspace, client *db_client.DbClient, controlFilter workspace.ResourceFilter, targets ...modconfig.ModTreeItem) (*ExecutionTree, error) { +func NewExecutionTree(ctx context.Context, w *workspace.PowerpipeWorkspace, client *db_client.DbClient, controlFilter pworkspace.ResourceFilter, targets ...modconfig.ModTreeItem) (*ExecutionTree, error) { // now populate the ExecutionTree executionTree := &ExecutionTree{ - Workspace: workspace, + Workspace: w, client: client, ControlRuns: make(map[string]*ControlRun), } @@ -61,7 +63,7 @@ func NewExecutionTree(ctx context.Context, workspace *workspace.Workspace, clien resolvedItem = targets[0] } else { // create a root benchmark with `items` as it's children - resolvedItem = modconfig.NewRootBenchmarkWithChildren(workspace.Mod, targets).(modconfig.ModTreeItem) + resolvedItem = resources.NewRootBenchmarkWithChildren(w.Mod, targets).(modconfig.ModTreeItem) } // build tree of result groups, starting with a synthetic 'root' node @@ -95,7 +97,7 @@ func (*ExecutionTree) IsExportSourceData() {} // AddControl checks whether control should be included in the tree // if so, creates a ControlRun, which is added to the parent group -func (e *ExecutionTree) AddControl(ctx context.Context, control *modconfig.Control, group *ResultGroup) error { +func (e *ExecutionTree) AddControl(ctx context.Context, control *resources.Control, group *ResultGroup) error { // note we use short name to determine whether to include a control if e.ShouldIncludeControl(control.Name()) { // check if we have a run already @@ -173,7 +175,7 @@ func (e *ExecutionTree) waitForActiveRunsToComplete(ctx context.Context, paralle return parallelismLock.Acquire(waitCtx, maxParallelGoRoutines) } -func (e *ExecutionTree) populateControlFilterMap(controlFilter workspace.ResourceFilter) error { +func (e *ExecutionTree) populateControlFilterMap(controlFilter pworkspace.ResourceFilter) error { // if we derived or were passed a where clause, run the filter if controlFilter.Empty() { return nil @@ -199,9 +201,10 @@ func (e *ExecutionTree) ShouldIncludeControl(controlName string) bool { // Get a map of control names from the introspection table steampipe_control // This is used to implement the 'where' control filtering -func (e *ExecutionTree) getControlMapFromFilter(controlFilter workspace.ResourceFilter) (map[string]struct{}, error) { +func (e *ExecutionTree) getControlMapFromFilter(controlFilter pworkspace.ResourceFilter) (map[string]struct{}, error) { var res = make(map[string]struct{}) - controls, err := workspace.FilterWorkspaceResourcesOfType[*modconfig.Control](e.Workspace, controlFilter) + // TODO K pass workspace interface instead + controls, err := pworkspace.FilterWorkspaceResourcesOfType[*resources.Control](&e.Workspace.Workspace, controlFilter) if err != nil { return nil, err } diff --git a/internal/controlexecute/result_group.go b/internal/controlexecute/result_group.go index c694a213..01e7eef4 100644 --- a/internal/controlexecute/result_group.go +++ b/internal/controlexecute/result_group.go @@ -17,6 +17,7 @@ import ( "github.com/turbot/pipe-fittings/steampipeconfig" "github.com/turbot/powerpipe/internal/controlstatus" "github.com/turbot/powerpipe/internal/db_client" + "github.com/turbot/powerpipe/internal/resources" "golang.org/x/sync/semaphore" ) @@ -81,7 +82,7 @@ func NewRootResultGroup(ctx context.Context, executionTree *ExecutionTree, rootI } // if root item is a benchmark, create new result group with root as parent - if control, ok := rootItem.(*modconfig.Control); ok { + if control, ok := rootItem.(*resources.Control); ok { // if root item is a control, add control run if err := executionTree.AddControl(ctx, control, root); err != nil { return nil, err @@ -116,18 +117,18 @@ func NewResultGroup(ctx context.Context, executionTree *ExecutionTree, treeItem // populate additional properties (this avoids adding GetDocumentation, GetDisplay and GetType to all ModTreeItems) switch t := treeItem.(type) { - case *modconfig.Benchmark: + case *resources.Benchmark: group.Documentation = t.GetDocumentation() group.Display = t.GetDisplay() group.Type = t.GetType() - case *modconfig.Control: + case *resources.Control: group.Documentation = t.GetDocumentation() group.Display = t.GetDisplay() group.Type = t.GetType() } // add child groups for children which are benchmarks for _, c := range treeItem.GetChildren() { - if benchmark, ok := c.(*modconfig.Benchmark); ok { + if benchmark, ok := c.(*resources.Benchmark); ok { // create a result group for this item benchmarkGroup, err := NewResultGroup(ctx, executionTree, benchmark, group) if err != nil { @@ -139,7 +140,7 @@ func NewResultGroup(ctx context.Context, executionTree *ExecutionTree, treeItem group.addResultGroup(benchmarkGroup) } } - if control, ok := c.(*modconfig.Control); ok { + if control, ok := c.(*resources.Control); ok { if err := executionTree.AddControl(ctx, control, group); err != nil { return nil, err } diff --git a/internal/controlexecute/result_row.go b/internal/controlexecute/result_row.go index b1fb48c9..5fbcc21a 100644 --- a/internal/controlexecute/result_row.go +++ b/internal/controlexecute/result_row.go @@ -7,10 +7,10 @@ import ( typehelpers "github.com/turbot/go-kit/types" "github.com/turbot/pipe-fittings/constants" "github.com/turbot/pipe-fittings/error_helpers" - "github.com/turbot/pipe-fittings/modconfig" "github.com/turbot/pipe-fittings/queryresult" "github.com/turbot/pipe-fittings/utils" "github.com/turbot/powerpipe/internal/dashboardtypes" + "github.com/turbot/powerpipe/internal/resources" ) type ResultRows []*ResultRow @@ -55,7 +55,7 @@ type ResultRow struct { // parent control run Run *ControlRun `json:"-"` // source control - Control *modconfig.Control `json:"-" csv:"control_id:UnqualifiedName,control_title:Title,control_description:Description"` + Control *resources.Control `json:"-" csv:"control_id:UnqualifiedName,control_title:Title,control_description:Description"` } // GetDimensionValue returns the value for a dimension key. Returns an empty string with 'false' if not found diff --git a/internal/controlinit/init_data.go b/internal/controlinit/init_data.go index 0e4b51e2..7862f65c 100644 --- a/internal/controlinit/init_data.go +++ b/internal/controlinit/init_data.go @@ -3,8 +3,8 @@ package controlinit import ( "context" "fmt" - "github.com/spf13/cobra" + "github.com/spf13/cobra" "github.com/spf13/viper" "github.com/turbot/pipe-fittings/constants" "github.com/turbot/pipe-fittings/error_helpers" @@ -13,11 +13,12 @@ import ( "github.com/turbot/pipe-fittings/workspace" "github.com/turbot/powerpipe/internal/controldisplay" "github.com/turbot/powerpipe/internal/initialisation" + "github.com/turbot/powerpipe/internal/resources" ) type CheckTarget interface { modconfig.ModTreeItem - *modconfig.Benchmark | *modconfig.Control + *resources.Benchmark | *resources.Control } type InitData[T CheckTarget] struct { @@ -58,8 +59,8 @@ func NewInitData[T CheckTarget](ctx context.Context, cmd *cobra.Command, args [] i.Result.Error = err return i } - - if len(w.GetResourceMaps().Controls)+len(w.GetResourceMaps().Benchmarks) == 0 { + modResources := resources.GetModResources(w.Mod) + if len(modResources.Controls)+len(modResources.Benchmarks) == 0 { i.Result.AddWarnings("no controls or benchmarks found in current workspace") } diff --git a/internal/dashboardevents/dashboard_changed.go b/internal/dashboardevents/dashboard_changed.go index c0b5eb58..1427d14e 100644 --- a/internal/dashboardevents/dashboard_changed.go +++ b/internal/dashboardevents/dashboard_changed.go @@ -2,59 +2,60 @@ package dashboardevents import ( "github.com/turbot/pipe-fittings/modconfig" + "github.com/turbot/powerpipe/internal/resources" ) type DashboardChanged struct { - ChangedDashboards []*modconfig.DashboardTreeItemDiffs - ChangedContainers []*modconfig.DashboardTreeItemDiffs - ChangedControls []*modconfig.DashboardTreeItemDiffs - ChangedBenchmarks []*modconfig.DashboardTreeItemDiffs - ChangedCategories []*modconfig.DashboardTreeItemDiffs - ChangedCards []*modconfig.DashboardTreeItemDiffs - ChangedCharts []*modconfig.DashboardTreeItemDiffs - ChangedFlows []*modconfig.DashboardTreeItemDiffs - ChangedGraphs []*modconfig.DashboardTreeItemDiffs - ChangedHierarchies []*modconfig.DashboardTreeItemDiffs - ChangedImages []*modconfig.DashboardTreeItemDiffs - ChangedInputs []*modconfig.DashboardTreeItemDiffs - ChangedTables []*modconfig.DashboardTreeItemDiffs - ChangedTexts []*modconfig.DashboardTreeItemDiffs - ChangedNodes []*modconfig.DashboardTreeItemDiffs - ChangedEdges []*modconfig.DashboardTreeItemDiffs + ChangedDashboards []*modconfig.ModTreeItemDiffs + ChangedContainers []*modconfig.ModTreeItemDiffs + ChangedControls []*modconfig.ModTreeItemDiffs + ChangedBenchmarks []*modconfig.ModTreeItemDiffs + ChangedCategories []*modconfig.ModTreeItemDiffs + ChangedCards []*modconfig.ModTreeItemDiffs + ChangedCharts []*modconfig.ModTreeItemDiffs + ChangedFlows []*modconfig.ModTreeItemDiffs + ChangedGraphs []*modconfig.ModTreeItemDiffs + ChangedHierarchies []*modconfig.ModTreeItemDiffs + ChangedImages []*modconfig.ModTreeItemDiffs + ChangedInputs []*modconfig.ModTreeItemDiffs + ChangedTables []*modconfig.ModTreeItemDiffs + ChangedTexts []*modconfig.ModTreeItemDiffs + ChangedNodes []*modconfig.ModTreeItemDiffs + ChangedEdges []*modconfig.ModTreeItemDiffs - NewDashboards []*modconfig.Dashboard - NewContainers []*modconfig.DashboardContainer - NewControls []*modconfig.Control - NewBenchmarks []*modconfig.Benchmark - NewCards []*modconfig.DashboardCard - NewCategories []*modconfig.DashboardCategory - NewCharts []*modconfig.DashboardChart - NewFlows []*modconfig.DashboardFlow - NewGraphs []*modconfig.DashboardGraph - NewHierarchies []*modconfig.DashboardHierarchy - NewImages []*modconfig.DashboardImage - NewInputs []*modconfig.DashboardInput - NewTables []*modconfig.DashboardTable - NewTexts []*modconfig.DashboardText - NewNodes []*modconfig.DashboardNode - NewEdges []*modconfig.DashboardEdge + NewDashboards []*resources.Dashboard + NewContainers []*resources.DashboardContainer + NewControls []*resources.Control + NewBenchmarks []*resources.Benchmark + NewCards []*resources.DashboardCard + NewCategories []*resources.DashboardCategory + NewCharts []*resources.DashboardChart + NewFlows []*resources.DashboardFlow + NewGraphs []*resources.DashboardGraph + NewHierarchies []*resources.DashboardHierarchy + NewImages []*resources.DashboardImage + NewInputs []*resources.DashboardInput + NewTables []*resources.DashboardTable + NewTexts []*resources.DashboardText + NewNodes []*resources.DashboardNode + NewEdges []*resources.DashboardEdge - DeletedDashboards []*modconfig.Dashboard - DeletedContainers []*modconfig.DashboardContainer - DeletedControls []*modconfig.Control - DeletedBenchmarks []*modconfig.Benchmark - DeletedCards []*modconfig.DashboardCard - DeletedCategories []*modconfig.DashboardCategory - DeletedCharts []*modconfig.DashboardChart - DeletedFlows []*modconfig.DashboardFlow - DeletedGraphs []*modconfig.DashboardGraph - DeletedHierarchies []*modconfig.DashboardHierarchy - DeletedImages []*modconfig.DashboardImage - DeletedInputs []*modconfig.DashboardInput - DeletedTables []*modconfig.DashboardTable - DeletedTexts []*modconfig.DashboardText - DeletedNodes []*modconfig.DashboardNode - DeletedEdges []*modconfig.DashboardEdge + DeletedDashboards []*resources.Dashboard + DeletedContainers []*resources.DashboardContainer + DeletedControls []*resources.Control + DeletedBenchmarks []*resources.Benchmark + DeletedCards []*resources.DashboardCard + DeletedCategories []*resources.DashboardCategory + DeletedCharts []*resources.DashboardChart + DeletedFlows []*resources.DashboardFlow + DeletedGraphs []*resources.DashboardGraph + DeletedHierarchies []*resources.DashboardHierarchy + DeletedImages []*resources.DashboardImage + DeletedInputs []*resources.DashboardInput + DeletedTables []*resources.DashboardTable + DeletedTexts []*resources.DashboardText + DeletedNodes []*resources.DashboardNode + DeletedEdges []*resources.DashboardEdge } // IsDashboardEvent implements DashboardEvent interface @@ -306,8 +307,8 @@ func (c *DashboardChanged) WalkChangedResources(resourceFunc func(item modconfig return nil } -func (c *DashboardChanged) SetParentsChanged(item modconfig.ModTreeItem, prevResourceMaps *modconfig.ResourceMaps) { - if prevResourceMaps == nil { +func (c *DashboardChanged) SetParentsChanged(item modconfig.ModTreeItem, prevModResources *resources.PowerpipeModResources) { + if prevModResources == nil { return } @@ -315,14 +316,14 @@ func (c *DashboardChanged) SetParentsChanged(item modconfig.ModTreeItem, prevRes for _, parent := range parents { // if the parent DID NOT exist in the previous resource maps, do nothing parsedResourceName, _ := modconfig.ParseResourceName(parent.Name()) - if _, existingResource := prevResourceMaps.GetResource(parsedResourceName); existingResource { + if _, existingResource := prevModResources.GetResource(parsedResourceName); existingResource { c.AddChanged(parent) - c.SetParentsChanged(parent, prevResourceMaps) + c.SetParentsChanged(parent, prevModResources) } } } -func (c *DashboardChanged) diffsContain(diffs []*modconfig.DashboardTreeItemDiffs, item modconfig.ModTreeItem) bool { +func (c *DashboardChanged) diffsContain(diffs []*modconfig.ModTreeItemDiffs, item modconfig.ModTreeItem) bool { for _, d := range diffs { if d.Item.Name() == item.Name() { return true @@ -332,60 +333,60 @@ func (c *DashboardChanged) diffsContain(diffs []*modconfig.DashboardTreeItemDiff } func (c *DashboardChanged) AddChanged(item modconfig.ModTreeItem) { - diff := &modconfig.DashboardTreeItemDiffs{ + diff := &modconfig.ModTreeItemDiffs{ Name: item.Name(), Item: item, ChangedProperties: []string{"Children"}, } switch item.(type) { - case *modconfig.Dashboard: + case *resources.Dashboard: if !c.diffsContain(c.ChangedDashboards, item) { c.ChangedDashboards = append(c.ChangedDashboards, diff) } - case *modconfig.DashboardContainer: + case *resources.DashboardContainer: if !c.diffsContain(c.ChangedContainers, item) { c.ChangedContainers = append(c.ChangedContainers, diff) } - case *modconfig.Control: + case *resources.Control: if !c.diffsContain(c.ChangedControls, item) { c.ChangedControls = append(c.ChangedControls, diff) } - case *modconfig.Benchmark: + case *resources.Benchmark: if !c.diffsContain(c.ChangedBenchmarks, item) { c.ChangedBenchmarks = append(c.ChangedBenchmarks, diff) } - case *modconfig.DashboardCard: + case *resources.DashboardCard: if !c.diffsContain(c.ChangedCards, item) { c.ChangedCards = append(c.ChangedCards, diff) } - case *modconfig.DashboardCategory: + case *resources.DashboardCategory: if !c.diffsContain(c.ChangedCategories, item) { c.ChangedCategories = append(c.ChangedCategories, diff) } - case *modconfig.DashboardChart: + case *resources.DashboardChart: if !c.diffsContain(c.ChangedCharts, item) { c.ChangedCharts = append(c.ChangedCharts, diff) } - case *modconfig.DashboardHierarchy: + case *resources.DashboardHierarchy: if !c.diffsContain(c.ChangedHierarchies, item) { c.ChangedHierarchies = append(c.ChangedHierarchies, diff) } - case *modconfig.DashboardImage: + case *resources.DashboardImage: if !c.diffsContain(c.ChangedImages, item) { c.ChangedImages = append(c.ChangedImages, diff) } - case *modconfig.DashboardInput: + case *resources.DashboardInput: if !c.diffsContain(c.ChangedInputs, item) { c.ChangedInputs = append(c.ChangedInputs, diff) } - case *modconfig.DashboardTable: + case *resources.DashboardTable: if !c.diffsContain(c.ChangedTables, item) { c.ChangedTables = append(c.ChangedTables, diff) } - case *modconfig.DashboardText: + case *resources.DashboardText: if !c.diffsContain(c.ChangedTexts, item) { c.ChangedTexts = append(c.ChangedTexts, diff) } diff --git a/internal/dashboardexecute/check_run.go b/internal/dashboardexecute/check_run.go index 71787244..bbe3bccc 100644 --- a/internal/dashboardexecute/check_run.go +++ b/internal/dashboardexecute/check_run.go @@ -2,6 +2,7 @@ package dashboardexecute import ( "context" + "github.com/turbot/powerpipe/internal/resources" "github.com/turbot/pipe-fittings/backend" "github.com/turbot/pipe-fittings/modconfig" @@ -32,13 +33,13 @@ func (r *CheckRun) AsTreeNode() *steampipeconfig.SnapshotTreeNode { return r.Root.AsTreeNode() } -func NewCheckRun(resource modconfig.DashboardLeafNode, parent dashboardtypes.DashboardParent, executionTree *DashboardExecutionTree) (*CheckRun, error) { +func NewCheckRun(resource resources.DashboardLeafNode, parent dashboardtypes.DashboardParent, executionTree *DashboardExecutionTree) (*CheckRun, error) { r := &CheckRun{SessionId: executionTree.sessionId} // create NewDashboardTreeRunImpl // (we must create after creating the run as it requires a ref to the run) r.DashboardParentImpl = newDashboardParentImpl(resource, parent, r, executionTree) - r.NodeType = resource.BlockType() + r.NodeType = resource.GetBlockType() // set status to initialized r.Status = dashboardtypes.RunInitialized // add r into execution tree @@ -86,7 +87,7 @@ func (r *CheckRun) Initialise(ctx context.Context) { r.SetError(ctx, err) return } - executionTree, err := controlexecute.NewExecutionTree(ctx, r.executionTree.workspace.Workspace, client, workspace.ResourceFilter{}, r.resource) + executionTree, err := controlexecute.NewExecutionTree(ctx, r.executionTree.workspace, client, workspace.ResourceFilter{}, r.resource) if err != nil { // set the error status on the counter - this will raise counter error event r.SetError(ctx, err) diff --git a/internal/dashboardexecute/container_run.go b/internal/dashboardexecute/container_run.go index faf8a892..5d2242ed 100644 --- a/internal/dashboardexecute/container_run.go +++ b/internal/dashboardexecute/container_run.go @@ -3,9 +3,9 @@ package dashboardexecute import ( "context" "fmt" + "github.com/turbot/powerpipe/internal/resources" "log/slog" - "github.com/turbot/pipe-fittings/modconfig" "github.com/turbot/pipe-fittings/steampipeconfig" "github.com/turbot/powerpipe/internal/dashboardtypes" ) @@ -14,7 +14,7 @@ import ( type DashboardContainerRun struct { DashboardParentImpl - dashboardNode *modconfig.DashboardContainer + dashboardNode *resources.DashboardContainer } func (r *DashboardContainerRun) AsTreeNode() *steampipeconfig.SnapshotTreeNode { @@ -29,7 +29,7 @@ func (r *DashboardContainerRun) AsTreeNode() *steampipeconfig.SnapshotTreeNode { return res } -func NewDashboardContainerRun(container *modconfig.DashboardContainer, parent dashboardtypes.DashboardParent, executionTree *DashboardExecutionTree) (*DashboardContainerRun, error) { +func NewDashboardContainerRun(container *resources.DashboardContainer, parent dashboardtypes.DashboardParent, executionTree *DashboardExecutionTree) (*DashboardContainerRun, error) { children := container.GetChildren() r := &DashboardContainerRun{dashboardNode: container} @@ -49,25 +49,25 @@ func NewDashboardContainerRun(container *modconfig.DashboardContainer, parent da var childRun dashboardtypes.DashboardTreeRun var err error switch i := child.(type) { - case *modconfig.DashboardContainer: + case *resources.DashboardContainer: childRun, err = NewDashboardContainerRun(i, r, executionTree) if err != nil { return nil, err } - case *modconfig.Dashboard: + case *resources.Dashboard: childRun, err = NewDashboardRun(i, r, executionTree) if err != nil { return nil, err } - case *modconfig.Benchmark, *modconfig.Control: - childRun, err = NewCheckRun(i.(modconfig.DashboardLeafNode), r, executionTree) + case *resources.Benchmark, *resources.Control: + childRun, err = NewCheckRun(i.(resources.DashboardLeafNode), r, executionTree) if err != nil { return nil, err } default: // ensure this item is a DashboardLeafNode - leafNode, ok := i.(modconfig.DashboardLeafNode) + leafNode, ok := i.(resources.DashboardLeafNode) if !ok { return nil, fmt.Errorf("child %s does not implement DashboardLeafNode", i.Name()) } diff --git a/internal/dashboardexecute/dashboard_execution_tree.go b/internal/dashboardexecute/dashboard_execution_tree.go index adc1ea3f..c0ffcc5f 100644 --- a/internal/dashboardexecute/dashboard_execution_tree.go +++ b/internal/dashboardexecute/dashboard_execution_tree.go @@ -3,12 +3,13 @@ package dashboardexecute import ( "context" "fmt" + "github.com/spf13/viper" + "github.com/turbot/powerpipe/internal/resources" "golang.org/x/exp/maps" "log/slog" "sync" "time" - "github.com/spf13/viper" "github.com/turbot/pipe-fittings/backend" "github.com/turbot/pipe-fittings/constants" "github.com/turbot/pipe-fittings/modconfig" @@ -17,8 +18,8 @@ import ( "github.com/turbot/pipe-fittings/utils" "github.com/turbot/powerpipe/internal/dashboardevents" "github.com/turbot/powerpipe/internal/dashboardtypes" - "github.com/turbot/powerpipe/internal/dashboardworkspace" "github.com/turbot/powerpipe/internal/db_client" + "github.com/turbot/powerpipe/internal/workspace" ) // DashboardExecutionTree is a structure representing the control result hierarchy @@ -33,7 +34,7 @@ type DashboardExecutionTree struct { defaultClientMap *db_client.ClientMap // map of executing runs, keyed by full name runs map[string]dashboardtypes.DashboardTreeRun - workspace *dashboardworkspace.WorkspaceEvents + workspace *workspace.PowerpipeWorkspace runComplete chan dashboardtypes.DashboardTreeRun // map of subscribers to notify when an input value changes @@ -46,7 +47,7 @@ type DashboardExecutionTree struct { searchPathConfig backend.SearchPathConfig } -func newDashboardExecutionTree(rootResource modconfig.ModTreeItem, sessionId string, workspace *dashboardworkspace.WorkspaceEvents, defaultClientMap *db_client.ClientMap, opts ...backend.ConnectOption) (*DashboardExecutionTree, error) { +func newDashboardExecutionTree(rootResource modconfig.ModTreeItem, sessionId string, workspace *workspace.PowerpipeWorkspace, defaultClientMap *db_client.ClientMap, opts ...backend.ConnectOption) (*DashboardExecutionTree, error) { // now populate the DashboardExecutionTree executionTree := &DashboardExecutionTree{ dashboardName: rootResource.Name(), @@ -89,13 +90,13 @@ func newDashboardExecutionTree(rootResource modconfig.ModTreeItem, sessionId str func (e *DashboardExecutionTree) createRootItem(rootResource modconfig.ModTreeItem) (dashboardtypes.DashboardTreeRun, error) { switch r := rootResource.(type) { - case *modconfig.Dashboard: + case *resources.Dashboard: return NewDashboardRun(r, e, e) - case *modconfig.Benchmark: + case *resources.Benchmark: return NewCheckRun(r, e, e) - case *modconfig.Query: + case *resources.Query: // wrap this in a chart and a dashboard - dashboard, err := modconfig.NewQueryDashboard(r) + dashboard, err := resources.NewQueryDashboard(r) // TACTICAL - set the execution tree dashboard name from the query dashboard e.dashboardName = dashboard.Name() if err != nil { @@ -340,7 +341,7 @@ func (*DashboardExecutionTree) AsTreeNode() *steampipeconfig.SnapshotTreeNode { panic("should never call for DashboardExecutionTree") } -func (*DashboardExecutionTree) GetResource() modconfig.DashboardLeafNode { +func (*DashboardExecutionTree) GetResource() resources.DashboardLeafNode { panic("should never call for DashboardExecutionTree") } diff --git a/internal/dashboardexecute/dashboard_parent_impl.go b/internal/dashboardexecute/dashboard_parent_impl.go index d56024d1..dd118b1b 100644 --- a/internal/dashboardexecute/dashboard_parent_impl.go +++ b/internal/dashboardexecute/dashboard_parent_impl.go @@ -3,11 +3,11 @@ package dashboardexecute import ( "context" "fmt" + "github.com/turbot/powerpipe/internal/resources" "log/slog" "sync" "github.com/turbot/pipe-fittings/error_helpers" - "github.com/turbot/pipe-fittings/modconfig" "github.com/turbot/pipe-fittings/schema" "github.com/turbot/pipe-fittings/utils" "github.com/turbot/powerpipe/internal/dashboardtypes" @@ -22,7 +22,7 @@ type DashboardParentImpl struct { childStatusLock *sync.Mutex } -func newDashboardParentImpl(resource modconfig.DashboardLeafNode, parent dashboardtypes.DashboardParent, run dashboardtypes.DashboardTreeRun, executionTree *DashboardExecutionTree) DashboardParentImpl { +func newDashboardParentImpl(resource resources.DashboardLeafNode, parent dashboardtypes.DashboardParent, run dashboardtypes.DashboardTreeRun, executionTree *DashboardExecutionTree) DashboardParentImpl { return DashboardParentImpl{ DashboardTreeRunImpl: NewDashboardTreeRunImpl(resource, parent, run, executionTree), childStatusLock: new(sync.Mutex), diff --git a/internal/dashboardexecute/dashboard_run.go b/internal/dashboardexecute/dashboard_run.go index f081775b..f5ac81b0 100644 --- a/internal/dashboardexecute/dashboard_run.go +++ b/internal/dashboardexecute/dashboard_run.go @@ -3,9 +3,9 @@ package dashboardexecute import ( "context" "fmt" + "github.com/turbot/powerpipe/internal/resources" "log/slog" - "github.com/turbot/pipe-fittings/modconfig" "github.com/turbot/pipe-fittings/schema" "github.com/turbot/pipe-fittings/steampipeconfig" "github.com/turbot/powerpipe/internal/dashboardtypes" @@ -16,7 +16,7 @@ type DashboardRun struct { runtimeDependencyPublisherImpl parent dashboardtypes.DashboardParent - dashboard *modconfig.Dashboard + dashboard *resources.Dashboard } func (r *DashboardRun) AsTreeNode() *steampipeconfig.SnapshotTreeNode { @@ -36,7 +36,7 @@ func (r *DashboardRun) AsTreeNode() *steampipeconfig.SnapshotTreeNode { return res } -func NewDashboardRun(dashboard *modconfig.Dashboard, parent dashboardtypes.DashboardParent, executionTree *DashboardExecutionTree) (*DashboardRun, error) { +func NewDashboardRun(dashboard *resources.Dashboard, parent dashboardtypes.DashboardParent, executionTree *DashboardExecutionTree) (*DashboardRun, error) { r := &DashboardRun{ parent: parent, dashboard: dashboard, @@ -100,7 +100,7 @@ func (r *DashboardRun) Execute(ctx context.Context) { func (*DashboardRun) IsSnapshotPanel() {} // GetInput searches for an input with the given name -func (r *DashboardRun) GetInput(name string) (*modconfig.DashboardInput, bool) { +func (r *DashboardRun) GetInput(name string) (*resources.DashboardInput, bool) { return r.dashboard.GetInput(name) } @@ -123,25 +123,25 @@ func (r *DashboardRun) createChildRuns(executionTree *DashboardExecutionTree) er var childRun dashboardtypes.DashboardTreeRun var err error switch i := child.(type) { - case *modconfig.DashboardWith: + case *resources.DashboardWith: // ignore as with runs are created by RuntimeDependencyPublisherImpl continue - case *modconfig.Dashboard: + case *resources.Dashboard: childRun, err = NewDashboardRun(i, r, executionTree) if err != nil { return err } - case *modconfig.DashboardContainer: + case *resources.DashboardContainer: childRun, err = NewDashboardContainerRun(i, r, executionTree) if err != nil { return err } - case *modconfig.Benchmark, *modconfig.Control: - childRun, err = NewCheckRun(i.(modconfig.DashboardLeafNode), r, executionTree) + case *resources.Benchmark, *resources.Control: + childRun, err = NewCheckRun(i.(resources.DashboardLeafNode), r, executionTree) if err != nil { return err } - case *modconfig.DashboardInput: + case *resources.DashboardInput: // NOTE: clone the input to avoid mutating the original // TODO remove the need for this when we refactor input values resolution // TODO https://github.com/turbot/steampipe/issues/2864 @@ -157,7 +157,7 @@ func (r *DashboardRun) createChildRuns(executionTree *DashboardExecutionTree) er default: // ensure this item is a DashboardLeafNode - leafNode, ok := i.(modconfig.DashboardLeafNode) + leafNode, ok := i.(resources.DashboardLeafNode) if !ok { return fmt.Errorf("child %s does not implement DashboardLeafNode", i.Name()) } diff --git a/internal/dashboardexecute/dashboard_tree_run_impl.go b/internal/dashboardexecute/dashboard_tree_run_impl.go index 7e9bbddf..d6aba2c8 100644 --- a/internal/dashboardexecute/dashboard_tree_run_impl.go +++ b/internal/dashboardexecute/dashboard_tree_run_impl.go @@ -2,10 +2,10 @@ package dashboardexecute import ( "context" + "github.com/turbot/powerpipe/internal/resources" "log/slog" "github.com/turbot/pipe-fittings/error_helpers" - "github.com/turbot/pipe-fittings/modconfig" "github.com/turbot/pipe-fittings/steampipeconfig" "github.com/turbot/powerpipe/internal/dashboardevents" "github.com/turbot/powerpipe/internal/dashboardtypes" @@ -29,21 +29,21 @@ type DashboardTreeRunImpl struct { err error parent dashboardtypes.DashboardParent executionTree *DashboardExecutionTree - resource modconfig.DashboardLeafNode + resource resources.DashboardLeafNode // store the top level run which embeds this struct // we need this for setStatus which serialises the run for the message payload run dashboardtypes.DashboardTreeRun } -func NewDashboardTreeRunImpl(resource modconfig.DashboardLeafNode, parent dashboardtypes.DashboardParent, run dashboardtypes.DashboardTreeRun, executionTree *DashboardExecutionTree) DashboardTreeRunImpl { +func NewDashboardTreeRunImpl(resource resources.DashboardLeafNode, parent dashboardtypes.DashboardParent, run dashboardtypes.DashboardTreeRun, executionTree *DashboardExecutionTree) DashboardTreeRunImpl { // NOTE: we MUST declare children inline - therefore we cannot share children between runs in the tree // (if we supported the children property then we could reuse resources) // so FOR NOW it is safe to use the container name directly as the run name res := DashboardTreeRunImpl{ Name: resource.Name(), Title: resource.GetTitle(), - NodeType: resource.BlockType(), + NodeType: resource.GetBlockType(), Width: resource.GetWidth(), Display: resource.GetDisplay(), Description: resource.GetDescription(), @@ -127,7 +127,7 @@ func (r *DashboardTreeRunImpl) AsTreeNode() *steampipeconfig.SnapshotTreeNode { } // GetResource implements DashboardTreeRun -func (r *DashboardTreeRunImpl) GetResource() modconfig.DashboardLeafNode { +func (r *DashboardTreeRunImpl) GetResource() resources.DashboardLeafNode { return r.resource } diff --git a/internal/dashboardexecute/executor.go b/internal/dashboardexecute/executor.go index d5568ac0..9e706d3b 100644 --- a/internal/dashboardexecute/executor.go +++ b/internal/dashboardexecute/executor.go @@ -4,7 +4,6 @@ import ( "context" "encoding/json" "fmt" - "github.com/turbot/powerpipe/internal/db_client" "os" "strings" "sync" @@ -16,7 +15,8 @@ import ( "github.com/turbot/pipe-fittings/utils" "github.com/turbot/powerpipe/internal/dashboardevents" "github.com/turbot/powerpipe/internal/dashboardtypes" - "github.com/turbot/powerpipe/internal/dashboardworkspace" + "github.com/turbot/powerpipe/internal/db_client" + "github.com/turbot/powerpipe/internal/workspace" ) type DashboardExecutor struct { @@ -43,7 +43,7 @@ func NewDashboardExecutor(defaultClient *db_client.ClientMap) *DashboardExecutor var Executor *DashboardExecutor -func (e *DashboardExecutor) ExecuteDashboard(ctx context.Context, sessionId string, rootResource modconfig.ModTreeItem, inputs map[string]any, workspace *dashboardworkspace.WorkspaceEvents, opts ...backend.ConnectOption) (err error) { +func (e *DashboardExecutor) ExecuteDashboard(ctx context.Context, sessionId string, rootResource modconfig.ModTreeItem, inputs map[string]any, workspace *workspace.PowerpipeWorkspace, opts ...backend.ConnectOption) (err error) { var executionTree *DashboardExecutionTree defer func() { if err == nil && ctx.Err() != nil { @@ -112,9 +112,9 @@ func (e *DashboardExecutor) validateInputs(executionTree *DashboardExecutionTree return nil } -func (e *DashboardExecutor) LoadSnapshot(ctx context.Context, sessionId, snapshotName string, w *dashboardworkspace.WorkspaceEvents) (map[string]any, error) { +func (e *DashboardExecutor) LoadSnapshot(ctx context.Context, sessionId, snapshotName string, w *workspace.PowerpipeWorkspace) (map[string]any, error) { // find snapshot path in workspace - snapshotPath, ok := w.GetResourceMaps().Snapshots[snapshotName] + snapshotPath, ok := w.GetPowerpipeModResources().Snapshots[snapshotName] if !ok { return nil, fmt.Errorf("snapshot %s not found in %s (%s)", snapshotName, w.Mod.Name(), w.Path) } diff --git a/internal/dashboardexecute/leaf_run.go b/internal/dashboardexecute/leaf_run.go index 5cf57eec..194aee91 100644 --- a/internal/dashboardexecute/leaf_run.go +++ b/internal/dashboardexecute/leaf_run.go @@ -3,13 +3,13 @@ package dashboardexecute import ( "context" "fmt" + "github.com/turbot/powerpipe/internal/resources" "golang.org/x/exp/maps" "log/slog" "time" "github.com/turbot/pipe-fittings/backend" "github.com/turbot/pipe-fittings/error_helpers" - "github.com/turbot/pipe-fittings/modconfig" "github.com/turbot/pipe-fittings/queryresult" "github.com/turbot/pipe-fittings/schema" "github.com/turbot/pipe-fittings/statushooks" @@ -24,7 +24,7 @@ type LeafRun struct { // all RuntimeDependencySubscribers are also publishers as they have args/params RuntimeDependencySubscriberImpl - Resource modconfig.DashboardLeafNode `json:"-"` + Resource resources.DashboardLeafNode `json:"-"` // this is populated by retrieving Resource properties with the snapshot tag Properties map[string]any `json:"properties,omitempty"` Data *dashboardtypes.LeafData `json:"data,omitempty"` @@ -42,7 +42,7 @@ func (r *LeafRun) AsTreeNode() *steampipeconfig.SnapshotTreeNode { } } -func NewLeafRun(resource modconfig.DashboardLeafNode, parent dashboardtypes.DashboardParent, executionTree *DashboardExecutionTree, opts ...LeafRunOption) (*LeafRun, error) { +func NewLeafRun(resource resources.DashboardLeafNode, parent dashboardtypes.DashboardParent, executionTree *DashboardExecutionTree, opts ...LeafRunOption) (*LeafRun, error) { r := &LeafRun{ Resource: resource, Properties: make(map[string]any), @@ -67,7 +67,7 @@ func NewLeafRun(resource modconfig.DashboardLeafNode, parent dashboardtypes.Dash return nil, err } - r.NodeType = resource.BlockType() + r.NodeType = resource.GetBlockType() // if the node has no runtime dependencies, resolve the sql if !r.hasRuntimeDependencies() { @@ -119,7 +119,7 @@ func (r *LeafRun) createChildRuns(executionTree *DashboardExecutionTree) error { for i, c := range children { var opts []LeafRunOption - childRun, err := NewLeafRun(c.(modconfig.DashboardLeafNode), r, executionTree, opts...) + childRun, err := NewLeafRun(c.(resources.DashboardLeafNode), r, executionTree, opts...) if err != nil { errors = append(errors, err) continue @@ -258,7 +258,7 @@ func (r *LeafRun) combineChildData() { childLeafRun := c.(*LeafRun) data := childLeafRun.Data // if there is no data or this is a 'with', skip - if data == nil || childLeafRun.resource.BlockType() == schema.BlockTypeWith { + if data == nil || childLeafRun.resource.GetBlockType() == schema.BlockTypeWith { continue } for _, s := range data.Columns { diff --git a/internal/dashboardexecute/referenced_variables.go b/internal/dashboardexecute/referenced_variables.go index ba50ce17..06516078 100644 --- a/internal/dashboardexecute/referenced_variables.go +++ b/internal/dashboardexecute/referenced_variables.go @@ -2,11 +2,12 @@ package dashboardexecute import ( "fmt" + "github.com/turbot/powerpipe/internal/resources" "strings" "github.com/turbot/pipe-fittings/modconfig" "github.com/turbot/powerpipe/internal/dashboardtypes" - "github.com/turbot/powerpipe/internal/dashboardworkspace" + "github.com/turbot/powerpipe/internal/workspace" ) // GetReferencedVariables builds map of variables values containing only those mod variables which are referenced @@ -14,7 +15,7 @@ import ( // . // the VariableValues map will contain these variables with the name format .var., // so we must convert the name -func GetReferencedVariables(root dashboardtypes.DashboardTreeRun, w *dashboardworkspace.WorkspaceEvents) (map[string]string, error) { +func GetReferencedVariables(root dashboardtypes.DashboardTreeRun, w *workspace.PowerpipeWorkspace) (map[string]string, error) { var referencedVariables = make(map[string]string) addReferencedVars := func(refs []*modconfig.ResourceReference) { @@ -50,7 +51,7 @@ func GetReferencedVariables(root dashboardtypes.DashboardTreeRun, w *dashboardwo } case *CheckRun: switch n := r.resource.(type) { - case *modconfig.Benchmark: + case *resources.Benchmark: err := n.WalkResources( func(resource modconfig.ModTreeItem) (bool, error) { if resourceWithMetadata, ok := resource.(modconfig.ResourceWithMetadata); ok { @@ -62,7 +63,7 @@ func GetReferencedVariables(root dashboardtypes.DashboardTreeRun, w *dashboardwo if err != nil { return nil, err } - case *modconfig.Control: + case *resources.Control: addReferencedVars(n.GetReferences()) } } diff --git a/internal/dashboardexecute/runtime_dependency_publisher.go b/internal/dashboardexecute/runtime_dependency_publisher.go index 9013106f..6dc128df 100644 --- a/internal/dashboardexecute/runtime_dependency_publisher.go +++ b/internal/dashboardexecute/runtime_dependency_publisher.go @@ -1,13 +1,13 @@ package dashboardexecute import ( - "github.com/turbot/pipe-fittings/modconfig" "github.com/turbot/powerpipe/internal/dashboardtypes" + "github.com/turbot/powerpipe/internal/resources" ) type RuntimeDependencyPublisher interface { dashboardtypes.DashboardTreeRun - ProvidesRuntimeDependency(dependency *modconfig.RuntimeDependency) bool + ProvidesRuntimeDependency(dependency *resources.RuntimeDependency) bool SubscribeToRuntimeDependency(name string, opts ...RuntimeDependencyPublishOption) chan *dashboardtypes.ResolvedRuntimeDependencyValue PublishRuntimeDependencyValue(name string, result *dashboardtypes.ResolvedRuntimeDependencyValue) GetWithRuns() map[string]*LeafRun diff --git a/internal/dashboardexecute/runtime_dependency_publisher_impl.go b/internal/dashboardexecute/runtime_dependency_publisher_impl.go index 104798bb..6c52c009 100644 --- a/internal/dashboardexecute/runtime_dependency_publisher_impl.go +++ b/internal/dashboardexecute/runtime_dependency_publisher_impl.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "github.com/turbot/powerpipe/internal/resources" "log/slog" "strconv" "sync" @@ -22,19 +23,19 @@ type runtimeDependencyPublisherImpl struct { subscriptions map[string][]*RuntimeDependencyPublishTarget withValueMutex *sync.Mutex withRuns map[string]*LeafRun - inputs map[string]*modconfig.DashboardInput + inputs map[string]*resources.DashboardInput } -func newRuntimeDependencyPublisherImpl(resource modconfig.DashboardLeafNode, parent dashboardtypes.DashboardParent, run dashboardtypes.DashboardTreeRun, executionTree *DashboardExecutionTree) runtimeDependencyPublisherImpl { +func newRuntimeDependencyPublisherImpl(resource resources.DashboardLeafNode, parent dashboardtypes.DashboardParent, run dashboardtypes.DashboardTreeRun, executionTree *DashboardExecutionTree) runtimeDependencyPublisherImpl { b := runtimeDependencyPublisherImpl{ DashboardParentImpl: newDashboardParentImpl(resource, parent, run, executionTree), subscriptions: make(map[string][]*RuntimeDependencyPublishTarget), - inputs: make(map[string]*modconfig.DashboardInput), + inputs: make(map[string]*resources.DashboardInput), withRuns: make(map[string]*LeafRun), withValueMutex: new(sync.Mutex), } // if the resource is a query provider, get params and set status - if queryProvider, ok := resource.(modconfig.QueryProvider); ok { + if queryProvider, ok := resource.(resources.QueryProvider); ok { // get params b.Params = queryProvider.GetParams() if queryProvider.RequiresExecution(queryProvider) || len(queryProvider.GetChildren()) > 0 { @@ -59,14 +60,14 @@ func (p *runtimeDependencyPublisherImpl) GetName() string { return p.Name } -func (p *runtimeDependencyPublisherImpl) ProvidesRuntimeDependency(dependency *modconfig.RuntimeDependency) bool { +func (p *runtimeDependencyPublisherImpl) ProvidesRuntimeDependency(dependency *resources.RuntimeDependency) bool { resourceName := dependency.SourceResourceName() switch dependency.PropertyPath.ItemType { case schema.BlockTypeWith: // we cannot use withRuns here as if withs have dependencies on each other, // this function may be called before all runs have been added // instead, look directly at the underlying resource withs - if wp, ok := p.resource.(modconfig.WithProvider); ok { + if wp, ok := p.resource.(resources.WithProvider); ok { for _, w := range wp.GetWiths() { if w.UnqualifiedName == resourceName { return true @@ -122,7 +123,7 @@ func (p *runtimeDependencyPublisherImpl) GetWithRuns() map[string]*LeafRun { func (p *runtimeDependencyPublisherImpl) initWiths() error { // if the resource is a runtime dependency provider, create with runs and resolve dependencies - wp, ok := p.resource.(modconfig.WithProvider) + wp, ok := p.resource.(resources.WithProvider) if !ok { return nil } @@ -263,7 +264,7 @@ func populateData(withData *dashboardtypes.LeafData, result *dashboardtypes.Reso } } -func (p *runtimeDependencyPublisherImpl) createWithRuns(withs []*modconfig.DashboardWith, executionTree *DashboardExecutionTree) error { +func (p *runtimeDependencyPublisherImpl) createWithRuns(withs []*resources.DashboardWith, executionTree *DashboardExecutionTree) error { for _, w := range withs { // NOTE: set the name of the run to be the scoped name withRunName := fmt.Sprintf("%s.%s", p.GetName(), w.UnqualifiedName) diff --git a/internal/dashboardexecute/runtime_dependency_subscriber_impl.go b/internal/dashboardexecute/runtime_dependency_subscriber_impl.go index 7e1f2b8c..00d92090 100644 --- a/internal/dashboardexecute/runtime_dependency_subscriber_impl.go +++ b/internal/dashboardexecute/runtime_dependency_subscriber_impl.go @@ -12,6 +12,7 @@ import ( "github.com/turbot/pipe-fittings/modconfig" "github.com/turbot/pipe-fittings/schema" "github.com/turbot/powerpipe/internal/dashboardtypes" + "github.com/turbot/powerpipe/internal/resources" "golang.org/x/exp/maps" ) @@ -29,7 +30,7 @@ type RuntimeDependencySubscriberImpl struct { RuntimeDependencyNames []string `json:"dependencies,omitempty"` } -func NewRuntimeDependencySubscriber(resource modconfig.DashboardLeafNode, parent dashboardtypes.DashboardParent, run dashboardtypes.DashboardTreeRun, executionTree *DashboardExecutionTree) *RuntimeDependencySubscriberImpl { +func NewRuntimeDependencySubscriber(resource resources.DashboardLeafNode, parent dashboardtypes.DashboardParent, run dashboardtypes.DashboardTreeRun, executionTree *DashboardExecutionTree) *RuntimeDependencySubscriberImpl { b := &RuntimeDependencySubscriberImpl{ runtimeDependencies: make(map[string]*dashboardtypes.ResolvedRuntimeDependency), } @@ -48,7 +49,7 @@ func (s *RuntimeDependencySubscriberImpl) GetBaseDependencySubscriber() RuntimeD // if the resource is a runtime dependency provider, create with runs and resolve dependencies func (s *RuntimeDependencySubscriberImpl) initRuntimeDependencies(executionTree *DashboardExecutionTree) error { - if _, ok := s.resource.(modconfig.RuntimeDependencyProvider); !ok { + if _, ok := s.resource.(resources.RuntimeDependencyProvider); !ok { return nil } @@ -68,11 +69,11 @@ func (s *RuntimeDependencySubscriberImpl) initRuntimeDependencies(executionTree func (s *RuntimeDependencySubscriberImpl) initBaseRuntimeDependencySubscriber(executionTree *DashboardExecutionTree) error { if base := s.resource.(modconfig.HclResource).GetBase(); base != nil { - if _, ok := base.(modconfig.RuntimeDependencyProvider); ok { + if _, ok := base.(resources.RuntimeDependencyProvider); ok { // create base dependency subscriber // pass ourselves as 'run' // - this is only used when sending update events, which will not happen for the baseDependencySubscriber - s.baseDependencySubscriber = NewRuntimeDependencySubscriber(base.(modconfig.DashboardLeafNode), nil, s, executionTree) + s.baseDependencySubscriber = NewRuntimeDependencySubscriber(base.(resources.DashboardLeafNode), nil, s, executionTree) err := s.baseDependencySubscriber.initRuntimeDependencies(executionTree) if err != nil { return err @@ -87,7 +88,7 @@ func (s *RuntimeDependencySubscriberImpl) initBaseRuntimeDependencySubscriber(ex // if this node has runtime dependencies, find the publisher of the dependency and create a dashboardtypes.ResolvedRuntimeDependency // which we use to resolve the values func (s *RuntimeDependencySubscriberImpl) resolveRuntimeDependencies() error { - rdp, ok := s.resource.(modconfig.RuntimeDependencyProvider) + rdp, ok := s.resource.(resources.RuntimeDependencyProvider) if !ok { return nil } @@ -136,7 +137,7 @@ func (s *RuntimeDependencySubscriberImpl) resolveRuntimeDependencies() error { return nil } -func (s *RuntimeDependencySubscriberImpl) findRuntimeDependencyPublisher(runtimeDependency *modconfig.RuntimeDependency) RuntimeDependencyPublisher { +func (s *RuntimeDependencySubscriberImpl) findRuntimeDependencyPublisher(runtimeDependency *resources.RuntimeDependency) RuntimeDependencyPublisher { // the runtime dependency publisher is either the root dashboard run, // or if this resource (or in case of a node/edge, the resource parent) has a base, // the baseDependencySubscriber for that base @@ -312,7 +313,7 @@ func (s *RuntimeDependencySubscriberImpl) findRuntimeDependencyForParentProperty // resolve the sql for this leaf run into the source sql and resolved args func (s *RuntimeDependencySubscriberImpl) resolveSQLAndArgs() error { slog.Debug("resolveSQLAndArgs", "name", s.resource.Name()) - queryProvider, ok := s.resource.(modconfig.QueryProvider) + queryProvider, ok := s.resource.(resources.QueryProvider) if !ok { // not a query provider - nothing to do return nil @@ -348,12 +349,12 @@ func (s *RuntimeDependencySubscriberImpl) resolveSQLAndArgs() error { // otherwise just resolve the args // merge the base args with the runtime args - runtimeArgs, err = modconfig.MergeArgs(queryProvider, runtimeArgs) + runtimeArgs, err = resources.MergeArgs(queryProvider, runtimeArgs) if err != nil { return err } - args, err := modconfig.ResolveArgs(queryProvider, runtimeArgs) + args, err := resources.ResolveArgs(queryProvider, runtimeArgs) if err != nil { return err } @@ -362,7 +363,7 @@ func (s *RuntimeDependencySubscriberImpl) resolveSQLAndArgs() error { return nil } -func (s *RuntimeDependencySubscriberImpl) populateParamDefaults(provider modconfig.QueryProvider) error { +func (s *RuntimeDependencySubscriberImpl) populateParamDefaults(provider resources.QueryProvider) error { paramDefs := provider.GetParams() for _, paramDef := range paramDefs { if dep := s.findRuntimeDependencyForParentProperty(paramDef.UnqualifiedName); dep != nil { @@ -379,8 +380,8 @@ func (s *RuntimeDependencySubscriberImpl) populateParamDefaults(provider modconf } // convert runtime dependencies into arg map -func (s *RuntimeDependencySubscriberImpl) buildRuntimeDependencyArgs() (*modconfig.QueryArgs, error) { - res := modconfig.NewQueryArgs() +func (s *RuntimeDependencySubscriberImpl) buildRuntimeDependencyArgs() (*resources.QueryArgs, error) { + res := resources.NewQueryArgs() slog.Debug("buildRuntimeDependencyArgs - %d runtime dependencies", s.resource.Name(), len(s.runtimeDependencies)) diff --git a/internal/dashboardexecute/snapshot.go b/internal/dashboardexecute/snapshot.go index c89c3757..e8e3636c 100644 --- a/internal/dashboardexecute/snapshot.go +++ b/internal/dashboardexecute/snapshot.go @@ -6,10 +6,10 @@ import ( "github.com/turbot/pipe-fittings/modconfig" "github.com/turbot/pipe-fittings/steampipeconfig" "github.com/turbot/powerpipe/internal/dashboardevents" - "github.com/turbot/powerpipe/internal/dashboardworkspace" + "github.com/turbot/powerpipe/internal/workspace" ) -func GenerateSnapshot(ctx context.Context, w *dashboardworkspace.WorkspaceEvents, rootResource modconfig.ModTreeItem, inputs map[string]any) (snapshot *steampipeconfig.SteampipeSnapshot, err error) { +func GenerateSnapshot(ctx context.Context, w *workspace.PowerpipeWorkspace, rootResource modconfig.ModTreeItem, inputs map[string]any) (snapshot *steampipeconfig.SteampipeSnapshot, err error) { // no session for manual execution sessionId := "" errorChannel := make(chan error) @@ -39,7 +39,7 @@ func GenerateSnapshot(ctx context.Context, w *dashboardworkspace.WorkspaceEvents // if the root resource has no corresponding filename, this must be a query snapshot - update the filename root if rootResource.GetDeclRange().Filename == "" { - fileRootName = rootResource.BlockType() + fileRootName = rootResource.GetBlockType() } snapshot.FileNameRoot = fileRootName diff --git a/internal/dashboardserver/payload.go b/internal/dashboardserver/payload.go index a4dcc844..060b2a31 100644 --- a/internal/dashboardserver/payload.go +++ b/internal/dashboardserver/payload.go @@ -4,8 +4,9 @@ import ( "context" "encoding/json" "fmt" - "github.com/spf13/viper" + "github.com/turbot/powerpipe/internal/resources" + typeHelpers "github.com/turbot/go-kit/types" "github.com/turbot/pipe-fittings/app_specific" "github.com/turbot/pipe-fittings/backend" @@ -16,21 +17,22 @@ import ( "github.com/turbot/powerpipe/internal/dashboardassets" "github.com/turbot/powerpipe/internal/dashboardevents" "github.com/turbot/powerpipe/internal/dashboardexecute" - "github.com/turbot/powerpipe/internal/dashboardworkspace" "github.com/turbot/powerpipe/internal/db_client" + "github.com/turbot/powerpipe/internal/workspace" "github.com/turbot/steampipe-plugin-sdk/v5/sperr" ) -func buildServerMetadataPayload(workspaceResources *modconfig.ResourceMaps, pipesMetadata *steampipeconfig.PipesMetadata) ([]byte, error) { +func buildServerMetadataPayload(rm modconfig.ModResources, pipesMetadata *steampipeconfig.PipesMetadata) ([]byte, error) { + workspaceResources := rm.(*resources.PowerpipeModResources) installedMods := make(map[string]*ModMetadata) for _, mod := range workspaceResources.Mods { // Ignore current mod - if mod.FullName == workspaceResources.Mod.FullName { + if mod.GetFullName() == workspaceResources.Mod.GetFullName() { continue } - installedMods[mod.FullName] = &ModMetadata{ - Title: typeHelpers.SafeString(mod.Title), - FullName: mod.FullName, + installedMods[mod.GetFullName()] = &ModMetadata{ + Title: typeHelpers.SafeString(mod.GetTitle()), + FullName: mod.GetFullName(), ShortName: mod.ShortName, } } @@ -72,8 +74,8 @@ func buildServerMetadataPayload(workspaceResources *modconfig.ResourceMaps, pipe if mod := workspaceResources.Mod; mod != nil { payload.Metadata.Mod = &ModMetadata{ - Title: typeHelpers.SafeString(mod.Title), - FullName: mod.FullName, + Title: typeHelpers.SafeString(mod.GetTitle()), + FullName: mod.GetFullName(), ShortName: mod.ShortName, } } @@ -85,7 +87,7 @@ func buildServerMetadataPayload(workspaceResources *modconfig.ResourceMaps, pipe return json.Marshal(payload) } -func buildDashboardMetadataPayload(ctx context.Context, dashboard modconfig.ModTreeItem, w *dashboardworkspace.WorkspaceEvents) ([]byte, error) { +func buildDashboardMetadataPayload(ctx context.Context, dashboard modconfig.ModTreeItem, w *workspace.PowerpipeWorkspace) ([]byte, error) { defaultDatabase, defaultSearchPathConfig, err := db_client.GetDefaultDatabaseConfig() if err != nil { return nil, err @@ -133,11 +135,11 @@ func getSearchPathMetadata(ctx context.Context, database string, searchPathConfi return nil, nil } -func addBenchmarkChildren(benchmark *modconfig.Benchmark, recordTrunk bool, trunk []string, trunks map[string][][]string) []ModAvailableBenchmark { +func addBenchmarkChildren(benchmark *resources.Benchmark, recordTrunk bool, trunk []string, trunks map[string][][]string) []ModAvailableBenchmark { var children []ModAvailableBenchmark for _, child := range benchmark.GetChildren() { switch t := child.(type) { - case *modconfig.Benchmark: + case *resources.Benchmark: childTrunk := make([]string, len(trunk)+1) copy(childTrunk, trunk) childTrunk[len(childTrunk)-1] = t.FullName @@ -157,8 +159,7 @@ func addBenchmarkChildren(benchmark *modconfig.Benchmark, recordTrunk bool, trun return children } -func buildAvailableDashboardsPayload(workspaceResources *modconfig.ResourceMaps) ([]byte, error) { - +func buildAvailableDashboardsPayload(workspaceResources *resources.PowerpipeModResources) ([]byte, error) { payload := AvailableDashboardsPayload{ Action: "available_dashboards", Dashboards: make(map[string]ModAvailableDashboard), @@ -171,7 +172,9 @@ func buildAvailableDashboardsPayload(workspaceResources *modconfig.ResourceMaps) // build a map of the dashboards provided by each mod // iterate over the dashboards for the top level mod - this will include the dashboards from dependency mods - for _, dashboard := range workspaceResources.Mod.ResourceMaps.Dashboards { + topLevelResources := resources.GetModResources(workspaceResources.Mod) + + for _, dashboard := range topLevelResources.Dashboards { mod := dashboard.Mod // add this dashboard payload.Dashboards[dashboard.FullName] = ModAvailableDashboard{ @@ -179,12 +182,12 @@ func buildAvailableDashboardsPayload(workspaceResources *modconfig.ResourceMaps) FullName: dashboard.FullName, ShortName: dashboard.ShortName, Tags: dashboard.Tags, - ModFullName: mod.FullName, + ModFullName: mod.GetFullName(), } } benchmarkTrunks := make(map[string][][]string) - for _, benchmark := range workspaceResources.Mod.ResourceMaps.Benchmarks { + for _, benchmark := range topLevelResources.Benchmarks { if benchmark.IsAnonymous() { continue } @@ -212,7 +215,7 @@ func buildAvailableDashboardsPayload(workspaceResources *modconfig.ResourceMaps) Tags: benchmark.Tags, IsTopLevel: isTopLevel, Children: addBenchmarkChildren(benchmark, isTopLevel, trunk, benchmarkTrunks), - ModFullName: mod.FullName, + ModFullName: mod.GetFullName(), } payload.Benchmarks[benchmark.FullName] = availableBenchmark diff --git a/internal/dashboardserver/server.go b/internal/dashboardserver/server.go index 678572fc..bc5b6bfd 100644 --- a/internal/dashboardserver/server.go +++ b/internal/dashboardserver/server.go @@ -20,7 +20,7 @@ import ( "github.com/turbot/pipe-fittings/steampipeconfig" "github.com/turbot/powerpipe/internal/dashboardevents" "github.com/turbot/powerpipe/internal/dashboardexecute" - "github.com/turbot/powerpipe/internal/dashboardworkspace" + "github.com/turbot/powerpipe/internal/workspace" "gopkg.in/olahol/melody.v1" ) @@ -28,10 +28,10 @@ type Server struct { mutex *sync.Mutex dashboardClients map[string]*DashboardClientInfo webSocket *melody.Melody - workspace *dashboardworkspace.WorkspaceEvents + workspace *workspace.PowerpipeWorkspace } -func NewServer(ctx context.Context, w *dashboardworkspace.WorkspaceEvents, webSocket *melody.Melody) (*Server, error) { +func NewServer(ctx context.Context, w *workspace.PowerpipeWorkspace, webSocket *melody.Melody) (*Server, error) { OutputWait(ctx, "Starting WorkspaceEvents Server") var dashboardClients = make(map[string]*DashboardClientInfo) @@ -203,15 +203,15 @@ func (s *Server) HandleDashboardEvent(ctx context.Context, event dashboardevents OutputMessage(ctx, "Available Dashboards updated") // Emit dashboard metadata event in case there is a new mod - else the UI won't know about this mod - // TODO KAI verify we are ok to NOT send the cloud metadata here - payload, payloadError = buildServerMetadataPayload(s.workspace.GetResourceMaps(), &steampipeconfig.PipesMetadata{}) //s.workspace.PipesMetadata) + payload, payloadError = buildServerMetadataPayload(s.workspace.GetModResources(), &steampipeconfig.PipesMetadata{}) if payloadError != nil { return } _ = s.webSocket.Broadcast(payload) // Emit available dashboards event - payload, payloadError = buildAvailableDashboardsPayload(s.workspace.GetResourceMaps()) + workspaceResources := s.workspace.GetPowerpipeModResources() + payload, payloadError = buildAvailableDashboardsPayload(workspaceResources) if payloadError != nil { return } @@ -346,7 +346,7 @@ func (s *Server) handleMessageFunc(ctx context.Context) func(session *melody.Ses switch request.Action { case "get_server_metadata": // TODO KAI verify we are ok to NOT send the cloud metadata here - payload, err := buildServerMetadataPayload(s.workspace.GetResourceMaps(), &steampipeconfig.PipesMetadata{}) //s.workspace.PipesMetadata) + payload, err := buildServerMetadataPayload(s.workspace.GetModResources(), &steampipeconfig.PipesMetadata{}) if err != nil { OutputError(ctx, sperr.WrapWithMessage(err, "error building payload for get_metadata")) } @@ -362,7 +362,7 @@ func (s *Server) handleMessageFunc(ctx context.Context) func(session *melody.Ses } _ = session.Write(payload) case "get_available_dashboards": - payload, err := buildAvailableDashboardsPayload(s.workspace.GetResourceMaps()) + payload, err := buildAvailableDashboardsPayload(s.workspace.GetPowerpipeModResources()) if err != nil { OutputError(ctx, sperr.WrapWithMessage(err, "error building payload for get_available_dashboards")) } @@ -497,7 +497,7 @@ func (s *Server) getResource(name string) modconfig.ModTreeItem { return resource.(modconfig.ModTreeItem) } -func getDashboardsInterestedInResourceChanges(dashboardsBeingWatched []string, existingChangedDashboardNames []string, changedItems []*modconfig.DashboardTreeItemDiffs) []string { +func getDashboardsInterestedInResourceChanges(dashboardsBeingWatched []string, existingChangedDashboardNames []string, changedItems []*modconfig.ModTreeItemDiffs) []string { var changedDashboardNames []string for _, changedItem := range changedItems { diff --git a/internal/dashboardtypes/dashboard_tree_run.go b/internal/dashboardtypes/dashboard_tree_run.go index 5906aab6..38105192 100644 --- a/internal/dashboardtypes/dashboard_tree_run.go +++ b/internal/dashboardtypes/dashboard_tree_run.go @@ -3,8 +3,8 @@ package dashboardtypes import ( "context" - "github.com/turbot/pipe-fittings/modconfig" "github.com/turbot/pipe-fittings/steampipeconfig" + "github.com/turbot/powerpipe/internal/resources" ) // DashboardTreeRun is an interface implemented by all dashboard run nodes @@ -22,5 +22,5 @@ type DashboardTreeRun interface { GetInputsDependingOn(string) []string GetNodeType() string AsTreeNode() *steampipeconfig.SnapshotTreeNode - GetResource() modconfig.DashboardLeafNode + GetResource() resources.DashboardLeafNode } diff --git a/internal/dashboardtypes/resolved_runtime_dependency.go b/internal/dashboardtypes/resolved_runtime_dependency.go index 13e57419..958275b1 100644 --- a/internal/dashboardtypes/resolved_runtime_dependency.go +++ b/internal/dashboardtypes/resolved_runtime_dependency.go @@ -6,13 +6,13 @@ import ( "sync" "github.com/turbot/go-kit/helpers" - "github.com/turbot/pipe-fittings/modconfig" + "github.com/turbot/powerpipe/internal/resources" ) // ResolvedRuntimeDependency is a wrapper for RuntimeDependency which contains the resolved value // we must wrap it so that we do not mutate the underlying workspace data when resolving dependency values type ResolvedRuntimeDependency struct { - Dependency *modconfig.RuntimeDependency + Dependency *resources.RuntimeDependency valueLock sync.Mutex Value any // the name of the run which publishes this dependency @@ -20,7 +20,7 @@ type ResolvedRuntimeDependency struct { valueChannel chan *ResolvedRuntimeDependencyValue } -func NewResolvedRuntimeDependency(dep *modconfig.RuntimeDependency, valueChannel chan *ResolvedRuntimeDependencyValue, publisherName string) *ResolvedRuntimeDependency { +func NewResolvedRuntimeDependency(dep *resources.RuntimeDependency, valueChannel chan *ResolvedRuntimeDependencyValue, publisherName string) *ResolvedRuntimeDependency { return &ResolvedRuntimeDependency{ Dependency: dep, valueChannel: valueChannel, diff --git a/internal/dashboardworkspace/dashboard_workspace.go b/internal/dashboardworkspace/dashboard_workspace.go deleted file mode 100644 index 39cb46d8..00000000 --- a/internal/dashboardworkspace/dashboard_workspace.go +++ /dev/null @@ -1,42 +0,0 @@ -package dashboardworkspace - -import ( - "context" - "log/slog" - - "github.com/turbot/pipe-fittings/modconfig" - "github.com/turbot/pipe-fittings/workspace" - "github.com/turbot/powerpipe/internal/dashboardevents" -) - -// WorkspaceEvents is a wrapper around workspace.WorkspaceEvents that adds dashboard specific event handling -type WorkspaceEvents struct { - *workspace.Workspace - // event handlers - dashboardEventHandlers []dashboardevents.DashboardEventHandler - // channel used to send dashboard events to the handleDashboardEvent goroutine - dashboardEventChan chan dashboardevents.DashboardEvent -} - -func NewWorkspaceEvents(workspace *workspace.Workspace) *WorkspaceEvents { - w := &WorkspaceEvents{ - Workspace: workspace, - } - - w.OnFileWatcherError = func(ctx context.Context, err error) { - w.PublishDashboardEvent(ctx, &dashboardevents.WorkspaceError{Error: err}) - } - w.OnFileWatcherEvent = func(ctx context.Context, resourceMaps, prevResourceMaps *modconfig.ResourceMaps) { - w.raiseDashboardChangedEvents(ctx, resourceMaps, prevResourceMaps) - } - return w -} -func (w *WorkspaceEvents) Close() { - w.Workspace.Close() - if ch := w.dashboardEventChan; ch != nil { - // NOTE: set nil first - w.dashboardEventChan = nil - slog.Debug("closing dashboardEventChan") - close(ch) - } -} diff --git a/internal/db_client/database_config.go b/internal/db_client/database_config.go index ec2a12df..9dcbb7fa 100644 --- a/internal/db_client/database_config.go +++ b/internal/db_client/database_config.go @@ -23,7 +23,7 @@ func GetDatabaseConfigForResource(resource modconfig.ModTreeItem, workspaceMod * } // NOTE: if the resource is in a dependency mod, check whether database or search path has been specified for it - depName := resource.GetMod().DependencyName + depName := resource.(modconfig.ModItem).GetMod().DependencyName if depName != "" { // look for this mod in the workspace mod require @@ -41,13 +41,13 @@ func GetDatabaseConfigForResource(resource modconfig.ModTreeItem, workspaceMod * searchPathConfig.SearchPathPrefix = modRequirement.SearchPathPrefix } // if the parent mod has a database set, use it - if modDb := resource.GetMod().Database; modDb != nil { + if modDb := resource.(modconfig.ModItem).GetMod().GetDatabase(); modDb != nil { database = *modDb } - if modSearchPath := resource.GetMod().SearchPath; len(modSearchPath) > 0 { + if modSearchPath := resource.(modconfig.ModItem).GetMod().GetSearchPath(); len(modSearchPath) > 0 { searchPathConfig.SearchPath = modSearchPath } - if modSearchPathPrefix := resource.GetMod().SearchPathPrefix; len(modSearchPathPrefix) > 0 { + if modSearchPathPrefix := resource.(modconfig.ModItem).GetMod().GetSearchPathPrefix(); len(modSearchPathPrefix) > 0 { searchPathConfig.SearchPathPrefix = modSearchPathPrefix } diff --git a/internal/display/list_resources.go b/internal/display/list_resources.go index 0a63fc26..8db06b80 100644 --- a/internal/display/list_resources.go +++ b/internal/display/list_resources.go @@ -15,6 +15,8 @@ import ( localcmdconfig "github.com/turbot/powerpipe/internal/cmdconfig" localconstants "github.com/turbot/powerpipe/internal/constants" "github.com/turbot/powerpipe/internal/powerpipeconfig" + "github.com/turbot/powerpipe/internal/resources" + pworkspace "github.com/turbot/powerpipe/internal/workspace" ) func ListResources[T modconfig.ModTreeItem](cmd *cobra.Command) { @@ -23,12 +25,13 @@ func ListResources[T modconfig.ModTreeItem](cmd *cobra.Command) { modLocation := viper.GetString(constants.ArgModLocation) // build options to specify which blocks we need to load (based on type T opts := getListLoadWorkspaceOpts[T]() - w, errAndWarnings := workspace.LoadWorkspacePromptingForVariables(ctx, modLocation, opts...) + w, errAndWarnings := pworkspace.LoadWorkspacePromptingForVariables(ctx, modLocation, opts...) error_helpers.FailOnError(errAndWarnings.GetError()) // get resource filter depending on resource type and output type - resourceFilter := getListResourceFilter[T](w) - resources, err := workspace.FilterWorkspaceResourcesOfType[T](w, resourceFilter) + // TODO K pass workspace interface instead + resourceFilter := getListResourceFilter[T](&w.Workspace) + resources, err := workspace.FilterWorkspaceResourcesOfType[T](&w.Workspace, resourceFilter) if err != nil { error_helpers.ShowErrorWithMessage(ctx, err, "failed to filter resources") return @@ -56,7 +59,7 @@ func getListResourceFilter[T modconfig.ModTreeItem](w *workspace.Workspace) work var res = workspace.ResourceFilter{} var empty T - if _, ok := any(empty).(*modconfig.Benchmark); ok { + if _, ok := any(empty).(*resources.Benchmark); ok { // if T is benchmark, and if output is pretty or plain, only show top level benchmarks if viper.GetString(constants.ArgOutput) == constants.OutputFormatPretty || viper.GetString(constants.ArgOutput) == constants.OutputFormatPlain { @@ -87,20 +90,20 @@ func getListResourceFilter[T modconfig.ModTreeItem](w *workspace.Workspace) work } // build LoadWorkspaceOptions to specify which blocks we need to load (based on type T) -func getListLoadWorkspaceOpts[T modconfig.ModTreeItem]() []workspace.LoadWorkspaceOption { +func getListLoadWorkspaceOpts[T modconfig.ModTreeItem]() []pworkspace.LoadPowerpipeWorkspaceOption { var empty T - var opts = []workspace.LoadWorkspaceOption{ + var opts = []pworkspace.LoadPowerpipeWorkspaceOption{ // pass connections - workspace.WithPipelingConnections(powerpipeconfig.GlobalConfig.PipelingConnections), + pworkspace.WithPipelingConnections(powerpipeconfig.GlobalConfig.PipelingConnections), // disable late binding - workspace.WithLateBinding(false), - workspace.WithVariableValidation(false), + pworkspace.WithLateBinding(false), + pworkspace.WithVariableValidation(false), } switch any(empty).(type) { case *modconfig.Mod: - opts = append(opts, workspace.WithBlockType([]string{schema.BlockTypeMod})) + opts = append(opts, pworkspace.WithBlockType([]string{schema.BlockTypeMod})) case *modconfig.Variable: - opts = append(opts, workspace.WithBlockType([]string{schema.BlockTypeVariable})) + opts = append(opts, pworkspace.WithBlockType([]string{schema.BlockTypeVariable})) } return opts } @@ -111,7 +114,7 @@ func ShowResource[T modconfig.ModTreeItem](cmd *cobra.Command, args []string) { modLocation := viper.GetString(constants.ArgModLocation) // build options to specify which blocks we need to load (based on type T opts := getListLoadWorkspaceOpts[T]() - w, errAndWarnings := workspace.LoadWorkspacePromptingForVariables(ctx, modLocation, opts...) + w, errAndWarnings := pworkspace.LoadWorkspacePromptingForVariables(ctx, modLocation, opts...) error_helpers.FailOnError(errAndWarnings.GetError()) if !w.ModfileExists() { error_helpers.FailOnError(localconstants.ErrorNoModDefinition{}) diff --git a/internal/initialisation/init_data.go b/internal/initialisation/init_data.go index 83728ba8..96a23bda 100644 --- a/internal/initialisation/init_data.go +++ b/internal/initialisation/init_data.go @@ -3,6 +3,7 @@ package initialisation import ( "context" "fmt" + "github.com/turbot/powerpipe/internal/cmdconfig" "log/slog" "github.com/spf13/cobra" @@ -18,21 +19,18 @@ import ( "github.com/turbot/pipe-fittings/plugin" "github.com/turbot/pipe-fittings/statushooks" "github.com/turbot/pipe-fittings/utils" - "github.com/turbot/pipe-fittings/workspace" - "github.com/turbot/powerpipe/internal/cmdconfig" localconstants "github.com/turbot/powerpipe/internal/constants" "github.com/turbot/powerpipe/internal/dashboardexecute" - "github.com/turbot/powerpipe/internal/dashboardworkspace" "github.com/turbot/powerpipe/internal/db_client" "github.com/turbot/powerpipe/internal/powerpipeconfig" + "github.com/turbot/powerpipe/internal/workspace" "github.com/turbot/steampipe-plugin-sdk/v5/sperr" "github.com/turbot/steampipe-plugin-sdk/v5/telemetry" ) type InitData[T modconfig.ModTreeItem] struct { - Workspace *workspace.Workspace - WorkspaceEvents *dashboardworkspace.WorkspaceEvents - Result *InitResult + Workspace *workspace.PowerpipeWorkspace + Result *InitResult ShutdownTelemetry func() ExportManager *export.Manager @@ -62,7 +60,7 @@ func NewInitData[T modconfig.ModTreeItem](ctx context.Context, cmd *cobra.Comman return NewErrorInitData[T](fmt.Errorf("failed to load workspace: %s", error_helpers.HandleCancelError(errAndWarnings.GetError()).Error())) } - if !w.ModfileExists() && commandRequiresModfile[T](cmd, cmdArgs) { + if !w.ModfileExists() && commandRequiresModfile(cmd, cmdArgs) { return NewErrorInitData[T](localconstants.ErrorNoModDefinition{}) } i := &InitData[T]{ @@ -73,9 +71,17 @@ func NewInitData[T modconfig.ModTreeItem](ctx context.Context, cmd *cobra.Comman i.Workspace = w i.Result.Warnings = errAndWarnings.Warnings + // resolve target resources + targets, err := cmdconfig.ResolveTargets[T](cmdArgs, w) + if err != nil { + i.Result.Error = err + return i + } + i.Targets = targets + // if the database is NOT set in viper, and the mod has a connection string, set it - if !viper.IsSet(constants.ArgDatabase) && w.Mod.Database != nil { - viper.Set(constants.ArgDatabase, *w.Mod.Database) + if !viper.IsSet(constants.ArgDatabase) && w.Mod.GetDatabase() != nil { + viper.Set(constants.ArgDatabase, *w.Mod.GetDatabase()) } // now do the actual initialisation @@ -84,7 +90,7 @@ func NewInitData[T modconfig.ModTreeItem](ctx context.Context, cmd *cobra.Comman return i } -func commandRequiresModfile[T modconfig.ModTreeItem](cmd *cobra.Command, args []string) bool { +func commandRequiresModfile(cmd *cobra.Command, args []string) bool { // all commands using initData require a modfile EXCEPT query run if it is a raw sql query if utils.CommandFullKey(cmd) != "powerpipe.query.run" { return true @@ -124,14 +130,7 @@ func (i *InitData[T]) Init(ctx context.Context, args ...string) { return } - // attempt to resolve the provided args into target resource(s) - i.resolveTargets(args) - if i.Result.Error != nil { - return - } - statushooks.SetStatus(ctx, "Initializing") - i.WorkspaceEvents = dashboardworkspace.NewWorkspaceEvents(i.Workspace) // initialise telemetry shutdownTelemetry, err := telemetry.Init(app_specific.AppName) @@ -190,18 +189,6 @@ func (i *InitData[T]) Init(ctx context.Context, args ...string) { dashboardexecute.Executor = dashboardexecute.NewDashboardExecutor(clientMap) } -// resolve target resource, args and any target specific search path -func (i *InitData[T]) resolveTargets(args []string) { - // resolve target resources - targets, err := cmdconfig.ResolveTargets[T](args, i.Workspace) - if err != nil { - i.Result.Error = err - return - } - - i.Targets = targets -} - func validateModRequirementsRecursively(mod *modconfig.Mod, client *db_client.DbClient) []string { var validationErrors []string @@ -219,7 +206,7 @@ func validateModRequirementsRecursively(mod *modconfig.Mod, client *db_client.Db } // validate dependent mods - for childDependencyName, childMod := range mod.ResourceMaps.Mods { + for childDependencyName, childMod := range mod.GetModResources().GetMods() { if childDependencyName == "local" || mod.DependencyName == childMod.DependencyName { // this is a reference to self - skip (otherwise we will end up with a recursion loop) continue @@ -249,8 +236,8 @@ func (i *InitData[T]) GetSingleTarget() (modconfig.ModTreeItem, error) { // cobra should validate this if len(i.Targets) != 1 { - var empty T - return empty, sperr.New("expected a single target") + + return nil, sperr.New("expected a single target") } return i.Targets[0], nil } diff --git a/internal/parse/decode_args.go b/internal/parse/decode_args.go new file mode 100644 index 00000000..be1db57f --- /dev/null +++ b/internal/parse/decode_args.go @@ -0,0 +1,312 @@ +package parse + +import ( + "fmt" + + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/gohcl" + "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/turbot/go-kit/helpers" + "github.com/turbot/pipe-fittings/hclhelpers" + "github.com/turbot/pipe-fittings/modconfig" + "github.com/turbot/pipe-fittings/parse" + "github.com/turbot/pipe-fittings/schema" + "github.com/turbot/powerpipe/internal/resources" + "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/gocty" +) + +func DecodeArgs(attr *hcl.Attribute, evalCtx *hcl.EvalContext, resource resources.QueryProvider) (*resources.QueryArgs, []*resources.RuntimeDependency, hcl.Diagnostics) { + var runtimeDependencies []*resources.RuntimeDependency + var args = resources.NewQueryArgs() + var diags hcl.Diagnostics + + v, valDiags := attr.Expr.Value(evalCtx) + ty := v.Type() + // determine which diags are runtime dependencies (which we allow) and which are not + if valDiags.HasErrors() { + for _, diag := range diags { + dependency := parse.DiagsToDependency(diag) + if dependency == nil || !dependency.IsRuntimeDependency() { + diags = append(diags, diag) + } + } + } + // now diags contains all diags which are NOT runtime dependencies + if diags.HasErrors() { + return nil, nil, diags + } + + var err error + + switch { + case ty.IsObjectType(): + var argMap map[string]any + argMap, runtimeDependencies, err = ctyObjectToArgMap(attr, v, evalCtx) + if err == nil { + err = args.SetArgMap(argMap) + } + case ty.IsTupleType(): + var argList []any + argList, runtimeDependencies, err = ctyTupleToArgArray(attr, v) + if err == nil { + err = args.SetArgList(argList) + } + default: + err = fmt.Errorf("'params' property must be either a map or an array") + } + + if err != nil { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: fmt.Sprintf("%s has invalid parameter config", resource.Name()), + Detail: err.Error(), + Subject: &attr.Range, + }) + } + return args, runtimeDependencies, diags +} + +func ctyTupleToArgArray(attr *hcl.Attribute, val cty.Value) ([]any, []*resources.RuntimeDependency, error) { + // convert the attribute to a slice + values := val.AsValueSlice() + + // build output array + res := make([]any, len(values)) + var runtimeDependencies []*resources.RuntimeDependency + + for idx, v := range values { + // if the value is unknown, this is a runtime dependency + if !v.IsKnown() { + runtimeDependency, err := identifyRuntimeDependenciesFromArray(attr, idx, schema.AttributeTypeArgs) + if err != nil { + return nil, nil, err + } + + runtimeDependencies = append(runtimeDependencies, runtimeDependency) + } else { + // decode the value into a go type + val, err := hclhelpers.CtyToGo(v) + if err != nil { + err := fmt.Errorf("invalid value provided for arg #%d: %v", idx, err) + return nil, nil, err + } + + res[idx] = val + } + } + return res, runtimeDependencies, nil +} + +func ctyObjectToArgMap(attr *hcl.Attribute, val cty.Value, evalCtx *hcl.EvalContext) (map[string]any, []*resources.RuntimeDependency, error) { + res := make(map[string]any) + var runtimeDependencies []*resources.RuntimeDependency + it := val.ElementIterator() + for it.Next() { + k, v := it.Element() + + // decode key + var key string + if err := gocty.FromCtyValue(k, &key); err != nil { + return nil, nil, err + } + + // if the value is unknown, this is a runtime dependency + if !v.IsKnown() { + runtimeDependency, err := identifyRuntimeDependenciesFromObject(attr, key, schema.AttributeTypeArgs, evalCtx) + if err != nil { + return nil, nil, err + } + runtimeDependencies = append(runtimeDependencies, runtimeDependency) + } else if getWrappedUnknownVal(v) { + runtimeDependency, err := identifyRuntimeDependenciesFromObject(attr, key, schema.AttributeTypeArgs, evalCtx) + if err != nil { + return nil, nil, err + } + runtimeDependencies = append(runtimeDependencies, runtimeDependency) + } else { + // decode the value into a go type + val, err := hclhelpers.CtyToGo(v) + if err != nil { + err := fmt.Errorf("invalid value provided for param '%s': %v", key, err) + return nil, nil, err + } + res[key] = val + } + } + + return res, runtimeDependencies, nil +} + +// TACTICAL - is the cty value an array with a single unknown value +func getWrappedUnknownVal(v cty.Value) bool { + ty := v.Type() + + switch { + + case ty.IsTupleType(): + values := v.AsValueSlice() + if len(values) == 1 && !values[0].IsKnown() { + return true + } + } + return false +} + +func identifyRuntimeDependenciesFromObject(attr *hcl.Attribute, targetProperty, parentProperty string, evalCtx *hcl.EvalContext) (*resources.RuntimeDependency, error) { + // find the expression for this key + argsExpr, ok := attr.Expr.(*hclsyntax.ObjectConsExpr) + if !ok { + return nil, fmt.Errorf("could not extract runtime dependency for arg %s", targetProperty) + } + for _, item := range argsExpr.Items { + nameCty, valDiags := item.KeyExpr.Value(evalCtx) + if valDiags.HasErrors() { + return nil, fmt.Errorf("could not extract runtime dependency for arg %s", targetProperty) + } + var name string + if err := gocty.FromCtyValue(nameCty, &name); err != nil { + return nil, err + } + if name == targetProperty { + dep, err := getRuntimeDepFromExpression(item.ValueExpr, targetProperty, parentProperty) + if err != nil { + return nil, err + } + + return dep, nil + } + } + return nil, fmt.Errorf("could not extract runtime dependency for arg %s - not found in attribute map", targetProperty) +} + +func getRuntimeDepFromExpression(expr hcl.Expression, targetProperty, parentProperty string) (*resources.RuntimeDependency, error) { + isArray, propertyPath, err := modconfig.PropertyPathFromExpression(expr) + if err != nil { + return nil, err + } + + if propertyPath.ItemType == schema.BlockTypeInput { + // tactical: validate input dependency + if err := validateInputRuntimeDependency(propertyPath); err != nil { + return nil, err + } + } + ret := &resources.RuntimeDependency{ + PropertyPath: propertyPath, + ParentPropertyName: parentProperty, + TargetPropertyName: &targetProperty, + IsArray: isArray, + } + return ret, nil +} + +func identifyRuntimeDependenciesFromArray(attr *hcl.Attribute, idx int, parentProperty string) (*resources.RuntimeDependency, error) { + // find the expression for this key + argsExpr, ok := attr.Expr.(*hclsyntax.TupleConsExpr) + if !ok { + return nil, fmt.Errorf("could not extract runtime dependency for arg #%d", idx) + } + for i, item := range argsExpr.Exprs { + if i == idx { + isArray, propertyPath, err := modconfig.PropertyPathFromExpression(item) + if err != nil { + return nil, err + } + // tactical: validate input dependency + if propertyPath.ItemType == schema.BlockTypeInput { + if err := validateInputRuntimeDependency(propertyPath); err != nil { + return nil, err + } + } + ret := &resources.RuntimeDependency{ + PropertyPath: propertyPath, + ParentPropertyName: parentProperty, + TargetPropertyIndex: &idx, + IsArray: isArray, + } + + return ret, nil + } + } + return nil, fmt.Errorf("could not extract runtime dependency for arg %d - not found in attribute list", idx) +} + +// tactical - if runtime dependency is an input, validate it is of correct format +// TODO - include this with the main runtime dependency validation, when it is rewritten https://github.com/turbot/steampipe/issues/2925 +func validateInputRuntimeDependency(propertyPath *modconfig.ParsedPropertyPath) error { + // input references must be of form self.input..value + if propertyPath.Scope != resources.RuntimeDependencyDashboardScope { + return fmt.Errorf("could not resolve runtime dependency resource %s", propertyPath.Original) + } + return nil +} + +func DecodeParam(block *hcl.Block, parseCtx *parse.ModParseContext) (*modconfig.ParamDef, []*resources.RuntimeDependency, hcl.Diagnostics) { + def := modconfig.NewParamDef(block) + var runtimeDependencies []*resources.RuntimeDependency + content, diags := block.Body.Content(parse.ParamDefBlockSchema) + + if attr, exists := content.Attributes["description"]; exists { + moreDiags := gohcl.DecodeExpression(attr.Expr, parseCtx.EvalCtx, &def.Description) + diags = append(diags, moreDiags...) + } + if attr, exists := content.Attributes["default"]; exists { + defaultValue, deps, moreDiags := decodeParamDefault(attr, parseCtx, def.UnqualifiedName) + diags = append(diags, moreDiags...) + if !helpers.IsNil(defaultValue) { + err := def.SetDefault(defaultValue) + if err != nil { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "invalid default config for " + def.UnqualifiedName, + Detail: err.Error(), + Subject: &attr.Range, + }) + return nil, nil, diags + } + } + runtimeDependencies = deps + } + return def, runtimeDependencies, diags +} + +func decodeParamDefault(attr *hcl.Attribute, parseCtx *parse.ModParseContext, paramName string) (any, []*resources.RuntimeDependency, hcl.Diagnostics) { + v, diags := attr.Expr.Value(parseCtx.EvalCtx) + + if v.IsKnown() { + // convert the raw default into a string representation + val, err := hclhelpers.CtyToGo(v) + if err != nil { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: fmt.Sprintf("%s has invalid default config", paramName), + Detail: err.Error(), + Subject: &attr.Range, + }) + return nil, nil, diags + } + return val, nil, nil + } + + // so value not known - is there a runtime dependency? + + // check for a runtime dependency + runtimeDependency, err := getRuntimeDepFromExpression(attr.Expr, "default", paramName) + if err != nil { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: fmt.Sprintf("%s has invalid parameter default config", paramName), + Detail: err.Error(), + Subject: &attr.Range, + }) + return nil, nil, diags + } + if runtimeDependency == nil { + // return the original diags + return nil, nil, diags + } + + // so we have a runtime dependency + return nil, []*resources.RuntimeDependency{runtimeDependency}, nil +} diff --git a/internal/parse/mod_decoder.go b/internal/parse/mod_decoder.go new file mode 100644 index 00000000..77240d40 --- /dev/null +++ b/internal/parse/mod_decoder.go @@ -0,0 +1,581 @@ +package parse + +import ( + "fmt" + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/turbot/pipe-fittings/hclhelpers" + "github.com/turbot/pipe-fittings/modconfig" + "github.com/turbot/pipe-fittings/parse" + "github.com/turbot/powerpipe/internal/resources" + + "github.com/turbot/pipe-fittings/schema" +) + +type PowerpipeModDecoder struct { + parse.DecoderImpl +} + +func NewPowerpipeModDecoder(opts ...parse.DecoderOption) parse.Decoder { + d := &PowerpipeModDecoder{ + DecoderImpl: parse.NewDecoderImpl(), + } + for _, blockType := range schema.NodeAndEdgeProviderBlocks { + d.DecodeFuncs[blockType] = d.decodeNodeAndEdgeProvider + } + for _, blockType := range schema.QueryProviderBlocks { + // only add func if one is not already set (i.e. from NodeAndEdgeProviderBlocks) + if _, ok := d.DecodeFuncs[blockType]; !ok { + d.DecodeFuncs[blockType] = d.decodeQueryProvider + } + } + d.DecodeFuncs[schema.BlockTypeDashboard] = d.decodeDashboard + d.DecodeFuncs[schema.BlockTypeContainer] = d.decodeDashboardContainer + d.DecodeFuncs[schema.BlockTypeBenchmark] = d.decodeBenchmark + // apply options + for _, opt := range opts { + opt(d) + } + // set the default + d.DefaultDecodeFunc = d.decodeResource + d.ValidateFunc = d.ValidateResource + + return d +} + +func (d *PowerpipeModDecoder) decodeNodeAndEdgeProvider(block *hcl.Block, parseCtx *parse.ModParseContext) (modconfig.HclResource, *parse.DecodeResult) { + res := parse.NewDecodeResult() + + // get shell resource + resource, diags := d.resourceForBlock(block, parseCtx) + res.HandleDecodeDiags(diags) + if diags.HasErrors() { + return nil, res + } + + nodeAndEdgeProvider, ok := resource.(resources.NodeAndEdgeProvider) + if !ok { + // coding error + panic(fmt.Sprintf("block type %s not convertible to a NodeAndEdgeProvider", block.Type)) + } + + // do a partial decode using an empty schema - use to pull out all body content in the remain block + _, r, diags := block.Body.PartialContent(&hcl.BodySchema{}) + body := r.(*hclsyntax.Body) + res.HandleDecodeDiags(diags) + if !res.Success() { + return nil, res + } + + // decode the body into 'resource' to populate all properties that can be automatically decoded + diags = parse.DecodeHclBody(body, parseCtx.EvalCtx, parseCtx, resource) + // handle any resulting diags, which may specify dependencies + res.HandleDecodeDiags(diags) + + // decode sql args and params + res.Merge(d.decodeQueryProviderBlocks(block, body, resource, parseCtx)) + + // now decode child blocks + if len(body.Blocks) > 0 { + blocksRes := d.decodeNodeAndEdgeProviderBlocks(body, nodeAndEdgeProvider, parseCtx) + res.Merge(blocksRes) + } + + return resource, res +} + +func (d *PowerpipeModDecoder) decodeNodeAndEdgeProviderBlocks(content *hclsyntax.Body, nodeAndEdgeProvider resources.NodeAndEdgeProvider, parseCtx *parse.ModParseContext) *parse.DecodeResult { + var res = parse.NewDecodeResult() + + for _, b := range content.Blocks { + block := b.AsHCLBlock() + switch block.Type { + case schema.BlockTypeCategory: + // decode block + category, blockRes := d.DecodeBlock(block, parseCtx) + res.Merge(blockRes) + if !blockRes.Success() { + continue + } + + // add the category to the nodeAndEdgeProvider + res.AddDiags(nodeAndEdgeProvider.AddCategory(category.(*resources.DashboardCategory))) + + // DO NOT add the category to the mod + + case schema.BlockTypeNode, schema.BlockTypeEdge: + child, childRes := d.decodeQueryProvider(block, parseCtx) + + // TACTICAL if child has any runtime dependencies, claim them + // this is to ensure if this resource is used as base, we can be correctly identified + // as the publisher of the runtime dependencies + for _, r := range child.(resources.QueryProvider).GetRuntimeDependencies() { + r.Provider = nodeAndEdgeProvider + } + + // populate metadata, set references and call OnDecoded + d.HandleModDecodeResult(child, childRes, block, parseCtx) + res.Merge(childRes) + if res.Success() { + moreDiags := nodeAndEdgeProvider.AddChild(child) + res.AddDiags(moreDiags) + } + case schema.BlockTypeWith: + with, withRes := d.DecodeBlock(block, parseCtx) + res.Merge(withRes) + if res.Success() { + moreDiags := nodeAndEdgeProvider.AddWith(with.(*resources.DashboardWith)) + res.AddDiags(moreDiags) + } + } + + } + + return res +} + +func (d *PowerpipeModDecoder) decodeQueryProvider(block *hcl.Block, parseCtx *parse.ModParseContext) (modconfig.HclResource, *parse.DecodeResult) { + res := parse.NewDecodeResult() + // get shell resource + resource, diags := d.resourceForBlock(block, parseCtx) + res.HandleDecodeDiags(diags) + if diags.HasErrors() { + return nil, res + } + + // decode the database attribute separately + // do a partial decode using a schema containing just database - use to pull out all other body content in the remain block + databaseContent, remain, diags := block.Body.PartialContent(&hcl.BodySchema{ + Attributes: []hcl.AttributeSchema{ + {Name: schema.AttributeTypeDatabase}, + }}) + + res.HandleDecodeDiags(diags) + if !res.Success() { + return nil, res + } + + // decode the body into 'resource' to populate all properties that can be automatically decoded + diags = parse.DecodeHclBody(remain, parseCtx.EvalCtx, parseCtx, resource) + res.HandleDecodeDiags(diags) + + // decode 'with',args and params blocks + res.Merge(d.decodeQueryProviderBlocks(block, remain.(*hclsyntax.Body), resource, parseCtx)) + + // resolve the connection string and (if set) search path + qp := resource.(resources.QueryProvider) + connectionString, searchPath, searchPathPrefix, diags := parse.ResolveConnectionString(databaseContent, parseCtx.EvalCtx) + if connectionString != nil { + qp.SetDatabase(connectionString) + } + if searchPath != nil { + qp.SetSearchPath(searchPath) + } + if searchPathPrefix != nil { + qp.SetSearchPathPrefix(searchPathPrefix) + } + res.HandleDecodeDiags(diags) + + return qp, res +} + +func (d *PowerpipeModDecoder) decodeQueryProviderBlocks(block *hcl.Block, content *hclsyntax.Body, resource modconfig.HclResource, parseCtx *parse.ModParseContext) *parse.DecodeResult { + var diags hcl.Diagnostics + res := parse.NewDecodeResult() + queryProvider, ok := resource.(resources.QueryProvider) + if !ok { + // coding error + panic(fmt.Sprintf("block type %s not convertible to a QueryProvider", block.Type)) + } + + if attr, exists := content.Attributes[schema.AttributeTypeArgs]; exists { + args, runtimeDependencies, diags := DecodeArgs(attr.AsHCLAttribute(), parseCtx.EvalCtx, queryProvider) + if diags.HasErrors() { + // handle dependencies + res.HandleDecodeDiags(diags) + } else { + queryProvider.SetArgs(args) + queryProvider.AddRuntimeDependencies(runtimeDependencies) + } + } + + var params []*modconfig.ParamDef + for _, b := range content.Blocks { + block = b.AsHCLBlock() + switch block.Type { + case schema.BlockTypeParam: + paramDef, runtimeDependencies, moreDiags := DecodeParam(block, parseCtx) + if !moreDiags.HasErrors() { + params = append(params, paramDef) + queryProvider.AddRuntimeDependencies(runtimeDependencies) + // add and references contained in the param block to the control refs + moreDiags = parse.AddReferences(resource, block, parseCtx) + } + diags = append(diags, moreDiags...) + } + } + + queryProvider.SetParams(params) + res.HandleDecodeDiags(diags) + return res +} + +func (d *PowerpipeModDecoder) decodeDashboard(block *hcl.Block, parseCtx *parse.ModParseContext) (modconfig.HclResource, *parse.DecodeResult) { + res := parse.NewDecodeResult() + dashboard := resources.NewDashboard(block, parseCtx.CurrentMod, parseCtx.DetermineBlockName(block)).(*resources.Dashboard) + + // do a partial decode using an empty schema - use to pull out all body content in the remain block + _, r, diags := block.Body.PartialContent(&hcl.BodySchema{}) + body := r.(*hclsyntax.Body) + res.HandleDecodeDiags(diags) + + // decode the body into 'dashboardContainer' to populate all properties that can be automatically decoded + diags = parse.DecodeHclBody(body, parseCtx.EvalCtx, parseCtx, dashboard) + // handle any resulting diags, which may specify dependencies + res.HandleDecodeDiags(diags) + + if dashboard.Base != nil && len(dashboard.Base.ChildNames) > 0 { + supportedChildren := []string{schema.BlockTypeContainer, schema.BlockTypeChart, schema.BlockTypeCard, schema.BlockTypeFlow, schema.BlockTypeGraph, schema.BlockTypeHierarchy, schema.BlockTypeImage, schema.BlockTypeInput, schema.BlockTypeTable, schema.BlockTypeText} + // TACTICAL: we should be passing in the block for the Base resource - but this is only used for diags + // and we do not expect to get any (as this function has already succeeded when the base was originally parsed) + children, _ := parse.ResolveChildrenFromNames(dashboard.Base.ChildNames, block, supportedChildren, parseCtx) + dashboard.Base.Children = children + } + if !res.Success() { + return dashboard, res + } + + // now decode child blocks + if len(body.Blocks) > 0 { + blocksRes := d.decodeDashboardBlocks(body, dashboard, parseCtx) + res.Merge(blocksRes) + } + + return dashboard, res +} + +func (d *PowerpipeModDecoder) decodeDashboardBlocks(content *hclsyntax.Body, dashboard *resources.Dashboard, parseCtx *parse.ModParseContext) *parse.DecodeResult { + var res = parse.NewDecodeResult() + // set dashboard as parent on the run context - this is used when generating names for anonymous blocks + parseCtx.PushParent(dashboard) + defer func() { + parseCtx.PopParent() + }() + + for _, b := range content.Blocks { + block := b.AsHCLBlock() + + // decode block + resource, blockRes := d.DecodeBlock(block, parseCtx) + res.Merge(blockRes) + if !blockRes.Success() { + continue + } + + // we expect either inputs or child report nodes + // add the resource to the mod + res.AddDiags(parse.AddResourceToMod(resource, block, parseCtx)) + // add to the dashboard children + // (we expect this cast to always succeed) + if child, ok := resource.(modconfig.ModTreeItem); ok { + res.AddDiags(dashboard.AddChild(child)) + } + + } + + moreDiags := dashboard.InitInputs() + res.AddDiags(moreDiags) + + return res +} + +func (d *PowerpipeModDecoder) decodeDashboardContainer(block *hcl.Block, parseCtx *parse.ModParseContext) (modconfig.HclResource, *parse.DecodeResult) { + res := parse.NewDecodeResult() + container := resources.NewDashboardContainer(block, parseCtx.CurrentMod, parseCtx.DetermineBlockName(block)).(*resources.DashboardContainer) + + // do a partial decode using an empty schema - use to pull out all body content in the remain block + _, r, diags := block.Body.PartialContent(&hcl.BodySchema{}) + body := r.(*hclsyntax.Body) + res.HandleDecodeDiags(diags) + if !res.Success() { + return nil, res + } + + // decode the body into 'dashboardContainer' to populate all properties that can be automatically decoded + diags = parse.DecodeHclBody(body, parseCtx.EvalCtx, parseCtx, container) + // handle any resulting diags, which may specify dependencies + res.HandleDecodeDiags(diags) + + // now decode child blocks + if len(body.Blocks) > 0 { + blocksRes := d.decodeDashboardContainerBlocks(body, container, parseCtx) + res.Merge(blocksRes) + } + + return container, res +} + +func (d *PowerpipeModDecoder) decodeDashboardContainerBlocks(content *hclsyntax.Body, dashboardContainer *resources.DashboardContainer, parseCtx *parse.ModParseContext) *parse.DecodeResult { + var res = parse.NewDecodeResult() + + // set container as parent on the run context - this is used when generating names for anonymous blocks + parseCtx.PushParent(dashboardContainer) + defer func() { + parseCtx.PopParent() + }() + + for _, b := range content.Blocks { + block := b.AsHCLBlock() + resource, blockRes := d.DecodeBlock(block, parseCtx) + res.Merge(blockRes) + if !blockRes.Success() { + continue + } + + // special handling for inputs + if b.Type == schema.BlockTypeInput { + input := resource.(*resources.DashboardInput) + dashboardContainer.Inputs = append(dashboardContainer.Inputs, input) + dashboardContainer.AddChild(input) + // the input will be added to the mod by the parent dashboard + + } else { + // for all other children, add to mod and children + res.AddDiags(parse.AddResourceToMod(resource, block, parseCtx)) + if child, ok := resource.(modconfig.ModTreeItem); ok { + dashboardContainer.AddChild(child) + } + } + } + + return res +} + +func (d *PowerpipeModDecoder) decodeBenchmark(block *hcl.Block, parseCtx *parse.ModParseContext) (modconfig.HclResource, *parse.DecodeResult) { + res := parse.NewDecodeResult() + benchmark := resources.NewBenchmark(block, parseCtx.CurrentMod, parseCtx.DetermineBlockName(block)).(*resources.Benchmark) + content, diags := block.Body.Content(parse.BenchmarkBlockSchema) + res.HandleDecodeDiags(diags) + + diags = parse.DecodeProperty(content, "children", &benchmark.ChildNames, parseCtx.EvalCtx) + res.HandleDecodeDiags(diags) + + diags = parse.DecodeProperty(content, "description", &benchmark.Description, parseCtx.EvalCtx) + res.HandleDecodeDiags(diags) + + diags = parse.DecodeProperty(content, "documentation", &benchmark.Documentation, parseCtx.EvalCtx) + res.HandleDecodeDiags(diags) + + diags = parse.DecodeProperty(content, "tags", &benchmark.Tags, parseCtx.EvalCtx) + res.HandleDecodeDiags(diags) + + diags = parse.DecodeProperty(content, "title", &benchmark.Title, parseCtx.EvalCtx) + res.HandleDecodeDiags(diags) + + diags = parse.DecodeProperty(content, "type", &benchmark.Type, parseCtx.EvalCtx) + res.HandleDecodeDiags(diags) + + diags = parse.DecodeProperty(content, "display", &benchmark.Display, parseCtx.EvalCtx) + res.HandleDecodeDiags(diags) + + // now add children + if res.Success() { + supportedChildren := []string{schema.BlockTypeBenchmark, schema.BlockTypeControl} + children, diags := parse.ResolveChildrenFromNames(benchmark.ChildNames.StringList(), block, supportedChildren, parseCtx) + res.HandleDecodeDiags(diags) + + // now set children and child name strings + benchmark.Children = children + benchmark.ChildNameStrings = parse.GetChildNameStringsFromModTreeItem(children) + } + + diags = parse.DecodeProperty(content, "base", &benchmark.Base, parseCtx.EvalCtx) + res.HandleDecodeDiags(diags) + if benchmark.Base != nil && len(benchmark.Base.ChildNames) > 0 { + supportedChildren := []string{schema.BlockTypeBenchmark, schema.BlockTypeControl} + // TACTICAL: we should be passing in the block for the Base resource - but this is only used for diags + // and we do not expect to get any (as this function has already succeeded when the base was originally parsed) + children, _ := parse.ResolveChildrenFromNames(benchmark.Base.ChildNameStrings, block, supportedChildren, parseCtx) + benchmark.Children = children + } + diags = parse.DecodeProperty(content, "width", &benchmark.Width, parseCtx.EvalCtx) + res.HandleDecodeDiags(diags) + return benchmark, res +} + +// generic decode function for any resource we do not have custom decode logic for +func (d *PowerpipeModDecoder) decodeResource(block *hcl.Block, parseCtx *parse.ModParseContext) (modconfig.HclResource, *parse.DecodeResult) { + res := parse.NewDecodeResult() + // get shell resource + resource, diags := d.resourceForBlock(block, parseCtx) + res.HandleDecodeDiags(diags) + if diags.HasErrors() { + return nil, res + } + + diags = parse.DecodeHclBody(block.Body, parseCtx.EvalCtx, parseCtx, resource) + if len(diags) > 0 { + res.HandleDecodeDiags(diags) + } + return resource, res +} + +// return a shell resource for the given block +func (d *PowerpipeModDecoder) resourceForBlock(block *hcl.Block, parseCtx *parse.ModParseContext) (modconfig.HclResource, hcl.Diagnostics) { + var resource modconfig.HclResource + // parseCtx already contains the current mod + mod := parseCtx.CurrentMod + blockName := parseCtx.DetermineBlockName(block) + + factoryFuncs := map[string]func(*hcl.Block, *modconfig.Mod, string) modconfig.HclResource{ + // for block type mod, just use the current mod + schema.BlockTypeMod: func(*hcl.Block, *modconfig.Mod, string) modconfig.HclResource { return mod }, + schema.BlockTypeQuery: resources.NewQuery, + schema.BlockTypeControl: resources.NewControl, + schema.BlockTypeBenchmark: resources.NewBenchmark, + schema.BlockTypeDashboard: resources.NewDashboard, + schema.BlockTypeContainer: resources.NewDashboardContainer, + schema.BlockTypeChart: resources.NewDashboardChart, + schema.BlockTypeCard: resources.NewDashboardCard, + schema.BlockTypeFlow: resources.NewDashboardFlow, + schema.BlockTypeGraph: resources.NewDashboardGraph, + schema.BlockTypeHierarchy: resources.NewDashboardHierarchy, + schema.BlockTypeImage: resources.NewDashboardImage, + schema.BlockTypeInput: resources.NewDashboardInput, + schema.BlockTypeTable: resources.NewDashboardTable, + schema.BlockTypeText: resources.NewDashboardText, + schema.BlockTypeNode: resources.NewDashboardNode, + schema.BlockTypeEdge: resources.NewDashboardEdge, + schema.BlockTypeCategory: resources.NewDashboardCategory, + schema.BlockTypeWith: resources.NewDashboardWith, + } + + factoryFunc, ok := factoryFuncs[block.Type] + if !ok { + return nil, hcl.Diagnostics{&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: fmt.Sprintf("ResourceForBlock called for unsupported block type %s", block.Type), + Subject: hclhelpers.BlockRangePointer(block), + }, + } + } + resource = factoryFunc(block, mod, blockName) + return resource, nil +} + +// validate the resource +func (d *PowerpipeModDecoder) ValidateResource(resource modconfig.HclResource) hcl.Diagnostics { + var diags hcl.Diagnostics + if qp, ok := resource.(resources.NodeAndEdgeProvider); ok { + moreDiags := validateNodeAndEdgeProvider(qp) + diags = append(diags, moreDiags...) + } else if qp, ok := resource.(resources.QueryProvider); ok { + moreDiags := validateQueryProvider(qp) + diags = append(diags, moreDiags...) + } + + if wp, ok := resource.(resources.WithProvider); ok { + moreDiags := validateRuntimeDependencyProvider(wp) + diags = append(diags, moreDiags...) + } + return diags +} + +func validateRuntimeDependencyProvider(wp resources.WithProvider) hcl.Diagnostics { + resource := wp.(modconfig.HclResource) + var diags hcl.Diagnostics + if len(wp.GetWiths()) > 0 && !resource.IsTopLevel() { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Only top level resources can have `with` blocks", + Detail: fmt.Sprintf("%s contains 'with' blocks but is not a top level resource.", resource.Name()), + Subject: resource.GetDeclRange(), + }) + } + return diags +} + +// validate that the provider does not contains both edges/nodes and a query/sql +// enrich the loaded nodes and edges with the fully parsed resources from the resourceMapProvider +func validateNodeAndEdgeProvider(resource resources.NodeAndEdgeProvider) hcl.Diagnostics { + // TODO [node_reuse] add NodeAndEdgeProviderImpl and move validate there + // https://github.com/turbot/steampipe/issues/2918 + + var diags hcl.Diagnostics + containsEdgesOrNodes := len(resource.GetEdges())+len(resource.GetNodes()) > 0 + definesQuery := resource.GetSQL() != nil || resource.GetQuery() != nil + + // cannot declare both edges/nodes AND sql/query + if definesQuery && containsEdgesOrNodes { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: fmt.Sprintf("%s contains edges/nodes AND has a query", resource.Name()), + Subject: resource.GetDeclRange(), + }) + } + + // if resource is NOT top level must have either edges/nodes OR sql/query + if !resource.IsTopLevel() && !definesQuery && !containsEdgesOrNodes { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: fmt.Sprintf("%s does not define a query or SQL, and has no edges/nodes", resource.Name()), + Subject: resource.GetDeclRange(), + }) + } + + diags = append(diags, validateSqlAndQueryNotBothSet(resource)...) + + diags = append(diags, validateParamAndQueryNotBothSet(resource)...) + + return diags +} + +func validateQueryProvider(resource resources.QueryProvider) hcl.Diagnostics { + var diags hcl.Diagnostics + + diags = append(diags, resource.ValidateQuery()...) + + diags = append(diags, validateSqlAndQueryNotBothSet(resource)...) + + diags = append(diags, validateParamAndQueryNotBothSet(resource)...) + + return diags +} + +func validateParamAndQueryNotBothSet(resource resources.QueryProvider) hcl.Diagnostics { + var diags hcl.Diagnostics + + // param block cannot be set if a query property is set - it is only valid if inline SQL ids defined + if len(resource.GetParams()) > 0 { + if resource.GetQuery() != nil { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: fmt.Sprintf("Deprecated usage: %s has 'query' property set so should not define 'param' blocks", resource.Name()), + Subject: resource.GetDeclRange(), + }) + } + if !resource.IsTopLevel() && !resource.ParamsInheritedFromBase() { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Deprecated usage: Only top level resources can have 'param' blocks", + Detail: fmt.Sprintf("%s contains 'param' blocks but is not a top level resource.", resource.Name()), + Subject: resource.GetDeclRange(), + }) + } + } + return diags +} + +func validateSqlAndQueryNotBothSet(resource resources.QueryProvider) hcl.Diagnostics { + var diags hcl.Diagnostics + // are both sql and query set? + if resource.GetSQL() != nil && resource.GetQuery() != nil { + // either Query or SQL property may be set - if Query property already set, error + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: fmt.Sprintf("%s has both 'SQL' and 'query' property set - only 1 of these may be set", resource.Name()), + Subject: resource.GetDeclRange(), + }) + } + return diags +} diff --git a/internal/parse/query_invocation.go b/internal/parse/query_invocation.go new file mode 100644 index 00000000..5789c7c4 --- /dev/null +++ b/internal/parse/query_invocation.go @@ -0,0 +1,184 @@ +package parse + +import ( + "fmt" + "strings" + + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/turbot/pipe-fittings/error_helpers" + "github.com/turbot/pipe-fittings/hclhelpers" + "github.com/turbot/powerpipe/internal/resources" +) + +// ParseQueryInvocation parses a query invocation and extracts the args (if any) +// supported formats are: +// +// 1) positional args +// query.my_query("val1","val2") +// +// 2) named args +// query.my_query(my_arg1 => "test", my_arg2 => "test2") +func ParseQueryInvocation(arg string) (string, *resources.QueryArgs, error) { + var args *resources.QueryArgs + + arg = strings.TrimSpace(arg) + query := arg + var err error + openBracketIdx := strings.Index(arg, "(") + closeBracketIdx := strings.LastIndex(arg, ")") + if openBracketIdx != -1 && closeBracketIdx == len(arg)-1 { + argsString := arg[openBracketIdx+1 : len(arg)-1] + args, err = parseArgs(argsString) + query = strings.TrimSpace(arg[:openBracketIdx]) + } + return query, args, err +} + +// parse the actual args string, i.e. the contents of the bracket +// supported formats are: +// +// 1) positional args +// "val1","val1" +// +// 2) named args +// my_arg1 => "val1", my_arg2 => "val2" +func parseArgs(argsString string) (*resources.QueryArgs, error) { + res := resources.NewQueryArgs() + if len(argsString) == 0 { + return res, nil + } + + // split on comma to get each arg string (taking quotes and brackets into account) + splitArgs, err := splitArgString(argsString) + if err != nil { + // return empty result, even if we have an error + return res, err + } + + // first check for named args + argMap, err := parseNamedArgs(splitArgs) + if err != nil { + return res, err + } + if err := res.SetArgMap(argMap); err != nil { + return res, err + } + + if res.Empty() { + // no named args - fall back on positional + argList, err := parsePositionalArgs(splitArgs) + if err != nil { + return res, err + } + if err := res.SetArgList(argList); err != nil { + return res, err + } + } + // return empty result, even if we have an error + return res, err +} + +func splitArgString(argsString string) ([]string, error) { + var argsList []string + openElements := map[string]int{ + "quote": 0, + "curly": 0, + "square": 0, + } + var currentWord string + for _, c := range argsString { + // should we split - are we in a block + if c == ',' && + openElements["quote"] == 0 && openElements["curly"] == 0 && openElements["square"] == 0 { + if len(currentWord) > 0 { + argsList = append(argsList, currentWord) + currentWord = "" + } + } else { + currentWord = currentWord + string(c) + } + + // handle brackets and quotes + switch c { + case '{': + if openElements["quote"] == 0 { + openElements["curly"]++ + } + case '}': + if openElements["quote"] == 0 { + openElements["curly"]-- + if openElements["curly"] < 0 { + return nil, fmt.Errorf("bad arg syntax") + } + } + case '[': + if openElements["quote"] == 0 { + openElements["square"]++ + } + case ']': + if openElements["quote"] == 0 { + openElements["square"]-- + if openElements["square"] < 0 { + return nil, fmt.Errorf("bad arg syntax") + } + } + case '"': + if openElements["quote"] == 0 { + openElements["quote"] = 1 + } else { + openElements["quote"] = 0 + } + } + } + if len(currentWord) > 0 { + argsList = append(argsList, currentWord) + } + return argsList, nil +} + +func parseArg(v string) (any, error) { + b, diags := hclsyntax.ParseExpression([]byte(v), "", hcl.Pos{}) + if diags.HasErrors() { + return "", error_helpers.HclDiagsToError("bad arg syntax", diags) + } + val, diags := b.Value(nil) + if diags.HasErrors() { + return "", error_helpers.HclDiagsToError("bad arg syntax", diags) + } + return hclhelpers.CtyToGo(val) +} + +func parseNamedArgs(argsList []string) (map[string]any, error) { + var res = make(map[string]any) + for _, p := range argsList { + argTuple := strings.Split(strings.TrimSpace(p), "=>") + if len(argTuple) != 2 { + // not all args have valid syntax - give up + return nil, nil + } + k := strings.TrimSpace(argTuple[0]) + val, err := parseArg(argTuple[1]) + if err != nil { + return nil, err + } + res[k] = val + } + return res, nil +} + +func parsePositionalArgs(argsList []string) ([]any, error) { + // convert to pointer array + res := make([]any, len(argsList)) + // just treat args as positional args + // strip spaces + for i, v := range argsList { + valStr, err := parseArg(v) + if err != nil { + return nil, err + } + res[i] = valStr + } + + return res, nil +} diff --git a/internal/parse/query_invocation_test.go b/internal/parse/query_invocation_test.go new file mode 100644 index 00000000..49a2eece --- /dev/null +++ b/internal/parse/query_invocation_test.go @@ -0,0 +1,129 @@ +package parse + +import ( + "fmt" + "testing" + + "github.com/turbot/pipe-fittings/utils" + "github.com/turbot/powerpipe/internal/resources" +) + +// NOTE: all query arg values must be JSON representations +type parseQueryInvocationTest struct { + input string + expected parseQueryInvocationResult +} + +type parseQueryInvocationResult struct { + queryName string + args *resources.QueryArgs +} + +var emptyArgs = resources.NewQueryArgs() +var testCasesParseQueryInvocation = map[string]parseQueryInvocationTest{ + "no brackets": { + input: `query.q1`, + expected: parseQueryInvocationResult{"query.q1", emptyArgs}, + }, + "no params": { + input: `query.q1()`, + expected: parseQueryInvocationResult{"query.q1", emptyArgs}, + }, + "invalid params 1": { + input: `query.q1(foo)`, + expected: parseQueryInvocationResult{ + queryName: `query.q1`, + args: &resources.QueryArgs{}, + }, + }, + "invalid params 4": { + input: `query.q1("foo", "bar"])`, + expected: parseQueryInvocationResult{ + queryName: `query.q1`, + + args: &resources.QueryArgs{}, + }, + }, + + "single positional param": { + input: `query.q1("foo")`, + expected: parseQueryInvocationResult{ + queryName: `query.q1`, + args: &resources.QueryArgs{ArgList: []*string{utils.ToStringPointer("foo")}}, + }, + }, + "single positional param extra spaces": { + input: `query.q1("foo" ) `, + expected: parseQueryInvocationResult{ + queryName: `query.q1`, + args: &resources.QueryArgs{ArgList: []*string{utils.ToStringPointer("foo")}}, + }, + }, + "multiple positional params": { + input: `query.q1("foo", "bar", "foo-bar")`, + expected: parseQueryInvocationResult{ + queryName: `query.q1`, + args: &resources.QueryArgs{ArgList: []*string{utils.ToStringPointer("foo"), utils.ToStringPointer("bar"), utils.ToStringPointer("foo-bar")}}, + }, + }, + "multiple positional params extra spaces": { + input: `query.q1("foo", "bar", "foo-bar" )`, + expected: parseQueryInvocationResult{ + queryName: `query.q1`, + args: &resources.QueryArgs{ArgList: []*string{utils.ToStringPointer("foo"), utils.ToStringPointer("bar"), utils.ToStringPointer("foo-bar")}}, + }, + }, + "single named param": { + input: `query.q1(p1 => "foo")`, + expected: parseQueryInvocationResult{ + queryName: `query.q1`, + args: &resources.QueryArgs{ArgMap: map[string]string{"p1": "foo"}}, + }, + }, + "single named param extra spaces": { + input: `query.q1( p1 => "foo" ) `, + expected: parseQueryInvocationResult{ + queryName: `query.q1`, + args: &resources.QueryArgs{ArgMap: map[string]string{"p1": "foo"}}, + }, + }, + "multiple named params": { + input: `query.q1(p1 => "foo", p2 => "bar")`, + expected: parseQueryInvocationResult{ + queryName: `query.q1`, + args: &resources.QueryArgs{ArgMap: map[string]string{"p1": "foo", "p2": "bar"}}, + }, + }, + "multiple named params extra spaces": { + input: ` query.q1 ( p1 => "foo" , p2 => "bar" ) `, + expected: parseQueryInvocationResult{ + queryName: `query.q1`, + args: &resources.QueryArgs{ArgMap: map[string]string{"p1": "foo", "p2": "bar"}}, + }, + }, + "named param with dot in value": { + input: `query.q1(p1 => "foo.bar")`, + expected: parseQueryInvocationResult{ + queryName: `query.q1`, + args: &resources.QueryArgs{ArgMap: map[string]string{"p1": "foo.bar"}}, + }, + }, +} + +func TestParseQueryInvocation(t *testing.T) { + for name, test := range testCasesParseQueryInvocation { + queryName, args, _ := ParseQueryInvocation(test.input) + if args == nil { + args = emptyArgs + } + if queryName != test.expected.queryName || !test.expected.args.Equals(args) { + //nolint:forbidigo // acceptable + fmt.Printf("") + t.Errorf("Test: '%s'' FAILED : expected:\nquery: %s params: %s\n\ngot:\nquery: %s params: %s", + name, + test.expected.queryName, + test.expected.args, + queryName, args) + } + } +} diff --git a/internal/parse/schema.go b/internal/parse/schema.go new file mode 100644 index 00000000..80d1d6a2 --- /dev/null +++ b/internal/parse/schema.go @@ -0,0 +1,57 @@ +package parse + +import ( + "github.com/hashicorp/hcl/v2" + "github.com/turbot/pipe-fittings/modconfig" + "github.com/turbot/pipe-fittings/schema" + "github.com/turbot/powerpipe/internal/resources" +) + +// GetResourceSchema adds any app specific blocks to the existing resource schema +func GetResourceSchema(resource modconfig.HclResource, res *hcl.BodySchema) *hcl.BodySchema { + // special cases for manually parsed attributes and blocks + switch resource.GetBlockType() { + case schema.BlockTypeDashboard, schema.BlockTypeContainer: + res.Blocks = append(res.Blocks, + hcl.BlockHeaderSchema{Type: schema.BlockTypeDashboard}, + hcl.BlockHeaderSchema{Type: schema.BlockTypeCard}, + hcl.BlockHeaderSchema{Type: schema.BlockTypeChart}, + hcl.BlockHeaderSchema{Type: schema.BlockTypeContainer}, + hcl.BlockHeaderSchema{Type: schema.BlockTypeFlow}, + hcl.BlockHeaderSchema{Type: schema.BlockTypeGraph}, + hcl.BlockHeaderSchema{Type: schema.BlockTypeHierarchy}, + hcl.BlockHeaderSchema{Type: schema.BlockTypeImage}, + hcl.BlockHeaderSchema{Type: schema.BlockTypeInput}, + hcl.BlockHeaderSchema{Type: schema.BlockTypeTable}, + hcl.BlockHeaderSchema{Type: schema.BlockTypeText}, + hcl.BlockHeaderSchema{Type: schema.BlockTypeWith}, + ) + case schema.BlockTypeQuery: + // remove `Query` from attributes + var querySchema = &hcl.BodySchema{} + for _, a := range res.Attributes { + if a.Name != schema.AttributeTypeQuery { + querySchema.Attributes = append(querySchema.Attributes, a) + } + } + res = querySchema + } + + if _, ok := resource.(resources.QueryProvider); ok { + res.Blocks = append(res.Blocks, hcl.BlockHeaderSchema{Type: schema.BlockTypeParam}) + // if this is NOT query, add args + if resource.GetBlockType() != schema.BlockTypeQuery { + res.Attributes = append(res.Attributes, hcl.AttributeSchema{Name: schema.AttributeTypeArgs}) + } + } + if _, ok := resource.(resources.NodeAndEdgeProvider); ok { + res.Blocks = append(res.Blocks, + hcl.BlockHeaderSchema{Type: schema.BlockTypeCategory}, + hcl.BlockHeaderSchema{Type: schema.BlockTypeNode}, + hcl.BlockHeaderSchema{Type: schema.BlockTypeEdge}) + } + if _, ok := resource.(resources.WithProvider); ok { + res.Blocks = append(res.Blocks, hcl.BlockHeaderSchema{Type: schema.BlockTypeWith}) + } + return res +} diff --git a/internal/resources/benchmark.go b/internal/resources/benchmark.go new file mode 100644 index 00000000..3a094925 --- /dev/null +++ b/internal/resources/benchmark.go @@ -0,0 +1,242 @@ +package resources + +import ( + "fmt" + "github.com/turbot/pipe-fittings/modconfig" + "sort" + "strings" + + "github.com/hashicorp/hcl/v2" + "github.com/turbot/go-kit/types" + typehelpers "github.com/turbot/go-kit/types" + "github.com/turbot/pipe-fittings/cty_helpers" + "github.com/turbot/pipe-fittings/printers" + "github.com/turbot/pipe-fittings/utils" + "github.com/zclconf/go-cty/cty" +) + +// Benchmark is a struct representing the Benchmark resource +type Benchmark struct { + modconfig.ResourceWithMetadataImpl + modconfig.ModTreeItemImpl + + // required to allow partial decoding + Remain hcl.Body `hcl:",remain" json:"-"` + + // child names as NamedItem structs - used to allow setting children via the 'children' property + ChildNames modconfig.NamedItemList `cty:"child_names" json:"-"` + // used for introspection tables + ChildNameStrings []string `cty:"child_name_strings" json:"children,omitempty"` + + // dashboard specific properties + Base *Benchmark `hcl:"base" json:"-"` + Width *int `cty:"width" hcl:"width" json:"width,omitempty"` + Type *string `cty:"type" hcl:"type" json:"type,omitempty"` + Display *string `cty:"display" hcl:"display" json:"display,omitempty"` +} + +func NewRootBenchmarkWithChildren(mod *modconfig.Mod, children []modconfig.ModTreeItem) modconfig.HclResource { + fullName := fmt.Sprintf("%s.%s.%s", mod.ShortName, "benchmark", "root") + benchmark := &Benchmark{ + ModTreeItemImpl: modconfig.ModTreeItemImpl{ + HclResourceImpl: modconfig.HclResourceImpl{ + ShortName: "root", + FullName: fullName, + UnqualifiedName: fmt.Sprintf("%s.%s", "benchmark", "root"), + BlockType: "benchmark", + }, + Mod: mod, + }, + } + + benchmark.AddChild(children...) + return benchmark +} + +func NewBenchmark(block *hcl.Block, mod *modconfig.Mod, shortName string) modconfig.HclResource { + benchmark := &Benchmark{ + ModTreeItemImpl: modconfig.NewModTreeItemImpl(block, mod, shortName), + } + benchmark.SetAnonymous(block) + return benchmark +} + +func (b *Benchmark) Equals(other *Benchmark) bool { + if other == nil { + return false + } + + return !b.Diff(other).HasChanges() +} + +// OnDecoded implements HclResource +func (b *Benchmark) OnDecoded(block *hcl.Block, _ modconfig.ModResourcesProvider) hcl.Diagnostics { + b.SetBaseProperties() + return nil +} + +func (b *Benchmark) String() string { + // build list of children's names + var children []string + for _, child := range b.GetChildren() { + children = append(children, child.Name()) + } + // build list of parents names + var parents []string + for _, p := range b.GetParents() { + parents = append(parents, p.Name()) + } + sort.Strings(children) + return fmt.Sprintf(` + ----- + Name: %s + Title: %s + Description: %s + Parent: %s + Children: + %s + `, + b.FullName, + types.SafeString(b.Title), + types.SafeString(b.Description), + strings.Join(parents, "\n "), + strings.Join(children, "\n ")) +} + +// GetChildControls return a flat list of controls underneath the benchmark in the tree +func (b *Benchmark) GetChildControls() []*Control { + var res []*Control + for _, child := range b.GetChildren() { + if control, ok := child.(*Control); ok { + res = append(res, control) + } else if benchmark, ok := child.(*Benchmark); ok { + res = append(res, benchmark.GetChildControls()...) + } + } + return res +} + +// GetWidth implements DashboardLeafNode +func (b *Benchmark) GetWidth() int { + if b.Width == nil { + return 0 + } + return *b.Width +} + +// GetDisplay implements DashboardLeafNode +func (b *Benchmark) GetDisplay() string { + return typehelpers.SafeString(b.Display) +} + +// GetType implements DashboardLeafNode +func (b *Benchmark) GetType() string { + return typehelpers.SafeString(b.Type) +} + +// GetUnqualifiedName implements DashboardLeafNode, ModTreeItem +func (b *Benchmark) GetUnqualifiedName() string { + return b.UnqualifiedName +} + +func (b *Benchmark) Diff(other *Benchmark) *modconfig.ModTreeItemDiffs { + res := &modconfig.ModTreeItemDiffs{ + Item: b, + Name: b.Name(), + } + + if !utils.SafeStringsEqual(b.Description, other.Description) { + res.AddPropertyDiff("Description") + } + if !utils.SafeStringsEqual(b.Documentation, other.Documentation) { + res.AddPropertyDiff("Documentation") + } + if !utils.SafeStringsEqual(b.Title, other.Title) { + res.AddPropertyDiff("Title") + } + if len(b.Tags) != len(other.Tags) { + res.AddPropertyDiff("Tags") + } else { + for k, v := range b.Tags { + if otherVal := other.Tags[k]; v != otherVal { + res.AddPropertyDiff("Tags") + } + } + } + + if !utils.SafeStringsEqual(b.Type, other.Type) { + res.AddPropertyDiff("Type") + } + + if len(b.ChildNameStrings) != len(other.ChildNameStrings) { + res.AddPropertyDiff("Childen") + } else { + myChildNames := b.ChildNameStrings + sort.Strings(myChildNames) + otherChildNames := other.ChildNameStrings + sort.Strings(otherChildNames) + if strings.Join(myChildNames, ",") != strings.Join(otherChildNames, ",") { + res.AddPropertyDiff("Childen") + } + } + + res.Merge(dashboardLeafNodeDiff(b, other)) + return res +} + +func (b *Benchmark) WalkResources(resourceFunc func(resource modconfig.ModTreeItem) (bool, error)) error { + for _, child := range b.GetChildren() { + continueWalking, err := resourceFunc(child) + if err != nil { + return err + } + if !continueWalking { + break + } + + if childContainer, ok := child.(*Benchmark); ok { + if err := childContainer.WalkResources(resourceFunc); err != nil { + return err + } + } + } + return nil +} + +// CtyValue implements CtyValueProvider +func (b *Benchmark) CtyValue() (cty.Value, error) { + return cty_helpers.GetCtyValue(b) +} + +func (b *Benchmark) SetBaseProperties() { + if b.Base == nil { + return + } + // copy base into the HclResourceImpl 'base' property so it is accessible to all nested structs + b.HclResourceImpl.SetBase(b.Base) + // call into parent nested struct SetBaseProperties + b.ModTreeItemImpl.SetBaseProperties() + + if b.Width == nil { + b.Width = b.Base.Width + } + + if b.Display == nil { + b.Display = b.Base.Display + } + + if len(b.GetChildren()) == 0 { + b.Children = b.Base.Children + b.ChildNameStrings = b.Base.ChildNameStrings + b.ChildNames = b.Base.ChildNames + } +} + +// GetShowData implements printers.Showable +func (b *Benchmark) GetShowData() *printers.RowData { + res := printers.NewRowData( + printers.FieldValue{Name: "Children", Value: b.ChildNameStrings}, + ) + res.Merge(b.ModTreeItemImpl.GetShowData()) + return res +} diff --git a/internal/resources/control.go b/internal/resources/control.go new file mode 100644 index 00000000..bffbcc95 --- /dev/null +++ b/internal/resources/control.go @@ -0,0 +1,245 @@ +package resources + +import ( + "fmt" + "github.com/turbot/pipe-fittings/modconfig" + "strings" + + "github.com/hashicorp/hcl/v2" + "github.com/turbot/go-kit/types" + typehelpers "github.com/turbot/go-kit/types" + "github.com/turbot/pipe-fittings/cty_helpers" + "github.com/turbot/pipe-fittings/printers" + "github.com/turbot/pipe-fittings/utils" + "github.com/zclconf/go-cty/cty" +) + +// Control is a struct representing the Control resource +type Control struct { + modconfig.ResourceWithMetadataImpl + QueryProviderImpl + + // required to allow partial decoding + Remain hcl.Body `hcl:",remain" json:"-"` + + Severity *string `cty:"severity" hcl:"severity" snapshot:"severity" json:"severity,omitempty"` + + // dashboard specific properties + Base *Control `hcl:"base" json:"-"` + Width *int `cty:"width" hcl:"width" json:"width,omitempty"` + Type *string `cty:"type" hcl:"type" json:"type,omitempty"` + Display *string `cty:"display" hcl:"display" json:"display,omitempty"` + + parents []modconfig.ModTreeItem +} + +func NewControl(block *hcl.Block, mod *modconfig.Mod, shortName string) modconfig.HclResource { + control := &Control{ + QueryProviderImpl: NewQueryProviderImpl(block, mod, shortName), + } + control.Args = NewQueryArgs() + control.SetAnonymous(block) + return control +} + +func (c *Control) Equals(other *Control) bool { + res := c.ShortName == other.ShortName && + c.FullName == other.FullName && + typehelpers.SafeString(c.Description) == typehelpers.SafeString(other.Description) && + typehelpers.SafeString(c.Documentation) == typehelpers.SafeString(other.Documentation) && + typehelpers.SafeString(c.Severity) == typehelpers.SafeString(other.Severity) && + typehelpers.SafeString(c.SQL) == typehelpers.SafeString(other.SQL) && + typehelpers.SafeString(c.Title) == typehelpers.SafeString(other.Title) + if !res { + return res + } + if len(c.Tags) != len(other.Tags) { + return false + } + for k, v := range c.Tags { + if otherVal := other.Tags[k]; v != otherVal { + return false + } + } + + // args + if c.Args == nil { + if other.Args != nil { + return false + } + } else { + // we have args + if other.Args == nil { + return false + } + if !c.Args.Equals(other.Args) { + return false + } + } + + // query + if c.Query == nil { + if other.Query != nil { + return false + } + } else { + // we have a query + if other.Query == nil { + return false + } + if !c.Query.Equals(other.Query) { + return false + } + } + + // params + if len(c.Params) != len(other.Params) { + return false + } + for i, p := range c.Params { + if !p.Equals(other.Params[i]) { + return false + } + } + + return true +} + +func (c *Control) String() string { + // build list of parents's names + parents := c.GetParentNames() + res := fmt.Sprintf(` + ----- + Name: %s + Title: %s + Description: %s + SQL: %s + Parents: %s +`, + c.FullName, + types.SafeString(c.Title), + types.SafeString(c.Description), + types.SafeString(c.SQL), + strings.Join(parents, "\n ")) + + // add param defs if there are any + if len(c.Params) > 0 { + var paramDefsStr = make([]string, len(c.Params)) + for i, def := range c.Params { + paramDefsStr[i] = def.String() + } + res += fmt.Sprintf("Params:\n\t%s\n ", strings.Join(paramDefsStr, "\n\t")) + } + + // add args + if c.Args != nil && !c.Args.Empty() { + res += fmt.Sprintf("Args:\n\t%s\n ", c.Args) + } + return res +} + +func (c *Control) GetParentNames() []string { + var parents []string + for _, p := range c.parents { + parents = append(parents, p.Name()) + } + return parents +} + +// OnDecoded implements HclResource +func (c *Control) OnDecoded(block *hcl.Block, resourceMapProvider modconfig.ModResourcesProvider) hcl.Diagnostics { + c.SetBaseProperties() + + return c.QueryProviderImpl.OnDecoded(block, resourceMapProvider) +} + +// GetWidth implements DashboardLeafNode +func (c *Control) GetWidth() int { + if c.Width == nil { + return 0 + } + return *c.Width +} + +// GetDisplay implements DashboardLeafNode +func (c *Control) GetDisplay() string { + return "" +} + +// GetType implements DashboardLeafNode +func (c *Control) GetType() string { + return typehelpers.SafeString(c.Type) +} + +func (c *Control) Diff(other *Control) *modconfig.ModTreeItemDiffs { + res := &modconfig.ModTreeItemDiffs{ + Item: c, + Name: c.Name(), + } + + if !utils.SafeStringsEqual(c.Description, other.Description) { + res.AddPropertyDiff("Description") + } + if !utils.SafeStringsEqual(c.Documentation, other.Documentation) { + res.AddPropertyDiff("Documentation") + } + if !utils.SafeStringsEqual(c.Severity, other.Severity) { + res.AddPropertyDiff("Severity") + } + if len(c.Tags) != len(other.Tags) { + res.AddPropertyDiff("Tags") + } else { + for k, v := range c.Tags { + if otherVal := other.Tags[k]; v != otherVal { + res.AddPropertyDiff("Tags") + } + } + } + + res.Merge(dashboardLeafNodeDiff(c, other)) + res.Merge(c.QueryProviderImpl.Diff(other)) + + return res +} + +// CtyValue implements CtyValueProvider +func (c *Control) CtyValue() (cty.Value, error) { + return cty_helpers.GetCtyValue(c) +} + +func (c *Control) SetBaseProperties() { + if c.Base == nil { + return + } + // copy base into the HclResourceImpl 'base' property so it is accessible to all nested structs + c.HclResourceImpl.SetBase(c.Base) + // call into parent nested struct SetBaseProperties + c.QueryProviderImpl.SetBaseProperties() + + if c.Severity == nil { + c.Severity = c.Base.Severity + } + + if c.Width == nil { + c.Width = c.Base.Width + } + if c.Type == nil { + c.Type = c.Base.Type + } + if c.Display == nil { + c.Display = c.Base.Display + } +} + +// GetShowData implements printers.Showable +func (c *Control) GetShowData() *printers.RowData { + res := printers.NewRowData( + printers.NewFieldValue("Severity", c.Severity), + printers.NewFieldValue("Width", c.Width), + printers.NewFieldValue("Type", c.Type), + printers.NewFieldValue("Display", c.Display), + ) + // merge fields from base, putting base fields first + res.Merge(c.QueryProviderImpl.GetShowData()) + return res +} diff --git a/internal/resources/dashboard.go b/internal/resources/dashboard.go new file mode 100644 index 00000000..76506e5b --- /dev/null +++ b/internal/resources/dashboard.go @@ -0,0 +1,484 @@ +package resources + +import ( + "fmt" + "strings" + + "github.com/hashicorp/hcl/v2" + "github.com/spf13/viper" + "github.com/stevenle/topsort" + typehelpers "github.com/turbot/go-kit/types" + "github.com/turbot/pipe-fittings/constants" + "github.com/turbot/pipe-fittings/cty_helpers" + "github.com/turbot/pipe-fittings/modconfig" + "github.com/turbot/pipe-fittings/schema" + "github.com/turbot/pipe-fittings/utils" + "github.com/zclconf/go-cty/cty" +) + +const rootRuntimeDependencyNode = "rootRuntimeDependencyNode" +const RuntimeDependencyDashboardScope = "self" + +// Dashboard is a struct representing the Dashboard resource +type Dashboard struct { + modconfig.ResourceWithMetadataImpl + modconfig.ModTreeItemImpl + WithProviderImpl + + // required to allow partial decoding + Remain hcl.Body `hcl:",remain" json:"-"` + + Width *int `cty:"width" hcl:"width" json:"width,omitempty"` + Display *string `cty:"display" hcl:"display" json:"display,omitempty"` + Inputs []*DashboardInput `cty:"inputs" json:"inputs,omitempty"` + UrlPath string `cty:"url_path" json:"url_path,omitempty"` + Base *Dashboard `hcl:"base" json:"-"` + // store children in a way which can be serialised via cty + ChildNames []string `cty:"children" json:"children,omitempty"` + // map of all inputs in our resource tree + selfInputsMap map[string]*DashboardInput + runtimeDependencyGraph *topsort.Graph +} + +func NewDashboard(block *hcl.Block, mod *modconfig.Mod, shortName string) modconfig.HclResource { + d := &Dashboard{ + ModTreeItemImpl: modconfig.NewModTreeItemImpl(block, mod, shortName), + } + d.SetAnonymous(block) + d.setUrlPath() + + return d +} + +// 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 + } + fullName := parsedName.ToFullName() + + // for query dashboard use generated title, for control use original title + if qp.GetBlockType() != schema.BlockTypeQuery { + title = qp.GetTitle() + } + + var dashboard = &Dashboard{ + ModTreeItemImpl: modconfig.ModTreeItemImpl{ + HclResourceImpl: modconfig.HclResourceImpl{ + ShortName: parsedName.Name, + FullName: fullName, + UnqualifiedName: parsedName.ToResourceName(), + Title: utils.ToStringPointer(title), + Description: utils.ToStringPointer(qp.GetDescription()), + Documentation: utils.ToStringPointer(qp.GetDocumentation()), + Tags: qp.GetTags(), + BlockType: schema.BlockTypeDashboard, + DeclRange: *qp.GetDeclRange(), + }, + Mod: qp.(modconfig.ModItem).GetMod(), + }, + } + + dashboard.setUrlPath() + + table, err := NewQueryDashboardTable(qp) + if err != nil { + return nil, err + } + dashboard.Children = []modconfig.ModTreeItem{table} + + return dashboard, nil +} + +func getQueryDashboardName(qp QueryProvider) (*modconfig.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 := modconfig.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, _ modconfig.ModResourcesProvider) hcl.Diagnostics { + diags := d.SetBaseProperties() + if diags.HasErrors() { + return diags + } + children := d.GetChildren() + d.ChildNames = make([]string, len(children)) + for i, child := range 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) *modconfig.ModTreeItemDiffs { + res := &modconfig.ModTreeItemDiffs{ + 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) AddChild(child modconfig.ModTreeItem) hcl.Diagnostics { + var diags hcl.Diagnostics + d.ModTreeItemImpl.AddChild(child) + + switch c := child.(type) { + case *DashboardInput: + d.Inputs = append(d.Inputs, c) + case *DashboardWith: + err := d.AddWith(c) + if err != nil { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "failed to add 'with' block to dashboard", + Detail: err.Error(), + Subject: &c.DeclRange, + }, + ) + } + } + return diags +} + +func (d *Dashboard) WalkResources(resourceFunc func(resource modconfig.HclResource) (bool, error)) error { + for _, child := range d.GetChildren() { + continueWalking, err := resourceFunc(child.(modconfig.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 modconfig.ModResourcesProvider) 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 modconfig.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 modconfig.HclResource, workspace modconfig.ModResourcesProvider) 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 modconfig.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 cty_helpers.GetCtyValue(d) +} + +func (d *Dashboard) SetBaseProperties() hcl.Diagnostics { + var diags hcl.Diagnostics + if d.Base == nil { + return diags + } + // copy base into the HclResourceImpl 'base' property so it is accessible to all nested structs + d.HclResourceImpl.SetBase(d.Base) + // call into parent nested struct SetBaseProperties + d.ModTreeItemImpl.SetBaseProperties() + + if d.Width == nil { + d.Width = d.Base.Width + } + + if len(d.GetChildren()) == 0 { + d.Children = d.Base.Children + d.ChildNames = d.Base.ChildNames + } + + return d.addBaseInputs(d.Base.Inputs) +} + +func (d *Dashboard) addBaseInputs(baseInputs []*DashboardInput) hcl.Diagnostics { + var diags hcl.Diagnostics + if len(baseInputs) == 0 { + return diags + } + // rebuild Inputs and children + inheritedInputs := make([]*DashboardInput, 0, len(baseInputs)) + inheritedChildren := make([]modconfig.ModTreeItem, 0, len(baseInputs)) + + for i, baseInput := range baseInputs { + input := baseInput.Clone() + input.SetDashboard(d) + // add to mod + moreDiags := d.Mod.AddResource(input) + diags = append(diags, moreDiags...) + // add to our inputs + inheritedInputs[i] = input + inheritedChildren[i] = input + } + + if !diags.HasErrors() { + // 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() + } + + return diags +} + +// 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 +} diff --git a/internal/resources/dashboard_card.go b/internal/resources/dashboard_card.go new file mode 100644 index 00000000..c30f4cd1 --- /dev/null +++ b/internal/resources/dashboard_card.go @@ -0,0 +1,171 @@ +package resources + +import ( + "github.com/hashicorp/hcl/v2" + typehelpers "github.com/turbot/go-kit/types" + "github.com/turbot/pipe-fittings/cty_helpers" + "github.com/turbot/pipe-fittings/modconfig" + "github.com/turbot/pipe-fittings/printers" + "github.com/turbot/pipe-fittings/utils" + "github.com/zclconf/go-cty/cty" +) + +// DashboardCard is a struct representing a leaf dashboard node +type DashboardCard struct { + modconfig.ResourceWithMetadataImpl + QueryProviderImpl + + // required to allow partial decoding + Remain hcl.Body `hcl:",remain" json:"-"` + + Label *string `cty:"label" hcl:"label" snapshot:"label" json:"label,omitempty"` + Value *string `cty:"value" hcl:"value" snapshot:"value" json:"value,omitempty"` + Icon *string `cty:"icon" hcl:"icon" snapshot:"icon" json:"icon,omitempty"` + HREF *string `cty:"href" hcl:"href" snapshot:"href" json:"href,omitempty"` + + Width *int `cty:"width" hcl:"width" json:"width,omitempty"` + Type *string `cty:"type" hcl:"type" json:"type,omitempty"` + Display *string `cty:"display" hcl:"display" json:"display,omitempty"` + Base *DashboardCard `hcl:"base" json:"-"` +} + +func NewDashboardCard(block *hcl.Block, mod *modconfig.Mod, shortName string) modconfig.HclResource { + c := &DashboardCard{ + QueryProviderImpl: NewQueryProviderImpl(block, mod, shortName), + } + + c.SetAnonymous(block) + return c +} + +func (c *DashboardCard) Equals(other *DashboardCard) bool { + diff := c.Diff(other) + return !diff.HasChanges() +} + +// OnDecoded implements HclResource +func (c *DashboardCard) OnDecoded(block *hcl.Block, resourceMapProvider modconfig.ModResourcesProvider) hcl.Diagnostics { + c.SetBaseProperties() + return c.QueryProviderImpl.OnDecoded(block, resourceMapProvider) +} + +func (c *DashboardCard) Diff(other *DashboardCard) *modconfig.ModTreeItemDiffs { + res := &modconfig.ModTreeItemDiffs{ + Item: c, + Name: c.Name(), + } + + if !utils.SafeStringsEqual(c.Label, other.Label) { + res.AddPropertyDiff("Instance") + } + + if !utils.SafeStringsEqual(c.Value, other.Value) { + res.AddPropertyDiff("Value") + } + + if !utils.SafeStringsEqual(c.Type, other.Type) { + res.AddPropertyDiff("Type") + } + + if !utils.SafeStringsEqual(c.Icon, other.Icon) { + res.AddPropertyDiff("Icon") + } + + if !utils.SafeStringsEqual(c.HREF, other.HREF) { + res.AddPropertyDiff("HREF") + } + + res.Merge(c.QueryProviderImpl.Diff(&other.QueryProviderImpl)) + res.PopulateChildDiffs(c, other) + res.Merge(dashboardLeafNodeDiff(c, other)) + + return res +} + +// GetWidth implements DashboardLeafNode +func (c *DashboardCard) GetWidth() int { + if c.Width == nil { + return 0 + } + return *c.Width +} + +// GetDisplay implements DashboardLeafNode +func (c *DashboardCard) GetDisplay() string { + return typehelpers.SafeString(c.Display) +} + +// GetDocumentation implements DashboardLeafNode, ModTreeItem +func (c *DashboardCard) GetDocumentation() string { + return "" +} + +// GetType implements DashboardLeafNode +func (c *DashboardCard) GetType() string { + return typehelpers.SafeString(c.Type) +} + +// ValidateQuery implements QueryProvider +func (c *DashboardCard) ValidateQuery() hcl.Diagnostics { + // query is optional - nothing to do + return nil +} + +// CtyValue implements CtyValueProvider +func (c *DashboardCard) CtyValue() (cty.Value, error) { + return cty_helpers.GetCtyValue(c) +} + +func (c *DashboardCard) SetBaseProperties() { + if c.Base == nil { + return + } + // copy base into the HclResourceImpl 'base' property so it is accessible to all nested structs + c.HclResourceImpl.SetBase(c.Base) + // call into parent nested struct SetBaseProperties + c.QueryProviderImpl.SetBaseProperties() + + if c.Label == nil { + c.Label = c.Base.Label + } + + if c.Value == nil { + c.Value = c.Base.Value + } + + if c.Type == nil { + c.Type = c.Base.Type + } + + if c.Display == nil { + c.Display = c.Base.Display + } + + if c.Icon == nil { + c.Icon = c.Base.Icon + } + + if c.HREF == nil { + c.HREF = c.Base.HREF + } + + if c.Width == nil { + c.Width = c.Base.Width + } +} + +// GetShowData implements printers.Showable +func (c *DashboardCard) GetShowData() *printers.RowData { + res := printers.NewRowData( + printers.NewFieldValue("Label", c.Label), + printers.NewFieldValue("Value", c.Value), + printers.NewFieldValue("Icon", c.Icon), + printers.NewFieldValue("HREF", c.HREF), + printers.NewFieldValue("Width", c.Width), + printers.NewFieldValue("Type", c.Type), + printers.NewFieldValue("Display", c.Display), + ) + // merge fields from base, putting base fields first + res.Merge(c.QueryProviderImpl.GetShowData()) + return res +} diff --git a/internal/resources/dashboard_category.go b/internal/resources/dashboard_category.go new file mode 100644 index 00000000..b0b83b01 --- /dev/null +++ b/internal/resources/dashboard_category.go @@ -0,0 +1,161 @@ +package resources + +import ( + "github.com/hashicorp/hcl/v2" + "github.com/turbot/pipe-fittings/cty_helpers" + "github.com/turbot/pipe-fittings/modconfig" + "github.com/turbot/pipe-fittings/utils" + "github.com/zclconf/go-cty/cty" +) + +type DashboardCategory struct { + modconfig.ResourceWithMetadataImpl + modconfig.ModTreeItemImpl + + // required to allow partial decoding + Remain hcl.Body `hcl:",remain" json:"-"` + + // TACTICAL: include a title property (with a different name to the property in HclResourceImpl for clarity) + // This is purely to ensure the title is included in the panel properties of snapshots + // Note: this will be parsed from HCL, but we must set this explicitly in SetBaseProperties if there is a base + CategoryTitle *string `cty:"title" hcl:"title" snapshot:"title" json:"-"` + CategoryName string `snapshot:"name" json:"-"` + Color *string `cty:"color" hcl:"color" snapshot:"color" json:"color,omitempty"` + Depth *int `cty:"depth" hcl:"depth" snapshot:"depth" json:"depth,omitempty"` + Icon *string `cty:"icon" hcl:"icon" snapshot:"icon" json:"icon,omitempty"` + HREF *string `cty:"href" hcl:"href" snapshot:"href" json:"href,omitempty"` + Fold *DashboardCategoryFold `cty:"fold" hcl:"fold,block" snapshot:"fold" json:"fold,omitempty"` + PropertyList DashboardCategoryPropertyList `cty:"property_list" hcl:"property,block" json:"-"` + Properties map[string]*DashboardCategoryProperty `cty:"properties" snapshot:"properties" json:"properties,omitempty"` + PropertyOrder []string `cty:"property_order" hcl:"property_order,optional" snapshot:"property_order" json:"property_order,omitempty"` + Base *DashboardCategory `hcl:"base" json:"base,omitempty"` +} + +func NewDashboardCategory(block *hcl.Block, mod *modconfig.Mod, shortName string) modconfig.HclResource { + c := &DashboardCategory{ + ModTreeItemImpl: modconfig.NewModTreeItemImpl(block, mod, shortName), + } + c.SetAnonymous(block) + return c +} + +// OnDecoded implements HclResource +func (c *DashboardCategory) OnDecoded(block *hcl.Block, _ modconfig.ModResourcesProvider) hcl.Diagnostics { + c.SetBaseProperties() + // populate properties map + if len(c.PropertyList) > 0 { + c.Properties = make(map[string]*DashboardCategoryProperty, len(c.PropertyList)) + for _, p := range c.PropertyList { + c.Properties[p.ShortName] = p + } + } + c.CategoryName = c.ResourceMetadata.ResourceName + return nil +} + +func (c *DashboardCategory) Equals(other *DashboardCategory) bool { + if other == nil { + return false + } + return !c.Diff(other).HasChanges() +} + +func (c *DashboardCategory) SetBaseProperties() { + if c.Base == nil { + return + } + // copy base into the HclResourceImpl 'base' property so it is accessible to all nested structs + c.HclResourceImpl.SetBase(c.Base) + // call into parent nested struct SetBaseProperties + c.ModTreeItemImpl.SetBaseProperties() + + // TACTICAL: DashboardCategory overrides the title property to ensure is included in the snapshot + c.CategoryTitle = c.Title + c.CategoryName = c.Name() + + if c.Color == nil { + c.Color = c.Base.Color + } + if c.Depth == nil { + c.Depth = c.Base.Depth + } + if c.Icon == nil { + c.Icon = c.Base.Icon + } + if c.HREF == nil { + c.HREF = c.Base.HREF + } + if c.Fold == nil { + c.Fold = c.Base.Fold + } + + if c.PropertyList == nil { + c.PropertyList = c.Base.PropertyList + } else { + c.PropertyList.Merge(c.Base.PropertyList) + } + + if c.PropertyOrder == nil { + c.PropertyOrder = c.Base.PropertyOrder + } +} + +func (c *DashboardCategory) Diff(other *DashboardCategory) *modconfig.ModTreeItemDiffs { + res := &modconfig.ModTreeItemDiffs{ + Item: c, + Name: c.Name(), + } + + if (c.Fold == nil) != (other.Fold == nil) { + res.AddPropertyDiff("Fold") + } + if c.Fold != nil && !c.Fold.Equals(other.Fold) { + res.AddPropertyDiff("Fold") + } + + if len(c.PropertyList) != len(other.PropertyList) { + res.AddPropertyDiff("Properties") + } else { + for i, p := range c.Properties { + if !p.Equals(other.Properties[i]) { + res.AddPropertyDiff("Properties") + } + } + } + + if len(c.PropertyOrder) != len(other.PropertyOrder) { + res.AddPropertyDiff("PropertyOrder") + } else { + for i, p := range c.PropertyOrder { + if p != other.PropertyOrder[i] { + res.AddPropertyDiff("PropertyOrder") + } + } + } + + if !utils.SafeStringsEqual(c.Name, other.Name) { + res.AddPropertyDiff("Name") + } + if !utils.SafeStringsEqual(c.Title, other.Title) { + res.AddPropertyDiff("Title") + } + if !utils.SafeStringsEqual(c.Color, other.Color) { + res.AddPropertyDiff("Color") + } + if !utils.SafeStringsEqual(c.Depth, other.Depth) { + res.AddPropertyDiff("Depth") + } + if !utils.SafeStringsEqual(c.Icon, other.Icon) { + res.AddPropertyDiff("Icon") + } + if !utils.SafeStringsEqual(c.HREF, other.HREF) { + res.AddPropertyDiff("HREF") + } + + return res +} + +// CtyValue implements CtyValueProvider +func (c *DashboardCategory) CtyValue() (cty.Value, error) { + return cty_helpers.GetCtyValue(c) +} diff --git a/internal/resources/dashboard_category_fold.go b/internal/resources/dashboard_category_fold.go new file mode 100644 index 00000000..32ad649e --- /dev/null +++ b/internal/resources/dashboard_category_fold.go @@ -0,0 +1,21 @@ +package resources + +import ( + "github.com/turbot/pipe-fittings/utils" +) + +type DashboardCategoryFold struct { + Title *string `cty:"title" hcl:"title" snapshot:"title" json:"title,omitempty"` + Threshold *int `cty:"threshold" hcl:"threshold" snapshot:"threshold" json:"threshold,omitempty"` + Icon *string `cty:"icon" hcl:"icon" snapshot:"icon" json:"icon,omitempty"` +} + +func (f DashboardCategoryFold) Equals(other *DashboardCategoryFold) bool { + if other == nil { + return false + } + + return utils.SafeStringsEqual(f.Title, other.Title) && + f.Threshold == other.Threshold && + utils.SafeStringsEqual(f.Icon, other.Icon) +} diff --git a/internal/resources/dashboard_category_helpers.go b/internal/resources/dashboard_category_helpers.go new file mode 100644 index 00000000..998f0e38 --- /dev/null +++ b/internal/resources/dashboard_category_helpers.go @@ -0,0 +1,24 @@ +package resources + +import ( + "fmt" + "github.com/hashicorp/hcl/v2" + "github.com/turbot/pipe-fittings/modconfig" +) + +// enrich the shell category by fetching from the ModResourcesProvider +// this is used when a category has been retrieved via a HCL reference - as cty does not serialise all properties +func enrichCategory(shellCategory *DashboardCategory, parent modconfig.HclResource, resourceMapProvider modconfig.ModResourcesProvider) (*DashboardCategory, hcl.Diagnostics) { + var diags hcl.Diagnostics + modResources := resourceMapProvider.GetModResources().(*PowerpipeModResources) + fullCategory, ok := modResources.DashboardCategories[shellCategory.Name()] + if !ok { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: fmt.Sprintf("%s contains edge %s but this has not been loaded", parent.Name(), shellCategory.Name()), + Subject: parent.GetDeclRange(), + }) + return nil, diags + } + return fullCategory, diags +} diff --git a/internal/resources/dashboard_category_list.go b/internal/resources/dashboard_category_list.go new file mode 100644 index 00000000..e7e57fd5 --- /dev/null +++ b/internal/resources/dashboard_category_list.go @@ -0,0 +1,19 @@ +package resources + +type DashboardCategoryList []*DashboardCategory + +func (c *DashboardCategoryList) Merge(other DashboardCategoryList) { + if other == nil { + return + } + var categoryMap = make(map[string]bool) + for _, category := range *c { + categoryMap[category.Name()] = true + } + + for _, otherCategory := range other { + if !categoryMap[otherCategory.Name()] { + *c = append(*c, otherCategory) + } + } +} diff --git a/internal/resources/dashboard_category_property.go b/internal/resources/dashboard_category_property.go new file mode 100644 index 00000000..8aff149b --- /dev/null +++ b/internal/resources/dashboard_category_property.go @@ -0,0 +1,23 @@ +package resources + +import ( + "github.com/turbot/pipe-fittings/utils" +) + +type DashboardCategoryProperty struct { + ShortName string `hcl:"name,label" snapshot:"name" json:"name"` + Display *string `cty:"display" hcl:"display" snapshot:"display" json:"display,omitempty"` + Wrap *string `cty:"wrap" hcl:"wrap" snapshot:"wrap" json:"wrap,omitempty"` + HREF *string `cty:"href" hcl:"href" snapshot:"href" json:"href,omitempty"` +} + +func (c DashboardCategoryProperty) Equals(other *DashboardCategoryProperty) bool { + if other == nil { + return false + } + + return utils.SafeStringsEqual(c.ShortName, other.ShortName) && + utils.SafeStringsEqual(c.Display, other.Display) && + utils.SafeStringsEqual(c.Wrap, other.Wrap) && + utils.SafeStringsEqual(c.HREF, other.HREF) +} diff --git a/internal/resources/dashboard_category_property_list.go b/internal/resources/dashboard_category_property_list.go new file mode 100644 index 00000000..de02ae90 --- /dev/null +++ b/internal/resources/dashboard_category_property_list.go @@ -0,0 +1,19 @@ +package resources + +type DashboardCategoryPropertyList []*DashboardCategoryProperty + +func (c *DashboardCategoryPropertyList) Merge(other DashboardCategoryPropertyList) { + if other == nil { + return + } + var propertyMap = make(map[string]bool) + for _, property := range *c { + propertyMap[property.ShortName] = true + } + + for _, otherProperty := range other { + if !propertyMap[otherProperty.ShortName] { + *c = append(*c, otherProperty) + } + } +} diff --git a/internal/resources/dashboard_chart.go b/internal/resources/dashboard_chart.go new file mode 100644 index 00000000..d43714d3 --- /dev/null +++ b/internal/resources/dashboard_chart.go @@ -0,0 +1,198 @@ +package resources + +import ( + "github.com/hashicorp/hcl/v2" + typehelpers "github.com/turbot/go-kit/types" + "github.com/turbot/pipe-fittings/cty_helpers" + "github.com/turbot/pipe-fittings/modconfig" + "github.com/turbot/pipe-fittings/printers" + "github.com/turbot/pipe-fittings/utils" + "github.com/zclconf/go-cty/cty" +) + +// DashboardChart is a struct representing a leaf dashboard node +type DashboardChart struct { + modconfig.ResourceWithMetadataImpl + QueryProviderImpl + + // required to allow partial decoding + Remain hcl.Body `hcl:",remain" json:"-"` + + Width *int `cty:"width" hcl:"width" json:"width,omitempty"` + Type *string `cty:"type" hcl:"type" json:"type,omitempty"` + Display *string `cty:"display" hcl:"display" json:"display,omitempty"` + Legend *DashboardChartLegend `cty:"legend" hcl:"legend,block" snapshot:"legend" json:"legend,omitempty"` + SeriesList DashboardChartSeriesList `cty:"series_list" hcl:"series,block" json:"series,omitempty"` + Axes *DashboardChartAxes `cty:"axes" hcl:"axes,block" snapshot:"axes" json:"axes,omitempty"` + Grouping *string `cty:"grouping" hcl:"grouping" snapshot:"grouping" json:"grouping,omitempty"` + Transform *string `cty:"transform" hcl:"transform" snapshot:"transform" json:"transform,omitempty"` + Series map[string]*DashboardChartSeries `cty:"series" snapshot:"series"` + Base *DashboardChart `hcl:"base" json:"-"` +} + +func NewDashboardChart(block *hcl.Block, mod *modconfig.Mod, shortName string) modconfig.HclResource { + c := &DashboardChart{ + QueryProviderImpl: NewQueryProviderImpl(block, mod, shortName), + } + + c.SetAnonymous(block) + return c +} + +func (c *DashboardChart) Equals(other *DashboardChart) bool { + diff := c.Diff(other) + return !diff.HasChanges() +} + +// OnDecoded implements HclResource +func (c *DashboardChart) OnDecoded(block *hcl.Block, resourceMapProvider modconfig.ModResourcesProvider) hcl.Diagnostics { + c.SetBaseProperties() + // populate series map + if len(c.SeriesList) > 0 { + c.Series = make(map[string]*DashboardChartSeries, len(c.SeriesList)) + for _, s := range c.SeriesList { + s.OnDecoded() + c.Series[s.Name] = s + } + } + return c.QueryProviderImpl.OnDecoded(block, resourceMapProvider) +} + +func (c *DashboardChart) Diff(other *DashboardChart) *modconfig.ModTreeItemDiffs { + res := &modconfig.ModTreeItemDiffs{ + Item: c, + Name: c.Name(), + } + + if !utils.SafeStringsEqual(c.Type, other.Type) { + res.AddPropertyDiff("Type") + } + + if !utils.SafeStringsEqual(c.Grouping, other.Grouping) { + res.AddPropertyDiff("Grouping") + } + + if !utils.SafeStringsEqual(c.Transform, other.Transform) { + res.AddPropertyDiff("Transform") + } + + if len(c.SeriesList) != len(other.SeriesList) { + res.AddPropertyDiff("Series") + } else { + for i, s := range c.Series { + if !s.Equals(other.Series[i]) { + res.AddPropertyDiff("Series") + } + } + } + + if c.Legend != nil { + if !c.Legend.Equals(other.Legend) { + res.AddPropertyDiff("Legend") + } + } else if other.Legend != nil { + res.AddPropertyDiff("Legend") + } + + if c.Axes != nil { + if !c.Axes.Equals(other.Axes) { + res.AddPropertyDiff("Axes") + } + } else if other.Axes != nil { + res.AddPropertyDiff("Axes") + } + + res.PopulateChildDiffs(c, other) + c.QueryProviderImpl.Diff(other) + res.Merge(dashboardLeafNodeDiff(c, other)) + + return res +} + +// GetWidth implements DashboardLeafNode +func (c *DashboardChart) GetWidth() int { + if c.Width == nil { + return 0 + } + return *c.Width +} + +// GetDisplay implements DashboardLeafNode +func (c *DashboardChart) GetDisplay() string { + return typehelpers.SafeString(c.Display) +} + +// GetType implements DashboardLeafNode +func (c *DashboardChart) GetType() string { + return typehelpers.SafeString(c.Type) +} + +// CtyValue implements CtyValueProvider +func (c *DashboardChart) CtyValue() (cty.Value, error) { + return cty_helpers.GetCtyValue(c) +} + +func (c *DashboardChart) SetBaseProperties() { + if c.Base == nil { + return + } + // copy base into the HclResourceImpl 'base' property so it is accessible to all nested structs + c.HclResourceImpl.SetBase(c.Base) + // call into parent nested struct SetBaseProperties + c.QueryProviderImpl.SetBaseProperties() + + if c.Type == nil { + c.Type = c.Base.Type + } + + if c.Display == nil { + c.Display = c.Base.Display + } + + if c.Axes == nil { + c.Axes = c.Base.Axes + } else { + c.Axes.Merge(c.Base.Axes) + } + + if c.Grouping == nil { + c.Grouping = c.Base.Grouping + } + + if c.Transform == nil { + c.Transform = c.Base.Transform + } + + if c.Legend == nil { + c.Legend = c.Base.Legend + } else { + c.Legend.Merge(c.Base.Legend) + } + + if c.SeriesList == nil { + c.SeriesList = c.Base.SeriesList + } else { + c.SeriesList.Merge(c.Base.SeriesList) + } + + if c.Width == nil { + c.Width = c.Base.Width + } +} + +// GetShowData implements printers.Showable +func (c *DashboardChart) GetShowData() *printers.RowData { + res := printers.NewRowData( + printers.NewFieldValue("Width", c.Width), + printers.NewFieldValue("Type", c.Type), + printers.NewFieldValue("Display", c.Display), + printers.NewFieldValue("Grouping", c.Grouping), + printers.NewFieldValue("Transform", c.Transform), + printers.NewFieldValue("Legend", c.Legend), + printers.NewFieldValue("Series", c.Series), + printers.NewFieldValue("Axes", c.Axes), + ) + // merge fields from base, putting base fields first + res.Merge(c.QueryProviderImpl.GetShowData()) + return res +} diff --git a/internal/resources/dashboard_chart_axes.go b/internal/resources/dashboard_chart_axes.go new file mode 100644 index 00000000..45263e47 --- /dev/null +++ b/internal/resources/dashboard_chart_axes.go @@ -0,0 +1,196 @@ +package resources + +import ( + "github.com/turbot/pipe-fittings/utils" +) + +type DashboardChartAxes struct { + X *DashboardChartAxesX `cty:"x" hcl:"x,block" json:"x,omitempty"` + Y *DashboardChartAxesY `cty:"y" hcl:"y,block" json:"y,omitempty"` +} + +func (a *DashboardChartAxes) Equals(other *DashboardChartAxes) bool { + if other == nil { + return false + } + + if a.X != nil { + if other.X == nil { + return false + } + if !a.X.Equals(other.X) { + return false + } + + } else if other.X != nil { + return false + } + + if a.Y != nil { + if other.Y == nil { + return false + } + if !a.Y.Equals(other.Y) { + return false + } + + } else if other.Y != nil { + return false + } + + return true + +} + +func (a *DashboardChartAxes) Merge(other *DashboardChartAxes) { + if other == nil { + return + } + if a.X == nil { + a.X = other.X + } else { + a.X.Merge(other.X) + } + if a.Y == nil { + a.Y = other.Y + } else { + a.Y.Merge(other.Y) + } +} + +type DashboardChartAxesX struct { + Title *DashboardChartAxisTitle `cty:"title" hcl:"title,block" json:"title,omitempty"` + Labels *DashboardChartLabels `cty:"labels" hcl:"labels,block" json:"labels,omitempty"` + Min *int `cty:"min" hcl:"min" json:"min,omitempty"` + Max *int `cty:"max" hcl:"max" json:"max,omitempty"` +} + +func (x *DashboardChartAxesX) Equals(other *DashboardChartAxesX) bool { + if other == nil { + return false + } + + if !(utils.SafeIntEqual(x.Min, other.Min) && + utils.SafeIntEqual(x.Max, other.Max)) { + return false + } + + if x.Title != nil { + if !x.Title.Equals(other.Title) { + return false + } + } else if other.Title != nil { + return false + } + + if x.Labels != nil { + return x.Labels.Equals(other.Labels) + } else if other.Labels != nil { + return false + } + + return true +} + +func (x *DashboardChartAxesX) Merge(other *DashboardChartAxesX) { + if x.Title == nil { + x.Title = other.Title + } else { + x.Title.Merge(other.Title) + } + if x.Labels == nil { + x.Labels = other.Labels + } else { + x.Labels.Merge(other.Labels) + } + if x.Min == nil { + x.Min = other.Min + } + if x.Max == nil { + x.Max = other.Max + } +} + +type DashboardChartAxesY struct { + Title *DashboardChartAxisTitle `cty:"title" hcl:"title,block" json:"title,omitempty"` + Labels *DashboardChartLabels `cty:"labels" hcl:"labels,block" json:"labels,omitempty"` + Min *int `cty:"min" hcl:"min" json:"min,omitempty"` + Max *int `cty:"max" hcl:"max" json:"max,omitempty"` +} + +func (y *DashboardChartAxesY) Equals(other *DashboardChartAxesY) bool { + if other == nil { + return false + } + + if !(utils.SafeIntEqual(y.Min, other.Min) && + utils.SafeIntEqual(y.Max, other.Max)) { + return false + } + + if y.Title != nil { + if !y.Title.Equals(other.Title) { + return false + } + } else if other.Title != nil { + return false + } + + if y.Labels != nil { + return y.Labels.Equals(other.Labels) + } else if other.Labels != nil { + return false + } + return true +} + +func (y *DashboardChartAxesY) Merge(other *DashboardChartAxesY) { + if y.Title == nil { + y.Title = other.Title + } else { + y.Title.Merge(other.Title) + } + if y.Labels == nil { + y.Labels = other.Labels + } else { + y.Labels.Merge(other.Labels) + } + if y.Min == nil { + y.Min = other.Min + } + if y.Max == nil { + y.Max = other.Max + } +} + +type DashboardChartAxisTitle struct { + Display *string `cty:"display" hcl:"display" json:"display,omitempty"` + Align *string `cty:"align" hcl:"align" json:"align,omitempty"` + Value *string `cty:"value" hcl:"value" json:"value,omitempty"` +} + +func (t *DashboardChartAxisTitle) Equals(other *DashboardChartAxisTitle) bool { + if other == nil { + return false + } + + if !(utils.SafeStringsEqual(t.Display, other.Display) && + utils.SafeStringsEqual(t.Align, other.Align) && + utils.SafeStringsEqual(t.Value, other.Value)) { + return false + } + + return true +} + +func (t *DashboardChartAxisTitle) Merge(other *DashboardChartAxisTitle) { + if t.Display == nil { + t.Display = other.Display + } + if t.Align == nil { + t.Align = other.Align + } + if t.Value == nil { + t.Value = other.Value + } +} diff --git a/internal/resources/dashboard_chart_labels.go b/internal/resources/dashboard_chart_labels.go new file mode 100644 index 00000000..1639685f --- /dev/null +++ b/internal/resources/dashboard_chart_labels.go @@ -0,0 +1,26 @@ +package resources + +import "github.com/turbot/pipe-fittings/utils" + +type DashboardChartLabels struct { + Display *string `cty:"display" hcl:"display" json:"display,omitempty"` + Format *string `cty:"format" hcl:"format" json:"format,omitempty"` +} + +func (l *DashboardChartLabels) Equals(other *DashboardChartLabels) bool { + if other == nil { + return false + } + + return utils.SafeStringsEqual(l.Display, other.Display) && + utils.SafeStringsEqual(l.Format, other.Format) +} + +func (l *DashboardChartLabels) Merge(other *DashboardChartLabels) { + if l.Display == nil { + l.Display = other.Display + } + if l.Format == nil { + l.Format = other.Format + } +} diff --git a/internal/resources/dashboard_chart_legend.go b/internal/resources/dashboard_chart_legend.go new file mode 100644 index 00000000..950af002 --- /dev/null +++ b/internal/resources/dashboard_chart_legend.go @@ -0,0 +1,26 @@ +package resources + +import "github.com/turbot/pipe-fittings/utils" + +type DashboardChartLegend struct { + Display *string `cty:"display" hcl:"display" json:"display,omitempty"` + Position *string `cty:"position" hcl:"position" json:"position,omitempty"` +} + +func (l *DashboardChartLegend) Equals(other *DashboardChartLegend) bool { + if other == nil { + return false + } + + return utils.SafeStringsEqual(l.Display, other.Display) && + utils.SafeStringsEqual(l.Position, other.Position) +} + +func (l *DashboardChartLegend) Merge(other *DashboardChartLegend) { + if l.Display == nil { + l.Display = other.Display + } + if l.Position == nil { + l.Position = other.Position + } +} diff --git a/internal/resources/dashboard_chart_series.go b/internal/resources/dashboard_chart_series.go new file mode 100644 index 00000000..ff5169d5 --- /dev/null +++ b/internal/resources/dashboard_chart_series.go @@ -0,0 +1,39 @@ +package resources + +import "github.com/turbot/pipe-fittings/utils" + +type DashboardChartSeries struct { + Name string `hcl:"name,label" json:"name"` + Title *string `cty:"title" hcl:"title" json:"title,omitempty"` + Color *string `cty:"color" hcl:"color" json:"color,omitempty"` + Points map[string]*DashboardChartSeriesPoint `cty:"points" json:"points,omitempty"` + PointsList []*DashboardChartSeriesPoint `hcl:"point,block" json:"-"` +} + +func (s *DashboardChartSeries) Equals(other *DashboardChartSeries) bool { + if other == nil { + return false + } + + if len(s.PointsList) != len(other.PointsList) { + return false + } + for i, p := range s.PointsList { + if !p.Equals(other.PointsList[i]) { + return false + } + } + + return utils.SafeStringsEqual(s.Name, other.Name) && + utils.SafeStringsEqual(s.Title, other.Title) && + utils.SafeStringsEqual(s.Color, other.Color) +} + +func (s *DashboardChartSeries) OnDecoded() { + if len(s.PointsList) > 0 { + s.Points = make(map[string]*DashboardChartSeriesPoint, len(s.PointsList)) + for _, p := range s.PointsList { + s.Points[p.Name] = p + } + } +} diff --git a/internal/resources/dashboard_chart_series_list.go b/internal/resources/dashboard_chart_series_list.go new file mode 100644 index 00000000..02e3ef0d --- /dev/null +++ b/internal/resources/dashboard_chart_series_list.go @@ -0,0 +1,19 @@ +package resources + +type DashboardChartSeriesList []*DashboardChartSeries + +func (s *DashboardChartSeriesList) Merge(other DashboardChartSeriesList) { + if other == nil { + return + } + var seriesMap = make(map[string]bool) + for _, series := range *s { + seriesMap[series.Name] = true + } + + for _, otherSeries := range other { + if !seriesMap[otherSeries.Name] { + *s = append(*s, otherSeries) + } + } +} diff --git a/internal/resources/dashboard_chart_series_point.go b/internal/resources/dashboard_chart_series_point.go new file mode 100644 index 00000000..a794722c --- /dev/null +++ b/internal/resources/dashboard_chart_series_point.go @@ -0,0 +1,17 @@ +package resources + +import "github.com/turbot/pipe-fittings/utils" + +type DashboardChartSeriesPoint struct { + Name string `hcl:"name,label" json:"-"` + Color *string `cty:"color" hcl:"color" json:"color,omitempty"` +} + +func (s DashboardChartSeriesPoint) Equals(other *DashboardChartSeriesPoint) bool { + if other == nil { + return false + } + + return utils.SafeStringsEqual(s.Name, other.Name) && + utils.SafeStringsEqual(s.Color, other.Color) +} diff --git a/internal/resources/dashboard_container.go b/internal/resources/dashboard_container.go new file mode 100644 index 00000000..7a18c188 --- /dev/null +++ b/internal/resources/dashboard_container.go @@ -0,0 +1,136 @@ +package resources + +import ( + "github.com/hashicorp/hcl/v2" + "github.com/stevenle/topsort" + typehelpers "github.com/turbot/go-kit/types" + "github.com/turbot/pipe-fittings/cty_helpers" + "github.com/turbot/pipe-fittings/modconfig" + "github.com/turbot/pipe-fittings/printers" + "github.com/turbot/pipe-fittings/utils" + "github.com/zclconf/go-cty/cty" +) + +// TODO [node_reuse] add DashboardLeafNodeImpl + +// DashboardContainer is a struct representing the Dashboard and Container resource +type DashboardContainer struct { + modconfig.ResourceWithMetadataImpl + modconfig.ModTreeItemImpl + + // required to allow partial decoding + Remain hcl.Body `hcl:",remain" json:"-"` + + Width *int `cty:"width" hcl:"width" json:"width,omitempty"` + Display *string `cty:"display" hcl:"display" json:"display,omitempty"` + Inputs []*DashboardInput `cty:"inputs" json:"inputs,omitempty"` + // store children in a way which can be serialised via cty + ChildNames []string `cty:"children" json:"children,omitempty"` + + //nolint:unused // TODO: unused attribute + runtimeDependencyGraph *topsort.Graph +} + +func NewDashboardContainer(block *hcl.Block, mod *modconfig.Mod, shortName string) modconfig.HclResource { + c := &DashboardContainer{ + ModTreeItemImpl: modconfig.NewModTreeItemImpl(block, mod, shortName), + } + c.SetAnonymous(block) + + return c +} + +func (c *DashboardContainer) Equals(other *DashboardContainer) bool { + diff := c.Diff(other) + return !diff.HasChanges() +} + +// OnDecoded implements HclResource +func (c *DashboardContainer) OnDecoded(block *hcl.Block, _ modconfig.ModResourcesProvider) hcl.Diagnostics { + c.ChildNames = make([]string, len(c.GetChildren())) + for i, child := range c.GetChildren() { + c.ChildNames[i] = child.Name() + } + return nil +} + +// GetWidth implements DashboardLeafNode +func (c *DashboardContainer) GetWidth() int { + if c.Width == nil { + return 0 + } + return *c.Width +} + +// GetDisplay implements DashboardLeafNode +func (c *DashboardContainer) GetDisplay() string { + return typehelpers.SafeString(c.Display) +} + +// GetType implements DashboardLeafNode +func (c *DashboardContainer) GetType() string { + return "" +} + +func (c *DashboardContainer) Diff(other *DashboardContainer) *modconfig.ModTreeItemDiffs { + res := &modconfig.ModTreeItemDiffs{ + Item: c, + Name: c.Name(), + } + + if !utils.SafeStringsEqual(c.FullName, other.FullName) { + res.AddPropertyDiff("Name") + } + + if !utils.SafeStringsEqual(c.Title, other.Title) { + res.AddPropertyDiff("Title") + } + + if !utils.SafeIntEqual(c.Width, other.Width) { + res.AddPropertyDiff("Width") + } + + if !utils.SafeStringsEqual(c.Display, other.Display) { + res.AddPropertyDiff("Display") + } + + res.PopulateChildDiffs(c, other) + return res +} + +func (c *DashboardContainer) WalkResources(resourceFunc func(resource modconfig.HclResource) (bool, error)) error { + for _, child := range c.Children { + continueWalking, err := resourceFunc(child.(modconfig.HclResource)) + if err != nil { + return err + } + if !continueWalking { + break + } + + if childContainer, ok := child.(*DashboardContainer); ok { + if err := childContainer.WalkResources(resourceFunc); err != nil { + return err + } + } + } + return nil +} + +// CtyValue implements CtyValueProvider +func (c *DashboardContainer) CtyValue() (cty.Value, error) { + return cty_helpers.GetCtyValue(c) +} + +// GetShowData implements printers.Showable +func (c *DashboardContainer) GetShowData() *printers.RowData { + res := printers.NewRowData( + printers.NewFieldValue("Width", c.Width), + printers.NewFieldValue("Display", c.Display), + printers.NewFieldValue("Inputs", c.Inputs), + printers.NewFieldValue("Children", c.ChildNames), + ) + // merge fields from base, putting base fields first + res.Merge(c.ModTreeItemImpl.GetShowData()) + return res +} diff --git a/internal/resources/dashboard_edge.go b/internal/resources/dashboard_edge.go new file mode 100644 index 00000000..78ebe633 --- /dev/null +++ b/internal/resources/dashboard_edge.go @@ -0,0 +1,111 @@ +package resources + +import ( + "github.com/hashicorp/hcl/v2" + "github.com/turbot/pipe-fittings/cty_helpers" + "github.com/turbot/pipe-fittings/modconfig" + "github.com/zclconf/go-cty/cty" +) + +// DashboardEdge is a struct representing a leaf dashboard node +type DashboardEdge struct { + modconfig.ResourceWithMetadataImpl + QueryProviderImpl + + // required to allow partial decoding + Remain hcl.Body `hcl:",remain" json:"-"` + + Category *DashboardCategory `cty:"category" hcl:"category" snapshot:"category" json:"category,omitempty"` + Base *DashboardEdge `hcl:"base" json:"-"` +} + +func NewDashboardEdge(block *hcl.Block, mod *modconfig.Mod, shortName string) modconfig.HclResource { + e := &DashboardEdge{ + QueryProviderImpl: NewQueryProviderImpl(block, mod, shortName), + } + + e.SetAnonymous(block) + return e +} + +func (e *DashboardEdge) Equals(other *DashboardEdge) bool { + diff := e.Diff(other) + return !diff.HasChanges() +} + +// OnDecoded implements HclResource +func (e *DashboardEdge) OnDecoded(_ *hcl.Block, resourceMapProvider modconfig.ModResourcesProvider) hcl.Diagnostics { + e.SetBaseProperties() + + // when we reference resources (i.e. category), + // not all properties are retrieved as they are no cty serialisable + // repopulate category from resourceMapProvider + if e.Category != nil { + fullCategory, diags := enrichCategory(e.Category, e, resourceMapProvider) + if diags.HasErrors() { + return diags + } + e.Category = fullCategory + } + return nil +} + +func (e *DashboardEdge) Diff(other *DashboardEdge) *modconfig.ModTreeItemDiffs { + res := &modconfig.ModTreeItemDiffs{ + Item: e, + Name: e.Name(), + } + if (e.Category == nil) != (other.Category == nil) { + res.AddPropertyDiff("Category") + } + + if e.Category != nil && !e.Category.Equals(other.Category) { + res.AddPropertyDiff("Category") + } + + res.PopulateChildDiffs(e, other) + res.Merge(e.QueryProviderImpl.Diff(other)) + res.Merge(dashboardLeafNodeDiff(e, other)) + + return res +} + +// GetWidth implements DashboardLeafNode +func (e *DashboardEdge) GetWidth() int { + return 0 +} + +// GetDisplay implements DashboardLeafNode +func (e *DashboardEdge) GetDisplay() string { + return "" +} + +// GetDocumentation implements DashboardLeafNode +func (e *DashboardEdge) GetDocumentation() string { + return "" +} + +// GetType implements DashboardLeafNode +func (e *DashboardEdge) GetType() string { + return "" +} + +// CtyValue implements CtyValueProvider +func (e *DashboardEdge) CtyValue() (cty.Value, error) { + return cty_helpers.GetCtyValue(e) +} + +func (e *DashboardEdge) SetBaseProperties() { + if e.Base == nil { + return + } + // copy base into the HclResourceImpl 'base' property so it is accessible to all nested structs + e.HclResourceImpl.SetBase(e.Base) + + // call into parent nested struct SetBaseProperties + e.QueryProviderImpl.SetBaseProperties() + + if e.Category == nil { + e.Category = e.Base.Category + } +} diff --git a/internal/resources/dashboard_edge_list.go b/internal/resources/dashboard_edge_list.go new file mode 100644 index 00000000..0acac91e --- /dev/null +++ b/internal/resources/dashboard_edge_list.go @@ -0,0 +1,45 @@ +package resources + +type DashboardEdgeList []*DashboardEdge + +func (l *DashboardEdgeList) Merge(other DashboardEdgeList) { + if other == nil { + return + } + var edgeMap = make(map[string]bool) + for _, edge := range *l { + edgeMap[edge.ShortName] = true + } + + for _, otherEdge := range other { + if !edgeMap[otherEdge.ShortName] { + *l = append(*l, otherEdge) + } + } +} + +func (l *DashboardEdgeList) Get(name string) *DashboardEdge { + for _, n := range *l { + if n.Name() == name { + return n + } + } + return nil +} + +func (l *DashboardEdgeList) Names() []string { + res := make([]string, len(*l)) + for i, e := range *l { + res[i] = e.Name() + } + return res +} + +func (l *DashboardEdgeList) Contains(other *DashboardEdge) bool { + for _, e := range *l { + if e == other { + return true + } + } + return false +} diff --git a/internal/resources/dashboard_flow.go b/internal/resources/dashboard_flow.go new file mode 100644 index 00000000..2b7234c6 --- /dev/null +++ b/internal/resources/dashboard_flow.go @@ -0,0 +1,251 @@ +package resources + +import ( + "fmt" + "github.com/turbot/pipe-fittings/modconfig" + + "github.com/hashicorp/hcl/v2" + typehelpers "github.com/turbot/go-kit/types" + "github.com/turbot/pipe-fittings/cty_helpers" + "github.com/turbot/pipe-fittings/printers" + "github.com/turbot/pipe-fittings/utils" + "github.com/zclconf/go-cty/cty" +) + +// DashboardFlow is a struct representing a leaf dashboard node +type DashboardFlow struct { + modconfig.ResourceWithMetadataImpl + QueryProviderImpl + WithProviderImpl + + // required to allow partial decoding + Remain hcl.Body `hcl:",remain" json:"-"` + + Nodes DashboardNodeList `cty:"node_list" json:"-"` + Edges DashboardEdgeList `cty:"edge_list" json:"-"` + NodeNames []string `json:"nodes" snapshot:"nodes"` + EdgeNames []string `json:"edges" snapshot:"edges"` + + Categories map[string]*DashboardCategory `cty:"categories" json:"categories" snapshot:"categories"` + + Width *int `cty:"width" hcl:"width" json:"width,omitempty"` + Type *string `cty:"type" hcl:"type" json:"type,omitempty"` + Display *string `cty:"display" hcl:"display" json:"display,omitempty"` + + Base *DashboardFlow `hcl:"base" json:"-"` +} + +func NewDashboardFlow(block *hcl.Block, mod *modconfig.Mod, shortName string) modconfig.HclResource { + f := &DashboardFlow{ + Categories: make(map[string]*DashboardCategory), + QueryProviderImpl: NewQueryProviderImpl(block, mod, shortName), + } + f.SetAnonymous(block) + return f +} + +func (f *DashboardFlow) Equals(other *DashboardFlow) bool { + diff := f.Diff(other) + return !diff.HasChanges() +} + +// OnDecoded implements HclResource +func (f *DashboardFlow) OnDecoded(block *hcl.Block, resourceMapProvider modconfig.ModResourcesProvider) hcl.Diagnostics { + f.SetBaseProperties() + if len(f.Nodes) > 0 { + f.NodeNames = f.Nodes.Names() + } + if len(f.Edges) > 0 { + f.EdgeNames = f.Edges.Names() + } + return f.QueryProviderImpl.OnDecoded(block, resourceMapProvider) +} + +// TODO [node_reuse] Add DashboardLeafNodeImpl and move this there https://github.com/turbot/steampipe/issues/2926 +// GetChildren implements ModTreeItem +func (f *DashboardFlow) GetChildren() []modconfig.ModTreeItem { + // return nodes and edges (if any) + children := make([]modconfig.ModTreeItem, len(f.Nodes)+len(f.Edges)) + for i, n := range f.Nodes { + children[i] = n + } + offset := len(f.Nodes) + for i, e := range f.Edges { + children[i+offset] = e + } + return children +} + +func (f *DashboardFlow) Diff(other *DashboardFlow) *modconfig.ModTreeItemDiffs { + res := &modconfig.ModTreeItemDiffs{ + Item: f, + Name: f.Name(), + } + + if !utils.SafeStringsEqual(f.Type, other.Type) { + res.AddPropertyDiff("Type") + } + + if len(f.Categories) != len(other.Categories) { + res.AddPropertyDiff("Categories") + } else { + for name, c := range f.Categories { + if !c.Equals(other.Categories[name]) { + res.AddPropertyDiff("Categories") + } + } + } + + res.PopulateChildDiffs(f, other) + res.Merge(f.QueryProviderImpl.Diff(other)) + res.Merge(dashboardLeafNodeDiff(f, other)) + + return res +} + +// GetWidth implements DashboardLeafNode +func (f *DashboardFlow) GetWidth() int { + if f.Width == nil { + return 0 + } + return *f.Width +} + +// GetDisplay implements DashboardLeafNode +func (f *DashboardFlow) GetDisplay() string { + return typehelpers.SafeString(f.Display) +} + +// GetType implements DashboardLeafNode +func (f *DashboardFlow) GetType() string { + return typehelpers.SafeString(f.Type) +} + +// ValidateQuery implements QueryProvider +func (*DashboardFlow) ValidateQuery() hcl.Diagnostics { + // query is optional - nothing to do + return nil +} + +// GetEdges implements NodeAndEdgeProvider +func (f *DashboardFlow) GetEdges() DashboardEdgeList { + return f.Edges +} + +// GetNodes implements NodeAndEdgeProvider +func (f *DashboardFlow) GetNodes() DashboardNodeList { + return f.Nodes +} + +// SetEdges implements NodeAndEdgeProvider +func (f *DashboardFlow) SetEdges(edges DashboardEdgeList) { + f.Edges = edges +} + +// SetNodes implements NodeAndEdgeProvider +func (f *DashboardFlow) SetNodes(nodes DashboardNodeList) { + f.Nodes = nodes +} + +// AddCategory implements NodeAndEdgeProvider +func (f *DashboardFlow) AddCategory(category *DashboardCategory) hcl.Diagnostics { + categoryName := category.ShortName + if _, ok := f.Categories[categoryName]; ok { + return hcl.Diagnostics{&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: fmt.Sprintf("%s has duplicate category %s", f.Name(), categoryName), + Subject: category.GetDeclRange(), + }} + } + f.Categories[categoryName] = category + return nil +} + +// AddChild implements NodeAndEdgeProvider +func (f *DashboardFlow) AddChild(child modconfig.HclResource) hcl.Diagnostics { + var diags hcl.Diagnostics + switch c := child.(type) { + case *DashboardNode: + f.Nodes = append(f.Nodes, c) + case *DashboardEdge: + f.Edges = append(f.Edges, c) + default: + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: fmt.Sprintf("DashboardFlow does not support children of type %s", child.GetBlockType()), + Subject: f.GetDeclRange(), + }) + return diags + } + // set ourselves as parent + err := child.(modconfig.ModTreeItem).AddParent(f) + if err != nil { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "failed to add parent to ModTreeItem", + Detail: err.Error(), + Subject: child.GetDeclRange(), + }) + } + + return diags +} + +// CtyValue implements CtyValueProvider +func (f *DashboardFlow) CtyValue() (cty.Value, error) { + return cty_helpers.GetCtyValue(f) +} + +func (f *DashboardFlow) SetBaseProperties() { + if f.Base == nil { + return + } + // copy base into the HclResourceImpl 'base' property so it is accessible to all nested structs + f.HclResourceImpl.SetBase(f.Base) + + // call into parent nested struct SetBaseProperties + f.QueryProviderImpl.SetBaseProperties() + + if f.Type == nil { + f.Type = f.Base.Type + } + + if f.Display == nil { + f.Display = f.Base.Display + } + + if f.Width == nil { + f.Width = f.Base.Width + } + + if f.Categories == nil { + f.Categories = f.Base.Categories + } else { + f.Categories = utils.MergeMaps(f.Categories, f.Base.Categories) + } + + if f.Edges == nil { + f.Edges = f.Base.Edges + } else { + f.Edges.Merge(f.Base.Edges) + } + if f.Nodes == nil { + f.Nodes = f.Base.Nodes + } else { + f.Nodes.Merge(f.Base.Nodes) + } +} + +// GetShowData implements printers.Showable +func (f *DashboardFlow) GetShowData() *printers.RowData { + res := printers.NewRowData( + printers.NewFieldValue("Width", f.Width), + printers.NewFieldValue("Type", f.Type), + printers.NewFieldValue("Display", f.Display), + printers.NewFieldValue("Nodes", f.Nodes), + printers.NewFieldValue("Edges", f.Edges), + ) + // merge fields from base, putting base fields first + res.Merge(f.QueryProviderImpl.GetShowData()) + return res +} diff --git a/internal/resources/dashboard_graph.go b/internal/resources/dashboard_graph.go new file mode 100644 index 00000000..e4b69e1b --- /dev/null +++ b/internal/resources/dashboard_graph.go @@ -0,0 +1,257 @@ +package resources + +import ( + "fmt" + "github.com/turbot/pipe-fittings/modconfig" + + "github.com/hashicorp/hcl/v2" + typehelpers "github.com/turbot/go-kit/types" + "github.com/turbot/pipe-fittings/cty_helpers" + "github.com/turbot/pipe-fittings/printers" + "github.com/turbot/pipe-fittings/utils" + "github.com/zclconf/go-cty/cty" +) + +// DashboardGraph is a struct representing a leaf dashboard node +type DashboardGraph struct { + modconfig.ResourceWithMetadataImpl + QueryProviderImpl + WithProviderImpl + + // required to allow partial decoding + Remain hcl.Body `hcl:",remain" json:"-"` + + Nodes DashboardNodeList `cty:"node_list" json:"nodes,omitempty"` + Edges DashboardEdgeList `cty:"edge_list" json:"edges,omitempty"` + NodeNames []string `snapshot:"nodes"` + EdgeNames []string `snapshot:"edges"` + + Categories map[string]*DashboardCategory `cty:"categories" json:"categories,omitempty" snapshot:"categories"` + Direction *string `cty:"direction" hcl:"direction" json:"direction,omitempty" snapshot:"direction"` + + // these properties are JSON serialised by the parent LeafRun + Width *int `cty:"width" hcl:"width" json:"width,omitempty"` + Type *string `cty:"type" hcl:"type" json:"type,omitempty"` + Display *string `cty:"display" hcl:"display" json:"display,omitempty"` + + Base *DashboardGraph `hcl:"base" json:"-"` +} + +func NewDashboardGraph(block *hcl.Block, mod *modconfig.Mod, shortName string) modconfig.HclResource { + g := &DashboardGraph{ + Categories: make(map[string]*DashboardCategory), + QueryProviderImpl: NewQueryProviderImpl(block, mod, shortName), + } + g.SetAnonymous(block) + return g +} + +func (g *DashboardGraph) Equals(other *DashboardGraph) bool { + diff := g.Diff(other) + return !diff.HasChanges() +} + +// OnDecoded implements HclResource +func (g *DashboardGraph) OnDecoded(block *hcl.Block, resourceMapProvider modconfig.ModResourcesProvider) hcl.Diagnostics { + g.SetBaseProperties() + if len(g.Nodes) > 0 { + g.NodeNames = g.Nodes.Names() + } + if len(g.Edges) > 0 { + g.EdgeNames = g.Edges.Names() + } + return g.QueryProviderImpl.OnDecoded(block, resourceMapProvider) +} + +// TODO [node_reuse] Add DashboardLeafNodeImpl and move this there https://github.com/turbot/steampipe/issues/2926 + +// GetChildren implements ModTreeItem +func (g *DashboardGraph) GetChildren() []modconfig.ModTreeItem { + // return nodes and edges (if any) + children := make([]modconfig.ModTreeItem, len(g.Nodes)+len(g.Edges)) + for i, n := range g.Nodes { + children[i] = n + } + offset := len(g.Nodes) + for i, e := range g.Edges { + children[i+offset] = e + } + return children +} + +func (g *DashboardGraph) Diff(other *DashboardGraph) *modconfig.ModTreeItemDiffs { + res := &modconfig.ModTreeItemDiffs{ + Item: g, + Name: g.Name(), + } + + if !utils.SafeStringsEqual(g.Type, other.Type) { + res.AddPropertyDiff("Type") + } + + if !utils.SafeStringsEqual(g.Direction, other.Direction) { + res.AddPropertyDiff("Direction") + } + + if len(g.Categories) != len(other.Categories) { + res.AddPropertyDiff("Categories") + } else { + for name, c := range g.Categories { + if !c.Equals(other.Categories[name]) { + res.AddPropertyDiff("Categories") + } + } + } + + res.PopulateChildDiffs(g, other) + res.Merge(g.QueryProviderImpl.Diff(other)) + res.Merge(dashboardLeafNodeDiff(g, other)) + + return res +} + +// GetWidth implements DashboardLeafNode +func (g *DashboardGraph) GetWidth() int { + if g.Width == nil { + return 0 + } + return *g.Width +} + +// GetDisplay implements DashboardLeafNode +func (g *DashboardGraph) GetDisplay() string { + return typehelpers.SafeString(g.Display) +} + +// GetType implements DashboardLeafNode +func (g *DashboardGraph) GetType() string { + return typehelpers.SafeString(g.Type) +} + +// GetEdges implements NodeAndEdgeProvider +func (g *DashboardGraph) GetEdges() DashboardEdgeList { + return g.Edges +} + +// GetNodes implements NodeAndEdgeProvider +func (g *DashboardGraph) GetNodes() DashboardNodeList { + return g.Nodes +} + +// SetEdges implements NodeAndEdgeProvider +func (g *DashboardGraph) SetEdges(edges DashboardEdgeList) { + g.Edges = edges +} + +// SetNodes implements NodeAndEdgeProvider +func (g *DashboardGraph) SetNodes(nodes DashboardNodeList) { + g.Nodes = nodes +} + +// AddCategory implements NodeAndEdgeProvider +func (g *DashboardGraph) AddCategory(category *DashboardCategory) hcl.Diagnostics { + categoryName := category.ShortName + if _, ok := g.Categories[categoryName]; ok { + return hcl.Diagnostics{&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: fmt.Sprintf("%s has duplicate category %s", g.Name(), categoryName), + Subject: category.GetDeclRange(), + }} + } + g.Categories[categoryName] = category + return nil +} + +// AddChild implements NodeAndEdgeProvider +func (g *DashboardGraph) AddChild(child modconfig.HclResource) hcl.Diagnostics { + var diags hcl.Diagnostics + switch c := child.(type) { + case *DashboardNode: + g.Nodes = append(g.Nodes, c) + case *DashboardEdge: + g.Edges = append(g.Edges, c) + default: + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: fmt.Sprintf("DashboardGraph does not support children of type %s", child.GetBlockType()), + Subject: g.GetDeclRange(), + }) + return diags + } + // set ourselves as parent + err := child.(modconfig.ModTreeItem).AddParent(g) + if err != nil { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "failed to add parent to ModTreeItem", + Detail: err.Error(), + Subject: child.GetDeclRange(), + }) + } + return diags +} + +// CtyValue implements CtyValueProvider +func (g *DashboardGraph) CtyValue() (cty.Value, error) { + return cty_helpers.GetCtyValue(g) +} + +func (g *DashboardGraph) SetBaseProperties() { + if g.Base == nil { + return + } + // copy base into the HclResourceImpl 'base' property so it is accessible to all nested structs + g.HclResourceImpl.SetBase(g.Base) + + // call into parent nested struct SetBaseProperties + g.QueryProviderImpl.SetBaseProperties() + + if g.Type == nil { + g.Type = g.Base.Type + } + + if g.Display == nil { + g.Display = g.Base.Display + } + + if g.Width == nil { + g.Width = g.Base.Width + } + + if g.Categories == nil { + g.Categories = g.Base.Categories + } else { + g.Categories = utils.MergeMaps(g.Categories, g.Base.Categories) + } + + if g.Direction == nil { + g.Direction = g.Base.Direction + } + + if g.Edges == nil { + g.Edges = g.Base.Edges + } else { + g.Edges.Merge(g.Base.Edges) + } + + if g.Nodes == nil { + g.Nodes = g.Base.Nodes + } else { + g.Nodes.Merge(g.Base.Nodes) + } +} + +// GetShowData implements printers.Showable +func (g *DashboardGraph) GetShowData() *printers.RowData { + res := printers.NewRowData( + printers.NewFieldValue("Width", g.Width), + printers.NewFieldValue("Type", g.Type), + printers.NewFieldValue("Display", g.Display), + printers.NewFieldValue("Nodes", g.Nodes), + printers.NewFieldValue("Edges", g.Edges), + printers.NewFieldValue("Direction", g.Direction), + ) + // merge fields from base, putting base fields first + res.Merge(g.QueryProviderImpl.GetShowData()) + return res +} diff --git a/internal/resources/dashboard_hierarchy.go b/internal/resources/dashboard_hierarchy.go new file mode 100644 index 00000000..1fd68383 --- /dev/null +++ b/internal/resources/dashboard_hierarchy.go @@ -0,0 +1,251 @@ +package resources + +import ( + "fmt" + "github.com/turbot/pipe-fittings/modconfig" + + "github.com/hashicorp/hcl/v2" + typehelpers "github.com/turbot/go-kit/types" + "github.com/turbot/pipe-fittings/cty_helpers" + "github.com/turbot/pipe-fittings/printers" + "github.com/turbot/pipe-fittings/utils" + "github.com/zclconf/go-cty/cty" +) + +// DashboardHierarchy is a struct representing a leaf dashboard node +type DashboardHierarchy struct { + modconfig.ResourceWithMetadataImpl + QueryProviderImpl + WithProviderImpl + + // required to allow partial decoding + Remain hcl.Body `hcl:",remain" json:"-"` + + Nodes DashboardNodeList `cty:"node_list" json:"nodes,omitempty"` + Edges DashboardEdgeList `cty:"edge_list" json:"edges,omitempty"` + NodeNames []string `snapshot:"nodes"` + EdgeNames []string `snapshot:"edges"` + + Categories map[string]*DashboardCategory `cty:"categories" json:"categories,omitempty" snapshot:"categories"` + Width *int `cty:"width" hcl:"width" json:"width,omitempty"` + Type *string `cty:"type" hcl:"type" json:"type,omitempty"` + Display *string `cty:"display" hcl:"display" json:"display,omitempty"` + + Base *DashboardHierarchy `hcl:"base" json:"-"` +} + +func NewDashboardHierarchy(block *hcl.Block, mod *modconfig.Mod, shortName string) modconfig.HclResource { + h := &DashboardHierarchy{ + Categories: make(map[string]*DashboardCategory), + QueryProviderImpl: NewQueryProviderImpl(block, mod, shortName), + } + h.SetAnonymous(block) + return h +} + +func (h *DashboardHierarchy) Equals(other *DashboardHierarchy) bool { + diff := h.Diff(other) + return !diff.HasChanges() +} + +// OnDecoded implements HclResource +func (h *DashboardHierarchy) OnDecoded(block *hcl.Block, resourceMapProvider modconfig.ModResourcesProvider) hcl.Diagnostics { + h.SetBaseProperties() + if len(h.Nodes) > 0 { + h.NodeNames = h.Nodes.Names() + } + if len(h.Edges) > 0 { + h.EdgeNames = h.Edges.Names() + } + return h.QueryProviderImpl.OnDecoded(block, resourceMapProvider) +} + +// TODO [node_reuse] Add DashboardLeafNodeImpl and move this there https://github.com/turbot/steampipe/issues/2926 + +// GetChildren implements ModTreeItem +func (h *DashboardHierarchy) GetChildren() []modconfig.ModTreeItem { + // return nodes and edges (if any) + children := make([]modconfig.ModTreeItem, len(h.Nodes)+len(h.Edges)) + for i, n := range h.Nodes { + children[i] = n + } + offset := len(h.Nodes) + for i, e := range h.Edges { + children[i+offset] = e + } + return children +} + +func (h *DashboardHierarchy) Diff(other *DashboardHierarchy) *modconfig.ModTreeItemDiffs { + res := &modconfig.ModTreeItemDiffs{ + Item: h, + Name: h.Name(), + } + + if !utils.SafeStringsEqual(h.Type, other.Type) { + res.AddPropertyDiff("Type") + } + + if len(h.Categories) != len(other.Categories) { + res.AddPropertyDiff("Categories") + } else { + for name, c := range h.Categories { + if !c.Equals(other.Categories[name]) { + res.AddPropertyDiff("Categories") + } + } + } + + res.PopulateChildDiffs(h, other) + res.Merge(h.QueryProviderImpl.Diff(other)) + res.Merge(dashboardLeafNodeDiff(h, other)) + + return res +} + +// GetWidth implements DashboardLeafNode +func (h *DashboardHierarchy) GetWidth() int { + if h.Width == nil { + return 0 + } + return *h.Width +} + +// GetDisplay implements DashboardLeafNode +func (h *DashboardHierarchy) GetDisplay() string { + return typehelpers.SafeString(h.Display) +} + +// GetDocumentation implements DashboardLeafNode, ModTreeItem +func (h *DashboardHierarchy) GetDocumentation() string { + return "" +} + +// GetType implements DashboardLeafNode +func (h *DashboardHierarchy) GetType() string { + return typehelpers.SafeString(h.Type) +} + +// GetEdges implements NodeAndEdgeProvider +func (h *DashboardHierarchy) GetEdges() DashboardEdgeList { + return h.Edges +} + +// GetNodes implements NodeAndEdgeProvider +func (h *DashboardHierarchy) GetNodes() DashboardNodeList { + return h.Nodes +} + +// SetEdges implements NodeAndEdgeProvider +func (h *DashboardHierarchy) SetEdges(edges DashboardEdgeList) { + h.Edges = edges +} + +// SetNodes implements NodeAndEdgeProvider +func (h *DashboardHierarchy) SetNodes(nodes DashboardNodeList) { + h.Nodes = nodes +} + +// AddCategory implements NodeAndEdgeProvider +func (h *DashboardHierarchy) AddCategory(category *DashboardCategory) hcl.Diagnostics { + categoryName := category.ShortName + if _, ok := h.Categories[categoryName]; ok { + return hcl.Diagnostics{&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: fmt.Sprintf("%s has duplicate category %s", h.Name(), categoryName), + Subject: category.GetDeclRange(), + }} + } + h.Categories[categoryName] = category + return nil +} + +// AddChild implements NodeAndEdgeProvider +func (h *DashboardHierarchy) AddChild(child modconfig.HclResource) hcl.Diagnostics { + var diags hcl.Diagnostics + switch c := child.(type) { + case *DashboardNode: + h.Nodes = append(h.Nodes, c) + case *DashboardEdge: + h.Edges = append(h.Edges, c) + default: + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: fmt.Sprintf("DashboardHierarchy does not support children of type %s", child.GetBlockType()), + Subject: h.GetDeclRange(), + }) + return diags + } + // set ourselves as parent + err := child.(modconfig.ModTreeItem).AddParent(h) + if err != nil { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "failed to add parent to ModTreeItem", + Detail: err.Error(), + Subject: child.GetDeclRange(), + }) + } + + return diags +} + +// CtyValue implements CtyValueProvider +func (h *DashboardHierarchy) CtyValue() (cty.Value, error) { + return cty_helpers.GetCtyValue(h) +} + +func (h *DashboardHierarchy) SetBaseProperties() { + if h.Base == nil { + return + } + // copy base into the HclResourceImpl 'base' property so it is accessible to all nested structs + h.HclResourceImpl.SetBase(h.Base) + + // call into parent nested struct SetBaseProperties + h.QueryProviderImpl.SetBaseProperties() + + if h.Type == nil { + h.Type = h.Base.Type + } + + if h.Display == nil { + h.Display = h.Base.Display + } + + if h.Width == nil { + h.Width = h.Base.Width + } + + if h.Categories == nil { + h.Categories = h.Base.Categories + } else { + h.Categories = utils.MergeMaps(h.Categories, h.Base.Categories) + } + + if h.Edges == nil { + h.Edges = h.Base.Edges + } else { + h.Edges.Merge(h.Base.Edges) + } + + if h.Nodes == nil { + h.Nodes = h.Base.Nodes + } else { + h.Nodes.Merge(h.Base.Nodes) + } +} + +// GetShowData implements printers.Showable +func (h *DashboardHierarchy) GetShowData() *printers.RowData { + res := printers.NewRowData( + printers.NewFieldValue("Width", h.Width), + printers.NewFieldValue("Type", h.Type), + printers.NewFieldValue("Display", h.Display), + printers.NewFieldValue("Nodes", h.Nodes), + printers.NewFieldValue("Edges", h.Edges), + ) + // merge fields from base, putting base fields first + res.Merge(h.QueryProviderImpl.GetShowData()) + return res +} diff --git a/internal/resources/dashboard_image.go b/internal/resources/dashboard_image.go new file mode 100644 index 00000000..552ee076 --- /dev/null +++ b/internal/resources/dashboard_image.go @@ -0,0 +1,142 @@ +package resources + +import ( + "github.com/hashicorp/hcl/v2" + typehelpers "github.com/turbot/go-kit/types" + "github.com/turbot/pipe-fittings/cty_helpers" + "github.com/turbot/pipe-fittings/modconfig" + "github.com/turbot/pipe-fittings/printers" + "github.com/turbot/pipe-fittings/utils" + "github.com/zclconf/go-cty/cty" +) + +// DashboardImage is a struct representing a leaf dashboard node +type DashboardImage struct { + modconfig.ResourceWithMetadataImpl + QueryProviderImpl + + // required to allow partial decoding + Remain hcl.Body `hcl:",remain" json:"-"` + + Src *string `cty:"src" hcl:"src" json:"src,omitempty" snapshot:"src"` + Alt *string `cty:"alt" hcl:"alt" json:"alt,omitempty" snapshot:"alt"` + + // these properties are JSON serialised by the parent LeafRun + Width *int `cty:"width" hcl:"width" json:"width,omitempty" ` + Display *string `cty:"display" hcl:"display" json:"display,omitempty"` + + Base *DashboardImage `hcl:"base" json:"-"` +} + +func NewDashboardImage(block *hcl.Block, mod *modconfig.Mod, shortName string) modconfig.HclResource { + i := &DashboardImage{ + QueryProviderImpl: NewQueryProviderImpl(block, mod, shortName), + } + i.SetAnonymous(block) + return i +} + +func (i *DashboardImage) Equals(other *DashboardImage) bool { + diff := i.Diff(other) + return !diff.HasChanges() +} + +// OnDecoded implements HclResource +func (i *DashboardImage) OnDecoded(block *hcl.Block, resourceMapProvider modconfig.ModResourcesProvider) hcl.Diagnostics { + i.SetBaseProperties() + return i.QueryProviderImpl.OnDecoded(block, resourceMapProvider) +} + +func (i *DashboardImage) Diff(other *DashboardImage) *modconfig.ModTreeItemDiffs { + res := &modconfig.ModTreeItemDiffs{ + Item: i, + Name: i.Name(), + } + if !utils.SafeStringsEqual(i.Src, other.Src) { + res.AddPropertyDiff("Src") + } + + if !utils.SafeStringsEqual(i.Alt, other.Alt) { + res.AddPropertyDiff("Alt") + } + + res.PopulateChildDiffs(i, other) + res.Merge(i.QueryProviderImpl.Diff(other)) + res.Merge(dashboardLeafNodeDiff(i, other)) + + return res +} + +// GetWidth implements DashboardLeafNode +func (i *DashboardImage) GetWidth() int { + if i.Width == nil { + return 0 + } + return *i.Width +} + +// GetDisplay implements DashboardLeafNode +func (i *DashboardImage) GetDisplay() string { + return typehelpers.SafeString(i.Display) +} + +// GetDocumentation implements DashboardLeafNode, ModTreeItem +func (*DashboardImage) GetDocumentation() string { + return "" +} + +// GetType implements DashboardLeafNode +func (*DashboardImage) GetType() string { + return "" +} + +// ValidateQuery implements QueryProvider +func (i *DashboardImage) ValidateQuery() hcl.Diagnostics { + // query is optional - nothing to do + return nil +} + +// CtyValue implements CtyValueProvider +func (i *DashboardImage) CtyValue() (cty.Value, error) { + return cty_helpers.GetCtyValue(i) +} + +func (i *DashboardImage) SetBaseProperties() { + if i.Base == nil { + return + } + // copy base into the HclResourceImpl 'base' property so it is accessible to all nested structs + i.HclResourceImpl.SetBase(i.Base) + + // call into parent nested struct SetBaseProperties + i.QueryProviderImpl.SetBaseProperties() + + if i.Src == nil { + i.Src = i.Base.Src + } + + if i.Alt == nil { + i.Alt = i.Base.Alt + } + + if i.Width == nil { + i.Width = i.Base.Width + } + + if i.Display == nil { + i.Display = i.Base.Display + } +} + +// GetShowData implements printers.Showable +func (i *DashboardImage) GetShowData() *printers.RowData { + res := printers.NewRowData( + printers.NewFieldValue("Width", i.Width), + printers.NewFieldValue("Display", i.Display), + printers.NewFieldValue("Src", i.Src), + printers.NewFieldValue("Alt", i.Alt), + ) + // merge fields from base, putting base fields first + res.Merge(i.QueryProviderImpl.GetShowData()) + return res +} diff --git a/internal/resources/dashboard_input.go b/internal/resources/dashboard_input.go new file mode 100644 index 00000000..ef91b69e --- /dev/null +++ b/internal/resources/dashboard_input.go @@ -0,0 +1,210 @@ +package resources + +import ( + "github.com/hashicorp/hcl/v2" + typehelpers "github.com/turbot/go-kit/types" + "github.com/turbot/pipe-fittings/cty_helpers" + "github.com/turbot/pipe-fittings/modconfig" + "github.com/turbot/pipe-fittings/printers" + "github.com/turbot/pipe-fittings/utils" + "github.com/zclconf/go-cty/cty" +) + +// DashboardInput is a struct representing a leaf dashboard node +type DashboardInput struct { + modconfig.ResourceWithMetadataImpl + QueryProviderImpl + + // required to allow partial decoding + Remain hcl.Body `hcl:",remain" json:"-"` + + DashboardName string `json:"dashboard,omitempty"` + Label *string `cty:"label" hcl:"label" json:"label,omitempty" snapshot:"label"` + Placeholder *string `cty:"placeholder" hcl:"placeholder" json:"placeholder,omitempty" snapshot:"placeholder"` + Options []*DashboardInputOption `cty:"options" hcl:"option,block" json:"options,omitempty" snapshot:"options"` + // tactical - exists purely so we can put "unqualified_name" in the snbapshot panel for the input + // TODO remove when input names are refactored https://github.com/turbot/steampipe/issues/2863 + InputName string `cty:"input_name" json:"unqualified_name" snapshot:"unqualified_name"` + + // these properties are JSON serialised by the parent LeafRun + Width *int `cty:"width" hcl:"width" json:"width,omitempty"` + Type *string `cty:"type" hcl:"type" json:"type,omitempty"` + Display *string `cty:"display" hcl:"display" json:"display,omitempty"` + Base *DashboardInput `hcl:"base" json:"-"` + dashboard *Dashboard +} + +func NewDashboardInput(block *hcl.Block, mod *modconfig.Mod, shortName string) modconfig.HclResource { + // input cannot be anonymous + i := &DashboardInput{ + QueryProviderImpl: NewQueryProviderImpl(block, mod, shortName), + } + + // tactical set input name + i.InputName = i.UnqualifiedName + + return i +} + +// TODO remove https://github.com/turbot/steampipe/issues/2864 +func (i *DashboardInput) Clone() *DashboardInput { + return &DashboardInput{ + ResourceWithMetadataImpl: i.ResourceWithMetadataImpl, + QueryProviderImpl: i.QueryProviderImpl, + Width: i.Width, + Type: i.Type, + Label: i.Label, + Placeholder: i.Placeholder, + Display: i.Display, + Options: i.Options, + InputName: i.InputName, + dashboard: i.dashboard, + } +} + +func (i *DashboardInput) Equals(other *DashboardInput) bool { + diff := i.Diff(other) + return !diff.HasChanges() +} + +// OnDecoded implements HclResource +func (i *DashboardInput) OnDecoded(block *hcl.Block, resourceMapProvider modconfig.ModResourcesProvider) hcl.Diagnostics { + i.SetBaseProperties() + return i.QueryProviderImpl.OnDecoded(block, resourceMapProvider) +} + +func (i *DashboardInput) Diff(other *DashboardInput) *modconfig.ModTreeItemDiffs { + res := &modconfig.ModTreeItemDiffs{ + Item: i, + Name: i.Name(), + } + + if !utils.SafeStringsEqual(i.Type, other.Type) { + res.AddPropertyDiff("Type") + } + + if !utils.SafeStringsEqual(i.Label, other.Label) { + res.AddPropertyDiff("Instance") + } + + if !utils.SafeStringsEqual(i.Placeholder, other.Placeholder) { + res.AddPropertyDiff("Placeholder") + } + + if len(i.Options) != len(other.Options) { + res.AddPropertyDiff("Options") + } else { + for idx, o := range i.Options { + if !other.Options[idx].Equals(o) { + res.AddPropertyDiff("Options") + } + } + } + + res.PopulateChildDiffs(i, other) + res.Merge(i.QueryProviderImpl.Diff(other)) + res.Merge(dashboardLeafNodeDiff(i, other)) + + return res +} + +// GetWidth implements DashboardLeafNode +func (i *DashboardInput) GetWidth() int { + if i.Width == nil { + return 0 + } + return *i.Width +} + +// GetDisplay implements DashboardLeafNode +func (i *DashboardInput) GetDisplay() string { + return typehelpers.SafeString(i.Display) +} + +// GetType implements DashboardLeafNode +func (i *DashboardInput) GetType() string { + return typehelpers.SafeString(i.Type) +} + +// SetDashboard sets the parent dashboard container +func (i *DashboardInput) SetDashboard(dashboard *Dashboard) { + i.dashboard = dashboard + i.DashboardName = dashboard.Name() +} + +// ValidateQuery implements QueryProvider +func (i *DashboardInput) ValidateQuery() hcl.Diagnostics { + // inputs with placeholder or options, or text type do not need a query + if i.Placeholder != nil || + len(i.Options) > 0 || + typehelpers.SafeString(i.Type) == "text" { + return nil + } + + return i.QueryProviderImpl.ValidateQuery() +} + +// DependsOnInput returns whether this input has a runtime dependency on the given input¬ +func (i *DashboardInput) DependsOnInput(changedInputName string) bool { + for _, r := range i.runtimeDependencies { + if r.SourceResourceName() == changedInputName { + return true + } + } + return false +} + +// CtyValue implements CtyValueProvider +func (i *DashboardInput) CtyValue() (cty.Value, error) { + return cty_helpers.GetCtyValue(i) +} + +func (i *DashboardInput) SetBaseProperties() { + if i.Base == nil { + return + } + // copy base into the HclResourceImpl 'base' property so it is accessible to all nested structs + i.HclResourceImpl.SetBase(i.Base) + + // call into parent nested struct SetBaseProperties + i.QueryProviderImpl.SetBaseProperties() + + if i.Type == nil { + i.Type = i.Base.Type + } + + if i.Display == nil { + i.Display = i.Base.Display + } + + if i.Label == nil { + i.Label = i.Base.Label + } + + if i.Placeholder == nil { + i.Placeholder = i.Base.Placeholder + } + + if i.Width == nil { + i.Width = i.Base.Width + } + + if i.Options == nil { + i.Options = i.Base.Options + } +} + +// GetShowData implements printers.Showable +func (i *DashboardInput) GetShowData() *printers.RowData { + res := printers.NewRowData( + printers.NewFieldValue("Width", i.Width), + printers.NewFieldValue("Type", i.Type), + printers.NewFieldValue("Display", i.Display), + printers.NewFieldValue("Label", i.Label), + printers.NewFieldValue("Placeholder", i.Placeholder), + printers.NewFieldValue("DashboardName", i.DashboardName), + ) + // merge fields from base, putting base fields first + res.Merge(i.QueryProviderImpl.GetShowData()) + return res +} diff --git a/internal/resources/dashboard_node.go b/internal/resources/dashboard_node.go new file mode 100644 index 00000000..5e8665e8 --- /dev/null +++ b/internal/resources/dashboard_node.go @@ -0,0 +1,126 @@ +package resources + +import ( + "github.com/hashicorp/hcl/v2" + "github.com/turbot/pipe-fittings/cty_helpers" + "github.com/turbot/pipe-fittings/modconfig" + "github.com/zclconf/go-cty/cty" +) + +// DashboardNode is a struct representing a leaf dashboard node +type DashboardNode struct { + modconfig.ResourceWithMetadataImpl + QueryProviderImpl + + // required to allow partial decoding + Remain hcl.Body `hcl:",remain" json:"-"` + + Category *DashboardCategory `cty:"category" hcl:"category" json:"category,omitempty" snapshot:"category"` + Base *DashboardNode `hcl:"base" json:"-"` +} + +func NewDashboardNode(block *hcl.Block, mod *modconfig.Mod, shortName string) modconfig.HclResource { + n := &DashboardNode{ + QueryProviderImpl: NewQueryProviderImpl(block, mod, shortName), + } + + n.SetAnonymous(block) + return n +} + +func (n *DashboardNode) Equals(other *DashboardNode) bool { + diff := n.Diff(other) + return !diff.HasChanges() +} + +// OnDecoded implements HclResource— +func (n *DashboardNode) OnDecoded(_ *hcl.Block, resourceMapProvider modconfig.ModResourcesProvider) hcl.Diagnostics { + n.SetBaseProperties() + + // when we reference resources (i.e. category), + // not all properties are retrieved as they are no cty serialisable + // repopulate category from resourceMapProvider + if n.Category != nil { + fullCategory, diags := enrichCategory(n.Category, n, resourceMapProvider) + if diags.HasErrors() { + return diags + } + n.Category = fullCategory + } + return nil +} + +func (n *DashboardNode) Diff(other *DashboardNode) *modconfig.ModTreeItemDiffs { + res := &modconfig.ModTreeItemDiffs{ + Item: n, + Name: n.Name(), + } + + if (n.Category == nil) != (other.Category == nil) { + res.AddPropertyDiff("Category") + } + if n.Category != nil && !n.Category.Equals(other.Category) { + res.AddPropertyDiff("Category") + } + + res.Merge(n.QueryProviderImpl.Diff(&other.QueryProviderImpl)) + res.Merge(dashboardLeafNodeDiff(n, other)) + res.PopulateChildDiffs(n, other) + + return res +} + +// GetWidth implements DashboardLeafNode +func (n *DashboardNode) GetWidth() int { + return 0 +} + +// GetDisplay implements DashboardLeafNode +func (n *DashboardNode) GetDisplay() string { + return "" +} + +// GetType implements DashboardLeafNode +func (n *DashboardNode) GetType() string { + return "" +} + +// CtyValue implements CtyValueProvider +func (n *DashboardNode) CtyValue() (cty.Value, error) { + return cty_helpers.GetCtyValue(n) +} + +func (n *DashboardNode) SetBaseProperties() { + if n.Base == nil { + return + } + // copy base into the HclResourceImpl 'base' property so it is accessible to all nested structs + n.HclResourceImpl.SetBase(n.Base) + + // call into parent nested struct SetBaseProperties + n.QueryProviderImpl.SetBaseProperties() + + if n.Title == nil { + n.Title = n.Base.Title + } + + if n.SQL == nil { + n.SQL = n.Base.SQL + } + + if n.Query == nil { + n.Query = n.Base.Query + } + + if n.Args == nil { + n.Args = n.Base.Args + } + + if n.Category == nil { + n.Category = n.Base.Category + } + + if n.Params == nil { + n.Params = n.Base.Params + } +} diff --git a/internal/resources/dashboard_node_list.go b/internal/resources/dashboard_node_list.go new file mode 100644 index 00000000..4cf9107c --- /dev/null +++ b/internal/resources/dashboard_node_list.go @@ -0,0 +1,45 @@ +package resources + +type DashboardNodeList []*DashboardNode + +func (l *DashboardNodeList) Merge(other DashboardNodeList) { + if other == nil { + return + } + var nodeMap = make(map[string]bool) + for _, node := range *l { + nodeMap[node.ShortName] = true + } + + for _, otherNode := range other { + if !nodeMap[otherNode.ShortName] { + *l = append(*l, otherNode) + } + } +} + +func (l *DashboardNodeList) Get(name string) *DashboardNode { + for _, n := range *l { + if n.Name() == name { + return n + } + } + return nil +} + +func (l *DashboardNodeList) Names() []string { + res := make([]string, len(*l)) + for i, n := range *l { + res[i] = n.Name() + } + return res +} + +func (l *DashboardNodeList) Contains(other *DashboardNode) bool { + for _, e := range *l { + if e == other { + return true + } + } + return false +} diff --git a/internal/resources/dashboard_option.go b/internal/resources/dashboard_option.go new file mode 100644 index 00000000..f2b42534 --- /dev/null +++ b/internal/resources/dashboard_option.go @@ -0,0 +1,13 @@ +package resources + +import "github.com/turbot/pipe-fittings/utils" + +// DashboardInputOption is a struct representing dashboard input option +type DashboardInputOption struct { + Name string `hcl:"name,label" json:"name" snapshot:"name"` + Label *string `cty:"label" hcl:"label" json:"label,omitempty" snapshot:"label"` +} + +func (o DashboardInputOption) Equals(other *DashboardInputOption) bool { + return utils.SafeStringsEqual(o.Name, other.Name) && utils.SafeStringsEqual(o.Label, other.Label) +} diff --git a/internal/resources/dashboard_table.go b/internal/resources/dashboard_table.go new file mode 100644 index 00000000..b7c1f52f --- /dev/null +++ b/internal/resources/dashboard_table.go @@ -0,0 +1,186 @@ +package resources + +import ( + "github.com/hashicorp/hcl/v2" + typehelpers "github.com/turbot/go-kit/types" + "github.com/turbot/pipe-fittings/cty_helpers" + "github.com/turbot/pipe-fittings/modconfig" + "github.com/turbot/pipe-fittings/printers" + "github.com/turbot/pipe-fittings/schema" + "github.com/turbot/pipe-fittings/utils" + "github.com/zclconf/go-cty/cty" +) + +const SnapshotQueryTableName = "custom.table.results" + +// DashboardTable is a struct representing a leaf dashboard node +type DashboardTable struct { + modconfig.ResourceWithMetadataImpl + QueryProviderImpl + + // required to allow partial decoding + Remain hcl.Body `hcl:",remain" json:"-"` + + Width *int `cty:"width" hcl:"width" json:"width,omitempty"` + Type *string `cty:"type" hcl:"type" json:"type,omitempty"` + ColumnList DashboardTableColumnList `cty:"column_list" hcl:"column,block" json:"columns,omitempty"` + Columns map[string]*DashboardTableColumn `cty:"columns" snapshot:"columns"` + Display *string `cty:"display" hcl:"display" json:"display,omitempty" snapshot:"display"` + Base *DashboardTable `hcl:"base" json:"-"` +} + +func NewDashboardTable(block *hcl.Block, mod *modconfig.Mod, shortName string) modconfig.HclResource { + t := &DashboardTable{ + QueryProviderImpl: NewQueryProviderImpl(block, mod, shortName), + } + t.SetAnonymous(block) + return t +} + +// NewQueryDashboardTable creates a Table to wrap a query. +// This is used in order to execute queries as dashboards +func NewQueryDashboardTable(qp QueryProvider) (*DashboardTable, error) { + parsedName, err := modconfig.ParseResourceName(SnapshotQueryTableName) + if err != nil { + return nil, err + } + fullName := parsedName.ToFullName() + + c := &DashboardTable{ + QueryProviderImpl: QueryProviderImpl{ + RuntimeDependencyProviderImpl: RuntimeDependencyProviderImpl{ + ModTreeItemImpl: modconfig.ModTreeItemImpl{ + HclResourceImpl: modconfig.HclResourceImpl{ + ShortName: parsedName.Name, + FullName: fullName, + UnqualifiedName: parsedName.ToResourceName(), + Title: utils.ToStringPointer(qp.GetTitle()), + BlockType: schema.BlockTypeTable, + }, + Database: qp.GetDatabase(), + Mod: qp.(modconfig.ModItem).GetMod(), + }, + }, + Query: qp.GetQuery(), + SQL: qp.GetSQL(), + Params: qp.GetParams(), + Args: qp.GetArgs(), + }, + } + return c, nil +} + +func (t *DashboardTable) Equals(other *DashboardTable) bool { + diff := t.Diff(other) + return !diff.HasChanges() +} + +// OnDecoded implements HclResource +func (t *DashboardTable) OnDecoded(block *hcl.Block, resourceMapProvider modconfig.ModResourcesProvider) hcl.Diagnostics { + t.SetBaseProperties() + // populate columns map + if len(t.ColumnList) > 0 { + t.Columns = make(map[string]*DashboardTableColumn, len(t.ColumnList)) + for _, c := range t.ColumnList { + t.Columns[c.Name] = c + } + } + return t.QueryProviderImpl.OnDecoded(block, resourceMapProvider) +} + +func (t *DashboardTable) Diff(other *DashboardTable) *modconfig.ModTreeItemDiffs { + res := &modconfig.ModTreeItemDiffs{ + Item: t, + Name: t.Name(), + } + + if !utils.SafeStringsEqual(t.Type, other.Type) { + res.AddPropertyDiff("Type") + } + + if len(t.ColumnList) != len(other.ColumnList) { + res.AddPropertyDiff("Columns") + } else { + for i, c := range t.Columns { + if !c.Equals(other.Columns[i]) { + res.AddPropertyDiff("Columns") + } + } + } + + res.PopulateChildDiffs(t, other) + res.Merge(t.QueryProviderImpl.Diff(other)) + res.Merge(dashboardLeafNodeDiff(t, other)) + + return res +} + +// GetWidth implements DashboardLeafNode +func (t *DashboardTable) GetWidth() int { + if t.Width == nil { + return 0 + } + return *t.Width +} + +// GetDisplay implements DashboardLeafNode +func (t *DashboardTable) GetDisplay() string { + return typehelpers.SafeString(t.Display) +} + +// GetDocumentation implements DashboardLeafNode, ModTreeItem +func (*DashboardTable) GetDocumentation() string { + return "" +} + +// GetType implements DashboardLeafNode +func (t *DashboardTable) GetType() string { + return typehelpers.SafeString(t.Type) +} + +// CtyValue implements CtyValueProvider +func (t *DashboardTable) CtyValue() (cty.Value, error) { + return cty_helpers.GetCtyValue(t) +} + +func (t *DashboardTable) SetBaseProperties() { + if t.Base == nil { + return + } + // copy base into the HclResourceImpl 'base' property so it is accessible to all nested structs + t.HclResourceImpl.SetBase(t.Base) + + // call into parent nested struct SetBaseProperties + t.QueryProviderImpl.SetBaseProperties() + + if t.Width == nil { + t.Width = t.Base.Width + } + + if t.Type == nil { + t.Type = t.Base.Type + } + + if t.Display == nil { + t.Display = t.Base.Display + } + + if t.ColumnList == nil { + t.ColumnList = t.Base.ColumnList + } else { + t.ColumnList.Merge(t.Base.ColumnList) + } +} + +// GetShowData implements printers.Showable +func (t *DashboardTable) GetShowData() *printers.RowData { + res := printers.NewRowData( + printers.NewFieldValue("Width", t.Width), + printers.NewFieldValue("Type", t.Type), + printers.NewFieldValue("Display", t.Display), + printers.NewFieldValue("Columns", t.ColumnList), + ) + // merge fields from base, putting base fields first + res.Merge(t.QueryProviderImpl.GetShowData()) + return res +} diff --git a/internal/resources/dashboard_table_column.go b/internal/resources/dashboard_table_column.go new file mode 100644 index 00000000..96021853 --- /dev/null +++ b/internal/resources/dashboard_table_column.go @@ -0,0 +1,21 @@ +package resources + +import "github.com/turbot/pipe-fittings/utils" + +type DashboardTableColumn struct { + Name string `hcl:"name,label" json:"name" snapshot:"name"` + Display *string `cty:"display" hcl:"display" json:"display,omitempty" snapshot:"display"` + Wrap *string `cty:"wrap" hcl:"wrap" json:"wrap,omitempty" snapshot:"wrap"` + HREF *string `cty:"href" hcl:"href" json:"href,omitempty" snapshot:"href"` +} + +func (c DashboardTableColumn) Equals(other *DashboardTableColumn) bool { + if other == nil { + return false + } + + return utils.SafeStringsEqual(c.Name, other.Name) && + utils.SafeStringsEqual(c.Display, other.Display) && + utils.SafeStringsEqual(c.Wrap, other.Wrap) && + utils.SafeStringsEqual(c.HREF, other.HREF) +} diff --git a/internal/resources/dashboard_table_column_list.go b/internal/resources/dashboard_table_column_list.go new file mode 100644 index 00000000..1d1e8562 --- /dev/null +++ b/internal/resources/dashboard_table_column_list.go @@ -0,0 +1,19 @@ +package resources + +type DashboardTableColumnList []*DashboardTableColumn + +func (c *DashboardTableColumnList) Merge(other DashboardTableColumnList) { + if other == nil { + return + } + var columnMap = make(map[string]bool) + for _, column := range *c { + columnMap[column.Name] = true + } + + for _, otherColumn := range other { + if !columnMap[otherColumn.Name] { + *c = append(*c, otherColumn) + } + } +} diff --git a/internal/resources/dashboard_text.go b/internal/resources/dashboard_text.go new file mode 100644 index 00000000..0a902b8f --- /dev/null +++ b/internal/resources/dashboard_text.go @@ -0,0 +1,128 @@ +package resources + +import ( + "github.com/hashicorp/hcl/v2" + typehelpers "github.com/turbot/go-kit/types" + "github.com/turbot/pipe-fittings/cty_helpers" + "github.com/turbot/pipe-fittings/modconfig" + "github.com/turbot/pipe-fittings/printers" + "github.com/turbot/pipe-fittings/utils" + "github.com/zclconf/go-cty/cty" +) + +// DashboardText is a struct representing a leaf dashboard node +type DashboardText struct { + modconfig.ResourceWithMetadataImpl + modconfig.ModTreeItemImpl + + // required to allow partial decoding + Remain hcl.Body `hcl:",remain" json:"-"` + + Value *string `cty:"value" hcl:"value" snapshot:"value" json:"value,omitempty"` + Width *int `cty:"width" hcl:"width" json:"width,omitempty"` + Type *string `cty:"type" hcl:"type" json:"type,omitempty"` + Display *string `cty:"display" hcl:"display" json:"display,omitempty"` + + Base *DashboardText `hcl:"base" json:"-"` + Mod *modconfig.Mod `cty:"mod" json:"-"` +} + +func NewDashboardText(block *hcl.Block, mod *modconfig.Mod, shortName string) modconfig.HclResource { + t := &DashboardText{ + ModTreeItemImpl: modconfig.NewModTreeItemImpl(block, mod, shortName), + } + t.SetAnonymous(block) + return t +} + +func (t *DashboardText) Equals(other *DashboardText) bool { + diff := t.Diff(other) + return !diff.HasChanges() +} + +// OnDecoded implements HclResource +func (t *DashboardText) OnDecoded(*hcl.Block, modconfig.ModResourcesProvider) hcl.Diagnostics { + t.SetBaseProperties() + return nil +} + +func (t *DashboardText) Diff(other *DashboardText) *modconfig.ModTreeItemDiffs { + res := &modconfig.ModTreeItemDiffs{ + Item: t, + Name: t.Name(), + } + + if !utils.SafeStringsEqual(t.Type, other.Type) { + res.AddPropertyDiff("Type") + } + + if !utils.SafeStringsEqual(t.Value, other.Value) { + res.AddPropertyDiff("Value") + } + + res.PopulateChildDiffs(t, other) + res.Merge(dashboardLeafNodeDiff(t, other)) + return res +} + +// GetWidth implements DashboardLeafNode +func (t *DashboardText) GetWidth() int { + if t.Width == nil { + return 0 + } + return *t.Width +} + +// GetDisplay implements DashboardLeafNode +func (t *DashboardText) GetDisplay() string { + return typehelpers.SafeString(t.Display) +} + +// GetDocumentation implements DashboardLeafNode, ModTreeItem +func (*DashboardText) GetDocumentation() string { + return "" +} + +// GetType implements DashboardLeafNode +func (t *DashboardText) GetType() string { + return typehelpers.SafeString(t.Type) +} + +// CtyValue implements CtyValueProvider +func (t *DashboardText) CtyValue() (cty.Value, error) { + return cty_helpers.GetCtyValue(t) +} + +func (t *DashboardText) SetBaseProperties() { + if t.Base == nil { + return + } + if t.Title == nil { + t.Title = t.Base.Title + } + if t.Type == nil { + t.Type = t.Base.Type + } + if t.Display == nil { + t.Display = t.Base.Display + } + if t.Value == nil { + t.Value = t.Base.Value + } + if t.Width == nil { + t.Width = t.Base.Width + } +} + +// GetShowData implements printers.Showable +func (t *DashboardText) GetShowData() *printers.RowData { + res := printers.NewRowData( + printers.NewFieldValue("Width", t.Width), + printers.NewFieldValue("Type", t.Type), + printers.NewFieldValue("Display", t.Display), + printers.NewFieldValue("Value", t.Value), + ) + // merge fields from base, putting base fields first + res.Merge(t.ModTreeItemImpl.GetShowData()) + return res +} diff --git a/internal/resources/dashboard_with.go b/internal/resources/dashboard_with.go new file mode 100644 index 00000000..2baf3ad9 --- /dev/null +++ b/internal/resources/dashboard_with.go @@ -0,0 +1,65 @@ +package resources + +import ( + "github.com/hashicorp/hcl/v2" + "github.com/turbot/pipe-fittings/cty_helpers" + "github.com/turbot/pipe-fittings/modconfig" + "github.com/zclconf/go-cty/cty" +) + +// DashboardWith is a struct representing a leaf dashboard node +type DashboardWith struct { + modconfig.ResourceWithMetadataImpl + QueryProviderImpl + + // required to allow partial decoding + Remain hcl.Body `hcl:",remain" json:"-"` +} + +func NewDashboardWith(block *hcl.Block, mod *modconfig.Mod, shortName string) modconfig.HclResource { + // with blocks cannot be anonymous + return &DashboardWith{ + QueryProviderImpl: NewQueryProviderImpl(block, mod, shortName), + } +} + +func (w *DashboardWith) Equals(other *DashboardWith) bool { + diff := w.Diff(other) + return !diff.HasChanges() +} + +// OnDecoded implements HclResource +func (w *DashboardWith) OnDecoded(_ *hcl.Block, _ modconfig.ModResourcesProvider) hcl.Diagnostics { + return nil +} + +func (w *DashboardWith) Diff(other *DashboardWith) *modconfig.ModTreeItemDiffs { + res := &modconfig.ModTreeItemDiffs{ + Item: w, + Name: w.Name(), + } + + res.Merge(w.QueryProviderImpl.Diff(other)) + + return res +} + +// GetWidth implements DashboardLeafNode +func (*DashboardWith) GetWidth() int { + return 0 +} + +// GetDisplay implements DashboardLeafNode +func (*DashboardWith) GetDisplay() string { + return "" +} + +// GetType implements DashboardLeafNode +func (*DashboardWith) GetType() string { + return "" +} + +// CtyValue implements CtyValueProvider +func (w *DashboardWith) CtyValue() (cty.Value, error) { + return cty_helpers.GetCtyValue(w) +} diff --git a/internal/resources/diffs.go b/internal/resources/diffs.go new file mode 100644 index 00000000..5019e150 --- /dev/null +++ b/internal/resources/diffs.go @@ -0,0 +1,32 @@ +package resources + +import ( + "github.com/turbot/pipe-fittings/modconfig" + "maps" +) + +func dashboardLeafNodeDiff(l DashboardLeafNode, r DashboardLeafNode) *modconfig.ModTreeItemDiffs { + d := &modconfig.ModTreeItemDiffs{} + if l.Name() != r.Name() { + d.AddPropertyDiff("Name") + } + if l.GetTitle() != r.GetTitle() { + d.AddPropertyDiff("Title") + } + if l.GetWidth() != r.GetWidth() { + d.AddPropertyDiff("Width") + } + if l.GetDisplay() != r.GetDisplay() { + d.AddPropertyDiff("Display") + } + if l.GetDocumentation() != r.GetDocumentation() { + d.AddPropertyDiff("Documentation") + } + if l.GetType() != r.GetType() { + d.AddPropertyDiff("Type") + } + if !maps.Equal(l.GetTags(), r.GetTags()) { + d.AddPropertyDiff("Tags") + } + return d +} diff --git a/internal/resources/generic_type.go b/internal/resources/generic_type.go new file mode 100644 index 00000000..a8d91a93 --- /dev/null +++ b/internal/resources/generic_type.go @@ -0,0 +1,42 @@ +package resources + +import ( + "github.com/turbot/pipe-fittings/modconfig" + "github.com/turbot/pipe-fittings/schema" + "github.com/turbot/pipe-fittings/utils" + "strings" +) + +// GenericTypeToBlockType converts a resource type generic param into a block type +// NOTE special case handling for dashboard items +func GenericTypeToBlockType[T modconfig.ModTreeItem]() string { + var resourceType string + var empty T + switch any(empty).(type) { + case *modconfig.Variable: + resourceType = schema.BlockTypeVariable + case *DashboardCard: + resourceType = schema.BlockTypeCard + case *DashboardChart: + resourceType = schema.BlockTypeChart + case *DashboardContainer: + resourceType = schema.BlockTypeContainer + case *DashboardFlow: + resourceType = schema.BlockTypeFlow + case *DashboardGraph: + resourceType = schema.BlockTypeGraph + case *DashboardHierarchy: + resourceType = schema.BlockTypeHierarchy + case *DashboardImage: + resourceType = schema.BlockTypeImage + case *DashboardInput: + resourceType = schema.BlockTypeInput + case *DashboardTable: + resourceType = schema.BlockTypeTable + case *DashboardText: + resourceType = schema.BlockTypeText + default: + resourceType = strings.ToLower(utils.GetGenericTypeName[T]()) + } + return resourceType +} diff --git a/internal/resources/interfaces.go b/internal/resources/interfaces.go new file mode 100644 index 00000000..adc9c878 --- /dev/null +++ b/internal/resources/interfaces.go @@ -0,0 +1,60 @@ +package resources + +import ( + "github.com/hashicorp/hcl/v2" + "github.com/turbot/pipe-fittings/modconfig" +) + +type WithProvider interface { + AddWith(with *DashboardWith) hcl.Diagnostics + GetWiths() []*DashboardWith + GetWith(string) (*DashboardWith, bool) +} + +// NodeAndEdgeProvider must be implemented by any dashboard leaf node which supports edges and nodes +// (DashboardGraph, DashboardFlow, DashboardHierarchy) +// TODO [node_reuse] add NodeAndEdgeProviderImpl https://github.com/turbot/steampipe/issues/2918 +type NodeAndEdgeProvider interface { + QueryProvider + WithProvider + GetEdges() DashboardEdgeList + SetEdges(DashboardEdgeList) + GetNodes() DashboardNodeList + SetNodes(DashboardNodeList) + AddCategory(category *DashboardCategory) hcl.Diagnostics + AddChild(child modconfig.HclResource) hcl.Diagnostics +} + +// RuntimeDependencyProvider is implemented by all QueryProviders and Dashboard +type RuntimeDependencyProvider interface { + modconfig.ModTreeItem + AddRuntimeDependencies([]*RuntimeDependency) + GetRuntimeDependencies() map[string]*RuntimeDependency +} + +// QueryProvider must be implemented by resources which have query/sql +type QueryProvider interface { + RuntimeDependencyProvider + GetArgs() *QueryArgs + GetParams() []*modconfig.ParamDef + GetSQL() *string + GetQuery() *Query + SetArgs(*QueryArgs) + SetParams([]*modconfig.ParamDef) + GetResolvedQuery(*QueryArgs) (*ResolvedQuery, error) + RequiresExecution(QueryProvider) bool + ValidateQuery() hcl.Diagnostics + MergeParentArgs(QueryProvider, QueryProvider) hcl.Diagnostics + GetQueryProviderImpl() *QueryProviderImpl + ParamsInheritedFromBase() bool + ArgsInheritedFromBase() bool +} + +// DashboardLeafNode must be implemented by resources may be a leaf node in the dashboard execution tree +type DashboardLeafNode interface { + modconfig.ModTreeItem + modconfig.ResourceWithMetadata + GetDisplay() string + GetType() string + GetWidth() int +} diff --git a/internal/resources/mod_resources.go b/internal/resources/mod_resources.go new file mode 100644 index 00000000..bfc26ae5 --- /dev/null +++ b/internal/resources/mod_resources.go @@ -0,0 +1,1003 @@ +package resources + +import ( + "fmt" + "github.com/hashicorp/hcl/v2" + "github.com/turbot/pipe-fittings/modconfig" + "github.com/turbot/pipe-fittings/schema" + "github.com/turbot/pipe-fittings/utils" +) + +func GetModResources(mod *modconfig.Mod) *PowerpipeModResources { + modResources, ok := mod.GetModResources().(*PowerpipeModResources) + if !ok { + // should never happen + panic(fmt.Sprintf("mod.GetModResources() did not return a PowerpipeModResources: %T", mod.GetModResources())) + } + return modResources +} + +// PowerpipeModResources is a struct containing maps of all mod resource types +// This is provided to avoid db needing to reference workspace package +type PowerpipeModResources struct { + // the parent mod + Mod *modconfig.Mod + + Benchmarks map[string]*Benchmark + Controls map[string]*Control + Dashboards map[string]*Dashboard + DashboardCategories map[string]*DashboardCategory + DashboardCards map[string]*DashboardCard + DashboardCharts map[string]*DashboardChart + DashboardContainers map[string]*DashboardContainer + DashboardEdges map[string]*DashboardEdge + DashboardFlows map[string]*DashboardFlow + DashboardGraphs map[string]*DashboardGraph + DashboardHierarchies map[string]*DashboardHierarchy + DashboardImages map[string]*DashboardImage + DashboardInputs map[string]map[string]*DashboardInput + DashboardTables map[string]*DashboardTable + DashboardTexts map[string]*DashboardText + DashboardNodes map[string]*DashboardNode + GlobalDashboardInputs map[string]*DashboardInput + Locals map[string]*modconfig.Local + Variables map[string]*modconfig.Variable + // all mods (including deps) + Mods map[string]*modconfig.Mod + Queries map[string]*Query + References map[string]*modconfig.ResourceReference + // map of snapshot paths, keyed by snapshot name + Snapshots map[string]string +} + +func NewModResources(mod *modconfig.Mod, sourceMaps ...modconfig.ModResources) modconfig.ModResources { + res := emptyPowerpipeModResources() + res.Mod = mod + res.Mods[mod.GetInstallCacheKey()] = mod + res.AddMaps(sourceMaps...) + return res +} + +func emptyPowerpipeModResources() *PowerpipeModResources { + return &PowerpipeModResources{ + Controls: make(map[string]*Control), + Benchmarks: make(map[string]*Benchmark), + Dashboards: make(map[string]*Dashboard), + DashboardCards: make(map[string]*DashboardCard), + DashboardCharts: make(map[string]*DashboardChart), + DashboardContainers: make(map[string]*DashboardContainer), + DashboardEdges: make(map[string]*DashboardEdge), + DashboardFlows: make(map[string]*DashboardFlow), + DashboardGraphs: make(map[string]*DashboardGraph), + DashboardHierarchies: make(map[string]*DashboardHierarchy), + DashboardImages: make(map[string]*DashboardImage), + DashboardInputs: make(map[string]map[string]*DashboardInput), + DashboardTables: make(map[string]*DashboardTable), + DashboardTexts: make(map[string]*DashboardText), + DashboardNodes: make(map[string]*DashboardNode), + DashboardCategories: make(map[string]*DashboardCategory), + GlobalDashboardInputs: make(map[string]*DashboardInput), + Locals: make(map[string]*modconfig.Local), + Mods: make(map[string]*modconfig.Mod), + Queries: make(map[string]*Query), + References: make(map[string]*modconfig.ResourceReference), + Snapshots: make(map[string]string), + Variables: make(map[string]*modconfig.Variable), + } +} + +// QueryProviders returns a slice of all QueryProviders +func (m *PowerpipeModResources) QueryProviders() []QueryProvider { + res := make([]QueryProvider, m.queryProviderCount()) + idx := 0 + f := func(item modconfig.HclResource) (bool, error) { + if queryProvider, ok := item.(QueryProvider); ok { + res[idx] = queryProvider + idx++ + } + return true, nil + } + + // resource func does not return an error + _ = m.WalkResources(f) + + return res +} + +// TopLevelResources returns a new PowerpipeModResources containing only top level resources (i.e. no dependencies) +func (m *PowerpipeModResources) TopLevelResources() modconfig.ModResources { + res := NewModResources(m.Mod) + + f := func(item modconfig.HclResource) (bool, error) { + if modItem, ok := item.(modconfig.ModItem); ok { + if mod := modItem.GetMod(); mod != nil && mod.GetFullName() == m.Mod.GetFullName() { + // the only error we expect is a duplicate item error - ignore + _ = res.AddResource(item) + } + } + return true, nil + } + + // resource func does not return an error + _ = m.WalkResources(f) + + return res +} + +func (m *PowerpipeModResources) Equals(o modconfig.ModResources) bool { + other, ok := o.(*PowerpipeModResources) + if !ok { + return false + } + + if other == nil { + return false + } + + for name, query := range m.Queries { + if otherQuery, ok := other.Queries[name]; !ok { + return false + } else if !query.Equals(otherQuery) { + return false + } + } + for name := range other.Queries { + if _, ok := m.Queries[name]; !ok { + return false + } + } + + for name, control := range m.Controls { + if otherControl, ok := other.Controls[name]; !ok { + return false + } else if !control.Equals(otherControl) { + return false + } + } + for name := range other.Controls { + if _, ok := m.Controls[name]; !ok { + return false + } + } + + for name, benchmark := range m.Benchmarks { + if otherBenchmark, ok := other.Benchmarks[name]; !ok { + return false + } else if !benchmark.Equals(otherBenchmark) { + return false + } + } + for name := range other.Benchmarks { + if _, ok := m.Benchmarks[name]; !ok { + return false + } + } + + for name, variable := range m.Variables { + if otherVariable, ok := other.Variables[name]; !ok { + return false + } else if !variable.Equals(otherVariable) { + return false + } + } + for name := range other.Variables { + if _, ok := m.Variables[name]; !ok { + return false + } + } + + for name, dashboard := range m.Dashboards { + if otherDashboard, ok := other.Dashboards[name]; !ok { + return false + } else if !dashboard.Equals(otherDashboard) { + return false + } + } + for name := range other.Dashboards { + if _, ok := m.Dashboards[name]; !ok { + return false + } + } + + for name, container := range m.DashboardContainers { + if otherContainer, ok := other.DashboardContainers[name]; !ok { + return false + } else if !container.Equals(otherContainer) { + return false + } + } + for name := range other.DashboardContainers { + if _, ok := m.DashboardContainers[name]; !ok { + return false + } + } + + for name, cards := range m.DashboardCards { + if otherCard, ok := other.DashboardCards[name]; !ok { + return false + } else if !cards.Equals(otherCard) { + return false + } + } + for name := range other.DashboardCards { + if _, ok := m.DashboardCards[name]; !ok { + return false + } + } + + for name, charts := range m.DashboardCharts { + if otherChart, ok := other.DashboardCharts[name]; !ok { + return false + } else if !charts.Equals(otherChart) { + return false + } + } + for name := range other.DashboardCharts { + if _, ok := m.DashboardCharts[name]; !ok { + return false + } + } + + for name, flows := range m.DashboardFlows { + if otherFlow, ok := other.DashboardFlows[name]; !ok { + return false + } else if !flows.Equals(otherFlow) { + return false + } + } + for name := range other.DashboardFlows { + if _, ok := m.DashboardFlows[name]; !ok { + return false + } + } + + for name, flows := range m.DashboardGraphs { + if otherFlow, ok := other.DashboardGraphs[name]; !ok { + return false + } else if !flows.Equals(otherFlow) { + return false + } + } + for name := range other.DashboardGraphs { + if _, ok := m.DashboardGraphs[name]; !ok { + return false + } + } + + for name, hierarchies := range m.DashboardHierarchies { + if otherHierarchy, ok := other.DashboardHierarchies[name]; !ok { + return false + } else if !hierarchies.Equals(otherHierarchy) { + return false + } + } + + for name := range other.DashboardNodes { + if _, ok := m.DashboardNodes[name]; !ok { + return false + } + } + + for name := range other.DashboardEdges { + if _, ok := m.DashboardEdges[name]; !ok { + return false + } + } + for name := range other.DashboardCategories { + if _, ok := m.DashboardCategories[name]; !ok { + return false + } + } + + for name, images := range m.DashboardImages { + if otherImage, ok := other.DashboardImages[name]; !ok { + return false + } else if !images.Equals(otherImage) { + return false + } + } + for name := range other.DashboardImages { + if _, ok := m.DashboardImages[name]; !ok { + return false + } + } + + for name, input := range m.GlobalDashboardInputs { + if otherInput, ok := other.GlobalDashboardInputs[name]; !ok { + return false + } else if !input.Equals(otherInput) { + return false + } + } + for name := range other.DashboardInputs { + if _, ok := m.DashboardInputs[name]; !ok { + return false + } + } + + for dashboardName, inputsForDashboard := range m.DashboardInputs { + if otherInputsForDashboard, ok := other.DashboardInputs[dashboardName]; !ok { + return false + } else { + + for name, input := range inputsForDashboard { + if otherInput, ok := otherInputsForDashboard[name]; !ok { + return false + } else if !input.Equals(otherInput) { + return false + } + } + for name := range otherInputsForDashboard { + if _, ok := inputsForDashboard[name]; !ok { + return false + } + } + + } + } + for name := range other.DashboardInputs { + if _, ok := m.DashboardInputs[name]; !ok { + return false + } + } + + for name, table := range m.DashboardTables { + if otherTable, ok := other.DashboardTables[name]; !ok { + return false + } else if !table.Equals(otherTable) { + return false + } + } + for name, category := range m.DashboardCategories { + if otherCategory, ok := other.DashboardCategories[name]; !ok { + return false + } else if !category.Equals(otherCategory) { + return false + } + } + for name := range other.DashboardTables { + if _, ok := m.DashboardTables[name]; !ok { + return false + } + } + + for name, text := range m.DashboardTexts { + if otherText, ok := other.DashboardTexts[name]; !ok { + return false + } else if !text.Equals(otherText) { + return false + } + } + for name := range other.DashboardTexts { + if _, ok := m.DashboardTexts[name]; !ok { + return false + } + } + + for name, reference := range m.References { + if otherReference, ok := other.References[name]; !ok { + return false + } else if !reference.Equals(otherReference) { + return false + } + } + + for name := range other.References { + if _, ok := m.References[name]; !ok { + return false + } + } + + for name := range other.Locals { + if _, ok := m.Locals[name]; !ok { + return false + } + } + return true +} + +// GetResource tries to find a resource with the given name in the PowerpipeModResources +// NOTE: this does NOT support inputs, which are NOT uniquely named in a mod +func (m *PowerpipeModResources) GetResource(parsedName *modconfig.ParsedResourceName) (resource modconfig.HclResource, found bool) { + modName := parsedName.Mod + if modName == "" { + modName = m.Mod.ShortName + } + longName := fmt.Sprintf("%s.%s.%s", modName, parsedName.ItemType, parsedName.Name) + + // NOTE: we could use WalkResources, but this is quicker + + switch parsedName.ItemType { + case schema.BlockTypeBenchmark: + resource, found = m.Benchmarks[longName] + case schema.BlockTypeControl: + resource, found = m.Controls[longName] + case schema.BlockTypeDashboard: + resource, found = m.Dashboards[longName] + case schema.BlockTypeCard: + resource, found = m.DashboardCards[longName] + case schema.BlockTypeCategory: + resource, found = m.DashboardCategories[longName] + case schema.BlockTypeChart: + resource, found = m.DashboardCharts[longName] + case schema.BlockTypeContainer: + resource, found = m.DashboardContainers[longName] + case schema.BlockTypeEdge: + resource, found = m.DashboardEdges[longName] + case schema.BlockTypeFlow: + resource, found = m.DashboardFlows[longName] + case schema.BlockTypeGraph: + resource, found = m.DashboardGraphs[longName] + case schema.BlockTypeHierarchy: + resource, found = m.DashboardHierarchies[longName] + case schema.BlockTypeImage: + resource, found = m.DashboardImages[longName] + case schema.BlockTypeNode: + resource, found = m.DashboardNodes[longName] + case schema.BlockTypeTable: + resource, found = m.DashboardTables[longName] + case schema.BlockTypeText: + resource, found = m.DashboardTexts[longName] + case schema.BlockTypeInput: + // this function only supports global inputs + // if the input has a parent dashboard, you must use GetDashboardInput + resource, found = m.GlobalDashboardInputs[longName] + case schema.BlockTypeQuery: + resource, found = m.Queries[longName] + // note the special case for variables - "var" rather than "variable" + case schema.AttributeVar: + resource, found = m.Variables[longName] + case schema.BlockTypeMod: + for _, mod := range m.Mods { + if mod.ShortName == parsedName.Name { + resource = mod + found = true + break + } + } + + } + return resource, found +} + +// TODO K is this needed +//func (m *PowerpipeModResources) PopulateReferences() { +// utils.LogTime("ModPopulateReferences") +// defer utils.LogTime("ModPopulateReferences end") +// +// // only populate references if introspection is enabled +// switch viper.GetString(constants.ArgIntrospection) { +// case constants.IntrospectionInfo: +// m.References = make(map[string]*modconfig.ResourceReference) +// +// resourceFunc := func(resource modconfig.HclResource) (bool, error) { +// if resourceWithMetadata, ok := resource.(modconfig.ResourceWithMetadata); ok { +// for _, ref := range resourceWithMetadata.GetReferences() { +// m.References[ref.String()] = ref +// } +// +// // if this resource is a RuntimeDependencyProvider, add references from any 'withs' +// if nep, ok := resource.(NodeAndEdgeProvider); ok { +// m.populateNodeEdgeProviderRefs(nep) +// } else if rdp, ok := resource.(RuntimeDependencyProvider); ok { +// m.populateWithRefs(resource.GetUnqualifiedName(), rdp, getWithRoot(rdp)) +// } +// } +// +// // continue walking +// return true, nil +// } +// // resource func does not return an error +// _ = m.WalkResources(resourceFunc) +// } +//} + +// populate references for any nodes/edges which have reference a 'with' +//func (m *PowerpipeModResources) populateNodeEdgeProviderRefs(nep NodeAndEdgeProvider) { +// var withRoots = map[string]WithProvider{} +// for _, n := range nep.GetNodes() { +// // lazy populate with-root +// // (build map keyed by parent +// // - in theory if we inherit some nodes from base, they may have different parents) +// parent := n.GetParents()[0] +// if withRoots[parent.Name()] == nil && len(n.GetRuntimeDependencies()) > 0 { +// withRoots[parent.Name()] = getWithRoot(n) +// } +// m.populateWithRefs(nep.GetUnqualifiedName(), n, withRoots[parent.Name()]) +// } +// for _, e := range nep.GetEdges() { +// // lazy populate with root +// parent := e.GetParents()[0] +// if withRoots[parent.Name()] == nil && len(e.GetRuntimeDependencies()) > 0 { +// withRoots[parent.Name()] = getWithRoot(e) +// } +// +// m.populateWithRefs(nep.GetUnqualifiedName(), e, withRoots[parent.Name()]) +// } +//} + +// populate references for any 'with' blocks referenced by the RuntimeDependencyProvider +//func (m *PowerpipeModResources) populateWithRefs(name string, rdp RuntimeDependencyProvider, withRoot WithProvider) { +// // unexpected but behave nicely +// if withRoot == nil { +// return +// } +// for _, r := range rdp.GetRuntimeDependencies() { +// if r.PropertyPath.ItemType == schema.BlockTypeWith { +// // find the with +// w, ok := withRoot.GetWith(r.PropertyPath.ToResourceName()) +// if ok { +// for _, withRef := range w.References { +// // build a new reference changing the 'from' to the NodeAndEdgeProvider +// ref := withRef.CloneWithNewFrom(name) +// m.References[ref.String()] = ref +// } +// } +// } +// } +//} +// +//// search up the tree to find the root resource which will host any referenced 'withs' +//// this will either be a dashboard ot a NodeEdgeProvider +//func getWithRoot(rdp RuntimeDependencyProvider) WithProvider { +// var withRoot, _ = rdp.(WithProvider) +// // get the root resource which 'owns' any withs +// // (if our parent is the Mod, we are the root resource, otherwise traverse up until we find the mod +// parent := rdp.GetParents()[0] +// +// for parent.GetBlockType() != schema.BlockTypeMod { +// if wp, ok := parent.(WithProvider); ok { +// withRoot = wp +// } +// parent = parent.GetParents()[0] +// } +// return withRoot +//} + +func (m *PowerpipeModResources) Empty() bool { + return len(m.Mods)+ + len(m.Queries)+ + len(m.Controls)+ + len(m.Benchmarks)+ + len(m.Variables)+ + len(m.Dashboards)+ + len(m.DashboardContainers)+ + len(m.DashboardCards)+ + len(m.DashboardCharts)+ + len(m.DashboardFlows)+ + len(m.DashboardGraphs)+ + len(m.DashboardHierarchies)+ + len(m.DashboardNodes)+ + len(m.DashboardEdges)+ + len(m.DashboardCategories)+ + len(m.DashboardImages)+ + len(m.DashboardInputs)+ + len(m.DashboardTables)+ + len(m.DashboardTexts)+ + len(m.References) == 0 +} + +// this is used to create an optimized PowerpipeModResources containing only the queries which will be run +// +//nolint:unused // TODO: check this unused property +func (m *PowerpipeModResources) addControlOrQuery(provider QueryProvider) { + switch p := provider.(type) { + case *Query: + if p != nil { + m.Queries[p.FullName] = p + } + case *Control: + if p != nil { + m.Controls[p.FullName] = p + } + } +} + +// WalkResources calls resourceFunc for every resource in the mod +// if any resourceFunc returns false or an error, return immediately +func (m *PowerpipeModResources) WalkResources(resourceFunc func(item modconfig.HclResource) (bool, error)) error { + for _, r := range m.Mods { + if continueWalking, err := resourceFunc(r); err != nil || !continueWalking { + return err + } + } + for _, r := range m.Benchmarks { + if continueWalking, err := resourceFunc(r); err != nil || !continueWalking { + return err + } + } + for _, r := range m.Controls { + if continueWalking, err := resourceFunc(r); err != nil || !continueWalking { + return err + } + } + for _, r := range m.Dashboards { + if continueWalking, err := resourceFunc(r); err != nil || !continueWalking { + return err + } + } + for _, r := range m.DashboardCards { + if continueWalking, err := resourceFunc(r); err != nil || !continueWalking { + return err + } + } + for _, r := range m.DashboardCategories { + if continueWalking, err := resourceFunc(r); err != nil || !continueWalking { + return err + } + } + for _, r := range m.DashboardCharts { + if continueWalking, err := resourceFunc(r); err != nil || !continueWalking { + return err + } + } + for _, r := range m.DashboardContainers { + if continueWalking, err := resourceFunc(r); err != nil || !continueWalking { + return err + } + } + for _, r := range m.DashboardEdges { + if continueWalking, err := resourceFunc(r); err != nil || !continueWalking { + return err + } + } + for _, r := range m.DashboardFlows { + if continueWalking, err := resourceFunc(r); err != nil || !continueWalking { + return err + } + } + for _, r := range m.DashboardGraphs { + if continueWalking, err := resourceFunc(r); err != nil || !continueWalking { + return err + } + } + for _, r := range m.DashboardHierarchies { + if continueWalking, err := resourceFunc(r); err != nil || !continueWalking { + return err + } + } + for _, r := range m.DashboardImages { + if continueWalking, err := resourceFunc(r); err != nil || !continueWalking { + return err + } + } + for _, inputsForDashboard := range m.DashboardInputs { + for _, r := range inputsForDashboard { + if continueWalking, err := resourceFunc(r); err != nil || !continueWalking { + return err + } + } + } + for _, r := range m.DashboardNodes { + if continueWalking, err := resourceFunc(r); err != nil || !continueWalking { + return err + } + } + for _, r := range m.DashboardTables { + if continueWalking, err := resourceFunc(r); err != nil || !continueWalking { + return err + } + } + for _, r := range m.DashboardTexts { + if continueWalking, err := resourceFunc(r); err != nil || !continueWalking { + return err + } + } + for _, r := range m.GlobalDashboardInputs { + if continueWalking, err := resourceFunc(r); err != nil || !continueWalking { + return err + } + } + for _, r := range m.Locals { + if continueWalking, err := resourceFunc(r); err != nil || !continueWalking { + return err + } + } + for _, r := range m.Queries { + if continueWalking, err := resourceFunc(r); err != nil || !continueWalking { + return err + } + } + // we cannot walk source snapshots as they are not a HclResource + for _, r := range m.Variables { + if continueWalking, err := resourceFunc(r); err != nil || !continueWalking { + return err + } + } + return nil +} + +func (m *PowerpipeModResources) AddResource(item modconfig.HclResource) hcl.Diagnostics { + var diags hcl.Diagnostics + switch r := item.(type) { + case *Query: + name := r.Name() + if existing, ok := m.Queries[name]; ok { + diags = append(diags, modconfig.CheckForDuplicate(existing, item)...) + break + } + m.Queries[name] = r + + case *Control: + name := r.Name() + if existing, ok := m.Controls[name]; ok { + diags = append(diags, modconfig.CheckForDuplicate(existing, item)...) + break + } + m.Controls[name] = r + + case *Benchmark: + name := r.Name() + if existing, ok := m.Benchmarks[name]; ok { + diags = append(diags, modconfig.CheckForDuplicate(existing, item)...) + break + } + m.Benchmarks[name] = r + + case *Dashboard: + name := r.Name() + if existing, ok := m.Dashboards[name]; ok { + diags = append(diags, modconfig.CheckForDuplicate(existing, item)...) + break + } + m.Dashboards[name] = r + + case *DashboardContainer: + name := r.Name() + if existing, ok := m.DashboardContainers[name]; ok { + diags = append(diags, modconfig.CheckForDuplicate(existing, item)...) + break + } + m.DashboardContainers[name] = r + + case *DashboardCard: + name := r.Name() + if existing, ok := m.DashboardCards[name]; ok { + diags = append(diags, modconfig.CheckForDuplicate(existing, item)...) + break + } else { + m.DashboardCards[name] = r + } + + case *DashboardChart: + name := r.Name() + if existing, ok := m.DashboardCharts[name]; ok { + diags = append(diags, modconfig.CheckForDuplicate(existing, item)...) + break + } + m.DashboardCharts[name] = r + + case *DashboardFlow: + name := r.Name() + if existing, ok := m.DashboardFlows[name]; ok { + diags = append(diags, modconfig.CheckForDuplicate(existing, item)...) + break + } + m.DashboardFlows[name] = r + + case *DashboardGraph: + name := r.Name() + if existing, ok := m.DashboardGraphs[name]; ok { + diags = append(diags, modconfig.CheckForDuplicate(existing, item)...) + break + } + m.DashboardGraphs[name] = r + + case *DashboardHierarchy: + name := r.Name() + if existing, ok := m.DashboardHierarchies[name]; ok { + diags = append(diags, modconfig.CheckForDuplicate(existing, item)...) + break + } + m.DashboardHierarchies[name] = r + + case *DashboardNode: + name := r.Name() + if existing, ok := m.DashboardNodes[name]; ok { + diags = append(diags, modconfig.CheckForDuplicate(existing, item)...) + break + } + m.DashboardNodes[name] = r + + case *DashboardEdge: + name := r.Name() + if existing, ok := m.DashboardEdges[name]; ok { + diags = append(diags, modconfig.CheckForDuplicate(existing, item)...) + break + } + m.DashboardEdges[name] = r + + case *DashboardCategory: + name := r.Name() + if existing, ok := m.DashboardCategories[name]; ok { + diags = append(diags, modconfig.CheckForDuplicate(existing, item)...) + break + } + m.DashboardCategories[name] = r + + case *DashboardImage: + name := r.Name() + if existing, ok := m.DashboardImages[name]; ok { + diags = append(diags, modconfig.CheckForDuplicate(existing, item)...) + break + } + m.DashboardImages[name] = r + + case *DashboardInput: + // if input has a dashboard asssigned, add to DashboardInputs + name := r.Name() + if dashboardName := r.DashboardName; dashboardName != "" { + inputsForDashboard := m.DashboardInputs[dashboardName] + if inputsForDashboard == nil { + inputsForDashboard = make(map[string]*DashboardInput) + m.DashboardInputs[dashboardName] = inputsForDashboard + } + // no need to check for dupes as we have already checked before adding the input to th m od + inputsForDashboard[name] = r + break + } + + // so Dashboard Input must be global + if existing, ok := m.GlobalDashboardInputs[name]; ok { + diags = append(diags, modconfig.CheckForDuplicate(existing, item)...) + break + } + m.GlobalDashboardInputs[name] = r + + case *DashboardTable: + name := r.Name() + if existing, ok := m.DashboardTables[name]; ok { + diags = append(diags, modconfig.CheckForDuplicate(existing, item)...) + break + } + m.DashboardTables[name] = r + + case *DashboardText: + name := r.Name() + if existing, ok := m.DashboardTexts[name]; ok { + diags = append(diags, modconfig.CheckForDuplicate(existing, item)...) + break + } + m.DashboardTexts[name] = r + + case *modconfig.Variable: + name := r.Name() + if existing, ok := m.Variables[name]; ok { + diags = append(diags, modconfig.CheckForDuplicate(existing, item)...) + break + } + m.Variables[name] = r + + case *modconfig.Local: + name := r.Name() + if existing, ok := m.Locals[name]; ok { + diags = append(diags, modconfig.CheckForDuplicate(existing, item)...) + break + } + m.Locals[name] = r + + } + return diags +} + +func (m *PowerpipeModResources) AddSnapshots(snapshotPaths []string) { + for _, snapshotPath := range snapshotPaths { + snapshotName := fmt.Sprintf("snapshot.%s", utils.FilenameNoExtension(snapshotPath)) + m.Snapshots[snapshotName] = snapshotPath + } +} + +func (m *PowerpipeModResources) AddMaps(sourceMaps ...modconfig.ModResources) { + for _, s := range sourceMaps { + source := s.(*PowerpipeModResources) + for k, v := range source.Benchmarks { + m.Benchmarks[k] = v + } + for k, v := range source.Controls { + m.Controls[k] = v + } + for k, v := range source.Dashboards { + m.Dashboards[k] = v + } + for k, v := range source.DashboardContainers { + m.DashboardContainers[k] = v + } + for k, v := range source.DashboardCards { + m.DashboardCards[k] = v + } + for k, v := range source.DashboardCategories { + m.DashboardCategories[k] = v + } + for k, v := range source.DashboardCharts { + m.DashboardCharts[k] = v + } + for k, v := range source.DashboardEdges { + m.DashboardEdges[k] = v + } + for k, v := range source.DashboardFlows { + m.DashboardFlows[k] = v + } + for k, v := range source.DashboardGraphs { + m.DashboardGraphs[k] = v + } + for k, v := range source.DashboardHierarchies { + m.DashboardHierarchies[k] = v + } + for k, v := range source.DashboardNodes { + m.DashboardNodes[k] = v + } + for k, v := range source.DashboardImages { + m.DashboardImages[k] = v + } + for k, v := range source.DashboardInputs { + m.DashboardInputs[k] = v + } + for k, v := range source.DashboardTables { + m.DashboardTables[k] = v + } + for k, v := range source.DashboardTexts { + m.DashboardTexts[k] = v + } + for k, v := range source.GlobalDashboardInputs { + m.GlobalDashboardInputs[k] = v + } + for k, v := range source.Locals { + m.Locals[k] = v + } + for k, v := range source.Mods { + m.Mods[k] = v + } + for k, v := range source.Queries { + m.Queries[k] = v + } + for k, v := range source.Snapshots { + m.Snapshots[k] = v + } + for k, v := range source.Variables { + // TODO check why this was necessary and test variables thoroughly + // NOTE: only include variables from root mod - we add in the others separately + //if v.Mod.GetFullName() == m.Mod.GetFullName() { + m.Variables[k] = v + //} + } + } +} + +func (m *PowerpipeModResources) queryProviderCount() int { + numDashboardInputs := 0 + for _, inputs := range m.DashboardInputs { + numDashboardInputs += len(inputs) + } + + numItems := + len(m.Controls) + + len(m.DashboardCards) + + len(m.DashboardCharts) + + len(m.DashboardEdges) + + len(m.DashboardFlows) + + len(m.DashboardGraphs) + + len(m.DashboardHierarchies) + + len(m.DashboardImages) + + numDashboardInputs + + len(m.DashboardNodes) + + len(m.DashboardTables) + + len(m.GlobalDashboardInputs) + + len(m.Queries) + return numItems +} + +func (m *PowerpipeModResources) AddReference(ref *modconfig.ResourceReference) { + m.References[ref.String()] = ref +} + +func (m *PowerpipeModResources) GetReferences() map[string]*modconfig.ResourceReference { + return m.References +} + +func (m *PowerpipeModResources) GetVariables() map[string]*modconfig.Variable { + return m.Variables +} + +func (m *PowerpipeModResources) GetMods() map[string]*modconfig.Mod { + return m.Mods +} diff --git a/internal/resources/query.go b/internal/resources/query.go new file mode 100644 index 00000000..34a0d6a9 --- /dev/null +++ b/internal/resources/query.go @@ -0,0 +1,120 @@ +package resources + +import ( + "fmt" + "github.com/turbot/pipe-fittings/modconfig" + "strings" + + "github.com/hashicorp/hcl/v2" + "github.com/turbot/go-kit/types" + typehelpers "github.com/turbot/go-kit/types" + "github.com/turbot/pipe-fittings/cty_helpers" + "github.com/turbot/pipe-fittings/utils" + "github.com/zclconf/go-cty/cty" +) + +// Query is a struct representing the Query resource +type Query struct { + modconfig.ResourceWithMetadataImpl + QueryProviderImpl + + // required to allow partial decoding + Remain hcl.Body `hcl:",remain" json:"-"` + + // only here as otherwise gocty.ImpliedType panics + Unused string `cty:"unused" json:"-"` +} + +func NewQuery(block *hcl.Block, mod *modconfig.Mod, shortName string) modconfig.HclResource { + // queries cannot be anonymous + return &Query{ + QueryProviderImpl: NewQueryProviderImpl(block, mod, shortName), + } +} + +func (q *Query) Equals(other *Query) bool { + res := q.ShortName == other.ShortName && + q.FullName == other.FullName && + typehelpers.SafeString(q.Description) == typehelpers.SafeString(other.Description) && + typehelpers.SafeString(q.Documentation) == typehelpers.SafeString(other.Documentation) && + typehelpers.SafeString(q.SQL) == typehelpers.SafeString(other.SQL) && + typehelpers.SafeString(q.Title) == typehelpers.SafeString(other.Title) + if !res { + return res + } + + // tags + if q.Tags == nil { + if other.Tags != nil { + return false + } + } else { + // we have tags + if other.Tags == nil { + return false + } + for k, v := range q.Tags { + if otherVal, ok := (other.Tags)[k]; !ok && v != otherVal { + return false + } + } + } + + // params + if len(q.Params) != len(other.Params) { + return false + } + for i, p := range q.Params { + if !p.Equals(other.Params[i]) { + return false + } + } + + return true +} + +func (q *Query) String() string { + res := fmt.Sprintf(` + ----- + Name: %s + Title: %s + Description: %s + SQL: %s +`, q.FullName, types.SafeString(q.Title), types.SafeString(q.Description), types.SafeString(q.SQL)) + + // add param defs if there are any + if len(q.Params) > 0 { + var paramDefsStr = make([]string, len(q.Params)) + for i, def := range q.Params { + paramDefsStr[i] = def.String() + } + res += fmt.Sprintf("Params:\n\t%s\n ", strings.Join(paramDefsStr, "\n\t")) + } + return res +} + +// OnDecoded implements HclResource +func (q *Query) OnDecoded(*hcl.Block, modconfig.ModResourcesProvider) hcl.Diagnostics { + return nil +} + +// CtyValue implements CtyValueProvider +func (q *Query) CtyValue() (cty.Value, error) { + return cty_helpers.GetCtyValue(q) +} + +func (q *Query) Diff(other *Query) *modconfig.ModTreeItemDiffs { + res := &modconfig.ModTreeItemDiffs{ + Item: q, + Name: q.Name(), + } + + if !utils.SafeStringsEqual(q.FullName, other.FullName) { + res.AddPropertyDiff("Name") + } + + res.PopulateChildDiffs(q, other) + res.Merge(q.QueryProviderImpl.Diff(other)) + + return res +} diff --git a/internal/resources/query_args.go b/internal/resources/query_args.go new file mode 100644 index 00000000..a8eea2e1 --- /dev/null +++ b/internal/resources/query_args.go @@ -0,0 +1,408 @@ +package resources + +import ( + "encoding/json" + "fmt" + "github.com/turbot/pipe-fittings/modconfig" + "log/slog" + "strings" + + typehelpers "github.com/turbot/go-kit/types" + "github.com/turbot/pipe-fittings/printers" + "github.com/turbot/pipe-fittings/utils" +) + +// QueryArgs is a struct which contains the arguments used to invoke a query +// these may either be passed by name, in a map, or as a list of positional args +// NOTE: if both are present the named parameters are used +type QueryArgs struct { + // NOTE: ArgMap and ArgList should not be set directly, but should be set using the + // SetNamedArgVal and SetPositionalArgVal methods + // This is so that stringNamedArgs and stringPositionalArgs can be set correctly + ArgMap map[string]string `cty:"args" json:"args,omitempty"` + // args list may be sparsely populated (in case of runtime dependencies) + // so use *string + ArgList []*string `cty:"args_list" json:"args_list,omitempty"` + References []*modconfig.ResourceReference `cty:"refs" json:"refs,omitempty"` + // TACTICAL: map of positional and named args which are strings and therefor do NOT need JSON serialising + // (can be removed when we move to cty) + stringNamedArgs map[string]struct{} + stringPositionalArgs map[int]struct{} +} + +func (q *QueryArgs) String() string { + if q == nil { + return "" + } + if len(q.ArgList) > 0 { + argsStringList := q.ArgsStringList() + return fmt.Sprintf("Args list: %s", strings.Join(argsStringList, ",")) + } + if len(q.ArgMap) > 0 { + var strs = make([]string, len(q.ArgMap)) + idx := 0 + for k, v := range q.ArgMap { + strs[idx] = fmt.Sprintf("%s = %s", k, v) + idx++ + } + return fmt.Sprintf("args:\n\t%s", strings.Join(strs, "\n\t")) + } + return "" +} + +// ArgsStringList convert ArgLists into list of strings +func (q *QueryArgs) ArgsStringList() []string { + var argsStringList = make([]string, len(q.ArgList)) + for i, a := range q.ArgList { + argsStringList[i] = typehelpers.SafeString(a) + } + return argsStringList +} + +// ConvertArgsList convert ArgList into list of interface{} by unmarshalling +func (q *QueryArgs) ConvertArgsList() ([]any, error) { + var argList = make([]any, len(q.ArgList)) + + for i, a := range q.ArgList { + if a != nil { + // do we need to unmarshal? + if _, stringArg := q.stringPositionalArgs[i]; stringArg { + argList[i] = *a + } else { + // so this arg is stored as json - we need to deserialize + err := json.Unmarshal([]byte(*a), &argList[i]) + if err != nil { + return nil, err + } + } + } + } + return argList, nil +} + +func NewQueryArgs() *QueryArgs { + return &QueryArgs{ + ArgMap: make(map[string]string), + stringNamedArgs: make(map[string]struct{}), + stringPositionalArgs: make(map[int]struct{}), + } +} + +func (q *QueryArgs) Equals(other *QueryArgs) bool { + if other == nil { + return false + } + if q.Empty() { + return other.Empty() + } + if len(other.ArgMap) != len(q.ArgMap) || len(other.ArgList) != len(q.ArgList) { + return false + } + for k, v := range q.ArgMap { + if !utils.SafeStringsEqual(other.ArgMap[k], v) { + return false + } + } + for i, v := range q.ArgList { + if !utils.SafeStringsEqual(other.ArgList[i], v) { + return false + } + } + return true +} + +func (q *QueryArgs) Empty() bool { + return len(q.ArgMap)+len(q.ArgList) == 0 +} + +func (q *QueryArgs) Validate() error { + if len(q.ArgMap) > 0 && len(q.ArgList) > 0 { + return fmt.Errorf("args contain both positional and named parameters") + } + return nil +} + +// Merge merges the other args with ourselves, creating and returning a new QueryArgs with the result +// NOTE: other has precedence +func (q *QueryArgs) Merge(other *QueryArgs, source QueryProvider) (*QueryArgs, error) { + if other == nil { + return q, nil + } + + // ensure we valid before trying to merge (i.e. cannot define both arg list and arg map) + if err := q.Validate(); err != nil { + return nil, fmt.Errorf("argument validation failed for '%s': %s", source.Name(), err.Error()) + } + + // ensure the other args are valid + if err := other.Validate(); err != nil { + return nil, fmt.Errorf("runtime argument validation failed for '%s': %s", source.Name(), err.Error()) + } + + // create a new query args to store the merged result + result := NewQueryArgs() + result.stringNamedArgs = other.stringNamedArgs + result.stringPositionalArgs = other.stringPositionalArgs + + // named args + // first set values from other + for k, v := range other.ArgMap { + result.ArgMap[k] = v + + } + // now set any unset values from our map + for k, v := range q.ArgMap { + if _, ok := result.ArgMap[k]; !ok { + result.ArgMap[k] = v + if _, ok := q.stringNamedArgs[k]; ok { + result.stringNamedArgs[k] = struct{}{} + } + } + } + + // positional args + // so we must have an args list - figure out how long + listLength := len(q.ArgList) + if otherLen := len(other.ArgList); otherLen > listLength { + listLength = otherLen + } + if listLength > 0 { + result.ArgList = make([]*string, listLength) + + // first set values from other + copy(result.ArgList, other.ArgList) + + // now set any unset values from base list + for i, a := range q.ArgList { + if result.ArgList[i] == nil { + result.ArgList[i] = a + if _, ok := q.stringPositionalArgs[i]; ok { + result.stringPositionalArgs[i] = struct{}{} + } + } + } + } + + // validate the merged result + // runtime args must specify args in same way as base args (i.e. both must define either map or list) + if err := result.Validate(); err != nil { + return nil, fmt.Errorf("runtime argument validation failed when merging runtime args into '%s': %s", source.Name(), err.Error()) + } + + return result, nil +} + +func (q *QueryArgs) SetNamedArgVal(name string, value any) (err error) { + strVal, ok := value.(string) + if ok { + q.stringNamedArgs[name] = struct{}{} + } else { + strVal, err = q.ToString(value) + if err != nil { + return err + } + } + q.ArgMap[name] = strVal + return nil +} + +func (q *QueryArgs) AddPositionalArgVal(value any) error { + q.ArgList = append(q.ArgList, nil) + return q.SetPositionalArgVal(value, len(q.ArgList)-1) +} + +// SetPositionalArgVal sets the value of a positional arg +// NOTE: we add by index so we can populate stringPositionalArgs if needed +func (q *QueryArgs) SetPositionalArgVal(value any, idx int) (err error) { + if idx >= len(q.ArgList) { + return fmt.Errorf("positional arg index %d out of range", idx) + } + strVal, ok := value.(string) + if ok { + // no need to convert toi string - make a note + q.stringPositionalArgs[idx] = struct{}{} + } else { + strVal, err = q.ToString(value) + if err != nil { + return err + } + } + q.ArgList[idx] = &strVal + return nil +} + +func (q *QueryArgs) ToString(value any) (string, error) { + // format the arg value as a JSON string + jsonBytes, err := json.Marshal(value) + if err != nil { + return "", err + } + return string(jsonBytes), nil +} + +func (q *QueryArgs) SetArgMap(argMap map[string]any) error { + for k, v := range argMap { + if err := q.SetNamedArgVal(k, v); err != nil { + return err + } + } + return nil +} + +func (q *QueryArgs) SetArgList(argList []any) error { + q.ArgList = make([]*string, len(argList)) + for i, v := range argList { + if err := q.SetPositionalArgVal(v, i); err != nil { + return err + } + } + return nil +} + +func (q *QueryArgs) GetNamedArg(name string) (interface{}, bool, error) { + argStr, ok := q.ArgMap[name] + if !ok { + return nil, false, nil + } + // do we need to deserialise? + if _, isStringArg := q.stringNamedArgs[name]; isStringArg { + return argStr, true, nil + } + + var res any + if err := json.Unmarshal([]byte(argStr), &res); err != nil { + return nil, false, err + } + return res, true, nil +} + +func (q *QueryArgs) GetPositionalArg(idx int) (interface{}, bool, error) { + if idx > len(q.ArgList) { + return nil, false, fmt.Errorf("positional arg index %d out of range", idx) + } + argStrPtr := q.ArgList[idx] + if argStrPtr == nil { + return nil, false, nil + } + + // do we need to deserialise? + if _, isStringArg := q.stringPositionalArgs[idx]; isStringArg { + return *argStrPtr, true, nil + } + + var res any + if err := json.Unmarshal([]byte(*argStrPtr), &res); err != nil { + return nil, false, err + } + return res, true, nil +} +func (q *QueryArgs) resolveNamedParameters(queryProvider QueryProvider) (argVals []any, missingParams []string, err error) { + // if query params contains both positional and named params, error out + params := queryProvider.GetParams() + + argVals = make([]any, len(params)) + + // iterate through each param def and resolve the value + // build a map of which args have been matched (used to validate all args have param defs) + argsWithParamDef := make(map[string]bool) + for i, param := range params { + // first set default + defaultValue, err := param.GetDefault() + if err != nil { + return nil, nil, err + } + + // can we resolve a value for this param? + if argVal, ok, err := q.GetNamedArg(param.ShortName); ok { + if err != nil { + return nil, nil, err + } + argVals[i] = argVal + argsWithParamDef[param.ShortName] = true + + } else if defaultValue != nil { + // is there a default + argVals[i] = defaultValue + } else { + // no value provided and no default defined - add to missing list + missingParams = append(missingParams, param.ShortName) + } + } + + // verify we have param defs for all provided args + for arg := range q.ArgMap { + if _, ok := argsWithParamDef[arg]; !ok { + slog.Debug("no parameter definition found", "argument", arg) + } + } + + return argVals, missingParams, nil +} + +func (q *QueryArgs) resolvePositionalParameters(queryProvider QueryProvider) (argValues []any, missingParams []string, err error) { + // if query params contains both positional and named params, error out + // if there are param defs - we must be able to resolve all params + // if there are MORE defs than provided parameters, all remaining defs MUST provide a default + params := queryProvider.GetParams() + + // if no param defs are defined, just use the given values, using runtime dependencies where available + if len(params) == 0 { + // no params defined, so we return as many args as are provided + // (convert arg vals from json) + argValues, err = q.ConvertArgsList() + if err != nil { + return nil, nil, err + } + return argValues, nil, nil + } + + // verify we have enough args + if len(params) < len(q.ArgList) { + err = fmt.Errorf("resolvePositionalParameters failed for '%s' - %d %s were provided but there %s %d parameter %s", + queryProvider.Name(), + len(q.ArgList), + utils.Pluralize("argument", len(q.ArgList)), + utils.Pluralize("is", len(params)), + len(params), + utils.Pluralize("definition", len(params)), + ) + return + } + + // so there are param definitions - use these to populate argValues + argValues = make([]any, len(params)) + + for i, param := range params { + // first set default + defaultValue, err := param.GetDefault() + if err != nil { + return nil, nil, err + } + + if i < len(q.ArgList) && q.ArgList[i] != nil { + argVal, _, err := q.GetPositionalArg(i) + if err != nil { + return nil, nil, err + } + + argValues[i] = argVal + } else if defaultValue != nil { + // so we have run out of provided params - is there a default? + argValues[i] = defaultValue + } else { + // no value provided and no default defined - add to missing list + missingParams = append(missingParams, param.ShortName) + } + } + return argValues, missingParams, nil +} + +// GetShowData implements printers.Showable +func (q *QueryArgs) GetShowData() *printers.RowData { + res := printers.NewRowData( + printers.NewFieldValue("ArgMap", q.ArgMap), + printers.NewFieldValue("ArgList", q.ArgList), + printers.NewFieldValue("References", q.References), + ) + return res +} diff --git a/internal/resources/query_args_helpers.go b/internal/resources/query_args_helpers.go new file mode 100644 index 00000000..47d8a3cf --- /dev/null +++ b/internal/resources/query_args_helpers.go @@ -0,0 +1,84 @@ +package resources + +import ( + "fmt" + "log/slog" + "strings" + + "github.com/turbot/go-kit/helpers" + "github.com/turbot/pipe-fittings/utils" +) + +// MergeArgs ensures base and runtime args are non nil and merges them into single args +func MergeArgs(queryProvider QueryProvider, runtimeArgs *QueryArgs) (*QueryArgs, error) { + + baseArgs := queryProvider.GetArgs() + // ensure non nil + if baseArgs == nil { + baseArgs = NewQueryArgs() + } + if runtimeArgs == nil { + runtimeArgs = NewQueryArgs() + } + + return baseArgs.Merge(runtimeArgs, queryProvider) +} + +// ResolveArgs resolves the argument values, +// falling back on defaults from param definitions in the source (if present) +// it returns the arg values as a csv string which can be used in a query invocation +// (the arg values and param defaults will already have been converted to postgres format) +func ResolveArgs(qp QueryProvider, runtimeArgs *QueryArgs) ([]any, error) { + var argVals []any + var missingParams []string + var err error + // validate args + if runtimeArgs == nil { + runtimeArgs = &QueryArgs{} + } + + // merge the query provider args (if any) with the runtime args + sourceArgs := qp.GetArgs() + if sourceArgs == nil { + sourceArgs = &QueryArgs{} + } + mergedArgs, err := sourceArgs.Merge(runtimeArgs, qp) + if err != nil { + return nil, err + } + if namedArgCount := len(mergedArgs.ArgMap); namedArgCount > 0 { + // if named args are provided and the query does not define params, we cannot resolve the args + if len(qp.GetParams()) == 0 { + slog.Warn(fmt.Sprintf("%s defines %d named %s but has no parameters definitions", qp.Name(), namedArgCount, utils.Pluralize("arg", namedArgCount))) + } else { + // do params contain named params? + argVals, missingParams, err = mergedArgs.resolveNamedParameters(qp) + } + } else { + // resolve as positional parameters + // (or fall back to defaults if no positional params are present) + argVals, missingParams, err = mergedArgs.resolvePositionalParameters(qp) + } + if err != nil { + return nil, err + } + + // did we resolve them all? + if len(missingParams) > 0 { + // a better error will be constructed by the calling code + return nil, fmt.Errorf("%s", strings.Join(missingParams, ",")) + } + + // are there any params? + if len(argVals) == 0 { + return nil, nil + } + + // convert any array args into a strongly typed array + for i, v := range argVals { + argVals[i] = helpers.AnySliceToTypedSlice(v) + } + + // success! + return argVals, nil +} diff --git a/internal/resources/query_args_test.go b/internal/resources/query_args_test.go new file mode 100644 index 00000000..95443e87 --- /dev/null +++ b/internal/resources/query_args_test.go @@ -0,0 +1,352 @@ +package resources + +import ( + "github.com/turbot/go-kit/helpers" + "github.com/turbot/pipe-fittings/modconfig" + "github.com/turbot/pipe-fittings/utils" + "reflect" + "testing" +) + +type resolveParamsTest struct { + baseArgs *QueryArgs + runtimeArgs *QueryArgs + paramDefs []*modconfig.ParamDef + expected interface{} +} + +// NOTE: all QueryArgs values are Json representations of the arg value +// TODO really we should update the trest to set stringNamedArgs and stringPositionalArgs for each args object +// then we can store the string args as normal strings, not json strings +// TODO add other args types - arrays, json etc. + +var testCasesResolveParams = map[string]resolveParamsTest{ + + "named argsno defs": { + baseArgs: &QueryArgs{ + ArgMap: map[string]string{ + "p1": `"val1"`, + "p2": `"val2"`, + }, + }, + paramDefs: nil, + expected: []any(nil), + }, + "named args with defs": { + baseArgs: &QueryArgs{ + ArgMap: map[string]string{ + "p1": `"val1"`, + "p2": `"val2"`, + }, + }, + paramDefs: []*modconfig.ParamDef{ + {ShortName: "p1"}, + {ShortName: "p2"}, + }, + expected: []any{"val1", "val2"}, + }, + "named args with defs and partial runtime overrides": { + baseArgs: &QueryArgs{ + ArgMap: map[string]string{ + "p1": `"val1"`, + "p2": `"val2"`, + }, + }, + runtimeArgs: &QueryArgs{ + ArgMap: map[string]string{ + "p2": `"runtime val2"`, + }, + }, + paramDefs: []*modconfig.ParamDef{ + {ShortName: "p1"}, + {ShortName: "p2"}, + }, + expected: []any{"val1", "runtime val2"}, + }, + + "named args with defs and full runtime overrides": { + baseArgs: &QueryArgs{ + ArgMap: map[string]string{ + "p1": `"val1"`, + "p2": `"val2"`, + }, + }, + runtimeArgs: &QueryArgs{ + ArgMap: map[string]string{ + "p1": `"runtime val1"`, + "p2": `"runtime val2"`, + }, + }, + paramDefs: []*modconfig.ParamDef{ + {ShortName: "p1"}, + {ShortName: "p2"}, + }, + expected: []any{"runtime val1", "runtime val2"}, + }, + "named args with defs and runtime overrides with additional undefined arg": { + baseArgs: &QueryArgs{ + ArgMap: map[string]string{ + "p1": `"val1"`, + "p2": `"val2"`, + }, + }, + runtimeArgs: &QueryArgs{ + ArgMap: map[string]string{ + "p2": `"runtime val2"`, + "p3": `"runtime val3"`, + }, + }, + paramDefs: []*modconfig.ParamDef{ + {ShortName: "p1"}, + {ShortName: "p2"}, + }, + expected: []any{"val1", "runtime val2"}, + }, + + "named arg overrides only with defs": { + runtimeArgs: &QueryArgs{ + ArgMap: map[string]string{ + "p1": `"override val1"`, + "p2": `"override val2"`, + }, + }, + paramDefs: []*modconfig.ParamDef{ + {ShortName: "p1"}, + {ShortName: "p2"}, + }, + expected: []any{"override val1", "override val2"}, + }, + "named param defs with incomplete overrides": { + runtimeArgs: &QueryArgs{ + ArgMap: map[string]string{ + "p2": `"override val2"`, + }, + }, + paramDefs: []*modconfig.ParamDef{ + {ShortName: "p1"}, + {ShortName: "p2"}, + }, + expected: "ERROR", + }, + "named param defs with incomplete invalid overrides": { + runtimeArgs: &QueryArgs{ + ArgMap: map[string]string{ + "p3": `"override val2"`, + }, + }, + paramDefs: []*modconfig.ParamDef{ + {ShortName: "p1"}, + {ShortName: "p2"}, + }, + expected: "ERROR", + }, + "named param defs with defaults with incomplete overrides": { + runtimeArgs: &QueryArgs{ + ArgMap: map[string]string{ + "p2": `"override val2"`, + }, + }, + paramDefs: []*modconfig.ParamDef{ + {ShortName: "p1", Default: utils.ToStringPointer(`"val1"`)}, + {ShortName: "p2", Default: utils.ToStringPointer(`"val2"`)}, + }, + expected: []any{"val1", "override val2"}, + }, + "named param defs with defaults with undefined override": { + runtimeArgs: &QueryArgs{ + ArgMap: map[string]string{ + "p3": `"override val2"`, + }, + }, + paramDefs: []*modconfig.ParamDef{ + {ShortName: "p1", Default: utils.ToStringPointer(`"val1"`)}, + {ShortName: "p2", Default: utils.ToStringPointer(`"val2"`)}, + }, + expected: []any{"val1", "val2"}, + }, + + "partial named args with defs and defaults": { + baseArgs: &QueryArgs{ + ArgMap: map[string]string{ + "p1": `"val1"`, + }, + }, + paramDefs: []*modconfig.ParamDef{ + {ShortName: "p1", Default: utils.ToStringPointer(`"def_val1"`)}, + {ShortName: "p2", Default: utils.ToStringPointer(`"def_val2"`)}, + }, + expected: []any{"val1", "def_val2"}, + }, + "partial named args with defs defaults and partial override": { + baseArgs: &QueryArgs{ + ArgMap: map[string]string{ + "p1": `"val1"`, + }, + }, + runtimeArgs: &QueryArgs{ + ArgMap: map[string]string{ + "p2": `"override val2"`, + }, + }, + paramDefs: []*modconfig.ParamDef{ + {ShortName: "p1", Default: utils.ToStringPointer(`"def_val1"`)}, + {ShortName: "p2", Default: utils.ToStringPointer(`"def_val2"`)}, + {ShortName: "p3", Default: utils.ToStringPointer(`"def_val3"`)}, + }, + + expected: []any{"val1", "override val2", "def_val3"}, + }, + "partial named args with defs and unmatched defaults": { + // only a default for first param, which is populated from the provided positional param + baseArgs: &QueryArgs{ + ArgMap: map[string]string{ + "p1": `"val1"`, + }, + }, + paramDefs: []*modconfig.ParamDef{ + {ShortName: "p1", Default: utils.ToStringPointer(`"def_val1"`)}, + {ShortName: "p2"}, + }, + expected: "ERROR", + }, + + "positional params no defs": { + baseArgs: &QueryArgs{ + ArgList: []*string{utils.ToStringPointer(`"val1"`), utils.ToStringPointer(`"val2"`)}, + }, + paramDefs: nil, + + expected: []any{"val1", "val2"}, + }, + "positional params with partial runtime override no defs": { + baseArgs: &QueryArgs{ + ArgList: []*string{utils.ToStringPointer(`"val1"`), utils.ToStringPointer(`"val2"`)}, + }, + runtimeArgs: &QueryArgs{ + ArgList: []*string{nil, utils.ToStringPointer(`"override val2"`)}, + }, + paramDefs: nil, + expected: []any{"val1", "override val2"}, + }, + "positional params with full runtime override no defs": { + baseArgs: &QueryArgs{ + ArgList: []*string{utils.ToStringPointer(`"val1"`), utils.ToStringPointer(`"val2"`)}, + }, + runtimeArgs: &QueryArgs{ + ArgList: []*string{utils.ToStringPointer(`"override val1"`), utils.ToStringPointer(`"override val2"`)}, + }, + paramDefs: nil, + expected: []any{"override val1", "override val2"}, + }, + "partial positional args with defs and defaults": { + baseArgs: &QueryArgs{ + ArgList: []*string{utils.ToStringPointer(`"val1"`)}, + }, + paramDefs: []*modconfig.ParamDef{ + {ShortName: "p1", Default: utils.ToStringPointer(`"def_val1"`)}, + {ShortName: "p2", Default: utils.ToStringPointer(`"def_val2"`)}, + }, + expected: []any{"val1", "def_val2"}, + }, + "partial positional args with defs, overrides and defaults": { + baseArgs: &QueryArgs{ + ArgList: []*string{utils.ToStringPointer(`"val1"`)}, + }, + runtimeArgs: &QueryArgs{ + ArgList: []*string{nil, utils.ToStringPointer(`"override val2"`)}, + }, + paramDefs: []*modconfig.ParamDef{ + {ShortName: "p1", Default: utils.ToStringPointer(`"def_val1"`)}, + {ShortName: "p2", Default: utils.ToStringPointer(`"def_val2"`)}, + {ShortName: "p3", Default: utils.ToStringPointer(`"def_val3"`)}, + }, + expected: []any{"val1", "override val2", "def_val3"}, + }, + "partial positional args with defs and unmatched defaults": { + // only a default for first param, which is populated from the provided positional param + baseArgs: &QueryArgs{ + ArgList: []*string{utils.ToStringPointer(`"val1"`)}, + }, + paramDefs: []*modconfig.ParamDef{ + {ShortName: "p1", Default: utils.ToStringPointer(`"def_val1"`)}, + {ShortName: "p2"}, + }, + expected: "ERROR", + }, + + "positional and named args(expect error)": { + baseArgs: &QueryArgs{ + ArgList: []*string{utils.ToStringPointer(`"val1"`), utils.ToStringPointer(`"val2"`)}, + ArgMap: map[string]string{ + "p1": `"val1"`, + "p2": `"val2"`, + }, + }, + paramDefs: nil, + expected: "ERROR", + }, + "positional and override named args (expect error)": { + baseArgs: &QueryArgs{ + ArgList: []*string{utils.ToStringPointer(`"val1"`), utils.ToStringPointer(`"val2"`)}, + }, + runtimeArgs: &QueryArgs{ + ArgMap: map[string]string{ + "p1": `"val1"`, + "p2": `"val2"`, + }, + }, + paramDefs: nil, + expected: "ERROR", + }, + "named and override params (expect error)": { + baseArgs: &QueryArgs{ + ArgMap: map[string]string{ + "p1": `"val1"`, + "p2": `"val2"`, + }, + }, + runtimeArgs: &QueryArgs{ + ArgList: []*string{utils.ToStringPointer(`"val1"`), utils.ToStringPointer(`"val2"`)}, + }, + paramDefs: nil, + expected: "ERROR", + }, +} + +func TestResolveAsString(t *testing.T) { + testsToRun := []string{} + + for name, test := range testCasesResolveParams { + if len(testsToRun) > 0 && !helpers.StringSliceContains(testsToRun, name) { + continue + } + query := &Control{ + QueryProviderImpl: QueryProviderImpl{ + RuntimeDependencyProviderImpl: RuntimeDependencyProviderImpl{ + ModTreeItemImpl: modconfig.ModTreeItemImpl{ + HclResourceImpl: modconfig.HclResourceImpl{ + FullName: "control.test_control", + }, + }, + }, + Params: test.paramDefs, + Args: test.baseArgs, + }, + } + res, err := ResolveArgs(query, test.runtimeArgs) + if err != nil { + if test.expected != "ERROR" { + t.Errorf("Test: '%s'' FAILED : \nunexpected error %v", name, err) + } + continue + } + if test.expected == "ERROR" { + t.Errorf("Test: '%s'' FAILED - expected error", name) + continue + } + expected := test.expected.([]any) + if !reflect.DeepEqual(expected, res) { + t.Errorf("Test: '%s'' FAILED : \nexpected:\n %v, \ngot:\n %v\n", name, test.expected, res) + } + } +} diff --git a/internal/resources/query_provider_impl.go b/internal/resources/query_provider_impl.go new file mode 100644 index 00000000..2ca9a912 --- /dev/null +++ b/internal/resources/query_provider_impl.go @@ -0,0 +1,309 @@ +package resources + +import ( + "fmt" + "github.com/hashicorp/hcl/v2" + "github.com/turbot/go-kit/helpers" + typehelpers "github.com/turbot/go-kit/types" + "github.com/turbot/pipe-fittings/cty_helpers" + "github.com/turbot/pipe-fittings/modconfig" + "github.com/turbot/pipe-fittings/printers" + "github.com/turbot/pipe-fittings/schema" + "github.com/turbot/pipe-fittings/utils" + "github.com/zclconf/go-cty/cty" +) + +type QueryProviderImpl struct { + RuntimeDependencyProviderImpl + QueryProviderRemain hcl.Body `hcl:",remain" json:"-"` + + SQL *string `cty:"sql" hcl:"sql" json:"sql,omitempty"` + Query *Query `cty:"query" hcl:"query" json:"-"` + Args *QueryArgs `cty:"args" json:"args,omitempty"` + Params []*modconfig.ParamDef `cty:"params" json:"params,omitempty"` + QueryName *string `json:"query,omitempty"` + + disableCtySerialise bool + // flags to indicate if params and args were inherited from base resource + argsInheritedFromBase bool + paramsInheritedFromBase bool +} + +func NewQueryProviderImpl(block *hcl.Block, mod *modconfig.Mod, shortName string) QueryProviderImpl { + return QueryProviderImpl{ + RuntimeDependencyProviderImpl: RuntimeDependencyProviderImpl{ + ModTreeItemImpl: modconfig.NewModTreeItemImpl(block, mod, shortName), + }, + } +} + +// GetParams implements QueryProvider +func (q *QueryProviderImpl) GetParams() []*modconfig.ParamDef { + return q.Params +} + +// GetArgs implements QueryProvider +func (q *QueryProviderImpl) GetArgs() *QueryArgs { + return q.Args + +} + +// GetSQL implements QueryProvider +func (q *QueryProviderImpl) GetSQL() *string { + return q.SQL +} + +// GetQuery implements QueryProvider +func (q *QueryProviderImpl) GetQuery() *Query { + return q.Query +} + +// SetArgs implements QueryProvider +func (q *QueryProviderImpl) SetArgs(args *QueryArgs) { + q.Args = args +} + +// SetParams implements QueryProvider +func (q *QueryProviderImpl) SetParams(params []*modconfig.ParamDef) { + q.Params = params +} + +// ValidateQuery implements QueryProvider +// returns an error if neither sql or query are set +// it is overridden by resource types for which sql is optional +func (q *QueryProviderImpl) ValidateQuery() hcl.Diagnostics { + var diags hcl.Diagnostics + // Top level resources (with the exceptions of controls and queries) are never executed directly, + // only used as base for a nested resource. + // Therefore only nested resources, controls and queries MUST have sql or a query defined + queryRequired := !q.IsTopLevel() || + helpers.StringSliceContains([]string{schema.BlockTypeQuery, schema.BlockTypeControl}, q.GetBlockType()) + + if !queryRequired { + return nil + } + + if queryRequired && q.Query == nil && q.SQL == nil { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: fmt.Sprintf("%s does not define a query or SQL", q.Name()), + Subject: q.GetDeclRange(), + }) + } + return diags +} + +// RequiresExecution implements QueryProvider +func (q *QueryProviderImpl) RequiresExecution(queryProvider QueryProvider) bool { + return queryProvider.GetQuery() != nil || queryProvider.GetSQL() != nil +} + +// GetResolvedQuery return the SQL and args to run the query +func (q *QueryProviderImpl) GetResolvedQuery(runtimeArgs *QueryArgs) (*ResolvedQuery, error) { + argsArray, err := ResolveArgs(q, runtimeArgs) + if err != nil { + return nil, fmt.Errorf("failed to resolve args for %s: %s", q.Name(), err.Error()) + } + sql := typehelpers.SafeString(q.GetSQL()) + // we expect there to be sql on the query provider, NOT a Query + if sql == "" { + return nil, fmt.Errorf("getResolvedQuery faiuled - no sql set for '%s'", q.Name()) + } + + return &ResolvedQuery{ + Name: q.Name(), + ExecuteSQL: sql, + RawSQL: sql, + Args: argsArray, + }, nil +} + +// MergeParentArgs merges our args with our parent args (ours take precedence) +func (q *QueryProviderImpl) MergeParentArgs(queryProvider QueryProvider, parent QueryProvider) (diags hcl.Diagnostics) { + parentArgs := parent.GetArgs() + if parentArgs == nil { + return nil + } + + args, err := parentArgs.Merge(queryProvider.GetArgs(), parent) + if err != nil { + return hcl.Diagnostics{&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: err.Error(), + Subject: parent.(modconfig.HclResource).GetDeclRange(), + }} + } + + queryProvider.SetArgs(args) + return nil +} + +// GetQueryProviderImpl implements QueryProvider +func (q *QueryProviderImpl) GetQueryProviderImpl() *QueryProviderImpl { + return q +} + +// ParamsInheritedFromBase implements QueryProvider +// determine whether our params were inherited from base resource +func (q *QueryProviderImpl) ParamsInheritedFromBase() bool { + return q.paramsInheritedFromBase +} + +// ArgsInheritedFromBase implements QueryProvider +// determine whether our args were inherited from base resource +func (q *QueryProviderImpl) ArgsInheritedFromBase() bool { + return q.argsInheritedFromBase +} + +// CtyValue implements CtyValueProvider +func (q *QueryProviderImpl) CtyValue() (cty.Value, error) { + if q.disableCtySerialise { + return cty.Zero, nil + } + return cty_helpers.GetCtyValue(q) +} + +func (q *QueryProviderImpl) SetBaseProperties() { + q.RuntimeDependencyProviderImpl.SetBaseProperties() + if q.SQL == nil { + q.SQL = q.getBaseImpl().SQL + } + if q.Query == nil { + q.Query = q.getBaseImpl().Query + } + if q.Args == nil { + q.Args = q.getBaseImpl().Args + q.argsInheritedFromBase = true + } + if q.Params == nil { + q.Params = q.getBaseImpl().Params + q.paramsInheritedFromBase = true + } +} + +func (q *QueryProviderImpl) getBaseImpl() *QueryProviderImpl { + return q.GetBase().(QueryProvider).GetQueryProviderImpl() +} + +func (q *QueryProviderImpl) OnDecoded(block *hcl.Block, _ modconfig.ModResourcesProvider) hcl.Diagnostics { + q.populateQueryName() + + return nil +} + +func (q *QueryProviderImpl) populateQueryName() { + if q.Query != nil { + q.QueryName = &q.Query.FullName + } +} + +// GetShowData implements printers.Showable +func (q *QueryProviderImpl) GetShowData() *printers.RowData { + + res := printers.NewRowData( + printers.NewFieldValue("SQL", q.SQL), + printers.NewFieldValue("Query", q.Query), + printers.NewFieldValue("Args", q.Args), + printers.NewFieldValue("Params", q.Params), + ) + // merge fields from base, putting base fields first + res.Merge(q.RuntimeDependencyProviderImpl.GetShowData()) + return res +} + +func (q *QueryProviderImpl) Diff(other QueryProvider) *modconfig.ModTreeItemDiffs { + d := &modconfig.ModTreeItemDiffs{ + Item: q, + Name: q.Name(), + } + // sql + if !utils.SafeStringsEqual(q.GetSQL(), other.GetSQL()) { + d.AddPropertyDiff("SQL") + } + + // args + if lArgs := q.GetArgs(); lArgs == nil { + if other.GetArgs() != nil { + d.AddPropertyDiff("Args") + } + } else { + // we have args + if rArgs := other.GetArgs(); rArgs == nil { + d.AddPropertyDiff("Args") + } else if !lArgs.Equals(rArgs) { + d.AddPropertyDiff("Args") + } + } + + // query + if lQuery := q.GetQuery(); lQuery == nil { + if other.GetQuery() != nil { + d.AddPropertyDiff("Query") + } + } else { + // we have query + if rQuery := other.GetQuery(); rQuery == nil { + d.AddPropertyDiff("Query") + } else if !lQuery.Equals(rQuery) { + d.AddPropertyDiff("Query") + } + } + + // params + lParams := q.GetParams() + rParams := other.GetParams() + if len(lParams) != len(rParams) { + d.AddPropertyDiff("Params") + } else { + for i, lParam := range lParams { + if !lParam.Equals(rParams[i]) { + d.AddPropertyDiff("Params") + } + } + } + + // with + if lwp, ok := any(q).(WithProvider); ok { + rwp := other.(WithProvider) + lWiths := lwp.GetWiths() + rWiths := rwp.GetWiths() + if len(lWiths) != len(rWiths) { + d.AddPropertyDiff("With") + } else { + for i, lWith := range lWiths { + if !lWith.Equals(rWiths[i]) { + d.AddPropertyDiff("With") + } + } + } + + // have BASE withs changed + lbase := q.GetBase() + rbase := other.GetBase() + var lbaseWiths []*DashboardWith + var rbaseWiths []*DashboardWith + if lbase != nil { + lbaseWiths = lbase.(WithProvider).GetWiths() + } + if rbase != nil { + rbaseWiths = rbase.(WithProvider).GetWiths() + } + if len(lbaseWiths) != len(rbaseWiths) { + d.AddPropertyDiff("With") + } else { + for i, lBaseWith := range lbaseWiths { + if !lBaseWith.Equals(rbaseWiths[i]) { + d.AddPropertyDiff("With") + } + } + } + } + + return d +} + +func (b *QueryProviderImpl) GetNestedStructs() []modconfig.CtyValueProvider { + // return all nested structs - this is used to get the nested structs for the cty serialisation + // we return ourselves and our base structs + return append([]modconfig.CtyValueProvider{b}, b.RuntimeDependencyProviderImpl.GetNestedStructs()...) +} diff --git a/internal/resources/resolved_query.go b/internal/resources/resolved_query.go new file mode 100644 index 00000000..958f99c7 --- /dev/null +++ b/internal/resources/resolved_query.go @@ -0,0 +1,32 @@ +package resources + +import ( + "encoding/json" +) + +// ResolvedQuery contains the execute SQL, raw SQL and args string used to execute a query +type ResolvedQuery struct { + Name string + ExecuteSQL string + RawSQL string + Args []any + + IsMetaQuery bool +} + +// QueryArgs converts the ResolvedQuery into QueryArgs +func (r ResolvedQuery) QueryArgs() *QueryArgs { + res := NewQueryArgs() + + res.ArgList = make([]*string, len(r.Args)) + + for i, a := range r.Args { + // TACTICAL convert to JSON representation + jsonBytes, err := json.Marshal(a) + argStr := string(jsonBytes) + if err != nil { + res.ArgList[i] = &argStr + } + } + return res +} diff --git a/internal/resources/runtime_dependency.go b/internal/resources/runtime_dependency.go new file mode 100644 index 00000000..051f3b6b --- /dev/null +++ b/internal/resources/runtime_dependency.go @@ -0,0 +1,89 @@ +package resources + +import ( + "fmt" + "github.com/turbot/pipe-fittings/modconfig" +) + +type RuntimeDependency struct { + PropertyPath *modconfig.ParsedPropertyPath + TargetPropertyName *string + // TACTICAL the name of the parent property - either "args" or "param." + ParentPropertyName string + TargetPropertyIndex *int + + // TACTICAL - if set, wrap the dependency value in an array + // this provides support for args which convert a runtime dependency to an array, like: + // arns = [input.arn] + IsArray bool + + // resource which provides has the dependency + Provider modconfig.HclResource +} + +func (d *RuntimeDependency) SourceResourceName() string { + return d.PropertyPath.ToResourceName() +} + +func (d *RuntimeDependency) String() string { + if d.TargetPropertyIndex != nil { + return fmt.Sprintf("%s.%d->%s", d.ParentPropertyName, *d.TargetPropertyIndex, d.PropertyPath.String()) + } + + return fmt.Sprintf("%s.%s->%s", d.ParentPropertyName, *d.TargetPropertyName, d.PropertyPath.String()) +} + +func (d *RuntimeDependency) ValidateSource(dashboard *Dashboard, workspace modconfig.ModResourcesProvider) error { + // TODO [node_reuse] re-add parse time validation https://github.com/turbot/steampipe/issues/2925 + //resourceName := d.PropertyPath.ToResourceName() + //var found bool + ////var sourceResource HclResource + //switch d.PropertyPath.ItemType { + //// if this is a 'with' resolve from the parent resource + //case schema.BlockTypeParam: + // _, found = d.ParentResource.ResolveWithFromTree(resourceName) + //case schema.BlockTypeWith: + // _, found = d.ParentResource.ResolveWithFromTree(resourceName) + //// if this dependency has a 'self' prefix, resolve from the current dashboard container + //case schema.BlockTypeInput: + // _, found = dashboard.GetInput(resourceName) + // + // //default: + // // // otherwise, resolve from the global inputs + // // _, found = workspace.GetModResources().GlobalDashboardInputs[resourceName] + //} + //if !found { + // return fmt.Errorf("could not resolve runtime dependency resource %s", d.PropertyPath) + //} + + return nil +} + +func (d *RuntimeDependency) Equals(other *RuntimeDependency) bool { + // TargetPropertyPath + if d.PropertyPath.PropertyPath == nil { + if other.PropertyPath.PropertyPath != nil { + return false + } + } else { + // we have TargetPropertyPath + if other.PropertyPath.PropertyPath == nil { + return false + } + + if len(d.PropertyPath.PropertyPath) != len(other.PropertyPath.PropertyPath) { + return false + } + for i, c := range d.PropertyPath.PropertyPath { + if other.PropertyPath.PropertyPath[i] != c { + return false + } + } + } + + if d.SourceResourceName() != other.SourceResourceName() { + return false + } + + return true +} diff --git a/internal/resources/runtime_dependency_provider_impl.go b/internal/resources/runtime_dependency_provider_impl.go new file mode 100644 index 00000000..d3f951b1 --- /dev/null +++ b/internal/resources/runtime_dependency_provider_impl.go @@ -0,0 +1,35 @@ +package resources + +import ( + "github.com/hashicorp/hcl/v2" + "github.com/turbot/pipe-fittings/modconfig" +) + +type RuntimeDependencyProviderImpl struct { + modconfig.ModTreeItemImpl + // required to allow partial decoding + RuntimeDependencyProviderRemain hcl.Body `hcl:",remain" json:"-"` + + runtimeDependencies map[string]*RuntimeDependency +} + +func (b *RuntimeDependencyProviderImpl) AddRuntimeDependencies(dependencies []*RuntimeDependency) { + if b.runtimeDependencies == nil { + b.runtimeDependencies = make(map[string]*RuntimeDependency) + } + for _, dependency := range dependencies { + // set the dependency provider (this is used if this resource is inherited via base) + dependency.Provider = b + b.runtimeDependencies[dependency.String()] = dependency + } +} + +func (b *RuntimeDependencyProviderImpl) GetRuntimeDependencies() map[string]*RuntimeDependency { + return b.runtimeDependencies +} + +func (b *RuntimeDependencyProviderImpl) GetNestedStructs() []modconfig.CtyValueProvider { + // return all nested structs - this is used to get the nested structs for the cty serialisation + // we return ourselves and our base structs + return append([]modconfig.CtyValueProvider{b}, b.ModTreeItemImpl.GetNestedStructs()...) +} diff --git a/internal/resources/with_provider_impl.go b/internal/resources/with_provider_impl.go new file mode 100644 index 00000000..f974ba42 --- /dev/null +++ b/internal/resources/with_provider_impl.go @@ -0,0 +1,40 @@ +package resources + +import ( + "fmt" + "github.com/hashicorp/hcl/v2" + "golang.org/x/exp/maps" +) + +type WithProviderImpl struct { + // required to allow partial decoding + WithProviderRemain hcl.Body `hcl:",remain" json:"-"` + + // map of withs keyed by unqualified name + withs map[string]*DashboardWith +} + +func (b *WithProviderImpl) AddWith(with *DashboardWith) hcl.Diagnostics { + if b.withs == nil { + b.withs = make(map[string]*DashboardWith) + } + // if we already have this with, fail + if _, ok := b.withs[with.UnqualifiedName]; ok { + return hcl.Diagnostics{&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: fmt.Sprintf("duplicate with block '%s'", with.ShortName), + Subject: with.GetDeclRange(), + }} + } + b.withs[with.UnqualifiedName] = with + return nil +} + +func (b *WithProviderImpl) GetWiths() []*DashboardWith { + return maps.Values(b.withs) +} + +func (b *WithProviderImpl) GetWith(name string) (*DashboardWith, bool) { + w, ok := b.withs[name] + return w, ok +} diff --git a/internal/service/api/api.go b/internal/service/api/api.go index 28c8ef20..489e8a10 100644 --- a/internal/service/api/api.go +++ b/internal/service/api/api.go @@ -21,9 +21,9 @@ import ( "github.com/spf13/viper" "github.com/turbot/pipe-fittings/constants" "github.com/turbot/pipe-fittings/filepaths" - "github.com/turbot/pipe-fittings/workspace" "github.com/turbot/powerpipe/internal/dashboardserver" "github.com/turbot/powerpipe/internal/service/api/common" + pworkspace "github.com/turbot/powerpipe/internal/workspace" "gopkg.in/olahol/melody.v1" ) @@ -72,7 +72,7 @@ type APIService struct { webSocket *melody.Melody // the loaded workspace - workspace *workspace.Workspace + workspace *pworkspace.PowerpipeWorkspace } // APIServiceOption defines a type of function to configures the APIService. @@ -85,7 +85,7 @@ func WithWebSocket(webSocket *melody.Melody) APIServiceOption { } } -func WithWorkspace(workspace *workspace.Workspace) APIServiceOption { +func WithWorkspace(workspace *pworkspace.PowerpipeWorkspace) APIServiceOption { return func(api *APIService) error { api.workspace = workspace return nil diff --git a/internal/snapshot/snapshot_tag_test.go b/internal/snapshot/snapshot_tag_test.go index e0c22444..5722472d 100644 --- a/internal/snapshot/snapshot_tag_test.go +++ b/internal/snapshot/snapshot_tag_test.go @@ -1,60 +1,54 @@ package snapshot -import ( - "github.com/turbot/pipe-fittings/modconfig" - "github.com/turbot/pipe-fittings/utils" - "reflect" - "testing" -) - -func TestGetAsSnapshotPropertyMap(t *testing.T) { - type args struct { - item interface{} - } - tests := []struct { - name string - args args - want map[string]any - }{ - {name: "card", - args: args{ - item: modconfig.DashboardChart{ - QueryProviderImpl: modconfig.QueryProviderImpl{ - RuntimeDependencyProviderImpl: modconfig.RuntimeDependencyProviderImpl{ - ModTreeItemImpl: modconfig.ModTreeItemImpl{ - HclResourceImpl: modconfig.HclResourceImpl{ - FullName: "mod1.card.card1", - ShortName: "card1", - UnqualifiedName: "card.card1", - Description: utils.ToStringPointer("a card"), - }, - }, - }, - SQL: utils.ToStringPointer("select 1"), - }, - Axes: &modconfig.DashboardChartAxes{ - X: &modconfig.DashboardChartAxesX{ - Title: &modconfig.DashboardChartAxisTitle{ - Value: utils.ToStringPointer("x axis"), - }, - Min: utils.ToIntegerPointer(0), - Max: utils.ToIntegerPointer(1000), - }, - Y: &modconfig.DashboardChartAxesY{}, - }, - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := GetAsSnapshotPropertyMap(tt.args.item) - if err != nil { - t.Fail() - } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("GetAsSnapshotPropertyMap() = %v, want %v", got, tt.want) - } - }) - } -} +// +//func TestGetAsSnapshotPropertyMap(t *testing.T) { +// type args struct { +// item interface{} +// } +// tests := []struct { +// name string +// args args +// want map[string]any +// }{ +// {name: "card", +// args: args{ +// item: modconfig.DashboardChart{ +// QueryProviderImpl: modconfig.QueryProviderImpl{ +// RuntimeDependencyProviderImpl: modconfig.RuntimeDependencyProviderImpl{ +// ModTreeItemImpl: modconfig.ModTreeItemImpl{ +// HclResourceImpl: modconfig.HclResourceImpl{ +// FullName: "mod1.card.card1", +// ShortName: "card1", +// UnqualifiedName: "card.card1", +// Description: utils.ToStringPointer("a card"), +// }, +// }, +// }, +// SQL: utils.ToStringPointer("select 1"), +// }, +// Axes: &modconfig.DashboardChartAxes{ +// X: &modconfig.DashboardChartAxesX{ +// Title: &modconfig.DashboardChartAxisTitle{ +// Value: utils.ToStringPointer("x axis"), +// }, +// Min: utils.ToIntegerPointer(0), +// Max: utils.ToIntegerPointer(1000), +// }, +// Y: &modconfig.DashboardChartAxesY{}, +// }, +// }, +// }, +// }, +// } +// for _, tt := range tests { +// t.Run(tt.name, func(t *testing.T) { +// got, err := GetAsSnapshotPropertyMap(tt.args.item) +// if err != nil { +// t.Fail() +// } +// if !reflect.DeepEqual(got, tt.want) { +// t.Errorf("GetAsSnapshotPropertyMap() = %v, want %v", got, tt.want) +// } +// }) +// } +//} diff --git a/internal/workspace/load_workspace.go b/internal/workspace/load_workspace.go new file mode 100644 index 00000000..6b7f61f6 --- /dev/null +++ b/internal/workspace/load_workspace.go @@ -0,0 +1,70 @@ +package workspace + +import ( + "context" + "log/slog" + "time" + + "github.com/turbot/pipe-fittings/error_helpers" + "github.com/turbot/pipe-fittings/utils" + "github.com/turbot/pipe-fittings/workspace" +) + +func LoadWorkspacePromptingForVariables(ctx context.Context, workspacePath string, opts ...LoadPowerpipeWorkspaceOption) (*PowerpipeWorkspace, error_helpers.ErrorAndWarnings) { + t := time.Now() + defer func() { + slog.Debug("Workspace load complete", "duration (ms)", time.Since(t).Milliseconds()) + }() + w, errAndWarnings := Load(ctx, workspacePath, opts...) + if errAndWarnings.GetError() == nil { + return w, errAndWarnings + } + + // kif there wqs an error check if it was a missing variable error and if so prompt for variables + if err := workspace.HandleWorkspaceLoadError(ctx, errAndWarnings.GetError(), workspacePath); err != nil { + return nil, error_helpers.NewErrorsAndWarning(err) + } + + // ok we should have all variables now - reload workspace + return Load(ctx, workspacePath, opts...) +} + +// Load_ creates a Workspace and loads the workspace mod + +func Load(ctx context.Context, workspacePath string, opts ...LoadPowerpipeWorkspaceOption) (w *PowerpipeWorkspace, ew error_helpers.ErrorAndWarnings) { + cfg := newLoadPowerpipeWorkspaceConfig() + for _, o := range opts { + o(cfg) + } + + utils.LogTime("w.Load start") + defer utils.LogTime("w.Load end") + + w = NewPowerpipeWorkspace(workspacePath) + // check whether the workspace contains a modfile + // this will determine whether we load files recursively, and create pseudo resources for sql files + w.SetModfileExists() + + // load the .steampipe ignore file + if err := w.LoadExclusions(); err != nil { + return nil, error_helpers.NewErrorsAndWarning(err) + } + + w.SupportLateBinding = cfg.supportLateBinding + w.BlockTypeInclusions = cfg.blockTypeInclusions + w.ValidateVariables = cfg.validateVariables + w.PipelingConnections = cfg.pipelingConnections + + // if there is a mod file (or if we are loading resources even with no modfile), load them + if w.ModfileExists() || !cfg.skipResourceLoadIfNoModfile { + ew = w.LoadWorkspaceMod(ctx) + } + if ew.GetError() != nil { + return nil, ew + } + + // verify all runtime dependencies can be resolved + ew.Error = w.verifyResourceRuntimeDependencies() + + return w, ew +} diff --git a/internal/workspace/load_workspace_options.go b/internal/workspace/load_workspace_options.go new file mode 100644 index 00000000..99244436 --- /dev/null +++ b/internal/workspace/load_workspace_options.go @@ -0,0 +1,54 @@ +package workspace + +import ( + "github.com/turbot/pipe-fittings/connection" +) + +type LoadPowerpipeWorkspaceOption func(*LoadPowerpipeWorkspaceConfig) + +type LoadPowerpipeWorkspaceConfig struct { + skipResourceLoadIfNoModfile bool + pipelingConnections map[string]connection.PipelingConnection + blockTypeInclusions []string + validateVariables bool + supportLateBinding bool +} + +func newLoadPowerpipeWorkspaceConfig() *LoadPowerpipeWorkspaceConfig { + return &LoadPowerpipeWorkspaceConfig{ + pipelingConnections: make(map[string]connection.PipelingConnection), + validateVariables: true, + supportLateBinding: true, + } +} + +func WithPipelingConnections(pipelingConnections map[string]connection.PipelingConnection) LoadPowerpipeWorkspaceOption { + return func(m *LoadPowerpipeWorkspaceConfig) { + m.pipelingConnections = pipelingConnections + } +} + +func WithLateBinding(enabled bool) LoadPowerpipeWorkspaceOption { + return func(m *LoadPowerpipeWorkspaceConfig) { + m.supportLateBinding = enabled + } +} + +func WithBlockType(blockTypeInclusions []string) LoadPowerpipeWorkspaceOption { + return func(m *LoadPowerpipeWorkspaceConfig) { + m.blockTypeInclusions = blockTypeInclusions + } +} + +func WithVariableValidation(enabled bool) LoadPowerpipeWorkspaceOption { + return func(m *LoadPowerpipeWorkspaceConfig) { + m.validateVariables = enabled + } +} + +// TODO this is only needed as Pipe fittings tests rely on loading workspaces without modfiles +func WithSkipResourceLoadIfNoModfile(enabled bool) LoadPowerpipeWorkspaceOption { + return func(m *LoadPowerpipeWorkspaceConfig) { + m.skipResourceLoadIfNoModfile = enabled + } +} diff --git a/internal/workspace/powerpipe_workspace.go b/internal/workspace/powerpipe_workspace.go new file mode 100644 index 00000000..4f0ae6f1 --- /dev/null +++ b/internal/workspace/powerpipe_workspace.go @@ -0,0 +1,133 @@ +package workspace + +import ( + "context" + "fmt" + "github.com/hashicorp/hcl/v2" + typehelpers "github.com/turbot/go-kit/types" + "github.com/turbot/pipe-fittings/modconfig" + "github.com/turbot/pipe-fittings/workspace" + "github.com/turbot/powerpipe/internal/dashboardevents" + "github.com/turbot/powerpipe/internal/resources" + "log/slog" +) + +type PowerpipeWorkspace struct { + workspace.Workspace + // event handlers + dashboardEventHandlers []dashboardevents.DashboardEventHandler + // channel used to send dashboard events to the handleDashboardEvent goroutine + dashboardEventChan chan dashboardevents.DashboardEvent +} + +func NewPowerpipeWorkspace(workspacePath string) *PowerpipeWorkspace { + w := &PowerpipeWorkspace{ + Workspace: workspace.Workspace{ + Path: workspacePath, + VariableValues: make(map[string]string), + ValidateVariables: true, + Mod: modconfig.NewMod("local", workspacePath, hcl.Range{}), + }, + } + + w.OnFileWatcherError = func(ctx context.Context, err error) { + w.PublishDashboardEvent(ctx, &dashboardevents.WorkspaceError{Error: err}) + } + w.OnFileWatcherEvent = func(ctx context.Context, modResources, prevModResources modconfig.ModResources) { + w.raiseDashboardChangedEvents(ctx, modResources, prevModResources) + } + return w +} + +func (w *PowerpipeWorkspace) Close() { + w.Workspace.Close() + if ch := w.dashboardEventChan; ch != nil { + // NOTE: set nil first + w.dashboardEventChan = nil + slog.Debug("closing dashboardEventChan") + close(ch) + } +} + +func (w *PowerpipeWorkspace) verifyResourceRuntimeDependencies() error { + for _, d := range w.Mod.GetModResources().(*resources.PowerpipeModResources).Dashboards { + if err := d.ValidateRuntimeDependencies(w); err != nil { + return err + } + } + return nil +} + +// ResolveQueryFromQueryProvider resolves the query for the given QueryProvider +func (w *PowerpipeWorkspace) ResolveQueryFromQueryProvider(queryProvider resources.QueryProvider, runtimeArgs *resources.QueryArgs) (*resources.ResolvedQuery, error) { + slog.Debug("ResolveQueryFromQueryProvider", "resourceName", queryProvider.Name()) + + query := queryProvider.GetQuery() + sql := queryProvider.GetSQL() + + params := queryProvider.GetParams() + + // merge the base args with the runtime args + var err error + runtimeArgs, err = resources.MergeArgs(queryProvider, runtimeArgs) + if err != nil { + return nil, err + } + + // determine the source for the query + // - this will either be the control itself or any named query the control refers to + // either via its SQL proper ty (passing a query name) or Query property (using a reference to a query object) + + // if a query is provided, use that to resolve the sql + if query != nil { + return w.ResolveQueryFromQueryProvider(query, runtimeArgs) + } + + // must have sql is there is no query + if sql == nil { + return nil, fmt.Errorf("%s does not define either a 'sql' property or a 'query' property\n", queryProvider.Name()) + } + + queryProviderSQL := typehelpers.SafeString(sql) + slog.Debug("control defines inline SQL") + + // if the SQL refers to a named query, this is the same as if the 'Query' property is set + if namedQueryProvider, ok := w.GetQueryProvider(queryProviderSQL); ok { + // in this case, it is NOT valid for the query provider to define its own Param definitions + if params != nil { + return nil, fmt.Errorf("%s has an 'SQL' property which refers to %s, so it cannot define 'param' blocks", queryProvider.Name(), namedQueryProvider.Name()) + } + return w.ResolveQueryFromQueryProvider(namedQueryProvider, runtimeArgs) + } + + // so the sql is NOT a named query + return queryProvider.GetResolvedQuery(runtimeArgs) + +} + +func (w *PowerpipeWorkspace) GetQueryProvider(queryName string) (resources.QueryProvider, bool) { + parsedName, err := modconfig.ParseResourceName(queryName) + if err != nil { + return nil, false + } + // try to find the resource + if resource, ok := w.GetResource(parsedName); ok { + // found a resource - is it a query provider + if qp := resource.(resources.QueryProvider); ok { + return qp, true + } + slog.Debug("GetQueryProviderImpl found a mod resource resource for query but it is not a query provider", "resourceName", queryName) + } + + return nil, false +} + +// GetPowerpipeModResources returns the powerpipe PowerpipeModResources from the workspace, cast to the correct type +func (w *PowerpipeWorkspace) GetPowerpipeModResources() *resources.PowerpipeModResources { + modResources, ok := w.GetModResources().(*resources.PowerpipeModResources) + if !ok { + // should never happen + panic(fmt.Sprintf("mod.GetModResources() did not return a powerpipe PowerpipeModResources: %T", w.GetModResources())) + } + return modResources +} diff --git a/internal/workspace/resource_from_args.go b/internal/workspace/resource_from_args.go new file mode 100644 index 00000000..60897323 --- /dev/null +++ b/internal/workspace/resource_from_args.go @@ -0,0 +1,177 @@ +package workspace + +import ( + "fmt" + "strings" + + "github.com/hashicorp/hcl/v2" + "github.com/turbot/go-kit/helpers" + "github.com/turbot/pipe-fittings/modconfig" + "github.com/turbot/pipe-fittings/schema" + "github.com/turbot/pipe-fittings/sperr" + "github.com/turbot/pipe-fittings/utils" + "github.com/turbot/pipe-fittings/workspace" + pparse "github.com/turbot/powerpipe/internal/parse" + "github.com/turbot/powerpipe/internal/resources" +) + +// ResolveResourceAndArgsFromSQLString attempts to resolve 'arg' to a resource of type T and (optionally) query args +func ResolveResourceAndArgsFromSQLString[T modconfig.ModTreeItem](sqlString string, w *workspace.Workspace) (modconfig.ModTreeItem, *resources.QueryArgs, error) { + var err error + var empty T + + // 1) check if this is a resource + // if this looks like a named query provider invocation, parse the sql string for arguments + resource, args, err := extractResourceFromQueryString[T](sqlString, w) + if err != nil { + return empty, nil, err + } + + if resource != nil { + // success + return resource, args, nil + } + + // so we failed to resolve the resource from the input string + // check whether it _looks_ like a resource name (i.e. mod.type.name OR type.name) + if name, looksLikeResource := SqlLooksLikeExecutableResource(sqlString); looksLikeResource { + return empty, nil, fmt.Errorf("'%s' not found in %s (%s)", name, w.Mod.Name(), w.Path) + } + switch any(empty).(type) { + case *resources.Query: + // if the desired type is a query, and the sqlString DOES NOT look like a resource name, + // treat it as a raw query and create a Query to wrap it + q := createQueryResourceForCommandLineQuery(sqlString, w.Mod) + + // add to the workspace mod so the dashboard execution code can find it + if err := w.Mod.AddResource(q); err != nil { + return empty, nil, err + } + + return q, nil, nil + default: + // failed to resolve + return empty, nil, nil + } + +} + +// does the input look like a resource which can be executed as a query +// Note: if anything fails just return nil values +func extractResourceFromQueryString[T modconfig.ModTreeItem](input string, w *workspace.Workspace) (modconfig.ModTreeItem, *resources.QueryArgs, error) { + // can we extract a resource name from the string + parsedResourceName, err := extractResourceNameFromQuery[T](input) + if err != nil { + return nil, nil, err + } + if parsedResourceName == nil { + return nil, nil, nil + } + // ok we managed to extract a resource name - does this resource exist? + resource, ok := w.GetResource(parsedResourceName) + if !ok { + return nil, nil, nil + } + + // if the target is not the expected type, fail + target, ok := resource.(T) + if !ok { + typeName := utils.GetGenericTypeName[T]() + return nil, nil, sperr.New("target '%s' is not of the expected type '%s'", resource.GetUnqualifiedName(), typeName) + } + + _, args, err := pparse.ParseQueryInvocation(input) + if err != nil { + return nil, nil, err + } + + // success + return target, args, nil +} + +// convert the given command line query into a query resource and add to workspace +// this is to allow us to use existing dashboard execution code +func createQueryResourceForCommandLineQuery(queryString string, mod *modconfig.Mod) *resources.Query { + // build name + shortName := "command_line_query" + + // this is NOT a named query - create the query using RawSql + q := resources.NewQuery(&hcl.Block{Type: schema.BlockTypeQuery}, mod, shortName).(*resources.Query) + q.SQL = utils.ToStringPointer(queryString) + + // add empty metadata + q.SetMetadata(&modconfig.ResourceMetadata{}) + + // return the new resource + return q +} + +// attempt top extra a resource name of the given type from the input string +// look at string up the the first open bracket +func extractResourceNameFromQuery[T modconfig.ModTreeItem](input string) (*modconfig.ParsedResourceName, error) { + // convert the type T into a resource type name + resourceType := resources.GenericTypeToBlockType[T]() + // special case handling for variables + if resourceType == schema.BlockTypeVariable { + // variables are named var.xxxx, not variable.xxxx + resourceType = schema.AttributeVar + } + + // remove parameters from the input string before calling ParseResourceName + // as parameters may break parsing + openBracketIdx := strings.Index(input, "(") + if openBracketIdx != -1 { + input = input[:openBracketIdx] + } + + parsedName, err := parseResourceName(input, resourceType) + + // if the typo eis query, do not bubble error up, just return nil parsed name + // it is expected that this function may fail if a raw query is passed to it + if err != nil && resourceType == schema.BlockTypeQuery { + return nil, nil + } + + return parsedName, err +} + +func parseResourceName(targetName string, commandTargetType string) (*modconfig.ParsedResourceName, error) { + parsed := &modconfig.ParsedResourceName{} + parts := strings.Split(targetName, ".") + + switch len(parts) { + case 0: + return nil, sperr.New("empty name passed to resolveResourceName") + case 1: + // if no type was specified, deduce the type from the check command used + parsed.Name = parts[0] + parsed.ItemType = commandTargetType + case 2: + parsed.ItemType = parts[0] + parsed.Name = parts[1] + case 3: + parsed.Mod = parts[0] + parsed.ItemType = parts[1] + parsed.Name = parts[2] + default: + return nil, sperr.New("invalid name passed to ParseResourceName") + } + + return parsed, nil +} + +func SqlLooksLikeExecutableResource(input string) (string, bool) { + // remove parameters from the input string before calling ParseResourceName + // as parameters may break parsing + openBracketIdx := strings.Index(input, "(") + if openBracketIdx != -1 { + input = input[:openBracketIdx] + } + parsedName, err := modconfig.ParseResourceName(input) + if err == nil && helpers.StringSliceContains(schema.QueryProviderBlocks, parsedName.ItemType) { + return parsedName.ToResourceName(), true + } + // do not bubble error up, just return false + return "", false + +} diff --git a/internal/workspace/resources_of_type_test.go b/internal/workspace/resources_of_type_test.go new file mode 100644 index 00000000..d929f808 --- /dev/null +++ b/internal/workspace/resources_of_type_test.go @@ -0,0 +1,184 @@ +package workspace + +import ( + "github.com/turbot/pipe-fittings/workspace" + "github.com/turbot/powerpipe/internal/resources" + "testing" + + "github.com/hashicorp/hcl/v2" + "github.com/turbot/pipe-fittings/modconfig" + "github.com/turbot/pipe-fittings/utils" +) + +func makeControl(mod *modconfig.Mod, name, title, description, sql string, tags map[string]string) *resources.Control { + control := resources.NewControl(&hcl.Block{Type: "control"}, mod, name).(*resources.Control) + control.Title = &title + control.Description = &description + control.Tags = tags + control.SQL = &sql + return control +} + +type testCase[T modconfig.HclResource] struct { + name string + filter workspace.ResourceFilter + want map[string]struct{} +} + +func TestFilterWorkspaceResourcesOfType(t *testing.T) { + // Set the AppSpecificNewModResourcesFunc to the Powerpipe NewModResources function + modconfig.AppSpecificNewModResourcesFunc = resources.NewModResources + + var mod = modconfig.NewMod("test_mod", ".", hcl.Range{}) + mod.Resources = &resources.PowerpipeModResources{ + Benchmarks: map[string]*resources.Benchmark{}, + Controls: map[string]*resources.Control{ + "control1": makeControl(mod, "control1", "Control 1", "Control 1 description", "SELECT * FROM table1", map[string]string{"t1": "val1_foo", "t2": "val2_foo", "t3": "val3_foo"}), + "control2a": makeControl(mod, "control2a", "Control 2", "Control 2a description", "SELECT id FROM table2", map[string]string{"t1": "val1_foo", "t2": "val2_foo", "t3": "val3_foo_a"}), + "control2b": makeControl(mod, "control2b", "Control 2", "Control 2b description", "SELECT * FROM table2", map[string]string{"t1": "val1_foo", "t2": "val2_foo", "t3": "val3_foo_b"}), + "control3": makeControl(mod, "control3", "Control 3", "Control 3 description", "SELECT * FROM table3", map[string]string{"t1": "val1_bar", "t2": "val2_bar", "t3": "val3_bar"}), + "control4": makeControl(mod, "control4", "Control 4", "Control 4 description", "SELECT * FROM table4", map[string]string{"t1": "val1_bar", "t2": "val2_foo", "t3": "val3_bar"}), + }, + } + var w = &PowerpipeWorkspace{ + Workspace: workspace.Workspace{ + Mod: mod, + }, + } + + controlTests := []testCase[*resources.Control]{ + { + name: `where "name = 'control1'"`, + filter: workspace.ResourceFilter{ + Where: "name = 'control1'", + }, + + want: map[string]struct{}{ + "test_mod.control.control1": {}, + }, + }, + { + name: `where "name != 'control1'"`, + filter: workspace.ResourceFilter{ + Where: "name != 'control1'", + }, + want: map[string]struct{}{ + "test_mod.control.control2a": {}, + "test_mod.control.control2b": {}, + "test_mod.control.control3": {}, + "test_mod.control.control4": {}, + }, + }, + { + name: `where "name like 'control2%'"`, + filter: workspace.ResourceFilter{ + Where: `name like 'control2%'`, + }, + want: map[string]struct{}{ + "test_mod.control.control2a": {}, + "test_mod.control.control2b": {}, + }, + }, + { + name: `where "name ilike 'ConTrol2%'"`, + filter: workspace.ResourceFilter{ + Where: `name ilike 'ConTrol2%'`, + }, + want: map[string]struct{}{ + "test_mod.control.control2a": {}, + "test_mod.control.control2b": {}, + }, + }, + { + name: `where "name not like 'control2%'"`, + filter: workspace.ResourceFilter{ + Where: `name not like 'control2%'`, + }, + want: map[string]struct{}{ + "test_mod.control.control1": {}, + "test_mod.control.control3": {}, + "test_mod.control.control4": {}, + }, + }, + { + name: `tags t1=val1_foo t2=val2_foo`, + filter: workspace.ResourceFilter{ + Tags: map[string][]string{ + "t1": {"val1_foo"}, + "t2": {"val2_foo"}, + }, + }, + want: map[string]struct{}{ + "test_mod.control.control1": {}, + "test_mod.control.control2a": {}, + "test_mod.control.control2b": {}, + }, + }, + { + name: `tags t1=val1_bar t2=val2_bar`, + filter: workspace.ResourceFilter{ + Tags: map[string][]string{ + "t1": {"val1_bar"}, + "t2": {"val2_bar"}, + }, + }, + want: map[string]struct{}{ + "test_mod.control.control3": {}, + }, + }, + { + name: `tags t3=val3_foo t3=val3_bar`, + filter: workspace.ResourceFilter{ + Tags: map[string][]string{ + "t3": {"val3_foo", "val3_bar"}, + }, + }, + want: map[string]struct{}{ + "test_mod.control.control1": {}, + "test_mod.control.control3": {}, + "test_mod.control.control4": {}, + }, + }, + { + name: `tags t1=val1_foo t2=something_else [NO MATCHES]`, + filter: workspace.ResourceFilter{ + Tags: map[string][]string{ + "t1": {"val1_foo"}, + "t2": {"something_else"}, + }, + }, + want: map[string]struct{}{}, + }, + } + //var testFilter = "name like 'control1'" + var testFilter = "" + + executeTests[*resources.Control](t, controlTests, testFilter, w) +} + +func executeTests[T modconfig.HclResource](t *testing.T, controlTests []testCase[*resources.Control], testFilter string, w *PowerpipeWorkspace) { + for _, tt := range controlTests { + // apply test filter if specified + if testFilter != "" && tt.name != testFilter { + continue + } + t.Run(tt.name, func(t *testing.T) { + + got, err := workspace.FilterWorkspaceResourcesOfType[T](&w.Workspace, tt.filter) + if err != nil { + t.Fatalf("FilterWorkspaceResourcesOfType() test '%s' error = %v", tt.name, err) + } + if len(got) != len(tt.want) { + t.Fatalf("FilterWorkspaceResourcesOfType() test '%s' got %d %s, wanted %d", + tt.name, + len(got), utils.Pluralize("result", len(got)), + len(tt.want)) + } + for k := range got { + if _, found := tt.want[k]; !found { + t.Errorf("FilterWorkspaceResourcesOfType() test '%s' got %s but this was not expected", tt.name, k) + } + } + }) + } +} diff --git a/internal/dashboardworkspace/workspace_events.go b/internal/workspace/workspace_events.go similarity index 59% rename from internal/dashboardworkspace/workspace_events.go rename to internal/workspace/workspace_events.go index 9e8a0453..6980553d 100644 --- a/internal/dashboardworkspace/workspace_events.go +++ b/internal/workspace/workspace_events.go @@ -1,7 +1,8 @@ -package dashboardworkspace +package workspace import ( "context" + "github.com/turbot/powerpipe/internal/resources" "log/slog" "reflect" "sync/atomic" @@ -13,7 +14,7 @@ import ( var EventCount int64 = 0 -func (w *WorkspaceEvents) PublishDashboardEvent(ctx context.Context, e dashboardevents.DashboardEvent) { +func (w *PowerpipeWorkspace) PublishDashboardEvent(ctx context.Context, e dashboardevents.DashboardEvent) { if w.dashboardEventChan != nil { var doneChan = make(chan struct{}) go func() { @@ -34,7 +35,7 @@ func (w *WorkspaceEvents) PublishDashboardEvent(ctx context.Context, e dashboard // RegisterDashboardEventHandler starts the event handler goroutine if necessary and // adds the event handler to our list -func (w *WorkspaceEvents) RegisterDashboardEventHandler(ctx context.Context, handler dashboardevents.DashboardEventHandler) { +func (w *PowerpipeWorkspace) RegisterDashboardEventHandler(ctx context.Context, handler dashboardevents.DashboardEventHandler) { // if no event channel has been created we need to start the event handler goroutine if w.dashboardEventChan == nil { // create a fairly large channel buffer @@ -47,12 +48,12 @@ func (w *WorkspaceEvents) RegisterDashboardEventHandler(ctx context.Context, han // UnregisterDashboardEventHandlers clears all event handlers // used when generating multiple snapshots -func (w *WorkspaceEvents) UnregisterDashboardEventHandlers() { +func (w *PowerpipeWorkspace) UnregisterDashboardEventHandlers() { w.dashboardEventHandlers = nil } // this function is run as a goroutine to call registered event handlers for all received events -func (w *WorkspaceEvents) handleDashboardEvent(ctx context.Context) { +func (w *PowerpipeWorkspace) handleDashboardEvent(ctx context.Context) { for { e := <-w.dashboardEventChan atomic.AddInt64(&EventCount, -1) @@ -68,14 +69,17 @@ func (w *WorkspaceEvents) handleDashboardEvent(ctx context.Context) { } } -func (w *WorkspaceEvents) raiseDashboardChangedEvents(ctx context.Context, resourceMaps, prevResourceMaps *modconfig.ResourceMaps) { +func (w *PowerpipeWorkspace) raiseDashboardChangedEvents(ctx context.Context, r, p modconfig.ModResources) { event := &dashboardevents.DashboardChanged{} - // TODO reports can we use a ResourceMaps diff function to do all of this - we are duplicating logic + modResources := r.(*resources.PowerpipeModResources) + prevModResources := p.(*resources.PowerpipeModResources) + + // TODO reports can we use a PowerpipeModResources diff function to do all of this - we are duplicating logic // first detect changes to existing resources and deletions - for name, prev := range prevResourceMaps.Dashboards { - if current, ok := resourceMaps.Dashboards[name]; ok { + for name, prev := range prevModResources.Dashboards { + if current, ok := modResources.Dashboards[name]; ok { diff := prev.Diff(current) if diff.HasChanges() { event.ChangedDashboards = append(event.ChangedDashboards, diff) @@ -84,8 +88,8 @@ func (w *WorkspaceEvents) raiseDashboardChangedEvents(ctx context.Context, resou event.DeletedDashboards = append(event.DeletedDashboards, prev) } } - for name, prev := range prevResourceMaps.DashboardContainers { - if current, ok := resourceMaps.DashboardContainers[name]; ok { + for name, prev := range prevModResources.DashboardContainers { + if current, ok := modResources.DashboardContainers[name]; ok { diff := prev.Diff(current) if diff.HasChanges() { event.ChangedContainers = append(event.ChangedContainers, diff) @@ -94,8 +98,8 @@ func (w *WorkspaceEvents) raiseDashboardChangedEvents(ctx context.Context, resou event.DeletedContainers = append(event.DeletedContainers, prev) } } - for name, prev := range prevResourceMaps.DashboardCards { - if current, ok := resourceMaps.DashboardCards[name]; ok { + for name, prev := range prevModResources.DashboardCards { + if current, ok := modResources.DashboardCards[name]; ok { diff := prev.Diff(current) if diff.HasChanges() { event.ChangedCards = append(event.ChangedCards, diff) @@ -104,8 +108,8 @@ func (w *WorkspaceEvents) raiseDashboardChangedEvents(ctx context.Context, resou event.DeletedCards = append(event.DeletedCards, prev) } } - for name, prev := range prevResourceMaps.DashboardCharts { - if current, ok := resourceMaps.DashboardCharts[name]; ok { + for name, prev := range prevModResources.DashboardCharts { + if current, ok := modResources.DashboardCharts[name]; ok { diff := prev.Diff(current) if diff.HasChanges() { event.ChangedCharts = append(event.ChangedCharts, diff) @@ -114,8 +118,8 @@ func (w *WorkspaceEvents) raiseDashboardChangedEvents(ctx context.Context, resou event.DeletedCharts = append(event.DeletedCharts, prev) } } - for name, prev := range prevResourceMaps.Benchmarks { - if current, ok := resourceMaps.Benchmarks[name]; ok { + for name, prev := range prevModResources.Benchmarks { + if current, ok := modResources.Benchmarks[name]; ok { diff := prev.Diff(current) if diff.HasChanges() { event.ChangedBenchmarks = append(event.ChangedBenchmarks, diff) @@ -124,8 +128,8 @@ func (w *WorkspaceEvents) raiseDashboardChangedEvents(ctx context.Context, resou event.DeletedBenchmarks = append(event.DeletedBenchmarks, prev) } } - for name, prev := range prevResourceMaps.Controls { - if current, ok := resourceMaps.Controls[name]; ok { + for name, prev := range prevModResources.Controls { + if current, ok := modResources.Controls[name]; ok { diff := prev.Diff(current) if diff.HasChanges() { event.ChangedControls = append(event.ChangedControls, diff) @@ -134,8 +138,8 @@ func (w *WorkspaceEvents) raiseDashboardChangedEvents(ctx context.Context, resou event.DeletedControls = append(event.DeletedControls, prev) } } - for name, prev := range prevResourceMaps.DashboardFlows { - if current, ok := resourceMaps.DashboardFlows[name]; ok { + for name, prev := range prevModResources.DashboardFlows { + if current, ok := modResources.DashboardFlows[name]; ok { diff := prev.Diff(current) if diff.HasChanges() { event.ChangedFlows = append(event.ChangedFlows, diff) @@ -144,8 +148,8 @@ func (w *WorkspaceEvents) raiseDashboardChangedEvents(ctx context.Context, resou event.DeletedFlows = append(event.DeletedFlows, prev) } } - for name, prev := range prevResourceMaps.DashboardGraphs { - if current, ok := resourceMaps.DashboardGraphs[name]; ok { + for name, prev := range prevModResources.DashboardGraphs { + if current, ok := modResources.DashboardGraphs[name]; ok { diff := prev.Diff(current) if diff.HasChanges() { event.ChangedGraphs = append(event.ChangedGraphs, diff) @@ -154,8 +158,8 @@ func (w *WorkspaceEvents) raiseDashboardChangedEvents(ctx context.Context, resou event.DeletedGraphs = append(event.DeletedGraphs, prev) } } - for name, prev := range prevResourceMaps.DashboardHierarchies { - if current, ok := resourceMaps.DashboardHierarchies[name]; ok { + for name, prev := range prevModResources.DashboardHierarchies { + if current, ok := modResources.DashboardHierarchies[name]; ok { diff := prev.Diff(current) if diff.HasChanges() { event.ChangedHierarchies = append(event.ChangedHierarchies, diff) @@ -164,8 +168,8 @@ func (w *WorkspaceEvents) raiseDashboardChangedEvents(ctx context.Context, resou event.DeletedHierarchies = append(event.DeletedHierarchies, prev) } } - for name, prev := range prevResourceMaps.DashboardImages { - if current, ok := resourceMaps.DashboardImages[name]; ok { + for name, prev := range prevModResources.DashboardImages { + if current, ok := modResources.DashboardImages[name]; ok { diff := prev.Diff(current) if diff.HasChanges() { event.ChangedImages = append(event.ChangedImages, diff) @@ -174,8 +178,8 @@ func (w *WorkspaceEvents) raiseDashboardChangedEvents(ctx context.Context, resou event.DeletedImages = append(event.DeletedImages, prev) } } - for name, prev := range prevResourceMaps.DashboardNodes { - if current, ok := resourceMaps.DashboardNodes[name]; ok { + for name, prev := range prevModResources.DashboardNodes { + if current, ok := modResources.DashboardNodes[name]; ok { diff := prev.Diff(current) if diff.HasChanges() { event.ChangedNodes = append(event.ChangedNodes, diff) @@ -184,8 +188,8 @@ func (w *WorkspaceEvents) raiseDashboardChangedEvents(ctx context.Context, resou event.DeletedNodes = append(event.DeletedNodes, prev) } } - for name, prev := range prevResourceMaps.DashboardEdges { - if current, ok := resourceMaps.DashboardEdges[name]; ok { + for name, prev := range prevModResources.DashboardEdges { + if current, ok := modResources.DashboardEdges[name]; ok { diff := prev.Diff(current) if diff.HasChanges() { event.ChangedEdges = append(event.ChangedEdges, diff) @@ -194,8 +198,8 @@ func (w *WorkspaceEvents) raiseDashboardChangedEvents(ctx context.Context, resou event.DeletedEdges = append(event.DeletedEdges, prev) } } - for name, prev := range prevResourceMaps.GlobalDashboardInputs { - if current, ok := resourceMaps.GlobalDashboardInputs[name]; ok { + for name, prev := range prevModResources.GlobalDashboardInputs { + if current, ok := modResources.GlobalDashboardInputs[name]; ok { diff := prev.Diff(current) if diff.HasChanges() { event.ChangedInputs = append(event.ChangedInputs, diff) @@ -204,8 +208,8 @@ func (w *WorkspaceEvents) raiseDashboardChangedEvents(ctx context.Context, resou event.DeletedInputs = append(event.DeletedInputs, prev) } } - for name, prevInputsForDashboard := range prevResourceMaps.DashboardInputs { - if currentInputsForDashboard, ok := resourceMaps.DashboardInputs[name]; ok { + for name, prevInputsForDashboard := range prevModResources.DashboardInputs { + if currentInputsForDashboard, ok := modResources.DashboardInputs[name]; ok { for name, prev := range prevInputsForDashboard { if current, ok := currentInputsForDashboard[name]; ok { diff := prev.Diff(current) @@ -222,8 +226,8 @@ func (w *WorkspaceEvents) raiseDashboardChangedEvents(ctx context.Context, resou } } } - for name, prev := range prevResourceMaps.DashboardTables { - if current, ok := resourceMaps.DashboardTables[name]; ok { + for name, prev := range prevModResources.DashboardTables { + if current, ok := modResources.DashboardTables[name]; ok { diff := prev.Diff(current) if diff.HasChanges() { event.ChangedTables = append(event.ChangedTables, diff) @@ -232,8 +236,8 @@ func (w *WorkspaceEvents) raiseDashboardChangedEvents(ctx context.Context, resou event.DeletedTables = append(event.DeletedTables, prev) } } - for name, prev := range prevResourceMaps.DashboardCategories { - if current, ok := resourceMaps.DashboardCategories[name]; ok { + for name, prev := range prevModResources.DashboardCategories { + if current, ok := modResources.DashboardCategories[name]; ok { diff := prev.Diff(current) if diff.HasChanges() { event.ChangedCategories = append(event.ChangedCategories, diff) @@ -242,8 +246,8 @@ func (w *WorkspaceEvents) raiseDashboardChangedEvents(ctx context.Context, resou event.DeletedCategories = append(event.DeletedCategories, prev) } } - for name, prev := range prevResourceMaps.DashboardTexts { - if current, ok := resourceMaps.DashboardTexts[name]; ok { + for name, prev := range prevModResources.DashboardTexts { + if current, ok := modResources.DashboardTexts[name]; ok { diff := prev.Diff(current) if diff.HasChanges() { event.ChangedTexts = append(event.ChangedTexts, diff) @@ -254,79 +258,79 @@ func (w *WorkspaceEvents) raiseDashboardChangedEvents(ctx context.Context, resou } // now detect new resources - for name, p := range resourceMaps.Dashboards { - if _, ok := prevResourceMaps.Dashboards[name]; !ok { + for name, p := range modResources.Dashboards { + if _, ok := prevModResources.Dashboards[name]; !ok { event.NewDashboards = append(event.NewDashboards, p) } } - for name, p := range resourceMaps.DashboardContainers { - if _, ok := prevResourceMaps.DashboardContainers[name]; !ok { + for name, p := range modResources.DashboardContainers { + if _, ok := prevModResources.DashboardContainers[name]; !ok { event.NewContainers = append(event.NewContainers, p) } } - for name, p := range resourceMaps.DashboardCards { - if _, ok := prevResourceMaps.DashboardCards[name]; !ok { + for name, p := range modResources.DashboardCards { + if _, ok := prevModResources.DashboardCards[name]; !ok { event.NewCards = append(event.NewCards, p) } } - for name, p := range resourceMaps.DashboardCategories { - if _, ok := prevResourceMaps.DashboardCategories[name]; !ok { + for name, p := range modResources.DashboardCategories { + if _, ok := prevModResources.DashboardCategories[name]; !ok { event.NewCategories = append(event.NewCategories, p) } } - for name, p := range resourceMaps.DashboardCharts { - if _, ok := prevResourceMaps.DashboardCharts[name]; !ok { + for name, p := range modResources.DashboardCharts { + if _, ok := prevModResources.DashboardCharts[name]; !ok { event.NewCharts = append(event.NewCharts, p) } } - for name, p := range resourceMaps.Benchmarks { - if _, ok := prevResourceMaps.Benchmarks[name]; !ok { + for name, p := range modResources.Benchmarks { + if _, ok := prevModResources.Benchmarks[name]; !ok { event.NewBenchmarks = append(event.NewBenchmarks, p) } } - for name, p := range resourceMaps.Controls { - if _, ok := prevResourceMaps.Controls[name]; !ok { + for name, p := range modResources.Controls { + if _, ok := prevModResources.Controls[name]; !ok { event.NewControls = append(event.NewControls, p) } } - for name, p := range resourceMaps.DashboardFlows { - if _, ok := prevResourceMaps.DashboardFlows[name]; !ok { + for name, p := range modResources.DashboardFlows { + if _, ok := prevModResources.DashboardFlows[name]; !ok { event.NewFlows = append(event.NewFlows, p) } } - for name, p := range resourceMaps.DashboardGraphs { - if _, ok := prevResourceMaps.DashboardGraphs[name]; !ok { + for name, p := range modResources.DashboardGraphs { + if _, ok := prevModResources.DashboardGraphs[name]; !ok { event.NewGraphs = append(event.NewGraphs, p) } } - for name, p := range resourceMaps.DashboardHierarchies { - if _, ok := prevResourceMaps.DashboardHierarchies[name]; !ok { + for name, p := range modResources.DashboardHierarchies { + if _, ok := prevModResources.DashboardHierarchies[name]; !ok { event.NewHierarchies = append(event.NewHierarchies, p) } } - for name, p := range resourceMaps.DashboardImages { - if _, ok := prevResourceMaps.DashboardImages[name]; !ok { + for name, p := range modResources.DashboardImages { + if _, ok := prevModResources.DashboardImages[name]; !ok { event.NewImages = append(event.NewImages, p) } } - for name, p := range resourceMaps.DashboardNodes { - if _, ok := prevResourceMaps.DashboardNodes[name]; !ok { + for name, p := range modResources.DashboardNodes { + if _, ok := prevModResources.DashboardNodes[name]; !ok { event.NewNodes = append(event.NewNodes, p) } } - for name, p := range resourceMaps.DashboardEdges { - if _, ok := prevResourceMaps.DashboardEdges[name]; !ok { + for name, p := range modResources.DashboardEdges { + if _, ok := prevModResources.DashboardEdges[name]; !ok { event.NewEdges = append(event.NewEdges, p) } } - for name, p := range resourceMaps.GlobalDashboardInputs { - if _, ok := prevResourceMaps.GlobalDashboardInputs[name]; !ok { + for name, p := range modResources.GlobalDashboardInputs { + if _, ok := prevModResources.GlobalDashboardInputs[name]; !ok { event.NewInputs = append(event.NewInputs, p) } } - for name, currentInputsForDashboard := range resourceMaps.DashboardInputs { - if prevInputsForDashboard, ok := prevResourceMaps.DashboardInputs[name]; ok { + for name, currentInputsForDashboard := range modResources.DashboardInputs { + if prevInputsForDashboard, ok := prevModResources.DashboardInputs[name]; ok { for name, current := range currentInputsForDashboard { if _, ok := prevInputsForDashboard[name]; !ok { event.NewInputs = append(event.NewInputs, current) @@ -340,13 +344,13 @@ func (w *WorkspaceEvents) raiseDashboardChangedEvents(ctx context.Context, resou } } - for name, p := range resourceMaps.DashboardTables { - if _, ok := prevResourceMaps.DashboardTables[name]; !ok { + for name, p := range modResources.DashboardTables { + if _, ok := prevModResources.DashboardTables[name]; !ok { event.NewTables = append(event.NewTables, p) } } - for name, p := range resourceMaps.DashboardTexts { - if _, ok := prevResourceMaps.DashboardTexts[name]; !ok { + for name, p := range modResources.DashboardTexts { + if _, ok := prevModResources.DashboardTexts[name]; !ok { event.NewTexts = append(event.NewTexts, p) } } @@ -354,7 +358,7 @@ func (w *WorkspaceEvents) raiseDashboardChangedEvents(ctx context.Context, resou if event.HasChanges() { // for every changed resource, set parents as changed, up the tree f := func(item modconfig.ModTreeItem) (bool, error) { - event.SetParentsChanged(item, prevResourceMaps) + event.SetParentsChanged(item, prevModResources) return true, nil } err := event.WalkChangedResources(f)