// Copyright 2016-2022, Pulumi Corporation. // // Licensed 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 httpstate import ( "bytes" "context" "crypto/sha256" "encoding/hex" "encoding/json" "errors" "fmt" opentracing "github.com/opentracing/opentracing-go" "github.com/pulumi/pulumi/sdk/v3/go/common/promise" ) type deploymentDiffState struct { lastSavedDeployment deployment sequenceNumber int minimalDiffSize int buffer *bytes.Buffer } type deploymentDiff struct { sequenceNumber int checkpointHash string deploymentDelta json.RawMessage } func newDeploymentDiffState(minimalDiffSize int) *deploymentDiffState { return &deploymentDiffState{ sequenceNumber: 1, minimalDiffSize: minimalDiffSize, } } func (dds *deploymentDiffState) SequenceNumber() int { return dds.sequenceNumber } func (dds *deploymentDiffState) CanDiff() bool { return dds.lastSavedDeployment.raw != nil } // Size-based heuristics trying to estimate if the diff method will be // worth it and take less time than sending the entire deployment. func (dds *deploymentDiffState) ShouldDiff(new deployment) bool { if !dds.CanDiff() { return false } if len(dds.lastSavedDeployment.raw) < dds.minimalDiffSize { return false } if len(new.raw) < dds.minimalDiffSize { return false } return true } func (dds *deploymentDiffState) Diff(ctx context.Context, deployment deployment) (deploymentDiff, error) { if !dds.CanDiff() { return deploymentDiff{}, errors.New("Diff() cannot be called before Saved()") } tracingSpan, childCtx := opentracing.StartSpanFromContext(ctx, "Diff") defer tracingSpan.Finish() before := dds.lastSavedDeployment.raw after := deployment.raw checkpointHashPromise := promise.Run(func() (string, error) { return dds.computeHash(childCtx, after), nil }) delta, err := dds.computeEdits(childCtx, dds.lastSavedDeployment, deployment) if err != nil { return deploymentDiff{}, fmt.Errorf("Cannot marshal the edits: %w", err) } checkpointHash, err := checkpointHashPromise.Result(ctx) if err != nil { return deploymentDiff{}, fmt.Errorf("Cannot compute the checkpoint hash: %w", err) } tracingSpan.SetTag("before", len(before)) tracingSpan.SetTag("after", len(after)) tracingSpan.SetTag("diff", len(delta)) tracingSpan.SetTag("compression", 100.0*float64(len(delta))/float64(len(after))) tracingSpan.SetTag("hash", checkpointHash) diff := deploymentDiff{ checkpointHash: checkpointHash, deploymentDelta: delta, sequenceNumber: dds.sequenceNumber, } return diff, nil } // Indicates that a deployment was just saved to the service. func (dds *deploymentDiffState) Saved(ctx context.Context, deployment deployment) error { if dds.lastSavedDeployment.buf != nil { dds.buffer = dds.lastSavedDeployment.buf dds.buffer.Reset() } dds.lastSavedDeployment = deployment dds.sequenceNumber++ return nil } func (*deploymentDiffState) computeHash(ctx context.Context, deployment json.RawMessage) string { tracingSpan, _ := opentracing.StartSpanFromContext(ctx, "computeHash") defer tracingSpan.Finish() hash := sha256.Sum256(deployment) return hex.EncodeToString(hash[:]) }