Skip to content

Commit a55467b

Browse files
Cerebrovinnyautofix-ci[bot]RoseSecurityBenbentwo
authored
Fix: nested components for list pkg (#1225)
* fix: nested components display * add helmfile support * clean code and add helmfile support * [autofix.ci] apply automated fixes * clean up * [autofix.ci] apply automated fixes * refactor: simplify component name handling in query functions * add helmfile to settings * clean code * add yq tests for nested path and helmfile --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: RoseSecurity <[email protected]> Co-authored-by: Ben <[email protected]>
1 parent aaac85a commit a55467b

File tree

3 files changed

+252
-106
lines changed

3 files changed

+252
-106
lines changed

pkg/list/list_values.go

Lines changed: 197 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,19 @@ import (
1616

1717
// Error variables for list_values package.
1818
var (
19-
ErrInvalidStackPattern = errors.New("invalid stack pattern")
19+
ErrInvalidStackPattern = errors.New("invalid stack pattern")
20+
ErrEmptyTargetComponentName = errors.New("target component name cannot be empty")
21+
ErrComponentsSectionNotFound = errors.New("components section not found in stack")
22+
ErrComponentNotFoundInSections = errors.New("component not found in terraform or helmfile sections")
23+
ErrQueryFailed = errors.New("query execution failed")
2024
)
2125

2226
// Component and section name constants.
2327
const (
2428
// KeyTerraform is the key for terraform components.
2529
KeyTerraform = "terraform"
30+
// KeyHelmfile is the key for helmfile components.
31+
KeyHelmfile = "helmfile"
2632
// KeySettings is the key for settings section.
2733
KeySettings = "settings"
2834
// KeyMetadata is the key for metadata section.
@@ -75,7 +81,7 @@ func FilterAndListValues(stacksMap map[string]interface{}, options *FilterOption
7581
}
7682

7783
// Extract stack values
78-
extractedValues, err := extractComponentValues(stacksMap, options.Component, options.ComponentFilter, options.IncludeAbstract)
84+
extractedValues, err := extractComponentValuesFromAllStacks(stacksMap, options.Component, options.ComponentFilter, options.IncludeAbstract)
7985
if err != nil {
8086
return "", err
8187
}
@@ -117,125 +123,230 @@ func createComponentError(component, componentFilter string) error {
117123
}
118124
}
119125

120-
// extractComponentValues extracts the component values from all stacks.
121-
func extractComponentValues(stacksMap map[string]interface{}, component string, componentFilter string, includeAbstract bool) (map[string]interface{}, error) {
122-
values := make(map[string]interface{})
126+
func extractComponentValuesFromAllStacks(stacks map[string]interface{}, component, filter string, includeAbstract bool) (map[string]interface{}, error) {
127+
stackComponentValues := make(map[string]interface{})
123128

124-
// Check if this is a regular component and use it as filter if no specific filter
125-
isComponentSection := component != KeySettings && component != KeyMetadata
126-
if isComponentSection && componentFilter == "" {
127-
log.Debug("Using component as filter", KeyComponent, component)
128-
componentFilter = component
129-
component = ""
130-
}
131-
132-
log.Debug("Building YQ expression", KeyComponent, component, "componentFilter", componentFilter)
129+
component, filter = normalizeComponentAndFilterInputs(component, filter)
133130

134-
for stackName, stackData := range stacksMap {
135-
stack, ok := stackData.(map[string]interface{})
131+
for stackName, data := range stacks {
132+
stackMap, ok := data.(map[string]interface{})
136133
if !ok {
137-
log.Debug("stack data is not a map", KeyStack, stackName)
138134
continue
139135
}
140136

141-
// Build and execute YQ expression
142-
yqExpression := processComponentType(component, componentFilter, includeAbstract)
143-
queryResult, err := utils.EvaluateYqExpression(nil, stack, yqExpression)
144-
if err != nil || queryResult == nil {
145-
log.Debug("no values found",
146-
KeyStack, stackName, KeyComponent, component,
147-
"componentFilter", componentFilter, "yq_expression", yqExpression,
148-
"error", err)
149-
continue
137+
componentValue := extractComponentValueFromSingleStack(stackMap, stackName, component, filter, includeAbstract)
138+
if componentValue != nil {
139+
stackComponentValues[stackName] = componentValue
150140
}
141+
}
151142

152-
// Process the result based on component type
153-
values[stackName] = processQueryResult(component, queryResult)
143+
if len(stackComponentValues) == 0 {
144+
return nil, createComponentError(component, filter)
154145
}
155146

156-
if len(values) == 0 {
157-
return nil, createComponentError(component, componentFilter)
147+
return stackComponentValues, nil
148+
}
149+
150+
func normalizeComponentAndFilterInputs(component, filter string) (string, string) {
151+
isRegularComponent := component != KeySettings && component != KeyMetadata
152+
if isRegularComponent && filter == "" {
153+
log.Debug("Using component name as filter", KeyComponent, component)
154+
return "", component
158155
}
156+
return component, filter
157+
}
158+
159+
func extractComponentValueFromSingleStack(stackMap map[string]interface{}, stackName, component, filter string, includeAbstract bool) interface{} {
160+
targetComponentName := determineTargetComponentName(component, filter)
159161

160-
return values, nil
162+
componentType := detectComponentTypeInStack(stackMap, targetComponentName, stackName)
163+
if componentType == "" {
164+
return nil
165+
}
166+
167+
params := &QueryParams{
168+
StackName: stackName,
169+
StackMap: stackMap,
170+
Component: component,
171+
ComponentFilter: filter,
172+
TargetComponentName: targetComponentName,
173+
ComponentType: componentType,
174+
IncludeAbstract: includeAbstract,
175+
}
176+
177+
value, err := executeQueryForStack(params)
178+
if err != nil {
179+
log.Warn("Query failed", KeyStack, stackName, "error", err)
180+
return nil
181+
}
182+
183+
return value
161184
}
162185

163-
// processComponentType determines the YQ expression based on component type.
164-
func processComponentType(component string, componentFilter string, includeAbstract bool) string {
165-
// If this is a regular component query with a specific component filter
166-
if component == "" && componentFilter != "" {
167-
// Extract component name from path
168-
componentName := getComponentNameFromPath(componentFilter)
186+
func detectComponentTypeInStack(stackMap map[string]interface{}, targetComponent, stackName string) string {
187+
if targetComponent == "" {
188+
return KeyTerraform
189+
}
190+
191+
detectedType, err := determineComponentType(stackMap, targetComponent)
192+
if err != nil {
193+
log.Debug("Component not found", KeyStack, stackName, KeyComponent, targetComponent)
194+
return ""
195+
}
196+
197+
return detectedType
198+
}
199+
200+
// QueryParams holds all parameters needed for executing a query on a stack.
201+
type QueryParams struct {
202+
StackName string
203+
StackMap map[string]interface{}
204+
Component string
205+
ComponentFilter string
206+
TargetComponentName string
207+
ComponentType string
208+
IncludeAbstract bool
209+
}
210+
211+
func executeQueryForStack(params *QueryParams) (interface{}, error) {
212+
yqExpression := buildYqExpressionForComponent(
213+
params.Component,
214+
params.ComponentFilter,
215+
params.IncludeAbstract,
216+
params.ComponentType,
217+
)
218+
219+
queryResult, err := utils.EvaluateYqExpression(nil, params.StackMap, yqExpression)
220+
if err != nil {
221+
var logKey string
222+
var logValue string
223+
if params.TargetComponentName != "" {
224+
logKey = KeyComponent
225+
logValue = params.TargetComponentName
226+
} else {
227+
logKey = "section"
228+
logValue = params.Component
229+
}
230+
231+
log.Warn("YQ evaluation failed",
232+
KeyStack, params.StackName,
233+
"yqExpression", yqExpression,
234+
logKey, logValue,
235+
"error", err)
236+
return nil, fmt.Errorf("%w: %s", ErrQueryFailed, err.Error())
237+
}
238+
239+
if queryResult == nil {
240+
return nil, nil
241+
}
242+
243+
return extractRelevantDataFromQueryResult(params.Component, queryResult), nil
244+
}
169245

170-
// Return a direct path to the component.
171-
return fmt.Sprintf(".components.%s.%s", KeyTerraform, componentName)
246+
func determineTargetComponentName(component, componentFilter string) string {
247+
if componentFilter != "" {
248+
return componentFilter
249+
}
250+
251+
isRegularComponent := component != KeySettings && component != KeyMetadata
252+
if isRegularComponent {
253+
return component
254+
}
255+
256+
return ""
257+
}
258+
259+
func determineComponentType(stack map[string]interface{}, targetComponentName string) (string, error) {
260+
if targetComponentName == "" {
261+
return "", ErrEmptyTargetComponentName
262+
}
263+
264+
components, ok := stack[KeyComponents].(map[string]interface{})
265+
if !ok {
266+
return "", ErrComponentsSectionNotFound
267+
}
268+
269+
if isComponentInSection(components, KeyTerraform, targetComponentName) {
270+
log.Debug("Component found under terraform", KeyComponent, targetComponentName)
271+
return KeyTerraform, nil
272+
}
273+
274+
if isComponentInSection(components, KeyHelmfile, targetComponentName) {
275+
log.Debug("Component found under helmfile", KeyComponent, targetComponentName)
276+
return KeyHelmfile, nil
277+
}
278+
279+
return "", fmt.Errorf("%w: %s", ErrComponentNotFoundInSections, targetComponentName)
280+
}
281+
282+
func isComponentInSection(components map[string]interface{}, sectionKey, componentName string) bool {
283+
section, ok := components[sectionKey].(map[string]interface{})
284+
if !ok {
285+
return false
286+
}
287+
_, exists := section[componentName]
288+
return exists
289+
}
290+
291+
func buildYqExpressionForComponent(component string, componentFilter string, includeAbstract bool, componentType string) string {
292+
if component == "" && componentFilter != "" {
293+
return fmt.Sprintf(".components.%s.\"%s\"", componentType, componentFilter)
172294
}
173295

174-
// Handle special section queries.
175296
switch component {
176297
case KeySettings:
177-
if componentFilter != "" {
178-
componentName := getComponentNameFromPath(componentFilter)
179-
return fmt.Sprintf(".components.%s.%s", KeyTerraform, componentName)
180-
}
181-
return "select(.settings // .terraform.settings // .components.terraform.*.settings)"
298+
return buildSettingsExpression(componentFilter, componentType)
182299
case KeyMetadata:
183-
if componentFilter != "" {
184-
// For metadata with component filter, target the specific component.
185-
componentName := getComponentNameFromPath(componentFilter)
186-
return fmt.Sprintf(".components.%s.%s", KeyTerraform, componentName)
187-
}
188-
// For general metadata query.
189-
return DotChar + KeyMetadata
300+
return buildMetadataExpression(componentFilter, componentType)
190301
default:
191-
// Extract component name from path.
192-
componentName := getComponentNameFromPath(component)
302+
return buildComponentYqExpression(component, includeAbstract, componentType)
303+
}
304+
}
193305

194-
// Build query for component vars.
195-
return buildComponentYqExpression(componentName, includeAbstract)
306+
func buildSettingsExpression(componentFilter, componentType string) string {
307+
if componentFilter != "" {
308+
return fmt.Sprintf(".components.%s.\"%s\"", componentType, componentFilter)
196309
}
310+
return "select(.settings // " +
311+
".components." + KeyTerraform + ".*.settings // " +
312+
".components." + KeyHelmfile + ".*.settings)"
197313
}
198314

199-
// getComponentNameFromPath extracts the component name from a potentially nested path.
200-
func getComponentNameFromPath(component string) string {
201-
parts := strings.Split(component, "/")
202-
if len(parts) > 1 {
203-
return parts[len(parts)-1]
315+
func buildMetadataExpression(componentFilter, componentType string) string {
316+
if componentFilter != "" {
317+
// Use full component path and wrap in quotes for nested support
318+
return fmt.Sprintf(".components.%s.\"%s\"", componentType, componentFilter)
204319
}
205-
return component
320+
return DotChar + KeyMetadata
206321
}
207322

208-
// buildComponentYqExpression creates the YQ expression for extracting component vars.
209-
func buildComponentYqExpression(componentName string, includeAbstract bool) string {
210-
// Base expression to target the component
211-
yqExpression := fmt.Sprintf("%scomponents%s%s%s%s", DotChar, DotChar, KeyTerraform, DotChar, componentName)
323+
func buildComponentYqExpression(component string, includeAbstract bool, componentType string) string {
324+
path := fmt.Sprintf("%scomponents%s%s%s\"%s\"", DotChar, DotChar, componentType, DotChar, component)
212325

213-
// If not including abstract components, filter them out
214326
if !includeAbstract {
215-
// Only get component that either doesn't have abstract flag or has it set to false
216-
yqExpression += fmt.Sprintf(" | select(has(\"%s\") == false or %s%s == false)",
327+
path += fmt.Sprintf(" | select(has(\"%s\") == false or %s%s == false)",
217328
KeyAbstract, DotChar, KeyAbstract)
218329
}
219330

220-
// Get the vars
221-
yqExpression += fmt.Sprintf(" | %s%s", DotChar, KeyVars)
222-
223-
return yqExpression
331+
return path + fmt.Sprintf(" | %s%s", DotChar, KeyVars)
224332
}
225333

226-
// processQueryResult handles the query result based on component type.
227-
func processQueryResult(component string, queryResult interface{}) interface{} {
228-
// Process settings specially to handle nested settings key
229-
if component == KeySettings {
230-
if settings, ok := queryResult.(map[string]interface{}); ok {
231-
if settingsContent, ok := settings[KeySettings].(map[string]interface{}); ok {
232-
return settingsContent
233-
}
234-
}
334+
func extractRelevantDataFromQueryResult(component string, queryResult interface{}) interface{} {
335+
if component != KeySettings {
336+
return queryResult
235337
}
236338

237-
// Return the result as is for other components
238-
return queryResult
339+
settings, ok := queryResult.(map[string]interface{})
340+
if !ok {
341+
return queryResult
342+
}
343+
344+
settingsContent, ok := settings[KeySettings].(map[string]interface{})
345+
if !ok {
346+
return queryResult
347+
}
348+
349+
return settingsContent
239350
}
240351

241352
// applyFilters applies stack pattern and column limits to the values.
@@ -410,7 +521,7 @@ func processStackWithQuery(stackName string, stackData interface{}, query string
410521
return nil, false
411522
}
412523

413-
formattedResult := formatResultForDisplay(queryResult, query)
524+
formattedResult := formatResultForDisplay(queryResult)
414525
return formattedResult, formattedResult != nil
415526
}
416527

@@ -445,16 +556,10 @@ func applyQuery(filteredValues map[string]interface{}, query string, component s
445556
}
446557

447558
// formatResultForDisplay formats query results for display.
448-
func formatResultForDisplay(result interface{}, query string) interface{} {
449-
log.Debug("Formatting query result for display",
450-
"result_type", fmt.Sprintf(TypeFormatSpec, result),
451-
"query", query)
452-
559+
func formatResultForDisplay(result interface{}) interface{} {
453560
if result == nil {
454-
log.Debug("Skipping nil result")
455561
return nil
456562
}
457-
458563
return result
459564
}
460565

0 commit comments

Comments
 (0)