Skip to content

Fix: nested components for list pkg #1225

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
289 changes: 197 additions & 92 deletions pkg/list/list_values.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,19 @@

// Error variables for list_values package.
var (
ErrInvalidStackPattern = errors.New("invalid stack pattern")
ErrInvalidStackPattern = errors.New("invalid stack pattern")
ErrEmptyTargetComponentName = errors.New("target component name cannot be empty")
ErrComponentsSectionNotFound = errors.New("components section not found in stack")
ErrComponentNotFoundInSections = errors.New("component not found in terraform or helmfile sections")
ErrQueryFailed = errors.New("query execution failed")
)

// Component and section name constants.
const (
// KeyTerraform is the key for terraform components.
KeyTerraform = "terraform"
// KeyHelmfile is the key for helmfile components.
KeyHelmfile = "helmfile"
// KeySettings is the key for settings section.
KeySettings = "settings"
// KeyMetadata is the key for metadata section.
Expand Down Expand Up @@ -75,7 +81,7 @@
}

// Extract stack values
extractedValues, err := extractComponentValues(stacksMap, options.Component, options.ComponentFilter, options.IncludeAbstract)
extractedValues, err := extractComponentValuesFromAllStacks(stacksMap, options.Component, options.ComponentFilter, options.IncludeAbstract)
if err != nil {
return "", err
}
Expand Down Expand Up @@ -117,125 +123,230 @@
}
}

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

// Check if this is a regular component and use it as filter if no specific filter
isComponentSection := component != KeySettings && component != KeyMetadata
if isComponentSection && componentFilter == "" {
log.Debug("Using component as filter", KeyComponent, component)
componentFilter = component
component = ""
}

log.Debug("Building YQ expression", KeyComponent, component, "componentFilter", componentFilter)
component, filter = normalizeComponentAndFilterInputs(component, filter)

