From 2461748c79f479cae55feb6a081f078b435b18c3 Mon Sep 17 00:00:00 2001 From: Anton Tayanovskyy Date: Mon, 21 Nov 2022 11:26:09 -0500 Subject: [PATCH 01/18] Add GetCapabilities func to client --- pkg/backend/httpstate/client/api_endpoints.go | 2 ++ pkg/backend/httpstate/client/client.go | 26 ++++++++++++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/pkg/backend/httpstate/client/api_endpoints.go b/pkg/backend/httpstate/client/api_endpoints.go index 718ca9723ec1..ebfc88eb0dc2 100644 --- a/pkg/backend/httpstate/client/api_endpoints.go +++ b/pkg/backend/httpstate/client/api_endpoints.go @@ -78,6 +78,8 @@ func init() { routes.Path(path).Methods(method).Name(name) } + addEndpoint("GET", "/api/capabilities", "getCapabilities") + addEndpoint("GET", "/api/user", "getCurrentUser") addEndpoint("GET", "/api/user/stacks", "listUserStacks") addEndpoint("GET", "/api/stacks/{orgName}", "listOrganizationStacks") diff --git a/pkg/backend/httpstate/client/client.go b/pkg/backend/httpstate/client/client.go index eaeb483a1450..f47a382d34a8 100644 --- a/pkg/backend/httpstate/client/client.go +++ b/pkg/backend/httpstate/client/client.go @@ -321,7 +321,7 @@ func (pc *Client) GetLatestConfiguration(ctx context.Context, stackID StackIdent func (pc *Client) DoesProjectExist(ctx context.Context, owner string, projectName string) (bool, error) { if err := pc.restCall(ctx, "HEAD", getProjectPath(owner, projectName), nil, nil, nil); err != nil { // If this was a 404, return false - project not found. - if errResp, ok := err.(*apitype.ErrorResponse); ok && errResp.Code == http.StatusNotFound { + if is404(err) { return false, nil } @@ -1070,3 +1070,27 @@ func (pc *Client) GetDeploymentUpdates(ctx context.Context, stack StackIdentifie } return resp, nil } + +func (pc *Client) GetCapabilities(ctx context.Context) (*apitype.CapabilitiesResponse, error) { + var resp apitype.CapabilitiesResponse + err := pc.restCall(ctx, http.MethodGet, "/api/capabilities", nil, nil, &resp) + if is404(err) { + // The client continues to support legacy backends. They do not support /api/capabilities and are + // assumed here to have no additional capabilities. + return &apitype.CapabilitiesResponse{}, nil + } + if err != nil { + return nil, fmt.Errorf("querying capabilities failed: %w", err) + } + return &resp, nil +} + +func is404(err error) bool { + if err == nil { + return false + } + if errResp, ok := err.(*apitype.ErrorResponse); ok && errResp.Code == http.StatusNotFound { + return true + } + return false +} From 477995101e4dab6e5c60b9adb03d4ed75ca418e5 Mon Sep 17 00:00:00 2001 From: Anton Tayanovskyy Date: Mon, 21 Nov 2022 12:22:37 -0500 Subject: [PATCH 02/18] Add client unit test --- pkg/backend/httpstate/client/client_test.go | 40 +++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/pkg/backend/httpstate/client/client_test.go b/pkg/backend/httpstate/client/client_test.go index 6e6d95018032..4fd2b6bfbcff 100644 --- a/pkg/backend/httpstate/client/client_test.go +++ b/pkg/backend/httpstate/client/client_test.go @@ -179,3 +179,43 @@ func TestPatchUpdateCheckpointVerbatimPreservesIndent(t *testing.T) { assert.Equal(t, string(indented), string(request.UntypedDeployment)) } + +func TestGetCapabilities(t *testing.T) { + t.Run("legacy-service-404", func(t *testing.T) { + s := newMockServer(404, "NOT FOUND") + defer s.Close() + + c := newMockClient(s) + resp, err := c.GetCapabilities(context.Background()) + assert.NoError(t, err) + assert.NotNil(t, resp) + assert.Empty(t, resp.Capabilities) + }) + t.Run("updated-service-with-delta-checkpoint-capability", func(t *testing.T) { + cfg := apitype.DeltaCheckpointUploadsConfigV1{ + CheckpointCutoffSizeBytes: 1024 * 1024 * 4, + } + cfgJSON, err := json.Marshal(cfg) + require.NoError(t, err) + actualResp := apitype.CapabilitiesResponse{ + Capabilities: []apitype.APICapabilityConfig{{ + Version: 3, + Capability: apitype.DeltaCheckpointUploads, + Configuration: json.RawMessage(cfgJSON), + }}, + } + respJSON, err := json.Marshal(actualResp) + require.NoError(t, err) + s := newMockServer(200, string(respJSON)) + defer s.Close() + + c := newMockClient(s) + resp, err := c.GetCapabilities(context.Background()) + assert.NoError(t, err) + assert.NotNil(t, resp) + assert.Len(t, resp.Capabilities, 1) + assert.Equal(t, apitype.DeltaCheckpointUploads, resp.Capabilities[0].Capability) + assert.Equal(t, `{"checkpointCutoffSizeBytes":4194304}`, + string(resp.Capabilities[0].Configuration)) + }) +} From ccfd415d0d116ed8b6b7649760582fd52fc08c9b Mon Sep 17 00:00:00 2001 From: Anton Tayanovskyy Date: Mon, 21 Nov 2022 13:30:00 -0500 Subject: [PATCH 03/18] Add capability detector in httpstate backend --- pkg/backend/httpstate/backend.go | 66 +++++++++++++++++++++++++++++++- 1 file changed, 65 insertions(+), 1 deletion(-) diff --git a/pkg/backend/httpstate/backend.go b/pkg/backend/httpstate/backend.go index b3a80586431b..86b9289ed7ba 100644 --- a/pkg/backend/httpstate/backend.go +++ b/pkg/backend/httpstate/backend.go @@ -18,6 +18,7 @@ import ( "context" cryptorand "crypto/rand" "encoding/hex" + "encoding/json" "errors" "fmt" "io" @@ -119,6 +120,7 @@ type cloudBackend struct { url string client *client.Client currentProject *workspace.Project + capabilities func() capabilities } // Assert we implement the backend.Backend and backend.SpecificDeploymentExporter interfaces. @@ -139,11 +141,17 @@ func New(d diag.Sink, cloudURL string) (Backend, error) { currentProject = nil } + client := client.NewClient(cloudURL, apiToken, d) + + // Start a background request to detect capabilities. + getCaps := detectCapabilities(context.Background(), d, client) + return &cloudBackend{ d: d, url: cloudURL, - client: client.NewClient(cloudURL, apiToken, d), + client: client, currentProject: currentProject, + capabilities: getCaps, }, nil } @@ -1240,6 +1248,7 @@ func (b *cloudBackend) GetHistory( if err != nil { return nil, fmt.Errorf("failed to get stack updates: %w", err) } + // // Convert apitype.UpdateInfo objects to the backend type. var beUpdates []backend.UpdateInfo @@ -1753,3 +1762,58 @@ func (c httpstateBackendClient) GetStackResourceOutputs( ctx context.Context, name string) (resource.PropertyMap, error) { return backend.NewBackendClient(c.backend).GetStackResourceOutputs(ctx, name) } + +// Represents feature-detected capabilities of the service the backend is connected to. +type capabilities struct { + // If non-nil, indicates that delta checkpoint updates are supported. + deltaCheckpointUpdates *apitype.DeltaCheckpointUploadsConfigV1 +} + +// Builds a cached wrapper around doDetectCapabilities that starts the request in the background. +func detectCapabilities(ctx context.Context, d diag.Sink, client *client.Client) func() capabilities { + var caps capabilities + done := make(chan struct{}) + fetch := func() { + caps = doDetectCapabilities(ctx, d, client) + close(done) + } + get := func() capabilities { + <-done + return caps + } + go fetch() + return get +} + +func doDetectCapabilities(ctx context.Context, d diag.Sink, client *client.Client) capabilities { + resp, err := client.GetCapabilities(ctx) + if err != nil { + d.Warningf(diag.Message("" /*urn*/, "failed to get capabilities: %v"), err) + return capabilities{} + } + caps, err := decodeCapabilities(resp.Capabilities) + if err != nil { + d.Warningf(diag.Message("" /*urn*/, "failed to decode capabilities: %v"), err) + return capabilities{} + } + return caps +} + +func decodeCapabilities(wireLevel []apitype.APICapabilityConfig) (capabilities, error) { + var parsed capabilities + for _, entry := range wireLevel { + switch entry.Capability { + case apitype.DeltaCheckpointUploads: + // TODO should check entry.Version here? + var cap apitype.DeltaCheckpointUploadsConfigV1 + if err := json.Unmarshal(entry.Configuration, &cap); err != nil { + msg := "decoding DeltaCheckpointUploadsConfigV1 returned %w" + return capabilities{}, fmt.Errorf(msg, err) + } + parsed.deltaCheckpointUpdates = &cap + default: + continue + } + } + return parsed, nil +} From 401c58dd80dd61d2462307d75f1ac7198ecaecb0 Mon Sep 17 00:00:00 2001 From: Anton Tayanovskyy Date: Mon, 21 Nov 2022 13:41:14 -0500 Subject: [PATCH 04/18] Opt into delta PATCH updates based on capability API --- pkg/backend/httpstate/diffs.go | 29 ++++++++++++----------------- pkg/backend/httpstate/snapshot.go | 20 ++++---------------- 2 files changed, 16 insertions(+), 33 deletions(-) diff --git a/pkg/backend/httpstate/diffs.go b/pkg/backend/httpstate/diffs.go index af13601b0095..19a40ca6af8f 100644 --- a/pkg/backend/httpstate/diffs.go +++ b/pkg/backend/httpstate/diffs.go @@ -33,8 +33,6 @@ import ( type deploymentDiffState struct { lastSavedDeployment json.RawMessage sequenceNumber int - noChecksums bool - strictMode bool minimalDiffSize int } @@ -44,8 +42,11 @@ type deploymentDiff struct { deploymentDelta json.RawMessage } -func newDeploymentDiffState() *deploymentDiffState { - return &deploymentDiffState{sequenceNumber: 1} +func newDeploymentDiffState(minimalDiffSize int) *deploymentDiffState { + return &deploymentDiffState{ + sequenceNumber: 1, + minimalDiffSize: minimalDiffSize, + } } func (dds *deploymentDiffState) SequenceNumber() int { @@ -62,14 +63,10 @@ func (dds *deploymentDiffState) ShouldDiff(new *apitype.UntypedDeployment) bool if !dds.CanDiff() { return false } - small := dds.minimalDiffSize - if small == 0 { - small = 1024 * 32 - } - if len(dds.lastSavedDeployment) < small { + if len(dds.lastSavedDeployment) < dds.minimalDiffSize { return false } - if len(new.Deployment) < small { + if len(new.Deployment) < dds.minimalDiffSize { return false } return true @@ -99,13 +96,11 @@ func (dds *deploymentDiffState) Diff(ctx context.Context, var checkpointHash string checkpointHashReady := &sync.WaitGroup{} - if !dds.noChecksums { - checkpointHashReady.Add(1) - go func() { - defer checkpointHashReady.Done() - checkpointHash = dds.computeHash(childCtx, after) - }() - } + checkpointHashReady.Add(1) + go func() { + defer checkpointHashReady.Done() + checkpointHash = dds.computeHash(childCtx, after) + }() delta, err := dds.computeEdits(childCtx, string(before), string(after)) if err != nil { diff --git a/pkg/backend/httpstate/snapshot.go b/pkg/backend/httpstate/snapshot.go index 3892391e5c11..e612a05ff3f5 100644 --- a/pkg/backend/httpstate/snapshot.go +++ b/pkg/backend/httpstate/snapshot.go @@ -18,7 +18,6 @@ import ( "context" "encoding/json" "fmt" - "os" "github.com/pulumi/pulumi/pkg/v3/backend" "github.com/pulumi/pulumi/pkg/v3/backend/httpstate/client" @@ -26,7 +25,6 @@ import ( "github.com/pulumi/pulumi/pkg/v3/resource/stack" "github.com/pulumi/pulumi/pkg/v3/secrets" "github.com/pulumi/pulumi/sdk/v3/go/common/apitype" - "github.com/pulumi/pulumi/sdk/v3/go/common/util/cmdutil" "github.com/pulumi/pulumi/sdk/v3/go/common/util/logging" ) @@ -82,9 +80,6 @@ func (persister *cloudSnapshotPersister) Save(snapshot *deploy.Snapshot) error { return err } if err := persister.saveDiff(ctx, diff, token); err != nil { - if differ.strictMode { - return err - } if logging.V(3) { logging.V(3).Infof("ignoring error saving checkpoint "+ "with PatchUpdateCheckpointDelta, falling back to "+ @@ -139,17 +134,10 @@ func (cb *cloudBackend) newSnapshotPersister(ctx context.Context, update client. sm: sm, } - if cmdutil.IsTruthy(os.Getenv("PULUMI_OPTIMIZED_CHECKPOINT_PATCH")) { - p.deploymentDiffState = newDeploymentDiffState() - - if cmdutil.IsTruthy(os.Getenv("PULUMI_OPTIMIZED_CHECKPOINT_PATCH_STRICT")) { - p.deploymentDiffState.strictMode = true - } - - if cmdutil.IsTruthy(os.Getenv("PULUMI_OPTIMIZED_CHECKPOINT_PATCH_NO_CHECKSUMS")) { - p.deploymentDiffState.noChecksums = true - } + caps := cb.capabilities() + deltaCaps := caps.deltaCheckpointUpdates + if deltaCaps != nil { + p.deploymentDiffState = newDeploymentDiffState(deltaCaps.CheckpointCutoffSizeBytes) } - return p } From 10cda391299c0c261d66314d35d46185a4f9430a Mon Sep 17 00:00:00 2001 From: Anton Tayanovskyy Date: Mon, 21 Nov 2022 16:04:43 -0500 Subject: [PATCH 05/18] Detect capabilities in parallel with GetStack --- pkg/backend/httpstate/backend.go | 29 ++++++++++++++++------------- pkg/backend/httpstate/snapshot.go | 2 +- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/pkg/backend/httpstate/backend.go b/pkg/backend/httpstate/backend.go index 86b9289ed7ba..06c8c5774a24 100644 --- a/pkg/backend/httpstate/backend.go +++ b/pkg/backend/httpstate/backend.go @@ -30,6 +30,7 @@ import ( "regexp" "strconv" "strings" + "sync" "time" opentracing "github.com/opentracing/opentracing-go" @@ -120,7 +121,7 @@ type cloudBackend struct { url string client *client.Client currentProject *workspace.Project - capabilities func() capabilities + capabilities func(context.Context) capabilities } // Assert we implement the backend.Backend and backend.SpecificDeploymentExporter interfaces. @@ -142,16 +143,14 @@ func New(d diag.Sink, cloudURL string) (Backend, error) { } client := client.NewClient(cloudURL, apiToken, d) - - // Start a background request to detect capabilities. - getCaps := detectCapabilities(context.Background(), d, client) + capabilities := detectCapabilities(d, client) return &cloudBackend{ d: d, url: cloudURL, client: client, currentProject: currentProject, - capabilities: getCaps, + capabilities: capabilities, }, nil } @@ -775,6 +774,10 @@ func (b *cloudBackend) GetStack(ctx context.Context, stackRef backend.StackRefer return nil, err } + // GetStack is typically the initial call to a series of calls to the backend. Although logically unrelated, + // this is a good time to start detecting capabilities so that capability request is not on the critical path. + go b.capabilities(ctx) + stack, err := b.client.GetStack(ctx, stackID) if err != nil { // If this was a 404, return nil, nil as per this method's contract. @@ -1769,19 +1772,19 @@ type capabilities struct { deltaCheckpointUpdates *apitype.DeltaCheckpointUploadsConfigV1 } -// Builds a cached wrapper around doDetectCapabilities that starts the request in the background. -func detectCapabilities(ctx context.Context, d diag.Sink, client *client.Client) func() capabilities { +// Builds a lazy wrapper around doDetectCapabilities. +func detectCapabilities(d diag.Sink, client *client.Client) func(ctx context.Context) capabilities { + var once sync.Once var caps capabilities done := make(chan struct{}) - fetch := func() { - caps = doDetectCapabilities(ctx, d, client) - close(done) - } - get := func() capabilities { + get := func(ctx context.Context) capabilities { + once.Do(func() { + caps = doDetectCapabilities(ctx, d, client) + close(done) + }) <-done return caps } - go fetch() return get } diff --git a/pkg/backend/httpstate/snapshot.go b/pkg/backend/httpstate/snapshot.go index e612a05ff3f5..a2801a4216b9 100644 --- a/pkg/backend/httpstate/snapshot.go +++ b/pkg/backend/httpstate/snapshot.go @@ -134,7 +134,7 @@ func (cb *cloudBackend) newSnapshotPersister(ctx context.Context, update client. sm: sm, } - caps := cb.capabilities() + caps := cb.capabilities(ctx) deltaCaps := caps.deltaCheckpointUpdates if deltaCaps != nil { p.deploymentDiffState = newDeploymentDiffState(deltaCaps.CheckpointCutoffSizeBytes) From af84e8492f1a0064315a123a15202502eeee5213 Mon Sep 17 00:00:00 2001 From: Anton Tayanovskyy Date: Mon, 21 Nov 2022 16:19:09 -0500 Subject: [PATCH 06/18] Fix test compilation --- pkg/backend/httpstate/snapshot_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/backend/httpstate/snapshot_test.go b/pkg/backend/httpstate/snapshot_test.go index 0e57e5fd7f1c..596099e2d9a3 100644 --- a/pkg/backend/httpstate/snapshot_test.go +++ b/pkg/backend/httpstate/snapshot_test.go @@ -126,7 +126,7 @@ func TestCloudSnapshotPersisterUseOfDiffProtocol(t *testing.T) { UpdateKind: apitype.UpdateUpdate, UpdateID: updateID, }, newMockTokenSource(), nil) - persister.deploymentDiffState = newDeploymentDiffState() + persister.deploymentDiffState = newDeploymentDiffState(1) persister.deploymentDiffState.minimalDiffSize = 1 return persister } From a0d2a5dbd05d227b66d1afd7fb036c9639ce0cbd Mon Sep 17 00:00:00 2001 From: Anton Tayanovskyy Date: Mon, 21 Nov 2022 16:34:03 -0500 Subject: [PATCH 07/18] Add CHANGELOG --- ...service-to-opt-into-bandwidth-optimized-diff-protocol.yaml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelog/pending/20221121--backend-service--allows-the-service-to-opt-into-bandwidth-optimized-diff-protocol.yaml diff --git a/changelog/pending/20221121--backend-service--allows-the-service-to-opt-into-bandwidth-optimized-diff-protocol.yaml b/changelog/pending/20221121--backend-service--allows-the-service-to-opt-into-bandwidth-optimized-diff-protocol.yaml new file mode 100644 index 000000000000..f929c8632edf --- /dev/null +++ b/changelog/pending/20221121--backend-service--allows-the-service-to-opt-into-bandwidth-optimized-diff-protocol.yaml @@ -0,0 +1,4 @@ +changes: +- type: feat + scope: backend/service + description: Allows the service to opt into a bandwidth-optimized DIFF protocol for storing checkpoints. Previously this required setting the PULUMI_OPTIMIZED_CHECKPOINT_PATCH env variable on the client. This env variable is now deprecated. From 7f4bbc01535827869ebf7e720825ccd4de433f1a Mon Sep 17 00:00:00 2001 From: Anton Tayanovskyy Date: Mon, 21 Nov 2022 17:43:10 -0500 Subject: [PATCH 08/18] Update pkg/backend/httpstate/backend.go Co-authored-by: Justin Van Patten --- pkg/backend/httpstate/backend.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/backend/httpstate/backend.go b/pkg/backend/httpstate/backend.go index 06c8c5774a24..03db0c3516d9 100644 --- a/pkg/backend/httpstate/backend.go +++ b/pkg/backend/httpstate/backend.go @@ -1251,7 +1251,6 @@ func (b *cloudBackend) GetHistory( if err != nil { return nil, fmt.Errorf("failed to get stack updates: %w", err) } - // // Convert apitype.UpdateInfo objects to the backend type. var beUpdates []backend.UpdateInfo From e8eaba7d1977a4031ace5a236a1842aaeecdae4d Mon Sep 17 00:00:00 2001 From: Anton Tayanovskyy Date: Tue, 22 Nov 2022 11:39:16 -0500 Subject: [PATCH 09/18] Remove TODO --- pkg/backend/httpstate/backend.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/backend/httpstate/backend.go b/pkg/backend/httpstate/backend.go index 03db0c3516d9..44d17efc3d14 100644 --- a/pkg/backend/httpstate/backend.go +++ b/pkg/backend/httpstate/backend.go @@ -1806,7 +1806,6 @@ func decodeCapabilities(wireLevel []apitype.APICapabilityConfig) (capabilities, for _, entry := range wireLevel { switch entry.Capability { case apitype.DeltaCheckpointUploads: - // TODO should check entry.Version here? var cap apitype.DeltaCheckpointUploadsConfigV1 if err := json.Unmarshal(entry.Configuration, &cap); err != nil { msg := "decoding DeltaCheckpointUploadsConfigV1 returned %w" From d3277e6a5c006d647cab04ca0096a9c590b8637f Mon Sep 17 00:00:00 2001 From: Anton Tayanovskyy Date: Tue, 22 Nov 2022 11:47:45 -0500 Subject: [PATCH 10/18] Add DisableCapabilityProbing --- pkg/backend/httpstate/client/client.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pkg/backend/httpstate/client/client.go b/pkg/backend/httpstate/client/client.go index f47a382d34a8..a9c5597085a6 100644 --- a/pkg/backend/httpstate/client/client.go +++ b/pkg/backend/httpstate/client/client.go @@ -49,6 +49,9 @@ type Client struct { apiOrgs []string diag diag.Sink client restClient + + // If true, do not probe the backend with GET /api/capabilities and assume no capabilities. + DisableCapabilityProbing bool } // newClient creates a new Pulumi API client with the given URL and API token. It is a variable instead of a regular @@ -1072,6 +1075,10 @@ func (pc *Client) GetDeploymentUpdates(ctx context.Context, stack StackIdentifie } func (pc *Client) GetCapabilities(ctx context.Context) (*apitype.CapabilitiesResponse, error) { + if pc.DisableCapabilityProbing { + return &apitype.CapabilitiesResponse{}, nil + } + var resp apitype.CapabilitiesResponse err := pc.restCall(ctx, http.MethodGet, "/api/capabilities", nil, nil, &resp) if is404(err) { From 61ae4e8e1d890f554d4078a8b6d86959ecc6c70a Mon Sep 17 00:00:00 2001 From: Anton Tayanovskyy Date: Tue, 22 Nov 2022 11:51:47 -0500 Subject: [PATCH 11/18] Address PR feedback --- pkg/backend/httpstate/client/client.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/backend/httpstate/client/client.go b/pkg/backend/httpstate/client/client.go index a9c5597085a6..3b41d9192cc3 100644 --- a/pkg/backend/httpstate/client/client.go +++ b/pkg/backend/httpstate/client/client.go @@ -1096,7 +1096,8 @@ func is404(err error) bool { if err == nil { return false } - if errResp, ok := err.(*apitype.ErrorResponse); ok && errResp.Code == http.StatusNotFound { + var errResp *apitype.ErrorResponse + if errors.As(err, &errResp) && errResp.Code == http.StatusNotFound { return true } return false From e11187e5f5ac86a24a84d98f4bae628b2488e281 Mon Sep 17 00:00:00 2001 From: Anton Tayanovskyy Date: Tue, 22 Nov 2022 12:33:51 -0500 Subject: [PATCH 12/18] Fix unit test --- pkg/backend/httpstate/snapshot_test.go | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/pkg/backend/httpstate/snapshot_test.go b/pkg/backend/httpstate/snapshot_test.go index 596099e2d9a3..1d49b6b13afa 100644 --- a/pkg/backend/httpstate/snapshot_test.go +++ b/pkg/backend/httpstate/snapshot_test.go @@ -94,8 +94,18 @@ func TestCloudSnapshotPersisterUseOfDiffProtocol(t *testing.T) { } newMockServer := func() *httptest.Server { - return httptest.NewServer( - http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + return httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + switch req.URL.Path { + case "/api/capabilities": + resp := apitype.CapabilitiesResponse{Capabilities: []apitype.APICapabilityConfig{{ + Capability: apitype.DeltaCheckpointUploads, + Configuration: json.RawMessage(`{"checkpointCutoffSizeBytes":1}`), + }}} + err := json.NewEncoder(rw).Encode(resp) + assert.NoError(t, err) + return + case "/api/stacks/owner/project/stack/update/update-id/checkpointverbatim", + "/api/stacks/owner/project/stack/update/update-id/checkpointdelta": lastRequest = req rw.WriteHeader(200) message := `{}` @@ -107,7 +117,10 @@ func TestCloudSnapshotPersisterUseOfDiffProtocol(t *testing.T) { _, err = rw.Write([]byte(message)) assert.NoError(t, err) req.Body = io.NopCloser(bytes.NewBuffer(rbytes)) - })) + default: + panic(fmt.Sprintf("Path not supported: %v", req.URL.Path)) + } + })) } newMockTokenSource := func() tokenSourceCapability { @@ -126,8 +139,6 @@ func TestCloudSnapshotPersisterUseOfDiffProtocol(t *testing.T) { UpdateKind: apitype.UpdateUpdate, UpdateID: updateID, }, newMockTokenSource(), nil) - persister.deploymentDiffState = newDeploymentDiffState(1) - persister.deploymentDiffState.minimalDiffSize = 1 return persister } From 1e4aa31cf00cfd86e4f1c11323177c689f862ec5 Mon Sep 17 00:00:00 2001 From: Anton Tayanovskyy Date: Tue, 22 Nov 2022 18:17:24 -0500 Subject: [PATCH 13/18] Satisfy lint --- pkg/backend/httpstate/client/client_test.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/backend/httpstate/client/client_test.go b/pkg/backend/httpstate/client/client_test.go index 4fd2b6bfbcff..30eaef55b35d 100644 --- a/pkg/backend/httpstate/client/client_test.go +++ b/pkg/backend/httpstate/client/client_test.go @@ -181,7 +181,9 @@ func TestPatchUpdateCheckpointVerbatimPreservesIndent(t *testing.T) { } func TestGetCapabilities(t *testing.T) { + t.Parallel() t.Run("legacy-service-404", func(t *testing.T) { + t.Parallel() s := newMockServer(404, "NOT FOUND") defer s.Close() @@ -192,6 +194,7 @@ func TestGetCapabilities(t *testing.T) { assert.Empty(t, resp.Capabilities) }) t.Run("updated-service-with-delta-checkpoint-capability", func(t *testing.T) { + t.Parallel() cfg := apitype.DeltaCheckpointUploadsConfigV1{ CheckpointCutoffSizeBytes: 1024 * 1024 * 4, } From 183ee2eda7c3bcca5c650e8daa5732520d945278 Mon Sep 17 00:00:00 2001 From: aq17 Date: Tue, 22 Nov 2022 17:05:43 -0800 Subject: [PATCH 14/18] Do not fail pulumi policy new on invalid template(s) --- pkg/cmd/pulumi/new.go | 38 ++++++++++------------------ pkg/cmd/pulumi/policy_new.go | 18 +++++++++++-- sdk/go/common/workspace/templates.go | 12 ++++++++- 3 files changed, 41 insertions(+), 27 deletions(-) diff --git a/pkg/cmd/pulumi/new.go b/pkg/cmd/pulumi/new.go index 91dab7d70f56..4112fe05a0f9 100644 --- a/pkg/cmd/pulumi/new.go +++ b/pkg/cmd/pulumi/new.go @@ -823,32 +823,22 @@ func chooseTemplate(templates []workspace.Template, opts display.Options) (works // Customize the prompt a little bit (and disable color since it doesn't match our scheme). surveycore.DisableColor = true - var selectedOption workspace.Template - - for { - options, optionToTemplateMap := templatesToOptionArrayAndMap(templates, true) - nopts := len(options) - pageSize := optimalPageSize(optimalPageSizeOpts{nopts: nopts}) - message := fmt.Sprintf("\rPlease choose a template (%d/%d shown):\n", pageSize, nopts) - message = opts.Color.Colorize(colors.SpecPrompt + message + colors.Reset) - - var option string - if err := survey.AskOne(&survey.Select{ - Message: message, - Options: options, - PageSize: pageSize, - }, &option, surveyIcons(opts.Color)); err != nil { - return workspace.Template{}, errors.New(chooseTemplateErr) - } - - var has bool - selectedOption, has = optionToTemplateMap[option] - if has { - break - } + options, optionToTemplateMap := templatesToOptionArrayAndMap(templates, true) + nopts := len(options) + pageSize := optimalPageSize(optimalPageSizeOpts{nopts: nopts}) + message := fmt.Sprintf("\rPlease choose a template (%d/%d shown):\n", pageSize, nopts) + message = opts.Color.Colorize(colors.SpecPrompt + message + colors.Reset) + + var option string + if err := survey.AskOne(&survey.Select{ + Message: message, + Options: options, + PageSize: pageSize, + }, &option, surveyIcons(opts.Color)); err != nil { + return workspace.Template{}, errors.New(chooseTemplateErr) } - return selectedOption, nil + return optionToTemplateMap[option], nil } // parseConfig parses the config values passed via command line flags. diff --git a/pkg/cmd/pulumi/policy_new.go b/pkg/cmd/pulumi/policy_new.go index ad253a867c00..4be30af35481 100644 --- a/pkg/cmd/pulumi/policy_new.go +++ b/pkg/cmd/pulumi/policy_new.go @@ -149,6 +149,9 @@ func runNewPolicyPack(ctx context.Context, args newPolicyArgs) error { return err } } + if template.Errored() { + return fmt.Errorf("template '%s' is currently broken: %w", template.Name, template.Error) + } // Do a dry run, if we're not forcing files to be overwritten. if !args.force { @@ -366,15 +369,26 @@ func policyTemplatesToOptionArrayAndMap( // Build the array and map. var options []string + var brokenOptions []string nameToTemplateMap := make(map[string]workspace.PolicyPackTemplate) for _, template := range templates { + // If template is broken, indicate it in the project description. + if template.Errored() { + template.Description = brokenTemplateDescription + } + // Create the option string that combines the name, padding, and description. option := fmt.Sprintf(fmt.Sprintf("%%%ds %%s", -maxNameLength), template.Name, template.Description) - // Add it to the array and map. - options = append(options, option) nameToTemplateMap[option] = template + if template.Errored() { + brokenOptions = append(brokenOptions, option) + } else { + options = append(options, option) + } } + // After sorting the options, add the broken templates to the end sort.Strings(options) + options = append(options, brokenOptions...) return options, nameToTemplateMap } diff --git a/sdk/go/common/workspace/templates.go b/sdk/go/common/workspace/templates.go index b146084c3b54..2955d0667bd5 100644 --- a/sdk/go/common/workspace/templates.go +++ b/sdk/go/common/workspace/templates.go @@ -184,7 +184,11 @@ func (repo TemplateRepository) PolicyTemplates() ([]PolicyPackTemplate, error) { template, err := LoadPolicyPackTemplate(filepath.Join(path, name)) if err != nil && !errors.Is(err, fs.ErrNotExist) { - return nil, err + logging.V(2).Infof( + "Failed to load template %s: %s", + name, err.Error(), + ) + result = append(result, PolicyPackTemplate{Name: name, Error: err}) } else if err == nil { result = append(result, template) } @@ -217,6 +221,12 @@ type PolicyPackTemplate struct { Dir string // The directory containing PulumiPolicy.yaml. Name string // The name of the template. Description string // Description of the template. + Error error // Non-nil if the template is broken. +} + +// Errored returns if the template has an error +func (t PolicyPackTemplate) Errored() bool { + return t.Error != nil } // cleanupLegacyTemplateDir deletes an existing ~/.pulumi/templates directory if it isn't a git repository. From a97ba2101f776b38837a4d466a60fadf2d6fc498 Mon Sep 17 00:00:00 2001 From: Zaid Ajaj Date: Thu, 24 Nov 2022 11:03:00 +0100 Subject: [PATCH 15/18] Disable failing windows-winget-install wokflow --- .github/workflows/download-pulumi-cron.yml | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/.github/workflows/download-pulumi-cron.yml b/.github/workflows/download-pulumi-cron.yml index 39cf5e1edfc9..75f57321b5c1 100644 --- a/.github/workflows/download-pulumi-cron.yml +++ b/.github/workflows/download-pulumi-cron.yml @@ -94,27 +94,6 @@ jobs: run: | echo "Expected version ${{ steps.vars.outputs.expected-version }} but found ${{ steps.vars.outputs.installed-version }}" exit 1 - windows-winget-install: - name: Install Pulumi with WinGet on Windows - runs-on: windows-latest - steps: - - name: Install WinGet CLI - shell: powershell - run: | - try { winget --help } catch { Add-AppxPackage -Path https://aka.ms/getwinget } - - name: Install Pulumi Using Winget - run: winget install pulumi - - name: Pulumi Version Details - id: vars - shell: bash - run: | - echo "::set-output name=installed-version::$(pulumi version)" - echo "::set-output name=expected-version::v$(curl -sS https://www.pulumi.com/latest-version)" - - name: Error if incorrect version found - if: ${{ steps.vars.outputs.expected-version != steps.vars.outputs.installed-version }} - run: | - echo "Expected version ${{ steps.vars.outputs.expected-version }} but found ${{ steps.vars.outputs.installed-version }}" - exit 1 windows-direct-install: name: Install Pulumi via script on Windows runs-on: windows-latest From 25e4c493754bd6e0368b8a8187bbf8654e188a2b Mon Sep 17 00:00:00 2001 From: Ian Wahbe Date: Mon, 28 Nov 2022 10:56:04 -0800 Subject: [PATCH 16/18] Add `.` between `?` and `]` --- ...28--programgen-nodejs--add-between-and.yaml | 4 ++++ pkg/codegen/nodejs/gen_program_expressions.go | 18 +++++++++++++++--- pkg/codegen/testing/test/program_driver.go | 2 +- .../aws-s3-logging-pp/nodejs/aws-s3-logging.ts | 2 +- 4 files changed, 21 insertions(+), 5 deletions(-) create mode 100644 changelog/pending/20221128--programgen-nodejs--add-between-and.yaml diff --git a/changelog/pending/20221128--programgen-nodejs--add-between-and.yaml b/changelog/pending/20221128--programgen-nodejs--add-between-and.yaml new file mode 100644 index 000000000000..912903f06b7a --- /dev/null +++ b/changelog/pending/20221128--programgen-nodejs--add-between-and.yaml @@ -0,0 +1,4 @@ +changes: +- type: fix + scope: programgen/nodejs + description: Add `.` between `?` and `[`. diff --git a/pkg/codegen/nodejs/gen_program_expressions.go b/pkg/codegen/nodejs/gen_program_expressions.go index 9ef51257bde5..0632d27a0756 100644 --- a/pkg/codegen/nodejs/gen_program_expressions.go +++ b/pkg/codegen/nodejs/gen_program_expressions.go @@ -594,8 +594,20 @@ func (g *generator) genRelativeTraversal(w io.Writer, traversal hcl.Traversal, p contract.Failf("unexpected traversal part of type %T (%v)", part, part.SourceRange()) } + var indexPrefix string if model.IsOptionalType(model.GetTraversableType(parts[i])) { g.Fgen(w, "?") + // `expr?[expr]` is not valid typescript, since it looks like a ternary + // operator. + // + // Typescript solves this by inserting a `.` in before the `[`: `expr?.[expr]` + // + // We need to do the same when generating index based expressions. + indexPrefix = "." + } + + genIndex := func(inner string, value interface{}) { + g.Fgenf(w, "%s["+inner+"]", indexPrefix, value) } switch key.Type() { @@ -604,13 +616,13 @@ func (g *generator) genRelativeTraversal(w io.Writer, traversal hcl.Traversal, p if isLegalIdentifier(keyVal) { g.Fgenf(w, ".%s", keyVal) } else { - g.Fgenf(w, "[%q]", keyVal) + genIndex("%q", keyVal) } case cty.Number: idx, _ := key.AsBigFloat().Int64() - g.Fgenf(w, "[%d]", idx) + genIndex("%d", idx) default: - g.Fgenf(w, "[%q]", key.AsString()) + genIndex("%q", key.AsString()) } } } diff --git a/pkg/codegen/testing/test/program_driver.go b/pkg/codegen/testing/test/program_driver.go index 6b7669f2a20c..7c95b35a7cbc 100644 --- a/pkg/codegen/testing/test/program_driver.go +++ b/pkg/codegen/testing/test/program_driver.go @@ -88,7 +88,7 @@ var PulumiPulumiProgramTests = []ProgramTest{ { Directory: "aws-s3-logging", Description: "AWS S3 with logging", - SkipCompile: allProgLanguages.Except("python").Except("dotnet"), + SkipCompile: codegen.NewStringSet("go"), // Blocked on nodejs: TODO[pulumi/pulumi#8068] // Flaky in go: TODO[pulumi/pulumi#8123] }, diff --git a/pkg/codegen/testing/test/testdata/aws-s3-logging-pp/nodejs/aws-s3-logging.ts b/pkg/codegen/testing/test/testdata/aws-s3-logging-pp/nodejs/aws-s3-logging.ts index 51840abf7b34..68b8675231d8 100644 --- a/pkg/codegen/testing/test/testdata/aws-s3-logging-pp/nodejs/aws-s3-logging.ts +++ b/pkg/codegen/testing/test/testdata/aws-s3-logging-pp/nodejs/aws-s3-logging.ts @@ -5,4 +5,4 @@ const logs = new aws.s3.Bucket("logs", {}); const bucket = new aws.s3.Bucket("bucket", {loggings: [{ targetBucket: logs.bucket, }]}); -export const targetBucket = bucket.loggings.apply(loggings => loggings?[0]?.targetBucket); +export const targetBucket = bucket.loggings.apply(loggings => loggings?.[0]?.targetBucket); From 9f5e5b5db7ae5a4f1c4c1fe165841a5a59b59d19 Mon Sep 17 00:00:00 2001 From: Ian Wahbe Date: Mon, 28 Nov 2022 11:26:32 -0800 Subject: [PATCH 17/18] Fix capitalization for generated `fs.readdirSync` --- ...s--fix-capitalization-when-generating-fs-readdirsync.yaml | 4 ++++ pkg/codegen/nodejs/gen_program_expressions.go | 2 +- pkg/codegen/testing/test/program_driver.go | 5 ++--- .../test/testdata/aws-s3-folder-pp/nodejs/aws-s3-folder.ts | 2 +- 4 files changed, 8 insertions(+), 5 deletions(-) create mode 100644 changelog/pending/20221128--programgen-nodejs--fix-capitalization-when-generating-fs-readdirsync.yaml diff --git a/changelog/pending/20221128--programgen-nodejs--fix-capitalization-when-generating-fs-readdirsync.yaml b/changelog/pending/20221128--programgen-nodejs--fix-capitalization-when-generating-fs-readdirsync.yaml new file mode 100644 index 000000000000..94dafdbf717c --- /dev/null +++ b/changelog/pending/20221128--programgen-nodejs--fix-capitalization-when-generating-fs-readdirsync.yaml @@ -0,0 +1,4 @@ +changes: +- type: fix + scope: programgen/nodejs + description: Fix capitalization when generating `fs.readdirSync`. diff --git a/pkg/codegen/nodejs/gen_program_expressions.go b/pkg/codegen/nodejs/gen_program_expressions.go index 9ef51257bde5..888906582ede 100644 --- a/pkg/codegen/nodejs/gen_program_expressions.go +++ b/pkg/codegen/nodejs/gen_program_expressions.go @@ -429,7 +429,7 @@ func (g *generator) GenFunctionCallExpression(w io.Writer, expr *model.FunctionC case "readFile": g.Fgenf(w, "fs.readFileSync(%v)", expr.Args[0]) case "readDir": - g.Fgenf(w, "fs.readDirSync(%v)", expr.Args[0]) + g.Fgenf(w, "fs.readdirSync(%v)", expr.Args[0]) case "secret": g.Fgenf(w, "pulumi.secret(%v)", expr.Args[0]) case "split": diff --git a/pkg/codegen/testing/test/program_driver.go b/pkg/codegen/testing/test/program_driver.go index 6b7669f2a20c..ad4ce4c55636 100644 --- a/pkg/codegen/testing/test/program_driver.go +++ b/pkg/codegen/testing/test/program_driver.go @@ -66,13 +66,12 @@ var PulumiPulumiProgramTests = []ProgramTest{ { Directory: "aws-s3-folder", Description: "AWS S3 Folder", - ExpectNYIDiags: allProgLanguages.Except("go"), - SkipCompile: allProgLanguages.Except("dotnet"), + ExpectNYIDiags: codegen.NewStringSet("dotnet", "python"), + SkipCompile: codegen.NewStringSet("go", "python"), // Blocked on python: TODO[pulumi/pulumi#8062]: Re-enable this test. // Blocked on go: // TODO[pulumi/pulumi#8064] // TODO[pulumi/pulumi#8065] - // Blocked on nodejs: TODO[pulumi/pulumi#8063] }, { Directory: "aws-eks", diff --git a/pkg/codegen/testing/test/testdata/aws-s3-folder-pp/nodejs/aws-s3-folder.ts b/pkg/codegen/testing/test/testdata/aws-s3-folder-pp/nodejs/aws-s3-folder.ts index d7863011e199..9b969dd944d8 100644 --- a/pkg/codegen/testing/test/testdata/aws-s3-folder-pp/nodejs/aws-s3-folder.ts +++ b/pkg/codegen/testing/test/testdata/aws-s3-folder-pp/nodejs/aws-s3-folder.ts @@ -9,7 +9,7 @@ const siteBucket = new aws.s3.Bucket("siteBucket", {website: { const siteDir = "www"; // For each file in the directory, create an S3 object stored in `siteBucket` const files: aws.s3.BucketObject[] = []; -for (const range of fs.readDirSync(siteDir).map((v, k) => ({key: k, value: v}))) { +for (const range of fs.readdirSync(siteDir).map((v, k) => ({key: k, value: v}))) { files.push(new aws.s3.BucketObject(`files-${range.key}`, { bucket: siteBucket.id, key: range.value, From 095f5a9fbf45fbb259663e19ca0b3891b9f15de4 Mon Sep 17 00:00:00 2001 From: Ian Wahbe Date: Mon, 28 Nov 2022 16:14:07 -0800 Subject: [PATCH 18/18] Convert invoke results to ouputs when needed --- ...iate-invokes-to-ouputs-when-necessary.yaml | 4 +++ pkg/codegen/go/gen_program.go | 4 +-- pkg/codegen/go/gen_program_expressions.go | 28 +++++++++++++------ pkg/codegen/testing/test/program_driver.go | 3 -- .../testdata/aws-fargate-pp/go/aws-fargate.go | 11 ++++++-- 5 files changed, 34 insertions(+), 16 deletions(-) create mode 100644 changelog/pending/20221129--programgen-go--convert-the-result-of-immediate-invokes-to-ouputs-when-necessary.yaml diff --git a/changelog/pending/20221129--programgen-go--convert-the-result-of-immediate-invokes-to-ouputs-when-necessary.yaml b/changelog/pending/20221129--programgen-go--convert-the-result-of-immediate-invokes-to-ouputs-when-necessary.yaml new file mode 100644 index 000000000000..9985d3106368 --- /dev/null +++ b/changelog/pending/20221129--programgen-go--convert-the-result-of-immediate-invokes-to-ouputs-when-necessary.yaml @@ -0,0 +1,4 @@ +changes: +- type: fix + scope: programgen/go + description: Convert the result of immediate invokes to ouputs when necessary. diff --git a/pkg/codegen/go/gen_program.go b/pkg/codegen/go/gen_program.go index bbff50318e9f..edbbec1d1cf8 100644 --- a/pkg/codegen/go/gen_program.go +++ b/pkg/codegen/go/gen_program.go @@ -664,9 +664,7 @@ func (g *generator) genResource(w io.Writer, r *pcl.Resource) { if len(r.Inputs) > 0 { g.Fgenf(w, "&%s.%sArgs{\n", modOrAlias, typ) for _, attr := range r.Inputs { - g.Fgenf(w, "%s: ", strings.Title(attr.Name)) - g.Fgenf(w, "%.v,\n", attr.Value) - + g.Fgenf(w, "%s: %.v,\n", strings.Title(attr.Name), attr.Value) } g.Fprint(w, "}") } else { diff --git a/pkg/codegen/go/gen_program_expressions.go b/pkg/codegen/go/gen_program_expressions.go index b651afb06d22..a116d3eb3332 100644 --- a/pkg/codegen/go/gen_program_expressions.go +++ b/pkg/codegen/go/gen_program_expressions.go @@ -579,9 +579,11 @@ func (g *generator) genScopeTraversalExpression( _, isInput = schemaType.(*schema.InputType) } - if resource, ok := expr.Parts[0].(*pcl.Resource); ok { + var sourceIsPlain bool + switch root := expr.Parts[0].(type) { + case *pcl.Resource: isInput = false - if _, ok := pcl.GetSchemaForType(resource.InputType); ok { + if _, ok := pcl.GetSchemaForType(root.InputType); ok { // convert .id into .ID() last := expr.Traversal[len(expr.Traversal)-1] if attr, ok := last.(hcl.TraverseAttr); ok && attr.Name == "id" { @@ -589,29 +591,39 @@ func (g *generator) genScopeTraversalExpression( expr.Traversal = expr.Traversal[:len(expr.Traversal)-1] } } + case *pcl.LocalVariable: + if root, ok := root.Definition.Value.(*model.FunctionCallExpression); ok && !pcl.IsOutputVersionInvokeCall(root) { + sourceIsPlain = true + } } // TODO if it's an array type, we need a lowering step to turn []string -> pulumi.StringArray if isInput { - argType := g.argumentTypeName(expr, expr.Type(), isInput) - if strings.HasSuffix(argType, "Array") { + argTypeName := g.argumentTypeName(expr, expr.Type(), isInput) + if strings.HasSuffix(argTypeName, "Array") { destTypeName := g.argumentTypeName(expr, destType, isInput) - if argType != destTypeName { + // `argTypeName` == `destTypeName` and `argTypeName` ends with `Array`, we + // know that `destType` is an outputty type. If the source is plain (and thus + // not outputty), then the types can never line up and we will need a + // conversion helper method. + if argTypeName != destTypeName || sourceIsPlain { // use a helper to transform prompt arrays into inputty arrays var helper *promptToInputArrayHelper - if h, ok := g.arrayHelpers[argType]; ok { + if h, ok := g.arrayHelpers[argTypeName]; ok { helper = h } else { // helpers are emitted at the end in the postamble step helper = &promptToInputArrayHelper{ - destType: argType, + destType: argTypeName, } - g.arrayHelpers[argType] = helper + g.arrayHelpers[argTypeName] = helper } + // Wrap the emitted expression in a call to the generated helper function. g.Fgenf(w, "%s(", helper.getFnName()) defer g.Fgenf(w, ")") } } else { + // Wrap the emitted expression in a type conversion. g.Fgenf(w, "%s(", g.argumentTypeName(expr, expr.Type(), isInput)) defer g.Fgenf(w, ")") } diff --git a/pkg/codegen/testing/test/program_driver.go b/pkg/codegen/testing/test/program_driver.go index 6b7669f2a20c..51ca00b0c261 100644 --- a/pkg/codegen/testing/test/program_driver.go +++ b/pkg/codegen/testing/test/program_driver.go @@ -81,9 +81,6 @@ var PulumiPulumiProgramTests = []ProgramTest{ { Directory: "aws-fargate", Description: "AWS Fargate", - - // TODO[pulumi/pulumi#8440] - SkipCompile: codegen.NewStringSet("go"), }, { Directory: "aws-s3-logging", diff --git a/pkg/codegen/testing/test/testdata/aws-fargate-pp/go/aws-fargate.go b/pkg/codegen/testing/test/testdata/aws-fargate-pp/go/aws-fargate.go index a5c974d4eafb..a180575357c9 100644 --- a/pkg/codegen/testing/test/testdata/aws-fargate-pp/go/aws-fargate.go +++ b/pkg/codegen/testing/test/testdata/aws-fargate-pp/go/aws-fargate.go @@ -85,7 +85,7 @@ func main() { return err } webLoadBalancer, err := elasticloadbalancingv2.NewLoadBalancer(ctx, "webLoadBalancer", &elasticloadbalancingv2.LoadBalancerArgs{ - Subnets: subnets.Ids, + Subnets: toPulumiStringArray(subnets.Ids), SecurityGroups: pulumi.StringArray{ webSecurityGroup.ID(), }, @@ -153,7 +153,7 @@ func main() { TaskDefinition: appTask.Arn, NetworkConfiguration: &ecs.ServiceNetworkConfigurationArgs{ AssignPublicIp: pulumi.Bool(true), - Subnets: subnets.Ids, + Subnets: toPulumiStringArray(subnets.Ids), SecurityGroups: pulumi.StringArray{ webSecurityGroup.ID(), }, @@ -175,3 +175,10 @@ func main() { return nil }) } +func toPulumiStringArray(arr []string) pulumi.StringArray { + var pulumiArr pulumi.StringArray + for _, v := range arr { + pulumiArr = append(pulumiArr, pulumi.String(v)) + } + return pulumiArr +}