pkg/bundle/patch/package.go (145 lines of code) (raw):

// Licensed to Elasticsearch B.V. under one or more contributor // license agreements. See the NOTICE file distributed with // this work for additional information regarding copyright // ownership. Elasticsearch B.V. licenses this file to you under // the Apache License, Version 2.0 (the "License"); you may // not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. package patch import ( "encoding/base64" "fmt" "sort" "strings" "google.golang.org/protobuf/proto" bundlev1 "github.com/elastic/harp/api/gen/go/harp/bundle/v1" "github.com/elastic/harp/pkg/bundle" "github.com/elastic/harp/pkg/sdk/types" "golang.org/x/crypto/blake2b" ) // Validate bundle patch. func Validate(spec *bundlev1.Patch) error { // Check if spec is nil if spec == nil { return fmt.Errorf("unable to validate bundle patch: patch is nil") } if spec.ApiVersion != "harp.elastic.co/v1" { return fmt.Errorf("apiVersion should be 'harp.elastic.co/v1'") } if spec.Kind != "BundlePatch" { return fmt.Errorf("kind should be 'BundlePatch'") } if spec.Meta == nil { return fmt.Errorf("meta should be 'nil'") } if spec.Spec == nil { return fmt.Errorf("spec should be 'nil'") } // No error return nil } // Checksum calculates the bundle patch checksum. func Checksum(spec *bundlev1.Patch) (string, error) { // Validate bundle template if err := Validate(spec); err != nil { return "", fmt.Errorf("unable to validate spec: %w", err) } // Encode spec as protobuf payload, err := proto.Marshal(spec) if err != nil { return "", fmt.Errorf("unable to encode bundle patch: %w", err) } // Calculate checksum checksum := blake2b.Sum256(payload) // No error return base64.RawURLEncoding.EncodeToString(checksum[:]), nil } // Apply given patch to the given bundle. // //nolint:interfacer,gocyclo,funlen // Explicit type restriction func Apply(spec *bundlev1.Patch, b *bundlev1.Bundle, values map[string]interface{}, o ...OptionFunc) (*bundlev1.Bundle, error) { // Validate spec if err := Validate(spec); err != nil { return nil, fmt.Errorf("unable to validate spec: %w", err) } if b == nil { return nil, fmt.Errorf("cannot process nil bundle") } // Prepare selectors if len(spec.Spec.Rules) == 0 { return nil, fmt.Errorf("empty bundle patch") } // Copy bundle bCopy, ok := proto.Clone(b).(*bundlev1.Bundle) if !ok { return nil, fmt.Errorf("the cloned bundle does not have the expected type: %T", bCopy) } if bCopy.Packages == nil { bCopy.Packages = []*bundlev1.Package{} } // Default evaluation options dopts := &options{ stopAtRuleID: "", stopAtRuleIndex: -1, ignoreRuleIDs: []string{}, ignoreRuleIndexes: []int{}, } // Apply functions for _, opt := range o { opt(dopts) } // Process all creation rule first for i, r := range spec.Spec.Rules { // Ignore nil rule if r == nil { continue } // Ignore non creation rules and non strict matcher if !r.Package.Create || r.Selector.MatchPath.Strict == "" { continue } if shouldIgnoreThisRule(i, r.Id, dopts) { continue } if shouldStopAtThisRule(i, r.Id, dopts) { break } // Create a package p := &bundlev1.Package{ Name: r.Selector.MatchPath.Strict, } _, err := executeRule(r, p, values) if err != nil { return nil, fmt.Errorf("unable to execute rule index %d: %w", i, err) } // Add created package bCopy.Packages = append(bCopy.Packages, p) } for ri, r := range spec.Spec.Rules { // Ignore nil rule if r == nil { continue } if shouldIgnoreThisRule(ri, r.Id, dopts) { continue } if shouldStopAtThisRule(ri, r.Id, dopts) { break } // Process all packages for i, p := range bCopy.Packages { action, err := executeRule(r, p, values) if err != nil { return nil, fmt.Errorf("unable to execute rule index %d: %w", ri, err) } switch action { case packagedRemoved: bCopy.Packages = append(bCopy.Packages[:i], bCopy.Packages[i+1:]...) case packageUpdated: if WithAnnotations(spec) { // Add annotations to mark package as patched. bundle.Annotate(p, "patched", "true") bundle.Annotate(p, spec.Meta.Name, "true") } bCopy.Packages[i] = p case packageUnchanged: // No changes default: } } } // Sort packages sort.SliceStable(bCopy.Packages, func(i, j int) bool { return bCopy.Packages[i].Name < bCopy.Packages[j].Name }) // No error return bCopy, nil } func shouldStopAtThisRule(idx int, id string, opts *options) bool { // Stop at index if opts.stopAtRuleIndex > 0 && idx >= opts.stopAtRuleIndex { return true } // Stop at rule id if opts.stopAtRuleID != "" && strings.EqualFold(id, opts.stopAtRuleID) { return true } return false } func shouldIgnoreThisRule(idx int, id string, opts *options) bool { // Ignore using index if len(opts.ignoreRuleIndexes) > 0 { for _, v := range opts.ignoreRuleIndexes { if v == idx { return true } } } // Ignore using id if len(opts.ignoreRuleIDs) > 0 { return types.StringArray(opts.ignoreRuleIDs).Contains(id) } return false }