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  }