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 }