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 }