github.com/myhau/pulumi/pkg/v3@v3.70.2-0.20221116134521-f2775972e587/backend/httpstate/diffs.go (about)

     1  // Copyright 2016-2022, Pulumi Corporation.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package httpstate
    16  
    17  import (
    18  	"context"
    19  	"crypto/sha256"
    20  	"encoding/hex"
    21  	"encoding/json"
    22  	"fmt"
    23  	"sync"
    24  
    25  	"github.com/hexops/gotextdiff/myers"
    26  	"github.com/hexops/gotextdiff/span"
    27  
    28  	"github.com/pulumi/pulumi/sdk/v3/go/common/apitype"
    29  
    30  	opentracing "github.com/opentracing/opentracing-go"
    31  )
    32  
    33  type deploymentDiffState struct {
    34  	lastSavedDeployment json.RawMessage
    35  	sequenceNumber      int
    36  	noChecksums         bool
    37  	strictMode          bool
    38  	minimalDiffSize     int
    39  }
    40  
    41  type deploymentDiff struct {
    42  	sequenceNumber  int
    43  	checkpointHash  string
    44  	deploymentDelta json.RawMessage
    45  }
    46  
    47  func newDeploymentDiffState() *deploymentDiffState {
    48  	return &deploymentDiffState{sequenceNumber: 1}
    49  }
    50  
    51  func (dds *deploymentDiffState) SequenceNumber() int {
    52  	return dds.sequenceNumber
    53  }
    54  
    55  func (dds *deploymentDiffState) CanDiff() bool {
    56  	return dds.lastSavedDeployment != nil
    57  }
    58  
    59  // Size-based heuristics trying to estimate if the diff method will be
    60  // worth it and take less time than sending the entire deployment.
    61  func (dds *deploymentDiffState) ShouldDiff(new *apitype.UntypedDeployment) bool {
    62  	if !dds.CanDiff() {
    63  		return false
    64  	}
    65  	small := dds.minimalDiffSize
    66  	if small == 0 {
    67  		small = 1024 * 32
    68  	}
    69  	if len(dds.lastSavedDeployment) < small {
    70  		return false
    71  	}
    72  	if len(new.Deployment) < small {
    73  		return false
    74  	}
    75  	return true
    76  }
    77  
    78  func (dds *deploymentDiffState) Diff(ctx context.Context,
    79  	deployment *apitype.UntypedDeployment) (deploymentDiff, error) {
    80  
    81  	if !dds.CanDiff() {
    82  		return deploymentDiff{}, fmt.Errorf("Diff() cannot be called before Saved()")
    83  	}
    84  
    85  	if deployment.Version == 0 {
    86  		return deploymentDiff{}, fmt.Errorf("deployment.Version should be set")
    87  	}
    88  
    89  	tracingSpan, childCtx := opentracing.StartSpanFromContext(ctx, "Diff")
    90  	defer tracingSpan.Finish()
    91  
    92  	before := dds.lastSavedDeployment
    93  
    94  	after, err := marshalUntypedDeployment(deployment)
    95  	if err != nil {
    96  		return deploymentDiff{}, fmt.Errorf("marshalUntypedDeployment failed: %v", err)
    97  	}
    98  
    99  	var checkpointHash string
   100  	checkpointHashReady := &sync.WaitGroup{}
   101  
   102  	if !dds.noChecksums {
   103  		checkpointHashReady.Add(1)
   104  		go func() {
   105  			defer checkpointHashReady.Done()
   106  			checkpointHash = dds.computeHash(childCtx, after)
   107  		}()
   108  	}
   109  
   110  	delta, err := dds.computeEdits(childCtx, string(before), string(after))
   111  	if err != nil {
   112  		return deploymentDiff{}, fmt.Errorf("Cannot marshal the edits: %v", err)
   113  	}
   114  
   115  	checkpointHashReady.Wait()
   116  
   117  	tracingSpan.SetTag("before", len(before))
   118  	tracingSpan.SetTag("after", len(after))
   119  	tracingSpan.SetTag("diff", len(delta))
   120  	tracingSpan.SetTag("compression", 100.0*float64(len(delta))/float64(len(after)))
   121  	tracingSpan.SetTag("hash", checkpointHash)
   122  
   123  	diff := deploymentDiff{
   124  		checkpointHash:  checkpointHash,
   125  		deploymentDelta: delta,
   126  		sequenceNumber:  dds.sequenceNumber,
   127  	}
   128  
   129  	return diff, nil
   130  }
   131  
   132  // Indicates that a deployment was just saved to the service.
   133  func (dds *deploymentDiffState) Saved(ctx context.Context, deployment *apitype.UntypedDeployment) error {
   134  	d, err := marshalUntypedDeployment(deployment)
   135  	if err != nil {
   136  		return err
   137  	}
   138  	dds.lastSavedDeployment = d
   139  	dds.sequenceNumber++
   140  
   141  	return nil
   142  }
   143  
   144  func (*deploymentDiffState) computeHash(ctx context.Context, deployment json.RawMessage) string {
   145  	tracingSpan, _ := opentracing.StartSpanFromContext(ctx, "computeHash")
   146  	defer tracingSpan.Finish()
   147  	hash := sha256.Sum256(deployment)
   148  	return hex.EncodeToString(hash[:])
   149  }
   150  
   151  func (*deploymentDiffState) computeEdits(ctx context.Context, before, after string) (json.RawMessage, error) {
   152  	tracingSpan, _ := opentracing.StartSpanFromContext(ctx, "computeEdits")
   153  	defer tracingSpan.Finish()
   154  
   155  	edits := myers.ComputeEdits(span.URIFromURI(""), before, after)
   156  
   157  	delta, err := json.Marshal(edits)
   158  	if err != nil {
   159  		return nil, fmt.Errorf("Cannot marshal the edits: %v", err)
   160  	}
   161  
   162  	return delta, nil
   163  }