for stackName, stackData := range stacksMap {
stack, ok := stackData.(map[string]interface{})
for stackName, data := range stacks {
stackMap, ok := data.(map[string]interface{})
if !ok {
log.Debug("stack data is not a map", KeyStack, stackName)
continue
}

// Build and execute YQ expression
yqExpression := processComponentType(component, componentFilter, includeAbstract)
queryResult, err := utils.EvaluateYqExpression(nil, stack, yqExpression)
if err != nil || queryResult == nil {
log.Debug("no values found",
KeyStack, stackName, KeyComponent, component,
"componentFilter", componentFilter, "yq_expression", yqExpression,
"error", err)
continue
componentValue := extractComponentValueFromSingleStack(stackMap, stackName, component, filter, includeAbstract)
if componentValue != nil {
stackComponentValues[stackName] = componentValue
}
}

// Process the result based on component type
values[stackName] = processQueryResult(component, queryResult)
if len(stackComponentValues) == 0 {
return nil, createComponentError(component, filter)
}

if len(values) == 0 {
return nil, createComponentError(component, componentFilter)
return stackComponentValues, nil
}

func normalizeComponentAndFilterInputs(component, filter string) (string, string) {
isRegularComponent := component != KeySettings && component != KeyMetadata
if isRegularComponent && filter == "" {
log.Debug("Using component name as filter", KeyComponent, component)
return "", component
}
return component, filter
}

func extractComponentValueFromSingleStack(stackMap map[string]interface{}, stackName, component, filter string, includeAbstract bool) interface{} {
targetComponentName := determineTargetComponentName(component, filter)

return values, nil
componentType := detectComponentTypeInStack(stackMap, targetComponentName, stackName)
if componentType == "" {
return nil
}

params := &QueryParams{
StackName: stackName,
StackMap: stackMap,
Component: component,
ComponentFilter: filter,
TargetComponentName: targetComponentName,
ComponentType: componentType,
IncludeAbstract: includeAbstract,
}

value, err := executeQueryForStack(params)
if err != nil {
log.Warn("Query failed", KeyStack, stackName, "error", err)
return nil
}

Check warning on line 181 in pkg/list/list_values.go

View check run for this annotation

Codecov / codecov/patch

pkg/list/list_values.go#L179-L181

Added lines #L179 - L181 were not covered by tests

return value
}

// processComponentType determines the YQ expression based on component type.
func processComponentType(component string, componentFilter string, includeAbstract bool) string {
// If this is a regular component query with a specific component filter
if component == "" && componentFilter != "" {
// Extract component name from path
componentName := getComponentNameFromPath(componentFilter)
func detectComponentTypeInStack(stackMap map[string]interface{}, targetComponent, stackName string) string {
if targetComponent == "" {
return KeyTerraform
}

detectedType, err := determineComponentType(stackMap, targetComponent)
if err != nil {
log.Debug("Component not found", KeyStack, stackName, KeyComponent, targetComponent)
return ""
}

return detectedType
}

// QueryParams holds all parameters needed for executing a query on a stack.
type QueryParams struct {
StackName string
StackMap map[string]interface{}
Component string
ComponentFilter string
TargetComponentName string
ComponentType string
IncludeAbstract bool
}

func executeQueryForStack(params *QueryParams) (interface{}, error) {
yqExpression := buildYqExpressionForComponent(
params.Component,
params.ComponentFilter,
params.IncludeAbstract,
params.ComponentType,
)

queryResult, err := utils.EvaluateYqExpression(nil, params.StackMap, yqExpression)
if err != nil {
var logKey string
var logValue string
if params.TargetComponentName != "" {
logKey = KeyComponent
logValue = params.TargetComponentName
} else {
logKey = "section"
logValue = params.Component
}

Check warning on line 229 in pkg/list/list_values.go

View check run for this annotation

Codecov / codecov/patch

pkg/list/list_values.go#L221-L229

Added lines #L221 - L229 were not covered by tests

log.Warn("YQ evaluation failed",
KeyStack, params.StackName,
"yqExpression", yqExpression,
logKey, logValue,
"error", err)
return nil, fmt.Errorf("%w: %s", ErrQueryFailed, err.Error())

Check warning on line 236 in pkg/list/list_values.go

View check run for this annotation

Codecov / codecov/patch

pkg/list/list_values.go#L231-L236

Added lines #L231 - L236 were not covered by tests
}

if queryResult == nil {
return nil, nil
}

Check warning on line 241 in pkg/list/list_values.go

View check run for this annotation

Codecov / codecov/patch

pkg/list/list_values.go#L240-L241

Added lines #L240 - L241 were not covered by tests

return extractRelevantDataFromQueryResult(params.Component, queryResult), nil
}

// Return a direct path to the component.
return fmt.Sprintf(".components.%s.%s", KeyTerraform, componentName)
func determineTargetComponentName(component, componentFilter string) string {
if componentFilter != "" {
return componentFilter
}

isRegularComponent := component != KeySettings && component != KeyMetadata
if isRegularComponent {
return component
}

Check warning on line 254 in pkg/list/list_values.go

View check run for this annotation

Codecov / codecov/patch

pkg/list/list_values.go#L253-L254

Added lines #L253 - L254 were not covered by tests

return ""
}

func determineComponentType(stack map[string]interface{}, targetComponentName string) (string, error) {
if targetComponentName == "" {
return "", ErrEmptyTargetComponentName
}

Check warning on line 262 in pkg/list/list_values.go

View check run for this annotation

Codecov / codecov/patch

pkg/list/list_values.go#L261-L262

Added lines #L261 - L262 were not covered by tests

components, ok := stack[KeyComponents].(map[string]interface{})
if !ok {
return "", ErrComponentsSectionNotFound
}

Check warning on line 267 in pkg/list/list_values.go

View check run for this annotation

Codecov / codecov/patch

pkg/list/list_values.go#L266-L267

Added lines #L266 - L267 were not covered by tests

if isComponentInSection(components, KeyTerraform, targetComponentName) {
log.Debug("Component found under terraform", KeyComponent, targetComponentName)
return KeyTerraform, nil
}

if isComponentInSection(components, KeyHelmfile, targetComponentName) {
log.Debug("Component found under helmfile", KeyComponent, targetComponentName)
return KeyHelmfile, nil
}

Check warning on line 277 in pkg/list/list_values.go

View check run for this annotation

Codecov / codecov/patch

pkg/list/list_values.go#L275-L277

Added lines #L275 - L277 were not covered by tests

return "", fmt.Errorf("%w: %s", ErrComponentNotFoundInSections, targetComponentName)
}

func isComponentInSection(components map[string]interface{}, sectionKey, componentName string) bool {
section, ok := components[sectionKey].(map[string]interface{})
if !ok {
return false
}
_, exists := section[componentName]
return exists
}

func buildYqExpressionForComponent(component string, componentFilter string, includeAbstract bool, componentType string) string {
if component == "" && componentFilter != "" {
return fmt.Sprintf(".components.%s.\"%s\"", componentType, componentFilter)
}

// Handle special section queries.
switch component {
case KeySettings:
if componentFilter != "" {
componentName := getComponentNameFromPath(componentFilter)
return fmt.Sprintf(".components.%s.%s", KeyTerraform, componentName)
}
return "select(.settings // .terraform.settings // .components.terraform.*.settings)"
return buildSettingsExpression(componentFilter, componentType)
case KeyMetadata:
if componentFilter != "" {
// For metadata with component filter, target the specific component.
componentName := getComponentNameFromPath(componentFilter)
return fmt.Sprintf(".components.%s.%s", KeyTerraform, componentName)
}
// For general metadata query.
return DotChar + KeyMetadata
return buildMetadataExpression(componentFilter, componentType)
default:
// Extract component name from path.
componentName := getComponentNameFromPath(component)
return buildComponentYqExpression(component, includeAbstract, componentType)

Check warning on line 302 in pkg/list/list_values.go

View check run for this annotation

Codecov / codecov/patch

pkg/list/list_values.go#L302

Added line #L302 was not covered by tests
}
}

// Build query for component vars.
return buildComponentYqExpression(componentName, includeAbstract)
func buildSettingsExpression(componentFilter, componentType string) string {
if componentFilter != "" {
return fmt.Sprintf(".components.%s.\"%s\"", componentType, componentFilter)

Check warning on line 308 in pkg/list/list_values.go

View check run for this annotation

Codecov / codecov/patch

pkg/list/list_values.go#L308

Added line #L308 was not covered by tests
}
return "select(.settings // " +
".components." + KeyTerraform + ".*.settings // " +
".components." + KeyHelmfile + ".*.settings)"
}

// getComponentNameFromPath extracts the component name from a potentially nested path.
func getComponentNameFromPath(component string) string {
parts := strings.Split(component, "/")
if len(parts) > 1 {
return parts[len(parts)-1]
func buildMetadataExpression(componentFilter, componentType string) string {
if componentFilter != "" {
// Use full component path and wrap in quotes for nested support
return fmt.Sprintf(".components.%s.\"%s\"", componentType, componentFilter)

Check warning on line 318 in pkg/list/list_values.go

View check run for this annotation

Codecov / codecov/patch

pkg/list/list_values.go#L317-L318

Added lines #L317 - L318 were not covered by tests
}
return component
return DotChar + KeyMetadata
}

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

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

// Get the vars
yqExpression += fmt.Sprintf(" | %s%s", DotChar, KeyVars)

return yqExpression
return path + fmt.Sprintf(" | %s%s", DotChar, KeyVars)
}

// processQueryResult handles the query result based on component type.
func processQueryResult(component string, queryResult interface{}) interface{} {
// Process settings specially to handle nested settings key
if component == KeySettings {
if settings, ok := queryResult.(map[string]interface{}); ok {
if settingsContent, ok := settings[KeySettings].(map[string]interface{}); ok {
return settingsContent
}
}
func extractRelevantDataFromQueryResult(component string, queryResult interface{}) interface{} {
if component != KeySettings {
return queryResult
}

// Return the result as is for other components
return queryResult
settings, ok := queryResult.(map[string]interface{})
if !ok {
return queryResult
}

Check warning on line 342 in pkg/list/list_values.go

View check run for this annotation

Codecov / codecov/patch

pkg/list/list_values.go#L341-L342

Added lines #L341 - L342 were not covered by tests

settingsContent, ok := settings[KeySettings].(map[string]interface{})
if !ok {
return queryResult
}

Check warning on line 347 in pkg/list/list_values.go

View check run for this annotation

Codecov / codecov/patch

pkg/list/list_values.go#L346-L347

Added lines #L346 - L347 were not covered by tests

return settingsContent
}

// applyFilters applies stack pattern and column limits to the values.
Expand Down Expand Up @@ -410,7 +521,7 @@
return nil, false
}

formattedResult := formatResultForDisplay(queryResult, query)
formattedResult := formatResultForDisplay(queryResult)
return formattedResult, formattedResult != nil
}

Expand Down Expand Up @@ -445,16 +556,10 @@
}

// formatResultForDisplay formats query results for display.
func formatResultForDisplay(result interface{}, query string) interface{} {
log.Debug("Formatting query result for display",
"result_type", fmt.Sprintf(TypeFormatSpec, result),
"query", query)

func formatResultForDisplay(result interface{}) interface{} {
if result == nil {
log.Debug("Skipping nil result")
return nil
}

return result
}

Expand Down
Loading