@rancher/shell 3.0.7 → 3.0.8-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/images/vendor/githubapp.svg +13 -0
- package/assets/styles/base/_typography.scss +1 -1
- package/assets/styles/themes/_modern.scss +5 -5
- package/assets/translations/en-us.yaml +91 -11
- package/assets/translations/zh-hans.yaml +0 -4
- package/components/Inactivity.vue +222 -106
- package/components/InstallHelmCharts.vue +2 -2
- package/components/ResourceDetail/index.vue +1 -1
- package/components/SortableTable/index.vue +17 -2
- package/components/fleet/FleetConfigMapSelector.vue +117 -0
- package/components/fleet/FleetSecretSelector.vue +127 -0
- package/components/fleet/__tests__/FleetConfigMapSelector.test.ts +125 -0
- package/components/fleet/__tests__/FleetSecretSelector.test.ts +82 -0
- package/components/form/FileImageSelector.vue +13 -4
- package/components/form/FileSelector.vue +11 -2
- package/components/form/ResourceLabeledSelect.vue +1 -0
- package/components/form/__tests__/ResourceLabeledSelect.test.ts +90 -0
- package/components/nav/Header.vue +1 -0
- package/config/product/auth.js +1 -0
- package/config/query-params.js +1 -0
- package/config/settings.ts +8 -1
- package/config/types.js +2 -0
- package/dialog/AddonConfigConfirmationDialog.vue +45 -1
- package/edit/__tests__/fleet.cattle.io.helmop.test.ts +52 -11
- package/edit/auth/AuthProviderWarningBanners.vue +14 -1
- package/edit/auth/github-app-steps.vue +97 -0
- package/edit/auth/github-steps.vue +75 -0
- package/edit/auth/github.vue +94 -65
- package/edit/fleet.cattle.io.helmop.vue +51 -2
- package/edit/networking.k8s.io.networkpolicy/PolicyRuleTarget.vue +15 -5
- package/edit/provisioning.cattle.io.cluster/__tests__/rke2.test.ts +11 -9
- package/edit/provisioning.cattle.io.cluster/rke2.vue +56 -9
- package/edit/provisioning.cattle.io.cluster/tabs/AddOnConfig.vue +28 -2
- package/list/projectsecret.vue +1 -1
- package/machine-config/azure.vue +1 -1
- package/mixins/chart.js +1 -1
- package/models/__tests__/chart.test.ts +17 -9
- package/models/__tests__/compliance.cattle.io.clusterscanprofile.spec.js +30 -0
- package/models/catalog.cattle.io.app.js +1 -1
- package/models/chart.js +3 -1
- package/models/compliance.cattle.io.clusterscanprofile.js +1 -1
- package/models/management.cattle.io.authconfig.js +1 -0
- package/package.json +2 -2
- package/pages/auth/login.vue +5 -2
- package/pages/auth/verify.vue +1 -1
- package/pages/c/_cluster/apps/charts/AppChartCardSubHeader.vue +3 -2
- package/pages/c/_cluster/apps/charts/chart.vue +2 -2
- package/pages/c/_cluster/explorer/EventsTable.vue +89 -3
- package/pages/c/_cluster/explorer/tools/index.vue +3 -3
- package/pages/c/_cluster/settings/performance.vue +12 -25
- package/pages/home.vue +313 -12
- package/plugins/axios.js +2 -1
- package/plugins/dashboard-store/actions.js +1 -1
- package/plugins/dashboard-store/resource-class.js +17 -2
- package/plugins/steve/steve-pagination-utils.ts +2 -2
- package/scripts/extension/publish +1 -1
- package/store/auth.js +8 -3
- package/store/aws.js +8 -6
- package/store/features.js +1 -0
- package/store/index.js +9 -3
- package/store/prefs.js +6 -0
- package/types/kube/kube-api.ts +2 -1
- package/types/rancher/index.d.ts +1 -0
- package/types/resources/settings.d.ts +29 -7
- package/types/shell/index.d.ts +59 -0
- package/utils/__tests__/cluster.test.ts +379 -1
- package/utils/cluster.js +157 -3
- package/utils/dynamic-content/__tests__/config.test.ts +187 -0
- package/utils/dynamic-content/__tests__/index.test.ts +390 -0
- package/utils/dynamic-content/__tests__/info.test.ts +263 -0
- package/utils/dynamic-content/__tests__/new-release.test.ts +216 -0
- package/utils/dynamic-content/__tests__/support-notice.test.ts +262 -0
- package/utils/dynamic-content/__tests__/util.test.ts +235 -0
- package/utils/dynamic-content/config.ts +55 -0
- package/utils/dynamic-content/index.ts +273 -0
- package/utils/dynamic-content/info.ts +219 -0
- package/utils/dynamic-content/new-release.ts +126 -0
- package/utils/dynamic-content/support-notice.ts +169 -0
- package/utils/dynamic-content/types.d.ts +101 -0
- package/utils/dynamic-content/util.ts +122 -0
- package/utils/inactivity.ts +104 -0
- package/utils/pagination-utils.ts +19 -4
- package/utils/release-notes.ts +1 -1
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { removeMatchingNotifications, createLogger, LOCAL_STORAGE_CONTENT_DEBUG_LOG } from '../util';
|
|
2
|
+
import { Context, Configuration } from '../types';
|
|
3
|
+
|
|
4
|
+
describe('util.ts', () => {
|
|
5
|
+
describe('removeMatchingNotifications', () => {
|
|
6
|
+
let mockContext: Context;
|
|
7
|
+
let mockDispatch: jest.Mock;
|
|
8
|
+
let mockGetters: any;
|
|
9
|
+
let mockLogger: any;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
mockDispatch = jest.fn();
|
|
13
|
+
mockGetters = { 'notifications/all': [] };
|
|
14
|
+
mockLogger = { debug: jest.fn() };
|
|
15
|
+
mockContext = {
|
|
16
|
+
dispatch: mockDispatch,
|
|
17
|
+
getters: mockGetters,
|
|
18
|
+
logger: mockLogger,
|
|
19
|
+
// The following properties are not used by this function but are required by the type
|
|
20
|
+
axios: {},
|
|
21
|
+
isAdmin: true,
|
|
22
|
+
config: {} as any,
|
|
23
|
+
settings: {} as any,
|
|
24
|
+
};
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should return false and not remove anything if no notifications exist', async() => {
|
|
28
|
+
const found = await removeMatchingNotifications(mockContext, 'prefix-', 'current');
|
|
29
|
+
|
|
30
|
+
expect(found).toBe(false);
|
|
31
|
+
expect(mockDispatch).not.toHaveBeenCalled();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should return false and not remove anything if no notifications match the prefix', async() => {
|
|
35
|
+
mockGetters['notifications/all'] = [
|
|
36
|
+
{ id: 'other-1' },
|
|
37
|
+
{ id: 'other-2' },
|
|
38
|
+
];
|
|
39
|
+
const found = await removeMatchingNotifications(mockContext, 'prefix-', 'current');
|
|
40
|
+
|
|
41
|
+
expect(found).toBe(false);
|
|
42
|
+
expect(mockDispatch).not.toHaveBeenCalled();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should return true and not remove anything if the current notification is the only one matching', async() => {
|
|
46
|
+
mockGetters['notifications/all'] = [
|
|
47
|
+
{ id: 'prefix-current' },
|
|
48
|
+
{ id: 'other-1' },
|
|
49
|
+
];
|
|
50
|
+
const found = await removeMatchingNotifications(mockContext, 'prefix-', 'current');
|
|
51
|
+
|
|
52
|
+
expect(found).toBe(true);
|
|
53
|
+
expect(mockDispatch).not.toHaveBeenCalled();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should return false and remove a notification that matches the prefix but not the currentId', async() => {
|
|
57
|
+
mockGetters['notifications/all'] = [
|
|
58
|
+
{ id: 'prefix-old' },
|
|
59
|
+
{ id: 'other-1' },
|
|
60
|
+
];
|
|
61
|
+
const found = await removeMatchingNotifications(mockContext, 'prefix-', 'current');
|
|
62
|
+
|
|
63
|
+
expect(found).toBe(false);
|
|
64
|
+
expect(mockDispatch).toHaveBeenCalledTimes(1);
|
|
65
|
+
expect(mockDispatch).toHaveBeenCalledWith('notifications/remove', 'prefix-old');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should return true and remove old notifications when the current one also exists', async() => {
|
|
69
|
+
mockGetters['notifications/all'] = [
|
|
70
|
+
{ id: 'prefix-old-1' },
|
|
71
|
+
{ id: 'prefix-current' },
|
|
72
|
+
{ id: 'prefix-old-2' },
|
|
73
|
+
{ id: 'other-1' },
|
|
74
|
+
];
|
|
75
|
+
const found = await removeMatchingNotifications(mockContext, 'prefix-', 'current');
|
|
76
|
+
|
|
77
|
+
expect(found).toBe(true);
|
|
78
|
+
expect(mockDispatch).toHaveBeenCalledTimes(2);
|
|
79
|
+
expect(mockDispatch).toHaveBeenCalledWith('notifications/remove', 'prefix-old-1');
|
|
80
|
+
expect(mockDispatch).toHaveBeenCalledWith('notifications/remove', 'prefix-old-2');
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe('createLogger / log', () => {
|
|
85
|
+
let mockLocalStorage: { [key: string]: string };
|
|
86
|
+
let consoleErrorSpy: jest.SpyInstance;
|
|
87
|
+
let consoleInfoSpy: jest.SpyInstance;
|
|
88
|
+
let consoleDebugSpy: jest.SpyInstance;
|
|
89
|
+
let dispatchEventSpy: jest.SpyInstance;
|
|
90
|
+
|
|
91
|
+
beforeEach(() => {
|
|
92
|
+
// Mock localStorage
|
|
93
|
+
mockLocalStorage = {};
|
|
94
|
+
jest.spyOn(Storage.prototype, 'getItem').mockImplementation((key) => mockLocalStorage[key] || null);
|
|
95
|
+
jest.spyOn(Storage.prototype, 'setItem').mockImplementation((key, value) => {
|
|
96
|
+
mockLocalStorage[key] = value;
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Mock console
|
|
100
|
+
consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
|
101
|
+
consoleInfoSpy = jest.spyOn(console, 'info').mockImplementation(() => {});
|
|
102
|
+
consoleDebugSpy = jest.spyOn(console, 'debug').mockImplementation(() => {});
|
|
103
|
+
|
|
104
|
+
// Mock dispatchEvent
|
|
105
|
+
dispatchEventSpy = jest.spyOn(window, 'dispatchEvent').mockImplementation(() => true);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
afterEach(() => {
|
|
109
|
+
jest.restoreAllMocks();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('should always log errors to console, but only to localStorage if config.log is true', () => {
|
|
113
|
+
const config: Configuration = {
|
|
114
|
+
enabled: true, debug: false, log: false, endpoint: '', prime: false, distribution: 'community'
|
|
115
|
+
};
|
|
116
|
+
const logger = createLogger(config);
|
|
117
|
+
|
|
118
|
+
logger.error('test error');
|
|
119
|
+
|
|
120
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith('test error');
|
|
121
|
+
expect(mockLocalStorage[LOCAL_STORAGE_CONTENT_DEBUG_LOG]).toBeUndefined();
|
|
122
|
+
|
|
123
|
+
// Test with arg
|
|
124
|
+
logger.error('test error', 'with arg');
|
|
125
|
+
|
|
126
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith('test error', 'with arg');
|
|
127
|
+
expect(mockLocalStorage[LOCAL_STORAGE_CONTENT_DEBUG_LOG]).toBeUndefined();
|
|
128
|
+
|
|
129
|
+
config.log = true;
|
|
130
|
+
|
|
131
|
+
logger.error('test error with log');
|
|
132
|
+
|
|
133
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith('test error with log');
|
|
134
|
+
expect(mockLocalStorage[LOCAL_STORAGE_CONTENT_DEBUG_LOG]).toBeDefined();
|
|
135
|
+
expect(JSON.parse(mockLocalStorage[LOCAL_STORAGE_CONTENT_DEBUG_LOG])[0].message).toBe('test error with log');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('should log info to console and localStorage only if config.log is true', () => {
|
|
139
|
+
const config: Configuration = {
|
|
140
|
+
enabled: true, debug: false, log: false, endpoint: '', prime: false, distribution: 'community'
|
|
141
|
+
};
|
|
142
|
+
const logger = createLogger(config);
|
|
143
|
+
|
|
144
|
+
logger.info('test info');
|
|
145
|
+
|
|
146
|
+
expect(consoleInfoSpy).not.toHaveBeenCalled();
|
|
147
|
+
expect(mockLocalStorage[LOCAL_STORAGE_CONTENT_DEBUG_LOG]).toBeUndefined();
|
|
148
|
+
|
|
149
|
+
config.log = true;
|
|
150
|
+
logger.info('test info with log');
|
|
151
|
+
|
|
152
|
+
expect(consoleInfoSpy).toHaveBeenCalledWith('test info with log');
|
|
153
|
+
expect(mockLocalStorage[LOCAL_STORAGE_CONTENT_DEBUG_LOG]).toBeDefined();
|
|
154
|
+
expect(JSON.parse(mockLocalStorage[LOCAL_STORAGE_CONTENT_DEBUG_LOG])[0].message).toBe('test info with log');
|
|
155
|
+
|
|
156
|
+
// Test with arg
|
|
157
|
+
logger.info('test info', 'with arg');
|
|
158
|
+
|
|
159
|
+
expect(consoleInfoSpy).toHaveBeenCalledWith('test info', 'with arg');
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('should log debug to console only if config.debug is true, and localStorage if config.log is true', () => {
|
|
163
|
+
const config: Configuration = {
|
|
164
|
+
enabled: true, debug: false, log: false, endpoint: '', prime: false, distribution: 'community'
|
|
165
|
+
};
|
|
166
|
+
const logger = createLogger(config);
|
|
167
|
+
|
|
168
|
+
logger.debug('test debug');
|
|
169
|
+
expect(consoleDebugSpy).not.toHaveBeenCalled();
|
|
170
|
+
expect(mockLocalStorage[LOCAL_STORAGE_CONTENT_DEBUG_LOG]).toBeUndefined();
|
|
171
|
+
|
|
172
|
+
config.log = true;
|
|
173
|
+
logger.debug('test debug with log');
|
|
174
|
+
expect(consoleDebugSpy).not.toHaveBeenCalled();
|
|
175
|
+
expect(mockLocalStorage[LOCAL_STORAGE_CONTENT_DEBUG_LOG]).toBeDefined();
|
|
176
|
+
expect(JSON.parse(mockLocalStorage[LOCAL_STORAGE_CONTENT_DEBUG_LOG])[0].message).toBe('test debug with log');
|
|
177
|
+
|
|
178
|
+
config.debug = true;
|
|
179
|
+
logger.debug('test debug with debug and log');
|
|
180
|
+
expect(consoleDebugSpy).toHaveBeenCalledWith('test debug with debug and log');
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('should dispatch a custom event when logging to localStorage', () => {
|
|
184
|
+
const config: Configuration = {
|
|
185
|
+
enabled: true, debug: false, log: true, endpoint: '', prime: false, distribution: 'community'
|
|
186
|
+
};
|
|
187
|
+
const logger = createLogger(config);
|
|
188
|
+
|
|
189
|
+
logger.info('test event');
|
|
190
|
+
|
|
191
|
+
expect(dispatchEventSpy).toHaveBeenCalledTimes(1);
|
|
192
|
+
const event = dispatchEventSpy.mock.calls[0][0] as CustomEvent;
|
|
193
|
+
|
|
194
|
+
expect(event.type).toBe('dynamicContentLog');
|
|
195
|
+
expect(event.detail.message).toBe('test event');
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('should limit the number of log messages in localStorage', () => {
|
|
199
|
+
const config: Configuration = {
|
|
200
|
+
enabled: true, debug: false, log: true, endpoint: '', prime: false, distribution: 'community'
|
|
201
|
+
};
|
|
202
|
+
const logger = createLogger(config);
|
|
203
|
+
|
|
204
|
+
// MAX_LOG_MESSAGES is 50
|
|
205
|
+
for (let i = 0; i < 60; i++) {
|
|
206
|
+
logger.info(`message ${ i }`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const logs = JSON.parse(mockLocalStorage[LOCAL_STORAGE_CONTENT_DEBUG_LOG]);
|
|
210
|
+
|
|
211
|
+
expect(logs).toHaveLength(50);
|
|
212
|
+
expect(logs[0].message).toBe('message 59'); // Most recent
|
|
213
|
+
expect(logs[49].message).toBe('message 10'); // Oldest
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('should not throw if localStorage is corrupted', () => {
|
|
217
|
+
const config: Configuration = {
|
|
218
|
+
enabled: true, debug: false, log: true, endpoint: '', prime: false, distribution: 'community'
|
|
219
|
+
};
|
|
220
|
+
const logger = createLogger(config);
|
|
221
|
+
|
|
222
|
+
mockLocalStorage[LOCAL_STORAGE_CONTENT_DEBUG_LOG] = 'this is not valid json';
|
|
223
|
+
|
|
224
|
+
expect(() => {
|
|
225
|
+
logger.info('test message');
|
|
226
|
+
}).not.toThrow();
|
|
227
|
+
|
|
228
|
+
// It should have overwritten the bad data
|
|
229
|
+
const logs = JSON.parse(mockLocalStorage[LOCAL_STORAGE_CONTENT_DEBUG_LOG]);
|
|
230
|
+
|
|
231
|
+
expect(logs).toHaveLength(1);
|
|
232
|
+
expect(logs[0].message).toBe('test message');
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { SETTING } from '@shell/config/settings';
|
|
2
|
+
import { isRancherPrime } from '@shell/config/version';
|
|
3
|
+
import { Configuration, Distribution } from './types';
|
|
4
|
+
import { MANAGEMENT } from '@shell/config/types';
|
|
5
|
+
|
|
6
|
+
// Default endpoint ($dist is either 'community' or 'prime')
|
|
7
|
+
const DEFAULT_ENDPOINT = 'https://updates.rancher.io/rancher/$dist/updates';
|
|
8
|
+
|
|
9
|
+
// We only support retrieving content from secure endpoints
|
|
10
|
+
const HTTPS_PREFIX = 'https://';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Get configuration data based on the distribution and Rancher settings
|
|
14
|
+
*
|
|
15
|
+
* @param getters Store getters to access the store
|
|
16
|
+
* @returns Dynamic Content configuration
|
|
17
|
+
*/
|
|
18
|
+
export function getConfig(getters: any): Configuration {
|
|
19
|
+
const prime = isRancherPrime();
|
|
20
|
+
const distribution: Distribution = prime ? 'prime' : 'community';
|
|
21
|
+
|
|
22
|
+
// Default configuration
|
|
23
|
+
const config: Configuration = {
|
|
24
|
+
enabled: true,
|
|
25
|
+
debug: false,
|
|
26
|
+
log: false,
|
|
27
|
+
endpoint: DEFAULT_ENDPOINT,
|
|
28
|
+
prime,
|
|
29
|
+
distribution,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// Update 'enabled' and 'endpoint' from Rancher settings, if applicable
|
|
33
|
+
try {
|
|
34
|
+
const enabledSetting = getters['management/byId'](MANAGEMENT.SETTING, SETTING.DYNAMIC_CONTENT_ENABLED);
|
|
35
|
+
|
|
36
|
+
if (enabledSetting?.value) {
|
|
37
|
+
// Any value other than 'false' means enabled (can't disable on Prime)
|
|
38
|
+
config.enabled = config.prime ? enabledSetting.value !== 'false' : true;
|
|
39
|
+
config.debug = enabledSetting.value === 'debug';
|
|
40
|
+
config.log = enabledSetting.value === 'log' || config.debug;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Can only override the url when Prime
|
|
44
|
+
const urlSetting = getters['management/byId'](MANAGEMENT.SETTING, SETTING.DYNAMIC_CONTENT_ENDPOINT);
|
|
45
|
+
|
|
46
|
+
// Are we Prime, do we have a URL and does the URL start with the https prefix? If so, use it
|
|
47
|
+
if (prime && urlSetting?.value && urlSetting.value.startsWith(HTTPS_PREFIX)) {
|
|
48
|
+
config.endpoint = urlSetting.value;
|
|
49
|
+
}
|
|
50
|
+
} catch (e) {
|
|
51
|
+
console.error('Error reading dynamic content settings', e); // eslint-disable-line no-console
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return config;
|
|
55
|
+
}
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This is the main dynamic content file that provides the 'fetchAndProcessDynamicContent' function
|
|
3
|
+
*
|
|
4
|
+
* This is the main entry point for reading and processing dynamic content
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import day from 'dayjs';
|
|
8
|
+
import * as jsyaml from 'js-yaml';
|
|
9
|
+
import semver from 'semver';
|
|
10
|
+
import { isAdminUser } from '@shell/store/type-map';
|
|
11
|
+
import { getVersionData } from '@shell/config/version';
|
|
12
|
+
import { processReleaseVersion } from './new-release';
|
|
13
|
+
import { processSupportNotices } from './support-notice';
|
|
14
|
+
import { Context, DynamicContent, VersionInfo } from './types';
|
|
15
|
+
import { createLogger, LOCAL_STORAGE_CONTENT_DEBUG_LOG } from './util';
|
|
16
|
+
import { getConfig } from './config';
|
|
17
|
+
import { SystemInfoProvider } from './info';
|
|
18
|
+
|
|
19
|
+
const FETCH_DELAY = 3 * 1000; // Short delay to let UI settle before we fetch the updates document
|
|
20
|
+
const FETCH_REQUEST_TIMEOUT = 15000; // Time out the request after 15 seconds
|
|
21
|
+
const FETCH_CONCURRENT_SECONDS = 30; // Time to wait to ignore another in-progress fetch (seconds)
|
|
22
|
+
|
|
23
|
+
export const UPDATE_DATE_FORMAT = 'YYYY-MM-DD'; // Format of the fetch date
|
|
24
|
+
|
|
25
|
+
const LOCAL_STORAGE_UPDATE_FETCH_DATE = 'rancher-updates-fetch-next'; // Local storage setting that holds the date when we should next try and fetch content
|
|
26
|
+
const LOCAL_STORAGE_UPDATE_CONTENT = 'rancher-updates-last-content'; // Local storage setting that holds the last fetched content
|
|
27
|
+
const LOCAL_STORAGE_UPDATE_ERRORS = 'rancher-updates-fetch-errors'; // Local storage setting that holds the count of contiguous errors
|
|
28
|
+
const LOCAL_STORAGE_UPDATE_FETCHING = 'rancher-updates-fetching'; // Local storage setting that holds the date and time of the last fetch that was started
|
|
29
|
+
|
|
30
|
+
const BACKOFFS = [1, 1, 1, 2, 2, 3, 5]; // Backoff in days for the contiguous number of errors (i.e. after 1 errors, we wait 1 day, after 3 errors, we wait 2 days, etc.)
|
|
31
|
+
|
|
32
|
+
const DEFAULT_RELEASE_NOTES_URL = 'https://github.com/rancher/rancher/releases/tag/v$version'; // Default release notes URL
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Fetch dynamic content if needed and process it if it has changed since we last checked
|
|
36
|
+
*/
|
|
37
|
+
export async function fetchAndProcessDynamicContent(dispatch: Function, getters: any, axios: any) {
|
|
38
|
+
// Check that the product is Rancher
|
|
39
|
+
// => Check that we are NOT in single product mode (e.g. Harvester)
|
|
40
|
+
const isSingleProduct = getters['isSingleProduct'];
|
|
41
|
+
|
|
42
|
+
if (!!isSingleProduct) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const config = getConfig(getters);
|
|
47
|
+
|
|
48
|
+
// If not enabled via the configuration, then just return
|
|
49
|
+
if (!config.enabled) {
|
|
50
|
+
console.log('Dynamic content disabled through configuration'); // eslint-disable-line no-console
|
|
51
|
+
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const logger = createLogger(config);
|
|
56
|
+
|
|
57
|
+
// Common context to pass through to functions for store access, logging, etc
|
|
58
|
+
const context: Context = {
|
|
59
|
+
dispatch,
|
|
60
|
+
getters,
|
|
61
|
+
axios,
|
|
62
|
+
logger,
|
|
63
|
+
config,
|
|
64
|
+
isAdmin: isAdminUser(getters),
|
|
65
|
+
settings: {
|
|
66
|
+
releaseNotesUrl: DEFAULT_RELEASE_NOTES_URL,
|
|
67
|
+
suseExtensions: [],
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
logger.debug('Read configuration', context.config);
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
// Fetch the dynamic content if required, otherwise return the cached content or empty object if no content available
|
|
75
|
+
const content = await fetchDynamicContent(context);
|
|
76
|
+
|
|
77
|
+
// Version metadata
|
|
78
|
+
const versionData = getVersionData();
|
|
79
|
+
const version = semver.coerce(versionData.Version);
|
|
80
|
+
|
|
81
|
+
if (!version || !content) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const versionInfo: VersionInfo = {
|
|
86
|
+
version,
|
|
87
|
+
isPrime: config.prime,
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// If not logging, then clear out any log data from local storage
|
|
91
|
+
if (!config.log) {
|
|
92
|
+
window.localStorage.removeItem(LOCAL_STORAGE_CONTENT_DEBUG_LOG);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (content?.settings) {
|
|
96
|
+
// Update the settings data from the content, so that it is has the settings with their defaults or values from the dynamic content payload
|
|
97
|
+
context.settings = {
|
|
98
|
+
...context.settings,
|
|
99
|
+
...content.settings
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// If the cached content has a debug version then use that as an override for the current version number
|
|
104
|
+
// This is only for debug and testing purposes
|
|
105
|
+
if (content.settings?.debugVersion) {
|
|
106
|
+
versionInfo.version = semver.coerce(content.settings.debugVersion);
|
|
107
|
+
logger.debug(`Overriding version number to ${ content.settings.debugVersion }`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// We always process the content in case the Rancher version has changed or the date means that an announcement/notification should now be shown
|
|
111
|
+
|
|
112
|
+
// New release notifications and support notifications are shown to ALL community users, but only to admin users when Prime
|
|
113
|
+
if (!config.prime || context.isAdmin) {
|
|
114
|
+
// New release notifications
|
|
115
|
+
processReleaseVersion(context, content.releases, versionInfo);
|
|
116
|
+
|
|
117
|
+
// EOM, EOL notifications
|
|
118
|
+
processSupportNotices(context, content.support, versionInfo);
|
|
119
|
+
}
|
|
120
|
+
} catch (e) {
|
|
121
|
+
logger.error('Error reading or processing dynamic content', e);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* We use a signal to timeout the connection
|
|
127
|
+
* For air-gapped environments, this ensures the request will timeout after FETCH_REQUEST_TIMEOUT
|
|
128
|
+
* This timeout is set relatively low (15s). The default, otherwise, is 2 minutes.
|
|
129
|
+
*
|
|
130
|
+
* @param timeoutMs Time in milliseconds after which the abort signal should signal
|
|
131
|
+
*/
|
|
132
|
+
function newRequestAbortSignal(timeoutMs: number) {
|
|
133
|
+
const abortController = new AbortController();
|
|
134
|
+
|
|
135
|
+
setTimeout(() => abortController.abort(), timeoutMs || 0);
|
|
136
|
+
|
|
137
|
+
return abortController.signal;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Update the local storage data that tracks when to next fetch content and how many consecutive errors we have had
|
|
142
|
+
*
|
|
143
|
+
* @param didError Indicates if we should update to record content retrieved without error or with error
|
|
144
|
+
*/
|
|
145
|
+
function updateFetchInfo(didError: boolean) {
|
|
146
|
+
if (!didError) {
|
|
147
|
+
// No error, so check again tomorrow and remove the backoff setting, so it will get its default next time
|
|
148
|
+
const nextFetch = day().add(1, 'day');
|
|
149
|
+
const nextFetchString = nextFetch.format(UPDATE_DATE_FORMAT);
|
|
150
|
+
|
|
151
|
+
window.localStorage.setItem(LOCAL_STORAGE_UPDATE_FETCH_DATE, nextFetchString);
|
|
152
|
+
window.localStorage.removeItem(LOCAL_STORAGE_UPDATE_ERRORS);
|
|
153
|
+
} else {
|
|
154
|
+
// Did error, read the backoff, increase and add to the date
|
|
155
|
+
const contiguousErrorsString = window.localStorage.getItem(LOCAL_STORAGE_UPDATE_ERRORS) || '0';
|
|
156
|
+
|
|
157
|
+
let contiguousErrors = parseInt(contiguousErrorsString, 10);
|
|
158
|
+
|
|
159
|
+
// Increase the number of errors that have happened in a row
|
|
160
|
+
contiguousErrors++;
|
|
161
|
+
|
|
162
|
+
// Once we reach the max backoff, just stick with it
|
|
163
|
+
if (contiguousErrors >= BACKOFFS.length ) {
|
|
164
|
+
contiguousErrors = BACKOFFS.length - 1;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Now find the backoff (days) given the error count and calculate the date of the next fetch
|
|
168
|
+
const daysToAdd = BACKOFFS[contiguousErrors];
|
|
169
|
+
const nextFetch = day().add(daysToAdd, 'day');
|
|
170
|
+
const nextFetchString = nextFetch.format(UPDATE_DATE_FORMAT);
|
|
171
|
+
|
|
172
|
+
window.localStorage.setItem(LOCAL_STORAGE_UPDATE_FETCH_DATE, nextFetchString);
|
|
173
|
+
window.localStorage.setItem(LOCAL_STORAGE_UPDATE_ERRORS, contiguousErrors.toString());
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Fetch dynamic content (if needed)
|
|
179
|
+
*/
|
|
180
|
+
export async function fetchDynamicContent(context: Context): Promise<Partial<DynamicContent> | undefined> {
|
|
181
|
+
const { getters, logger, config } = context;
|
|
182
|
+
|
|
183
|
+
// Check if we already have done an update check today
|
|
184
|
+
let content: Partial<DynamicContent> = {};
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
const today = day();
|
|
188
|
+
const todayString = today.format(UPDATE_DATE_FORMAT);
|
|
189
|
+
const nextFetch = window.localStorage.getItem(LOCAL_STORAGE_UPDATE_FETCH_DATE) || todayString;
|
|
190
|
+
|
|
191
|
+
// Read the cached content from local storage if possible
|
|
192
|
+
content = JSON.parse(window.localStorage.getItem(LOCAL_STORAGE_UPDATE_CONTENT) || '{}');
|
|
193
|
+
|
|
194
|
+
const nextFetchDay = day(nextFetch);
|
|
195
|
+
|
|
196
|
+
// Just in case next day gets reset to the past or corrupt, otherwise next fetch needs to not be in the future
|
|
197
|
+
if (!nextFetchDay.isValid() || !nextFetchDay.isAfter(today)) {
|
|
198
|
+
logger.info(`Performing update check on ${ todayString }`);
|
|
199
|
+
logger.debug(`Performing update check on ${ todayString }`);
|
|
200
|
+
|
|
201
|
+
const activeFetch = window.localStorage.getItem(LOCAL_STORAGE_UPDATE_FETCHING);
|
|
202
|
+
|
|
203
|
+
if (activeFetch) {
|
|
204
|
+
const activeFetchDate = day(activeFetch);
|
|
205
|
+
|
|
206
|
+
if (activeFetchDate.isValid() && today.diff(activeFetchDate, 'second') < FETCH_CONCURRENT_SECONDS) {
|
|
207
|
+
logger.debug('Already fetching dynamic content in another tab (or previous tab closed while fetching) - skipping');
|
|
208
|
+
|
|
209
|
+
return content;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Set the local storage key that indicates a tab is fetching the content - prevents other tabs doing so at the same time
|
|
214
|
+
window.localStorage.setItem(LOCAL_STORAGE_UPDATE_FETCHING, today.toString());
|
|
215
|
+
|
|
216
|
+
// Wait a short while before fetching dynamic content
|
|
217
|
+
await new Promise((resolve) => setTimeout(resolve, FETCH_DELAY));
|
|
218
|
+
|
|
219
|
+
const systemData = new SystemInfoProvider(getters, content?.settings || {});
|
|
220
|
+
const qs = systemData.buildQueryString();
|
|
221
|
+
const distribution = config.prime ? 'prime' : 'community';
|
|
222
|
+
const url = `${ config.endpoint.replace('$dist', distribution) }?${ qs }`;
|
|
223
|
+
|
|
224
|
+
logger.debug(`Fetching dynamic content from: ${ url.split('?')[0] }`, url);
|
|
225
|
+
|
|
226
|
+
// We use axios directly so that we can pass in the abort signal to implement the connection timeout
|
|
227
|
+
const res = await context.axios({
|
|
228
|
+
url,
|
|
229
|
+
method: 'get',
|
|
230
|
+
timeout: FETCH_REQUEST_TIMEOUT,
|
|
231
|
+
noApiCsrf: true,
|
|
232
|
+
withCredentials: false,
|
|
233
|
+
signal: newRequestAbortSignal(FETCH_REQUEST_TIMEOUT),
|
|
234
|
+
responseType: 'text' // We always want the raw text back - otherwise YAML gives text and JSON gives object
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// The data should be YAML (or JSON) in the 'data' attribute
|
|
238
|
+
if (res?.data) {
|
|
239
|
+
try {
|
|
240
|
+
content = jsyaml.load(res.data) as any;
|
|
241
|
+
|
|
242
|
+
window.localStorage.setItem(LOCAL_STORAGE_UPDATE_CONTENT, JSON.stringify(content));
|
|
243
|
+
|
|
244
|
+
// Update the last date now
|
|
245
|
+
updateFetchInfo(false);
|
|
246
|
+
} catch (e) {
|
|
247
|
+
logger.error('Failed to parse YAML/JSON from dynamic content package', e);
|
|
248
|
+
}
|
|
249
|
+
} else {
|
|
250
|
+
logger.error('Error fetching dynamic content package (unexpected data)');
|
|
251
|
+
}
|
|
252
|
+
} else {
|
|
253
|
+
logger.info(`Skipping update check for dynamic content - next check due on ${ nextFetch } (today is ${ todayString })`);
|
|
254
|
+
|
|
255
|
+
// If debug mode, then wait a bit to simulate the delay we would have had if we were fetching
|
|
256
|
+
if (config.debug) {
|
|
257
|
+
await new Promise((resolve) => setTimeout(resolve, FETCH_DELAY));
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
} catch (e) {
|
|
261
|
+
logger.error('Error occurred reading dynamic content', e);
|
|
262
|
+
|
|
263
|
+
// We had an error, so update data in local storage so that we try again appropriately next time
|
|
264
|
+
updateFetchInfo(true);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
logger.debug('End fetchDynamicContent');
|
|
268
|
+
|
|
269
|
+
// Remove the local storage key that indicates a tab is fetching the content
|
|
270
|
+
window.localStorage.removeItem(LOCAL_STORAGE_UPDATE_FETCHING);
|
|
271
|
+
|
|
272
|
+
return content;
|
|
273
|
+
}
|