Skip to content

Commit 220d65c

Browse files
authored
fix(cloudformation): load stacks lazily to avoid error notification o… (#8349)
…n startup ## Problem New CloudFormation feature eagerly loaded stacks resulting in error notification if credentials were not set properly. Eager loading is not desirable even if credentials would work because user may not open the panel or intend to use the data. ## Solution Lazy load stacks. Tested: - stacks do not load on startup - switching regions does not eagerly load stacks - pagination with load more works as before - deploying template if stacks were never loaded triggers loading as stacks are refreshed (desired behavior) - if StacksNode is expanded and region is switched then stacks are loaded (desired behavior) Dead code removal: polling was disabled earlier and unused. --- - Treat all work as PUBLIC. Private `feature/x` branches will not be squash-merged at release time. - Your code changes must meet the guidelines in [CONTRIBUTING.md](https://github.com/aws/aws-toolkit-vscode/blob/master/CONTRIBUTING.md#guidelines). - License: I confirm that my contribution is made under the terms of the Apache 2.0 license.
1 parent a36f645 commit 220d65c

File tree

7 files changed

+325
-35
lines changed

7 files changed

+325
-35
lines changed

packages/core/src/awsService/cloudformation/auth/credentials.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@ export class AwsCredentialsService implements Disposable {
1919
private client: LanguageClient | undefined
2020

2121
constructor(
22-
private stacksManager: StacksManager,
23-
private resourcesManager: ResourcesManager,
24-
private regionManager: CloudFormationRegionManager
22+
private readonly stacksManager: StacksManager,
23+
private readonly resourcesManager: ResourcesManager,
24+
private readonly regionManager: CloudFormationRegionManager
2525
) {
2626
this.authChangeListener = globals.awsContext.onDidChangeContext(() => {
2727
void this.updateCredentialsFromActiveConnection()
@@ -53,7 +53,7 @@ export class AwsCredentialsService implements Disposable {
5353
await this.client.sendRequest('aws/credentials/iam/update', encryptedRequest)
5454
}
5555

56-
void this.stacksManager.reload()
56+
this.stacksManager.clear()
5757
void this.resourcesManager.reload()
5858
}
5959

packages/core/src/awsService/cloudformation/explorer/nodes/stacksNode.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,17 +33,23 @@ export class StacksNode extends AWSTreeNodeBase {
3333
}
3434

3535
public override async getChildren(): Promise<AWSTreeNodeBase[]> {
36+
await this.stacksManager.ensureLoaded()
3637
this.updateNode()
3738
const stacks = this.stacksManager.get()
3839
const nodes = stacks.map((stack: StackSummary) => new StackNode(stack, this.changeSetsManager))
3940
return this.stacksManager.hasMore() ? [...nodes, new LoadMoreStacksNode(this)] : nodes
4041
}
4142

4243
private updateNode(): void {
43-
const count = this.stacksManager.get().length
44-
const hasMore = this.stacksManager.hasMore()
45-
this.description = hasMore ? `(${count}+)` : `(${count})`
46-
this.contextValue = hasMore ? 'stackSectionWithMore' : 'stackSection'
44+
if (this.stacksManager.isLoaded()) {
45+
const count = this.stacksManager.get().length
46+
const hasMore = this.stacksManager.hasMore()
47+
this.description = hasMore ? `(${count}+)` : `(${count})`
48+
this.contextValue = hasMore ? 'stackSectionWithMore' : 'stackSection'
49+
} else {
50+
this.description = undefined
51+
this.contextValue = 'stackSection'
52+
}
4753
}
4854

4955
public async loadMoreStacks(): Promise<void> {

packages/core/src/awsService/cloudformation/stacks/stacksManager.ts

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,14 @@ type ListStacksResult = {
2323
}
2424

2525
const ListStacksRequest = new RequestType<ListStacksParams, ListStacksResult, void>('aws/cfn/stacks')
26-
const PollIntervalMs = 1000
2726

2827
type StacksChangeListener = (stacks: StackSummary[]) => void
2928

3029
export class StacksManager implements Disposable {
3130
private stacks: StackSummary[] = []
3231
private nextToken?: string
3332
private readonly listeners: StacksChangeListener[] = []
34-
private poller?: NodeJS.Timeout
33+
private loaded = false
3534

3635
constructor(private readonly client: LanguageClient) {}
3736

@@ -51,6 +50,23 @@ export class StacksManager implements Disposable {
5150
void this.loadStacks()
5251
}
5352

53+
isLoaded() {
54+
return this.loaded
55+
}
56+
57+
async ensureLoaded() {
58+
if (!this.loaded) {
59+
await this.loadStacks()
60+
}
61+
}
62+
63+
clear() {
64+
this.stacks = []
65+
this.nextToken = undefined
66+
this.loaded = false
67+
this.notifyListeners()
68+
}
69+
5470
updateStackStatus(stackName: string, stackStatus: string) {
5571
const stack = this.stacks.find((s) => s.StackName === stackName)
5672
if (stack) {
@@ -80,21 +96,8 @@ export class StacksManager implements Disposable {
8096
}
8197
}
8298

83-
startPolling() {
84-
this.poller ??= setInterval(() => {
85-
this.reload()
86-
}, PollIntervalMs)
87-
}
88-
89-
stopPolling() {
90-
if (this.poller) {
91-
clearInterval(this.poller)
92-
this.poller = undefined
93-
}
94-
}
95-
9699
dispose() {
97-
this.stopPolling()
100+
// do nothing
98101
}
99102

100103
private async loadStacks() {
@@ -106,16 +109,14 @@ export class StacksManager implements Disposable {
106109
})
107110
this.stacks = response.stacks
108111
this.nextToken = response.nextToken
112+
this.loaded = true
109113
} catch (error) {
110114
await handleLspError(error, 'Error loading stacks')
111115
this.stacks = []
112116
this.nextToken = undefined
113117
} finally {
114118
await setContext('aws.cloudformation.refreshingStacks', false)
115119
this.notifyListeners()
116-
if (this.stacks.length === 0) {
117-
this.stopPolling()
118-
}
119120
}
120121
}
121122

packages/core/src/test/awsService/cloudformation/auth/credentials.test.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ describe('AwsCredentialsService', function () {
1818

1919
beforeEach(function () {
2020
sandbox = sinon.createSandbox()
21-
mockStacksManager = { reload: sandbox.stub(), hasMore: sandbox.stub().returns(false) }
21+
mockStacksManager = { reload: sandbox.stub(), hasMore: sandbox.stub().returns(false), clear: sandbox.stub() }
2222
mockResourcesManager = { reload: sandbox.stub() }
2323
mockClient = { sendRequest: sandbox.stub() }
2424

@@ -31,13 +31,6 @@ describe('AwsCredentialsService', function () {
3131
sandbox.restore()
3232
})
3333

34-
describe('constructor', function () {
35-
it('should initialize credentials service', function () {
36-
credentialsService = new AwsCredentialsService(mockStacksManager, mockResourcesManager, mockRegionManager)
37-
assert(credentialsService !== undefined)
38-
})
39-
})
40-
4134
describe('createEncryptedCredentialsRequest', function () {
4235
beforeEach(function () {
4336
credentialsService = new AwsCredentialsService(mockStacksManager, mockResourcesManager, mockRegionManager)
@@ -89,5 +82,11 @@ describe('AwsCredentialsService', function () {
8982
// Test passes if no error thrown
9083
assert(true)
9184
})
85+
86+
it('should clear stacks', async function () {
87+
credentialsService = new AwsCredentialsService(mockStacksManager, mockResourcesManager, mockRegionManager)
88+
await credentialsService.initialize(mockClient)
89+
assert(mockStacksManager.clear.calledOnce)
90+
})
9291
})
9392
})
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
/*!
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import assert from 'assert'
7+
import * as sinon from 'sinon'
8+
import { TreeItemCollapsibleState } from 'vscode'
9+
import { StacksNode } from '../../../../../awsService/cloudformation/explorer/nodes/stacksNode'
10+
import { StacksManager } from '../../../../../awsService/cloudformation/stacks/stacksManager'
11+
import { ChangeSetsManager } from '../../../../../awsService/cloudformation/stacks/changeSetsManager'
12+
import { StackSummary } from '@aws-sdk/client-cloudformation'
13+
14+
describe('StacksNode', function () {
15+
let stacksNode: StacksNode
16+
let mockStacksManager: sinon.SinonStubbedInstance<StacksManager>
17+
let mockChangeSetsManager: ChangeSetsManager
18+
let sandbox: sinon.SinonSandbox
19+
20+
beforeEach(function () {
21+
sandbox = sinon.createSandbox()
22+
mockStacksManager = {
23+
get: sandbox.stub(),
24+
hasMore: sandbox.stub(),
25+
isLoaded: sandbox.stub(),
26+
ensureLoaded: sandbox.stub(),
27+
loadMoreStacks: sandbox.stub(),
28+
} as any
29+
mockChangeSetsManager = {} as ChangeSetsManager
30+
})
31+
32+
afterEach(function () {
33+
sandbox.restore()
34+
})
35+
36+
describe('constructor', function () {
37+
it('should set correct properties when not loaded', function () {
38+
mockStacksManager.get.returns([])
39+
mockStacksManager.hasMore.returns(false)
40+
mockStacksManager.isLoaded.returns(false)
41+
42+
stacksNode = new StacksNode(mockStacksManager as any, mockChangeSetsManager)
43+
44+
assert.strictEqual(stacksNode.label, 'Stacks')
45+
assert.strictEqual(stacksNode.collapsibleState, TreeItemCollapsibleState.Collapsed)
46+
assert.strictEqual(stacksNode.description, undefined)
47+
assert.strictEqual(stacksNode.contextValue, 'stackSection')
48+
})
49+
50+
it('should set description when loaded', function () {
51+
const mockStacks: StackSummary[] = [
52+
{ StackName: 'stack-1', StackStatus: 'CREATE_COMPLETE' } as StackSummary,
53+
{ StackName: 'stack-2', StackStatus: 'UPDATE_COMPLETE' } as StackSummary,
54+
]
55+
mockStacksManager.get.returns(mockStacks)
56+
mockStacksManager.hasMore.returns(false)
57+
mockStacksManager.isLoaded.returns(true)
58+
59+
stacksNode = new StacksNode(mockStacksManager as any, mockChangeSetsManager)
60+
61+
assert.strictEqual(stacksNode.description, '(2)')
62+
assert.strictEqual(stacksNode.contextValue, 'stackSection')
63+
})
64+
65+
it('should set contextValue to stackSectionWithMore when hasMore', function () {
66+
const mockStacks: StackSummary[] = [
67+
{ StackName: 'stack-1', StackStatus: 'CREATE_COMPLETE' } as StackSummary,
68+
]
69+
mockStacksManager.get.returns(mockStacks)
70+
mockStacksManager.hasMore.returns(true)
71+
mockStacksManager.isLoaded.returns(true)
72+
73+
stacksNode = new StacksNode(mockStacksManager as any, mockChangeSetsManager)
74+
75+
assert.strictEqual(stacksNode.description, '(1+)')
76+
assert.strictEqual(stacksNode.contextValue, 'stackSectionWithMore')
77+
})
78+
})
79+
80+
describe('getChildren', function () {
81+
beforeEach(function () {
82+
mockStacksManager.get.returns([])
83+
mockStacksManager.hasMore.returns(false)
84+
mockStacksManager.isLoaded.returns(false)
85+
stacksNode = new StacksNode(mockStacksManager as any, mockChangeSetsManager)
86+
})
87+
88+
it('should call ensureLoaded', async function () {
89+
mockStacksManager.ensureLoaded.resolves()
90+
91+
await stacksNode.getChildren()
92+
93+
assert.strictEqual(mockStacksManager.ensureLoaded.calledOnce, true)
94+
})
95+
96+
it('should return StackNode for each stack', async function () {
97+
const mockStacks: StackSummary[] = [
98+
{ StackName: 'stack-1', StackStatus: 'CREATE_COMPLETE' } as StackSummary,
99+
{ StackName: 'stack-2', StackStatus: 'UPDATE_COMPLETE' } as StackSummary,
100+
]
101+
mockStacksManager.ensureLoaded.resolves()
102+
mockStacksManager.get.returns(mockStacks)
103+
mockStacksManager.hasMore.returns(false)
104+
mockStacksManager.isLoaded.returns(true)
105+
106+
const children = await stacksNode.getChildren()
107+
108+
assert.strictEqual(children.length, 2)
109+
assert.strictEqual(children[0].label, 'stack-1')
110+
assert.strictEqual(children[1].label, 'stack-2')
111+
})
112+
113+
it('should include LoadMoreStacksNode when hasMore', async function () {
114+
const mockStacks: StackSummary[] = [
115+
{ StackName: 'stack-1', StackStatus: 'CREATE_COMPLETE' } as StackSummary,
116+
]
117+
mockStacksManager.ensureLoaded.resolves()
118+
mockStacksManager.get.returns(mockStacks)
119+
mockStacksManager.hasMore.returns(true)
120+
mockStacksManager.isLoaded.returns(true)
121+
122+
const children = await stacksNode.getChildren()
123+
124+
assert.strictEqual(children.length, 2)
125+
assert.strictEqual(children[0].label, 'stack-1')
126+
assert.strictEqual(children[1].label, '[Load More...]')
127+
assert.strictEqual(children[1].contextValue, 'loadMoreStacks')
128+
})
129+
130+
it('should update node description after load', async function () {
131+
const mockStacks: StackSummary[] = [
132+
{ StackName: 'stack-1', StackStatus: 'CREATE_COMPLETE' } as StackSummary,
133+
]
134+
mockStacksManager.ensureLoaded.resolves()
135+
mockStacksManager.get.returns(mockStacks)
136+
mockStacksManager.hasMore.returns(false)
137+
mockStacksManager.isLoaded.returns(true)
138+
139+
await stacksNode.getChildren()
140+
141+
assert.strictEqual(stacksNode.description, '(1)')
142+
assert.strictEqual(stacksNode.contextValue, 'stackSection')
143+
})
144+
145+
it('should return empty array when no stacks', async function () {
146+
mockStacksManager.ensureLoaded.resolves()
147+
mockStacksManager.get.returns([])
148+
mockStacksManager.hasMore.returns(false)
149+
mockStacksManager.isLoaded.returns(true)
150+
151+
const children = await stacksNode.getChildren()
152+
153+
assert.strictEqual(children.length, 0)
154+
})
155+
})
156+
157+
describe('loadMoreStacks', function () {
158+
beforeEach(function () {
159+
mockStacksManager.get.returns([])
160+
mockStacksManager.hasMore.returns(false)
161+
mockStacksManager.isLoaded.returns(false)
162+
stacksNode = new StacksNode(mockStacksManager as any, mockChangeSetsManager)
163+
})
164+
165+
it('should call stacksManager.loadMoreStacks', async function () {
166+
mockStacksManager.loadMoreStacks.resolves()
167+
168+
await stacksNode.loadMoreStacks()
169+
170+
assert.strictEqual(mockStacksManager.loadMoreStacks.calledOnce, true)
171+
})
172+
173+
it('should update node description after loading more', async function () {
174+
const mockStacks: StackSummary[] = [
175+
{ StackName: 'stack-1', StackStatus: 'CREATE_COMPLETE' } as StackSummary,
176+
{ StackName: 'stack-2', StackStatus: 'UPDATE_COMPLETE' } as StackSummary,
177+
]
178+
mockStacksManager.loadMoreStacks.resolves()
179+
mockStacksManager.get.returns(mockStacks)
180+
mockStacksManager.hasMore.returns(true)
181+
mockStacksManager.isLoaded.returns(true)
182+
183+
await stacksNode.loadMoreStacks()
184+
185+
assert.strictEqual(stacksNode.description, '(2+)')
186+
assert.strictEqual(stacksNode.contextValue, 'stackSectionWithMore')
187+
})
188+
189+
it('should update contextValue when no more stacks', async function () {
190+
const mockStacks: StackSummary[] = [
191+
{ StackName: 'stack-1', StackStatus: 'CREATE_COMPLETE' } as StackSummary,
192+
]
193+
mockStacksManager.loadMoreStacks.resolves()
194+
mockStacksManager.get.returns(mockStacks)
195+
mockStacksManager.hasMore.returns(false)
196+
mockStacksManager.isLoaded.returns(true)
197+
198+
await stacksNode.loadMoreStacks()
199+
200+
assert.strictEqual(stacksNode.description, '(1)')
201+
assert.strictEqual(stacksNode.contextValue, 'stackSection')
202+
})
203+
})
204+
})

0 commit comments

Comments
 (0)