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