21621: Add unit test for json copy to clipboard
[arvados.git] / services / workbench2 / src / common / config.ts
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 import Axios from 'axios';
6
7 export const WORKBENCH_CONFIG_URL =
8     process.env.REACT_APP_ARVADOS_CONFIG_URL || '/config.json';
9
10 interface WorkbenchConfig {
11     API_HOST: string;
12     VOCABULARY_URL?: string;
13     FILE_VIEWERS_CONFIG_URL?: string;
14 }
15
16 export interface ClusterConfigJSON {
17     API: {
18         UnfreezeProjectRequiresAdmin: boolean
19         MaxItemsPerResponse: number
20     },
21     ClusterID: string;
22     Containers: {
23         ReserveExtraRAM: number;
24     },
25     InstanceTypes?: {
26         [key: string]: {
27             AddedScratch: number;
28             CUDA?: {
29                 DeviceCount: number;
30                 DriverVersion: string;
31                 HardwareCapability: string;
32             };
33             IncludedScratch: number;
34             Preemptible: boolean;
35             Price: number;
36             ProviderType: string;
37             RAM: number;
38             VCPUs: number;
39         };
40     };
41     RemoteClusters: {
42         [key: string]: {
43             ActivateUsers: boolean
44             Host: string
45             Insecure: boolean
46             Proxy: boolean
47             Scheme: string
48         }
49     };
50     Services: {
51         Controller: {
52             ExternalURL: string;
53         };
54         Workbench1: {
55             ExternalURL: string;
56         };
57         Workbench2: {
58             ExternalURL: string;
59         };
60         Workbench: {
61             DisableSharingURLsUI: boolean;
62             ArvadosDocsite: string;
63             FileViewersConfigURL: string;
64             WelcomePageHTML: string;
65             InactivePageHTML: string;
66             SSHHelpPageHTML: string;
67             SSHHelpHostSuffix: string;
68             SiteName: string;
69             IdleTimeout: string;
70         };
71         Websocket: {
72             ExternalURL: string;
73         };
74         WebDAV: {
75             ExternalURL: string;
76         };
77         WebDAVDownload: {
78             ExternalURL: string;
79         };
80         WebShell: {
81             ExternalURL: string;
82         };
83     };
84     Workbench: {
85         DisableSharingURLsUI: boolean;
86         ArvadosDocsite: string;
87         FileViewersConfigURL: string;
88         WelcomePageHTML: string;
89         InactivePageHTML: string;
90         SSHHelpPageHTML: string;
91         SSHHelpHostSuffix: string;
92         SiteName: string;
93         IdleTimeout: string;
94         BannerUUID: string;
95         UserProfileFormFields: {};
96         UserProfileFormMessage: string;
97     };
98     Login: {
99         LoginCluster: string;
100         Google: {
101             Enable: boolean;
102         };
103         LDAP: {
104             Enable: boolean;
105         };
106         OpenIDConnect: {
107             Enable: boolean;
108         };
109         PAM: {
110             Enable: boolean;
111         };
112         SSO: {
113             Enable: boolean;
114         };
115         Test: {
116             Enable: boolean;
117         };
118     };
119     Collections: {
120         ForwardSlashNameSubstitution: string;
121         ManagedProperties?: {
122             [key: string]: {
123                 Function: string;
124                 Value: string;
125                 Protected?: boolean;
126             };
127         };
128         TrustAllContent: boolean;
129     };
130     Volumes: {
131         [key: string]: {
132             StorageClasses: {
133                 [key: string]: boolean;
134             };
135         };
136     };
137     Users: {
138         AnonymousUserToken: string;
139         SupportEmailAddress: string;
140     };
141 }
142
143 export class Config {
144     baseUrl!: string;
145     keepWebServiceUrl!: string;
146     keepWebInlineServiceUrl!: string;
147     remoteHosts!: {
148         [key: string]: string;
149     };
150     rootUrl!: string;
151     uuidPrefix!: string;
152     websocketUrl!: string;
153     workbenchUrl!: string;
154     workbench2Url!: string;
155     vocabularyUrl!: string;
156     fileViewersConfigUrl!: string;
157     loginCluster!: string;
158     clusterConfig!: ClusterConfigJSON;
159     apiRevision!: number;
160 }
161
162 export const buildConfig = (clusterConfig: ClusterConfigJSON): Config => {
163     const clusterConfigJSON = removeTrailingSlashes(clusterConfig);
164     const config = new Config();
165     config.rootUrl = clusterConfigJSON.Services.Controller.ExternalURL;
166     config.baseUrl = `${config.rootUrl}/${ARVADOS_API_PATH}`;
167     config.uuidPrefix = clusterConfigJSON.ClusterID;
168     config.websocketUrl = clusterConfigJSON.Services.Websocket.ExternalURL;
169     config.workbench2Url = clusterConfigJSON.Services.Workbench2.ExternalURL;
170     config.workbenchUrl = clusterConfigJSON.Services.Workbench1.ExternalURL;
171     config.keepWebServiceUrl =
172         clusterConfigJSON.Services.WebDAVDownload.ExternalURL;
173     config.keepWebInlineServiceUrl =
174         clusterConfigJSON.Services.WebDAV.ExternalURL;
175     config.loginCluster = clusterConfigJSON.Login.LoginCluster;
176     config.clusterConfig = clusterConfigJSON;
177     config.apiRevision = 0;
178     mapRemoteHosts(clusterConfigJSON, config);
179     return config;
180 };
181
182 export const getStorageClasses = (config: Config): string[] => {
183     const classes: Set<string> = new Set(['default']);
184     const volumes = config.clusterConfig.Volumes;
185     Object.keys(volumes).forEach((v) => {
186         Object.keys(volumes[v].StorageClasses || {}).forEach((sc) => {
187             if (volumes[v].StorageClasses[sc]) {
188                 classes.add(sc);
189             }
190         });
191     });
192     return Array.from(classes);
193 };
194
195 const getApiRevision = async (apiUrl: string) => {
196     try {
197         const dd = (await Axios.get<any>(`${apiUrl}/${DISCOVERY_DOC_PATH}`)).data;
198         return parseInt(dd.revision, 10) || 0;
199     } catch {
200         console.warn(
201             'Unable to get API Revision number, defaulting to zero. Some features may not work properly.'
202         );
203         return 0;
204     }
205 };
206
207 const removeTrailingSlashes = (
208     config: ClusterConfigJSON
209 ): ClusterConfigJSON => {
210     const svcs: any = {};
211     Object.keys(config.Services).forEach((s) => {
212         svcs[s] = config.Services[s];
213         if (svcs[s].hasOwnProperty('ExternalURL')) {
214             svcs[s].ExternalURL = svcs[s].ExternalURL.replace(/\/+$/, '');
215         }
216     });
217     return { ...config, Services: svcs };
218 };
219
220 export const fetchConfig = () => {
221     return Axios.get<WorkbenchConfig>(
222         WORKBENCH_CONFIG_URL + '?nocache=' + new Date().getTime()
223     )
224         .then((response) => response.data)
225         .catch(() => {
226             console.warn(
227                 `There was an exception getting the Workbench config file at ${WORKBENCH_CONFIG_URL}. Using defaults instead.`
228             );
229             return Promise.resolve(getDefaultConfig());
230         })
231         .then((workbenchConfig) => {
232             if (workbenchConfig.API_HOST === undefined) {
233                 throw new Error(
234                     `Unable to start Workbench. API_HOST is undefined in ${WORKBENCH_CONFIG_URL} or the environment.`
235                 );
236             }
237             return Axios.get<ClusterConfigJSON>(
238                 getClusterConfigURL(workbenchConfig.API_HOST)
239             ).then(async (response) => {
240                 const apiRevision = await getApiRevision(
241                     response.data.Services.Controller.ExternalURL.replace(/\/+$/, '')
242                 );
243                 const config = { ...buildConfig(response.data), apiRevision };
244                 const warnLocalConfig = (varName: string) =>
245                     console.warn(
246                         `A value for ${varName} was found in ${WORKBENCH_CONFIG_URL}. To use the Arvados centralized configuration instead, \
247 remove the entire ${varName} entry from ${WORKBENCH_CONFIG_URL}`
248                     );
249
250                 // Check if the workbench config has an entry for vocabulary and file viewer URLs
251                 // If so, use these values (even if it is an empty string), but print a console warning.
252                 // Otherwise, use the cluster config.
253                 let fileViewerConfigUrl;
254                 if (workbenchConfig.FILE_VIEWERS_CONFIG_URL !== undefined) {
255                     warnLocalConfig('FILE_VIEWERS_CONFIG_URL');
256                     fileViewerConfigUrl = workbenchConfig.FILE_VIEWERS_CONFIG_URL;
257                 } else {
258                     fileViewerConfigUrl =
259                         config.clusterConfig.Workbench.FileViewersConfigURL ||
260                         '/file-viewers-example.json';
261                 }
262                 config.fileViewersConfigUrl = fileViewerConfigUrl;
263
264                 if (workbenchConfig.VOCABULARY_URL !== undefined) {
265                     console.warn(
266                         `A value for VOCABULARY_URL was found in ${WORKBENCH_CONFIG_URL}. It will be ignored as the cluster already provides its own endpoint, you can safely remove it.`
267                     );
268                 }
269                 config.vocabularyUrl = getVocabularyURL(workbenchConfig.API_HOST);
270
271                 return { config, apiHost: workbenchConfig.API_HOST };
272             });
273         });
274 };
275
276 // Maps remote cluster hosts and removes the default RemoteCluster entry
277 export const mapRemoteHosts = (
278     clusterConfigJSON: ClusterConfigJSON,
279     config: Config
280 ) => {
281     config.remoteHosts = {};
282     Object.keys(clusterConfigJSON.RemoteClusters).forEach((k) => {
283         config.remoteHosts[k] = clusterConfigJSON.RemoteClusters[k].Host;
284     });
285     delete config.remoteHosts['*'];
286 };
287
288 export const mockClusterConfigJSON = (
289     config: Partial<ClusterConfigJSON>
290 ): ClusterConfigJSON => ({
291     API: {
292         UnfreezeProjectRequiresAdmin: false,
293         MaxItemsPerResponse: 1000,
294     },
295     ClusterID: '',
296     Containers: {
297         ReserveExtraRAM: 576716800,
298     },
299     RemoteClusters: {},
300     Services: {
301         Controller: { ExternalURL: '' },
302         Workbench1: { ExternalURL: '' },
303         Workbench2: { ExternalURL: '' },
304         Websocket: { ExternalURL: '' },
305         WebDAV: { ExternalURL: '' },
306         WebDAVDownload: { ExternalURL: '' },
307         WebShell: { ExternalURL: '' },
308         Workbench: {
309             DisableSharingURLsUI: false,
310             ArvadosDocsite: "",
311             FileViewersConfigURL: "",
312             WelcomePageHTML: "",
313             InactivePageHTML: "",
314             SSHHelpPageHTML: "",
315             SSHHelpHostSuffix: "",
316             SiteName: "",
317             IdleTimeout: "0s"
318         },
319     },
320     Workbench: {
321         DisableSharingURLsUI: false,
322         ArvadosDocsite: '',
323         FileViewersConfigURL: '',
324         WelcomePageHTML: '',
325         InactivePageHTML: '',
326         SSHHelpPageHTML: '',
327         SSHHelpHostSuffix: '',
328         SiteName: '',
329         IdleTimeout: '0s',
330         BannerUUID: "",
331         UserProfileFormFields: {},
332         UserProfileFormMessage: '',
333     },
334     Login: {
335         LoginCluster: '',
336         Google: {
337             Enable: false,
338         },
339         LDAP: {
340             Enable: false,
341         },
342         OpenIDConnect: {
343             Enable: false,
344         },
345         PAM: {
346             Enable: false,
347         },
348         SSO: {
349             Enable: false,
350         },
351         Test: {
352             Enable: false,
353         },
354     },
355     Collections: {
356         ForwardSlashNameSubstitution: '',
357         TrustAllContent: false,
358     },
359     Volumes: {},
360     Users: {
361         AnonymousUserToken: "",
362         SupportEmailAddress: "arvados@example.com",
363     },
364     ...config,
365 });
366
367 export const mockConfig = (config: Partial<Config>): Config => ({
368     baseUrl: '',
369     keepWebServiceUrl: '',
370     keepWebInlineServiceUrl: '',
371     remoteHosts: {},
372     rootUrl: '',
373     uuidPrefix: '',
374     websocketUrl: '',
375     workbenchUrl: '',
376     workbench2Url: '',
377     vocabularyUrl: '',
378     fileViewersConfigUrl: '',
379     loginCluster: '',
380     clusterConfig: mockClusterConfigJSON({}),
381     apiRevision: 0,
382     ...config,
383 });
384
385 const getDefaultConfig = (): WorkbenchConfig => {
386     let apiHost = '';
387     const envHost = process.env.REACT_APP_ARVADOS_API_HOST;
388     if (envHost !== undefined) {
389         console.warn(`Using default API host ${envHost}.`);
390         apiHost = envHost;
391     } else {
392         console.warn(
393             `No API host was found in the environment. Workbench may not be able to communicate with Arvados components.`
394         );
395     }
396     return {
397         API_HOST: apiHost,
398         VOCABULARY_URL: undefined,
399         FILE_VIEWERS_CONFIG_URL: undefined,
400     };
401 };
402
403 export const ARVADOS_API_PATH = 'arvados/v1';
404 export const CLUSTER_CONFIG_PATH = 'arvados/v1/config';
405 export const VOCABULARY_PATH = 'arvados/v1/vocabulary';
406 export const DISCOVERY_DOC_PATH = 'discovery/v1/apis/arvados/v1/rest';
407 export const getClusterConfigURL = (apiHost: string) =>
408     `https://${apiHost}/${CLUSTER_CONFIG_PATH}?nocache=${new Date().getTime()}`;
409 export const getVocabularyURL = (apiHost: string) =>
410     `https://${apiHost}/${VOCABULARY_PATH}?nocache=${new Date().getTime()}`;