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 }