github.com/zntrio/harp/v2@v2.0.9/pkg/bundle/patch/package.go (about) 1 // Licensed to Elasticsearch B.V. under one or more contributor 2 // license agreements. See the NOTICE file distributed with 3 // this work for additional information regarding copyright 4 // ownership. Elasticsearch B.V. licenses this file to you under 5 // the Apache License, Version 2.0 (the "License"); you may 6 // not use this file except in compliance with the License. 7 // You may obtain a copy of the License at 8 // 9 // http://www.apache.org/licenses/LICENSE-2.0 10 // 11 // Unless required by applicable law or agreed to in writing, 12 // software distributed under the License is distributed on an 13 // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 // KIND, either express or implied. See the License for the 15 // specific language governing permissions and limitations 16 // under the License. 17 18 package patch 19 20 import ( 21 "context" 22 "encoding/base64" 23 "fmt" 24 "sort" 25 "strings" 26 27 "google.golang.org/protobuf/proto" 28 29 bundlev1 "github.com/zntrio/harp/v2/api/gen/go/harp/bundle/v1" 30 "github.com/zntrio/harp/v2/pkg/bundle" 31 "github.com/zntrio/harp/v2/pkg/sdk/types" 32 33 "golang.org/x/crypto/blake2b" 34 ) 35 36 // Validate bundle patch. 37 func Validate(spec *bundlev1.Patch) error { 38 // Check if spec is nil 39 if spec == nil { 40 return fmt.Errorf("unable to validate bundle patch: patch is nil") 41 } 42 43 if spec.ApiVersion != "harp.elastic.co/v1" { 44 return fmt.Errorf("apiVersion should be 'harp.elastic.co/v1'") 45 } 46 47 if spec.Kind != "BundlePatch" { 48 return fmt.Errorf("kind should be 'BundlePatch'") 49 } 50 51 if spec.Meta == nil { 52 return fmt.Errorf("meta should be 'nil'") 53 } 54 55 if spec.Spec == nil { 56 return fmt.Errorf("spec should be 'nil'") 57 } 58 59 // No error 60 return nil 61 } 62 63 // Checksum calculates the bundle patch checksum. 64 func Checksum(spec *bundlev1.Patch) (string, error) { 65 // Validate bundle template 66 if err := Validate(spec); err != nil { 67 return "", fmt.Errorf("unable to validate spec: %w", err) 68 } 69 70 // Encode spec as protobuf 71 payload, err := proto.Marshal(spec) 72 if err != nil { 73 return "", fmt.Errorf("unable to encode bundle patch: %w", err) 74 } 75 76 // Calculate checksum 77 checksum := blake2b.Sum256(payload) 78 79 // No error 80 return base64.RawURLEncoding.EncodeToString(checksum[:]), nil 81 } 82 83 // Apply given patch to the given bundle. 84 // 85 //nolint:interfacer,gocyclo,funlen // Explicit type restriction 86 func Apply(ctx context.Context, spec *bundlev1.Patch, b *bundlev1.Bundle, values map[string]interface{}, o ...OptionFunc) (*bundlev1.Bundle, error) { 87 // Validate spec 88 if err := Validate(spec); err != nil { 89 return nil, fmt.Errorf("unable to validate spec: %w", err) 90 } 91 if b == nil { 92 return nil, fmt.Errorf("cannot process nil bundle") 93 } 94 95 // Prepare selectors 96 if len(spec.Spec.Rules) == 0 { 97 return nil, fmt.Errorf("empty bundle patch") 98 } 99 100 // Copy bundle 101 bCopy, ok := proto.Clone(b).(*bundlev1.Bundle) 102 if !ok { 103 return nil, fmt.Errorf("the cloned bundle does not have the expected type: %T", bCopy) 104 } 105 if bCopy.Packages == nil { 106 bCopy.Packages = []*bundlev1.Package{} 107 } 108 109 // Default evaluation options 110 dopts := &options{ 111 stopAtRuleID: "", 112 stopAtRuleIndex: -1, 113 ignoreRuleIDs: []string{}, 114 ignoreRuleIndexes: []int{}, 115 } 116 117 // Apply functions 118 for _, opt := range o { 119 opt(dopts) 120 } 121 122 // Process all creation rule first 123 for i, r := range spec.Spec.Rules { 124 // Ignore nil rule 125 if r == nil { 126 continue 127 } 128 129 // Ignore non creation rules and non strict matcher 130 if !r.Package.Create || r.Selector.MatchPath.Strict == "" { 131 continue 132 } 133 if shouldIgnoreThisRule(i, r.Id, dopts) { 134 continue 135 } 136 if shouldStopAtThisRule(i, r.Id, dopts) { 137 break 138 } 139 140 // Create a package 141 p := &bundlev1.Package{ 142 Name: r.Selector.MatchPath.Strict, 143 } 144 145 _, err := executeRule(ctx, r, p, values) 146 if err != nil { 147 return nil, fmt.Errorf("unable to execute rule index %d: %w", i, err) 148 } 149 150 // Add created package 151 bCopy.Packages = append(bCopy.Packages, p) 152 } 153 154 for ri, r := range spec.Spec.Rules { 155 // Ignore nil rule 156 if r == nil { 157 continue 158 } 159 if shouldIgnoreThisRule(ri, r.Id, dopts) { 160 continue 161 } 162 if shouldStopAtThisRule(ri, r.Id, dopts) { 163 break 164 } 165 166 // Process all packages 167 for i, p := range bCopy.Packages { 168 action, err := executeRule(ctx, r, p, values) 169 if err != nil { 170 return nil, fmt.Errorf("unable to execute rule index %d: %w", ri, err) 171 } 172 173 switch action { 174 case packagedRemoved: 175 bCopy.Packages = append(bCopy.Packages[:i], bCopy.Packages[i+1:]...) 176 case packageUpdated: 177 if WithAnnotations(spec) { 178 // Add annotations to mark package as patched. 179 bundle.Annotate(p, "patched", "true") 180 bundle.Annotate(p, spec.Meta.Name, "true") 181 } 182 bCopy.Packages[i] = p 183 case packageUnchanged: 184 // No changes 185 default: 186 } 187 } 188 } 189 190 // Sort packages 191 sort.SliceStable(bCopy.Packages, func(i, j int) bool { 192 return bCopy.Packages[i].Name < bCopy.Packages[j].Name 193 }) 194 195 // No error 196 return bCopy, nil 197 } 198 199 func shouldStopAtThisRule(idx int, id string, opts *options) bool { 200 // Stop at index 201 if opts.stopAtRuleIndex > 0 && idx >= opts.stopAtRuleIndex { 202 return true 203 } 204 // Stop at rule id 205 if opts.stopAtRuleID != "" && strings.EqualFold(id, opts.stopAtRuleID) { 206 return true 207 } 208 209 return false 210 } 211 212 func shouldIgnoreThisRule(idx int, id string, opts *options) bool { 213 // Ignore using index 214 if len(opts.ignoreRuleIndexes) > 0 { 215 for _, v := range opts.ignoreRuleIndexes { 216 if v == idx { 217 return true 218 } 219 } 220 } 221 222 // Ignore using id 223 if len(opts.ignoreRuleIDs) > 0 { 224 return types.StringArray(opts.ignoreRuleIDs).Contains(id) 225 } 226 227 return false 228 }