github.com/myhau/pulumi/pkg/v3@v3.70.2-0.20221116134521-f2775972e587/backend/httpstate/snapshot_test.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  	"bytes"
    19  	"compress/gzip"
    20  	"context"
    21  	"crypto/sha256"
    22  	"encoding/json"
    23  	"fmt"
    24  	"io"
    25  	"io/ioutil"
    26  	"testing"
    27  
    28  	"net/http"
    29  	"net/http/httptest"
    30  
    31  	"github.com/hexops/gotextdiff"
    32  	"github.com/stretchr/testify/assert"
    33  
    34  	"github.com/pulumi/pulumi/sdk/v3/go/common/apitype"
    35  	"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
    36  
    37  	"github.com/pulumi/pulumi/pkg/v3/backend/httpstate/client"
    38  	"github.com/pulumi/pulumi/pkg/v3/resource/deploy"
    39  )
    40  
    41  // Check that cloudSnapshotPersister can talk the diff-based
    42  // "checkpointverbatim" and "checkpointdelta" protocol when saving
    43  // snapshots.
    44  func TestCloudSnapshotPersisterUseOfDiffProtocol(t *testing.T) {
    45  	t.Parallel()
    46  	ctx := context.Background()
    47  
    48  	stackID := client.StackIdentifier{
    49  		Owner:   "owner",
    50  		Project: "project",
    51  		Stack:   "stack",
    52  	}
    53  	updateID := "update-id"
    54  
    55  	var persistedState json.RawMessage
    56  
    57  	var lastRequest *http.Request
    58  
    59  	lastRequestAsVerbatim := func() (ret apitype.PatchUpdateVerbatimCheckpointRequest) {
    60  		err := json.NewDecoder(lastRequest.Body).Decode(&ret)
    61  		assert.Equal(t, "/api/stacks/owner/project/stack/update/update-id/checkpointverbatim", lastRequest.URL.Path)
    62  		assert.NoError(t, err)
    63  		return
    64  	}
    65  
    66  	lastRequestAsDelta := func() (ret apitype.PatchUpdateCheckpointDeltaRequest) {
    67  		err := json.NewDecoder(lastRequest.Body).Decode(&ret)
    68  		assert.Equal(t, "/api/stacks/owner/project/stack/update/update-id/checkpointdelta", lastRequest.URL.Path)
    69  		assert.NoError(t, err)
    70  		return
    71  	}
    72  
    73  	handleVerbatim := func(req apitype.PatchUpdateVerbatimCheckpointRequest) {
    74  		persistedState = req.UntypedDeployment
    75  	}
    76  
    77  	handleDelta := func(req apitype.PatchUpdateCheckpointDeltaRequest) {
    78  		edits := []gotextdiff.TextEdit{}
    79  		if err := json.Unmarshal(req.DeploymentDelta, &edits); err != nil {
    80  			assert.NoError(t, err)
    81  		}
    82  		persistedState = json.RawMessage([]byte(gotextdiff.ApplyEdits(string(persistedState), edits)))
    83  		assert.Equal(t, req.CheckpointHash, fmt.Sprintf("%x", sha256.Sum256(persistedState)))
    84  	}
    85  
    86  	typedPersistedState := func() apitype.DeploymentV3 {
    87  		var ud apitype.UntypedDeployment
    88  		err := json.Unmarshal(persistedState, &ud)
    89  		assert.NoError(t, err)
    90  		var d3 apitype.DeploymentV3
    91  		err = json.Unmarshal(ud.Deployment, &d3)
    92  		assert.NoError(t, err)
    93  		return d3
    94  	}
    95  
    96  	newMockServer := func() *httptest.Server {
    97  		return httptest.NewServer(
    98  			http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
    99  				lastRequest = req
   100  				rw.WriteHeader(200)
   101  				message := `{}`
   102  				reader, err := gzip.NewReader(req.Body)
   103  				assert.NoError(t, err)
   104  				defer reader.Close()
   105  				rbytes, err := ioutil.ReadAll(reader)
   106  				assert.NoError(t, err)
   107  				_, err = rw.Write([]byte(message))
   108  				assert.NoError(t, err)
   109  				req.Body = io.NopCloser(bytes.NewBuffer(rbytes))
   110  			}))
   111  	}
   112  
   113  	newMockTokenSource := func() tokenSourceCapability {
   114  		return tokenSourceFn(func() (string, error) {
   115  			return "token", nil
   116  		})
   117  	}
   118  
   119  	initPersister := func() *cloudSnapshotPersister {
   120  		server := newMockServer()
   121  		backendGeneric, err := New(nil, server.URL)
   122  		assert.NoError(t, err)
   123  		backend := backendGeneric.(*cloudBackend)
   124  		persister := backend.newSnapshotPersister(ctx, client.UpdateIdentifier{
   125  			StackIdentifier: stackID,
   126  			UpdateKind:      apitype.UpdateUpdate,
   127  			UpdateID:        updateID,
   128  		}, newMockTokenSource(), nil)
   129  		persister.deploymentDiffState = newDeploymentDiffState()
   130  		persister.deploymentDiffState.minimalDiffSize = 1
   131  		return persister
   132  	}
   133  
   134  	persister := initPersister()
   135  
   136  	// Req 1: the first request sends indented data verbatim to establish a good baseline state for further diffs.
   137  
   138  	err := persister.Save(&deploy.Snapshot{
   139  		Resources: []*resource.State{
   140  			{URN: resource.URN("urn-1")},
   141  		},
   142  	})
   143  	assert.NoError(t, err)
   144  
   145  	req1 := lastRequestAsVerbatim()
   146  	assert.Equal(t, 1, req1.SequenceNumber)
   147  	assert.Equal(t, 3, req1.Version)
   148  	assert.Equal(t, "{\"version\":3,\"deployment\":{\n\"manifest\": {\n\"time\": \"0001-01-01T00:00:00Z\""+
   149  		",\n\"magic\": \"\",\n\"version\": \"\"\n},\n\"resources\": [\n{\n\"urn\": \"urn-1\",\n\"custom\":"+
   150  		" false,\n\"type\": \"\"\n}\n]\n}}", string(req1.UntypedDeployment))
   151  
   152  	handleVerbatim(req1)
   153  	assert.Equal(t, []apitype.ResourceV3{
   154  		{URN: resource.URN("urn-1")},
   155  	}, typedPersistedState().Resources)
   156  
   157  	// Req 2: then it switches to sending deltas as text diffs together with SHA-256 checksum of the expected
   158  	// resulting text representation of state.
   159  
   160  	err = persister.Save(&deploy.Snapshot{
   161  		Resources: []*resource.State{
   162  			{URN: resource.URN("urn-1")},
   163  			{URN: resource.URN("urn-2")},
   164  		},
   165  	})
   166  	assert.NoError(t, err)
   167  
   168  	req2 := lastRequestAsDelta()
   169  	assert.Equal(t, 2, req2.SequenceNumber)
   170  	assert.Equal(t, "[{\"Span\":{\"uri\":\"\",\"start\":{\"line\":12,\"column\":1,\"offset\":-1},\"end\""+
   171  		":{\"line\":12,\"column\":1,\"offset\":-1}},\"NewText\":\"},\\n\"},{\"Span\":{\"uri\":\"\","+
   172  		"\"start\":{\"line\":12,\"column\":1,\"offset\":-1},\"end\":{\"line\":12,"+
   173  		"\"column\":1,\"offset\":-1}},\"NewText\":\"{\\n\"},{\"Span\":{\"uri\":\"\",\"start\":"+
   174  		"{\"line\":12,\"column\":1,\"offset\":-1},\"end\":{\"line\":12,\"column\":1,\"offset\":-1}}"+
   175  		",\"NewText\":\"\\\"urn\\\": \\\"urn-2\\\",\\n\"},{\"Span\":{\"uri\":\"\",\"start\":"+
   176  		"{\"line\":12,\"column\":1,\"offset\":-1},\"end\":{\"line\":12,\"column\":1,\"offset\":-1}}"+
   177  		",\"NewText\":\"\\\"custom\\\": false,\\n\"},{\"Span\":{\"uri\":\"\",\"start\":{\"line\":12,"+
   178  		"\"column\":1,\"offset\":-1},\"end\":{\"line\":12,\"column\":1,\"offset\":-1}},\"NewText\":\""+
   179  		"\\\"type\\\": \\\"\\\"\\n\"}]", string(req2.DeploymentDelta))
   180  	assert.Equal(t, "75e2f82ca2735650366fba27b53ec97f310abd457aca27266fb29c4377ee00e7", req2.CheckpointHash)
   181  
   182  	handleDelta(req2)
   183  	assert.Equal(t, []apitype.ResourceV3{
   184  		{URN: resource.URN("urn-1")},
   185  		{URN: resource.URN("urn-2")},
   186  	}, typedPersistedState().Resources)
   187  
   188  	// Req 3: and continues using the diff protocol.
   189  
   190  	err = persister.Save(&deploy.Snapshot{
   191  		Resources: []*resource.State{
   192  			{URN: resource.URN("urn-1")},
   193  		},
   194  	})
   195  	assert.NoError(t, err)
   196  
   197  	req3 := lastRequestAsDelta()
   198  	assert.Equal(t, 3, req3.SequenceNumber)
   199  	assert.Equal(t, "[{\"Span\":{\"uri\":\"\",\"start\":{\"line\":12,\"column\":1,\"offset\":-1"+
   200  		"},\"end\":{\"line\":13,\"column\":1,\"offset\":-1}},\"NewText\":\"\"},{\"Span\":{"+
   201  		"\"uri\":\"\",\"start\":{\"line\":13,\"column\":1,\"offset\":-1},\"end\":{\"line\":14,"+
   202  		"\"column\":1,\"offset\":-1}},\"NewText\":\"\"},{\"Span\":{\"uri\":\"\",\"start\":"+
   203  		"{\"line\":14,\"column\":1,\"offset\":-1},\"end\":{\"line\":15,\"column\":1,"+
   204  		"\"offset\":-1}},\"NewText\":\"\"},{\"Span\":{\"uri\":\"\",\"start\":{\"line\":15,"+
   205  		"\"column\":1,\"offset\":-1},\"end\":{\"line\":16,\"column\":1,\"offset\":-1}},"+
   206  		"\"NewText\":\"\"},{\"Span\":{\"uri\":\"\",\"start\":{\"line\":16,\"column\":1,"+
   207  		"\"offset\":-1},\"end\":{\"line\":17,\"column\":1,\"offset\":-1}},\"NewText\":\"\"}]",
   208  		string(req3.DeploymentDelta))
   209  	assert.Equal(t, "5f2fd84a225e7c1895528b4b9394607d6b39aefa4ee74f57e018dadcb16bf2e2", req3.CheckpointHash)
   210  
   211  	handleDelta(req3)
   212  	assert.Equal(t, []apitype.ResourceV3{
   213  		{URN: resource.URN("urn-1")},
   214  	}, typedPersistedState().Resources)
   215  }
   216  
   217  type tokenSourceFn func() (string, error)
   218  
   219  var _ tokenSourceCapability = tokenSourceFn(nil)
   220  
   221  func (tsf tokenSourceFn) GetToken() (string, error) {
   222  	return tsf()
   223  }