github.com/turbot/steampipe@v1.7.0-rc.0.0.20240517123944-7cef272d4458/pkg/steampipeconfig/parse/workspace_profile.go (about)

     1  package parse
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"log"
     7  
     8  	"github.com/hashicorp/hcl/v2"
     9  	"github.com/hashicorp/hcl/v2/gohcl"
    10  	filehelpers "github.com/turbot/go-kit/files"
    11  	"github.com/turbot/go-kit/helpers"
    12  	"github.com/turbot/pipe-fittings/hclhelpers"
    13  	"github.com/turbot/steampipe-plugin-sdk/v5/plugin"
    14  	"github.com/turbot/steampipe/pkg/constants"
    15  	"github.com/turbot/steampipe/pkg/steampipeconfig/modconfig"
    16  	"github.com/turbot/steampipe/pkg/steampipeconfig/options"
    17  )
    18  
    19  func LoadWorkspaceProfiles(ctx context.Context, workspaceProfilePath string) (profileMap map[string]*modconfig.WorkspaceProfile, err error) {
    20  
    21  	defer func() {
    22  		if r := recover(); r != nil {
    23  			err = helpers.ToError(r)
    24  		}
    25  		// be sure to return the default
    26  		if profileMap != nil && profileMap["default"] == nil {
    27  			profileMap["default"] = &modconfig.WorkspaceProfile{ProfileName: "default"}
    28  		}
    29  	}()
    30  
    31  	// create profile map to populate
    32  	profileMap = map[string]*modconfig.WorkspaceProfile{}
    33  
    34  	configPaths, err := filehelpers.ListFilesWithContext(ctx, workspaceProfilePath, &filehelpers.ListOptions{
    35  		Flags:   filehelpers.FilesFlat,
    36  		Include: filehelpers.InclusionsFromExtensions([]string{constants.ConfigExtension}),
    37  	})
    38  	if err != nil {
    39  		return nil, err
    40  	}
    41  	if len(configPaths) == 0 {
    42  		return profileMap, nil
    43  	}
    44  
    45  	fileData, diags := LoadFileData(configPaths...)
    46  	if diags.HasErrors() {
    47  		return nil, plugin.DiagsToError("Failed to load workspace profiles", diags)
    48  	}
    49  
    50  	body, diags := ParseHclFiles(fileData)
    51  	if diags.HasErrors() {
    52  		return nil, plugin.DiagsToError("Failed to load workspace profiles", diags)
    53  	}
    54  
    55  	// do a partial decode
    56  	content, diags := body.Content(ConfigBlockSchema)
    57  	if diags.HasErrors() {
    58  		return nil, plugin.DiagsToError("Failed to load workspace profiles", diags)
    59  	}
    60  
    61  	parseCtx := NewWorkspaceProfileParseContext(workspaceProfilePath)
    62  	parseCtx.SetDecodeContent(content, fileData)
    63  
    64  	// build parse context
    65  	return parseWorkspaceProfiles(parseCtx)
    66  
    67  }
    68  func parseWorkspaceProfiles(parseCtx *WorkspaceProfileParseContext) (map[string]*modconfig.WorkspaceProfile, error) {
    69  	// we may need to decode more than once as we gather dependencies as we go
    70  	// continue decoding as long as the number of unresolved blocks decreases
    71  	prevUnresolvedBlocks := 0
    72  	for attempts := 0; ; attempts++ {
    73  		_, diags := decodeWorkspaceProfiles(parseCtx)
    74  		if diags.HasErrors() {
    75  			return nil, plugin.DiagsToError("Failed to decode all workspace profile files", diags)
    76  		}
    77  
    78  		// if there are no unresolved blocks, we are done
    79  		unresolvedBlocks := len(parseCtx.UnresolvedBlocks)
    80  		if unresolvedBlocks == 0 {
    81  			log.Printf("[TRACE] parse complete after %d decode passes", attempts+1)
    82  			break
    83  		}
    84  		// if the number of unresolved blocks has NOT reduced, fail
    85  		if prevUnresolvedBlocks != 0 && unresolvedBlocks >= prevUnresolvedBlocks {
    86  			str := parseCtx.FormatDependencies()
    87  			return nil, fmt.Errorf("failed to resolve workspace profile dependencies after %d attempts\nDependencies:\n%s", attempts+1, str)
    88  		}
    89  		// update prevUnresolvedBlocks
    90  		prevUnresolvedBlocks = unresolvedBlocks
    91  	}
    92  
    93  	return parseCtx.workspaceProfiles, nil
    94  
    95  }
    96  
    97  func decodeWorkspaceProfiles(parseCtx *WorkspaceProfileParseContext) (map[string]*modconfig.WorkspaceProfile, hcl.Diagnostics) {
    98  	profileMap := map[string]*modconfig.WorkspaceProfile{}
    99  
   100  	var diags hcl.Diagnostics
   101  	blocksToDecode, err := parseCtx.BlocksToDecode()
   102  	// build list of blocks to decode
   103  	if err != nil {
   104  		diags = append(diags, &hcl.Diagnostic{
   105  			Severity: hcl.DiagError,
   106  			Summary:  "failed to determine required dependency order",
   107  			Detail:   err.Error()})
   108  		return nil, diags
   109  	}
   110  
   111  	// now clear dependencies from run context - they will be rebuilt
   112  	parseCtx.ClearDependencies()
   113  
   114  	for _, block := range blocksToDecode {
   115  		if block.Type == modconfig.BlockTypeWorkspaceProfile {
   116  			workspaceProfile, res := decodeWorkspaceProfile(block, parseCtx)
   117  
   118  			if res.Success() {
   119  				// success - add to map
   120  				profileMap[workspaceProfile.ProfileName] = workspaceProfile
   121  			}
   122  			diags = append(diags, res.Diags...)
   123  		}
   124  	}
   125  	return profileMap, diags
   126  }
   127  
   128  // decodeWorkspaceProfileOption decodes an options block as a workspace profile property
   129  // setting the necessary overrides for special handling of the "dashboard" option which is different
   130  // from the global "dashboard" option
   131  func decodeWorkspaceProfileOption(block *hcl.Block) (options.Options, hcl.Diagnostics) {
   132  	return DecodeOptions(block, WithOverride(constants.CmdNameDashboard, &options.WorkspaceProfileDashboard{}))
   133  }
   134  
   135  func decodeWorkspaceProfile(block *hcl.Block, parseCtx *WorkspaceProfileParseContext) (*modconfig.WorkspaceProfile, *DecodeResult) {
   136  	res := newDecodeResult()
   137  	// get shell resource
   138  	resource := modconfig.NewWorkspaceProfile(block)
   139  
   140  	// do a partial decode to get options blocks into workspaceProfileOptions, with all other attributes in rest
   141  	workspaceProfileOptions, rest, diags := block.Body.PartialContent(WorkspaceProfileBlockSchema)
   142  	if diags.HasErrors() {
   143  		res.handleDecodeDiags(diags)
   144  		return nil, res
   145  	}
   146  
   147  	diags = gohcl.DecodeBody(rest, parseCtx.EvalCtx, resource)
   148  	if len(diags) > 0 {
   149  		res.handleDecodeDiags(diags)
   150  	}
   151  	// use a map keyed by a string for fast lookup
   152  	// we use an empty struct as the value type, so that
   153  	// we don't use up unnecessary memory
   154  	foundOptions := map[string]struct{}{}
   155  	for _, block := range workspaceProfileOptions.Blocks {
   156  		switch block.Type {
   157  		case "options":
   158  			optionsBlockType := block.Labels[0]
   159  			if _, found := foundOptions[optionsBlockType]; found {
   160  				// fail
   161  				diags = append(diags, &hcl.Diagnostic{
   162  					Severity: hcl.DiagError,
   163  					Subject:  hclhelpers.BlockRangePointer(block),
   164  					Summary:  fmt.Sprintf("Duplicate options type '%s'", optionsBlockType),
   165  				})
   166  			}
   167  			opts, moreDiags := decodeWorkspaceProfileOption(block)
   168  			if moreDiags.HasErrors() {
   169  				diags = append(diags, moreDiags...)
   170  				break
   171  			}
   172  			moreDiags = resource.SetOptions(opts, block)
   173  			if moreDiags.HasErrors() {
   174  				diags = append(diags, moreDiags...)
   175  			}
   176  			foundOptions[optionsBlockType] = struct{}{}
   177  		default:
   178  			// this should never happen
   179  			diags = append(diags, &hcl.Diagnostic{
   180  				Severity: hcl.DiagError,
   181  				Summary:  fmt.Sprintf("invalid block type '%s' - only 'options' blocks are supported for workspace profiles", block.Type),
   182  				Subject:  hclhelpers.BlockRangePointer(block),
   183  			})
   184  		}
   185  	}
   186  
   187  	handleWorkspaceProfileDecodeResult(resource, res, block, parseCtx)
   188  	return resource, res
   189  }
   190  
   191  func handleWorkspaceProfileDecodeResult(resource *modconfig.WorkspaceProfile, res *DecodeResult, block *hcl.Block, parseCtx *WorkspaceProfileParseContext) {
   192  	if res.Success() {
   193  		// call post decode hook
   194  		// NOTE: must do this BEFORE adding resource to run context to ensure we respect the base property
   195  		moreDiags := resource.OnDecoded()
   196  		res.addDiags(moreDiags)
   197  
   198  		moreDiags = parseCtx.AddResource(resource)
   199  		res.addDiags(moreDiags)
   200  		return
   201  	}
   202  
   203  	// failure :(
   204  	if len(res.Depends) > 0 {
   205  		moreDiags := parseCtx.AddDependencies(block, resource.Name(), res.Depends)
   206  		res.addDiags(moreDiags)
   207  	}
   208  }