github.com/Racer159/jackal@v0.32.7-0.20240401174413-0bd2339e4f2e/src/pkg/packager/filters/deploy.go (about)

     1  // SPDX-License-Identifier: Apache-2.0
     2  // SPDX-FileCopyrightText: 2021-Present The Jackal Authors
     3  
     4  // Package filters contains core implementations of the ComponentFilterStrategy interface.
     5  package filters
     6  
     7  import (
     8  	"fmt"
     9  	"slices"
    10  	"strings"
    11  
    12  	"github.com/Racer159/jackal/src/pkg/interactive"
    13  	"github.com/Racer159/jackal/src/types"
    14  	"github.com/agnivade/levenshtein"
    15  	"github.com/defenseunicorns/pkg/helpers"
    16  )
    17  
    18  // ForDeploy creates a new deployment filter.
    19  func ForDeploy(optionalComponents string, isInteractive bool) ComponentFilterStrategy {
    20  	requested := helpers.StringToSlice(optionalComponents)
    21  
    22  	return &deploymentFilter{
    23  		requested,
    24  		isInteractive,
    25  	}
    26  }
    27  
    28  // deploymentFilter is the default filter for deployments.
    29  type deploymentFilter struct {
    30  	requestedComponents []string
    31  	isInteractive       bool
    32  }
    33  
    34  // Errors for the deployment filter.
    35  var (
    36  	ErrMultipleSameGroup    = fmt.Errorf("cannot specify multiple components from the same group")
    37  	ErrNoDefaultOrSelection = fmt.Errorf("no default or selected component found")
    38  	ErrNotFound             = fmt.Errorf("no compatible components found")
    39  	ErrSelectionCanceled    = fmt.Errorf("selection canceled")
    40  )
    41  
    42  // Apply applies the filter.
    43  func (f *deploymentFilter) Apply(pkg types.JackalPackage) ([]types.JackalComponent, error) {
    44  	var selectedComponents []types.JackalComponent
    45  	groupedComponents := map[string][]types.JackalComponent{}
    46  	orderedComponentGroups := []string{}
    47  
    48  	// Group the components by Name and Group while maintaining order
    49  	for _, component := range pkg.Components {
    50  		groupKey := component.Name
    51  		if component.DeprecatedGroup != "" {
    52  			groupKey = component.DeprecatedGroup
    53  		}
    54  
    55  		if !slices.Contains(orderedComponentGroups, groupKey) {
    56  			orderedComponentGroups = append(orderedComponentGroups, groupKey)
    57  		}
    58  
    59  		groupedComponents[groupKey] = append(groupedComponents[groupKey], component)
    60  	}
    61  
    62  	isPartial := len(f.requestedComponents) > 0 && f.requestedComponents[0] != ""
    63  
    64  	if isPartial {
    65  		matchedRequests := map[string]bool{}
    66  
    67  		// NOTE: This does not use forIncludedComponents as it takes group, default and required status into account.
    68  		for _, groupKey := range orderedComponentGroups {
    69  			var groupDefault *types.JackalComponent
    70  			var groupSelected *types.JackalComponent
    71  
    72  			for _, component := range groupedComponents[groupKey] {
    73  				// Ensure we have a local version of the component to point to (otherwise the pointer might change on us)
    74  				component := component
    75  
    76  				selectState, matchedRequest := includedOrExcluded(component.Name, f.requestedComponents)
    77  
    78  				if !isRequired(component) {
    79  					if selectState == excluded {
    80  						// If the component was explicitly excluded, record the match and continue
    81  						matchedRequests[matchedRequest] = true
    82  						continue
    83  					} else if selectState == unknown && component.Default && groupDefault == nil {
    84  						// If the component is default but not included or excluded, remember the default
    85  						groupDefault = &component
    86  					}
    87  				} else {
    88  					// Force the selectState to included for Required components
    89  					selectState = included
    90  				}
    91  
    92  				if selectState == included {
    93  					// If the component was explicitly included, record the match
    94  					matchedRequests[matchedRequest] = true
    95  
    96  					// Then check for already selected groups
    97  					if groupSelected != nil {
    98  						return nil, fmt.Errorf("%w: group: %s selected: %s, %s", ErrMultipleSameGroup, component.DeprecatedGroup, groupSelected.Name, component.Name)
    99  					}
   100  
   101  					// Then append to the final list
   102  					selectedComponents = append(selectedComponents, component)
   103  					groupSelected = &component
   104  				}
   105  			}
   106  
   107  			// If nothing was selected from a group, handle the default
   108  			if groupSelected == nil && groupDefault != nil {
   109  				selectedComponents = append(selectedComponents, *groupDefault)
   110  			} else if len(groupedComponents[groupKey]) > 1 && groupSelected == nil && groupDefault == nil {
   111  				// If no default component was found, give up
   112  				componentNames := []string{}
   113  				for _, component := range groupedComponents[groupKey] {
   114  					componentNames = append(componentNames, component.Name)
   115  				}
   116  				return nil, fmt.Errorf("%w: choose from %s", ErrNoDefaultOrSelection, strings.Join(componentNames, ", "))
   117  			}
   118  		}
   119  
   120  		// Check that we have matched against all requests
   121  		for _, requestedComponent := range f.requestedComponents {
   122  			if _, ok := matchedRequests[requestedComponent]; !ok {
   123  				closeEnough := []string{}
   124  				for _, c := range pkg.Components {
   125  					d := levenshtein.ComputeDistance(c.Name, requestedComponent)
   126  					if d <= 5 {
   127  						closeEnough = append(closeEnough, c.Name)
   128  					}
   129  				}
   130  				return nil, fmt.Errorf("%w: %s, suggestions (%s)", ErrNotFound, requestedComponent, strings.Join(closeEnough, ", "))
   131  			}
   132  		}
   133  	} else {
   134  		for _, groupKey := range orderedComponentGroups {
   135  			group := groupedComponents[groupKey]
   136  			if len(group) > 1 {
   137  				if f.isInteractive {
   138  					component, err := interactive.SelectChoiceGroup(group)
   139  					if err != nil {
   140  						return nil, fmt.Errorf("%w: %w", ErrSelectionCanceled, err)
   141  					}
   142  					selectedComponents = append(selectedComponents, component)
   143  				} else {
   144  					foundDefault := false
   145  					componentNames := []string{}
   146  					for _, component := range group {
   147  						// If the component is default, then use it
   148  						if component.Default {
   149  							selectedComponents = append(selectedComponents, component)
   150  							foundDefault = true
   151  							break
   152  						}
   153  						// Add each component name to the list
   154  						componentNames = append(componentNames, component.Name)
   155  					}
   156  					if !foundDefault {
   157  						// If no default component was found, give up
   158  						return nil, fmt.Errorf("%w: choose from %s", ErrNoDefaultOrSelection, strings.Join(componentNames, ", "))
   159  					}
   160  				}
   161  			} else {
   162  				component := groupedComponents[groupKey][0]
   163  
   164  				if isRequired(component) {
   165  					selectedComponents = append(selectedComponents, component)
   166  					continue
   167  				}
   168  
   169  				if f.isInteractive {
   170  					selected, err := interactive.SelectOptionalComponent(component)
   171  					if err != nil {
   172  						return nil, fmt.Errorf("%w: %w", ErrSelectionCanceled, err)
   173  					}
   174  					if selected {
   175  						selectedComponents = append(selectedComponents, component)
   176  						continue
   177  					}
   178  				}
   179  
   180  				if component.Default {
   181  					selectedComponents = append(selectedComponents, component)
   182  					continue
   183  				}
   184  			}
   185  		}
   186  	}
   187  
   188  	return selectedComponents, nil
   189  }