argo-cd

Форк
0
532 строки · 35.8 Кб
1
import {AutocompleteField, Checkbox, DataLoader, DropDownMenu, FormField, HelpIcon, Select} from 'argo-ui';
2
import * as deepMerge from 'deepmerge';
3
import * as React from 'react';
4
import {FieldApi, Form, FormApi, FormField as ReactFormField, Text} from 'react-form';
5
import {RevisionHelpIcon, YamlEditor} from '../../../shared/components';
6
import * as models from '../../../shared/models';
7
import {services} from '../../../shared/services';
8
import {ApplicationParameters} from '../application-parameters/application-parameters';
9
import {ApplicationRetryOptions} from '../application-retry-options/application-retry-options';
10
import {ApplicationSyncOptionsField} from '../application-sync-options/application-sync-options';
11
import {RevisionFormField} from '../revision-form-field/revision-form-field';
12
import {SetFinalizerOnApplication} from './set-finalizer-on-application';
13
import './application-create-panel.scss';
14
import {getAppDefaultSource} from '../utils';
15

16
const jsonMergePatch = require('json-merge-patch');
17

18
const appTypes = new Array<{field: string; type: models.AppSourceType}>(
19
    {type: 'Helm', field: 'helm'},
20
    {type: 'Kustomize', field: 'kustomize'},
21
    {type: 'Directory', field: 'directory'},
22
    {type: 'Plugin', field: 'plugin'}
23
);
24

25
const DEFAULT_APP: Partial<models.Application> = {
26
    apiVersion: 'argoproj.io/v1alpha1',
27
    kind: 'Application',
28
    metadata: {
29
        name: ''
30
    },
31
    spec: {
32
        destination: {
33
            name: '',
34
            namespace: '',
35
            server: ''
36
        },
37
        source: {
38
            path: '',
39
            repoURL: '',
40
            targetRevision: 'HEAD'
41
        },
42
        sources: [],
43
        project: ''
44
    }
45
};
46

47
const AutoSyncFormField = ReactFormField((props: {fieldApi: FieldApi; className: string}) => {
48
    const manual = 'Manual';
49
    const auto = 'Automatic';
50
    const {
51
        fieldApi: {getValue, setValue}
52
    } = props;
53
    const automated = getValue() as models.Automated;
54

55
    return (
56
        <React.Fragment>
57
            <label>Sync Policy</label>
58
            <Select
59
                value={automated ? auto : manual}
60
                options={[manual, auto]}
61
                onChange={opt => {
62
                    setValue(opt.value === auto ? {prune: false, selfHeal: false} : null);
63
                }}
64
            />
65
            {automated && (
66
                <div className='application-create-panel__sync-params'>
67
                    <div className='checkbox-container'>
68
                        <Checkbox onChange={val => setValue({...automated, prune: val})} checked={!!automated.prune} id='policyPrune' />
69
                        <label htmlFor='policyPrune'>Prune Resources</label>
70
                        <HelpIcon title='If checked, Argo will delete resources if they are no longer defined in Git' />
71
                    </div>
72
                    <div className='checkbox-container'>
73
                        <Checkbox onChange={val => setValue({...automated, selfHeal: val})} checked={!!automated.selfHeal} id='policySelfHeal' />
74
                        <label htmlFor='policySelfHeal'>Self Heal</label>
75
                        <HelpIcon title='If checked, Argo will force the state defined in Git into the cluster when a deviation in the cluster is detected' />
76
                    </div>
77
                </div>
78
            )}
79
        </React.Fragment>
80
    );
81
});
82

83
function normalizeAppSource(app: models.Application, type: string): boolean {
84
    const source = getAppDefaultSource(app);
85
    const repoType = (source.hasOwnProperty('chart') && 'helm') || 'git';
86
    if (repoType !== type) {
87
        if (type === 'git') {
88
            source.path = source.chart;
89
            delete source.chart;
90
            source.targetRevision = 'HEAD';
91
        } else {
92
            source.chart = source.path;
93
            delete source.path;
94
            source.targetRevision = '';
95
        }
96
        return true;
97
    }
98
    return false;
99
}
100

101
export const ApplicationCreatePanel = (props: {
102
    app: models.Application;
103
    onAppChanged: (app: models.Application) => any;
104
    createApp: (app: models.Application) => any;
105
    getFormApi: (api: FormApi) => any;
106
}) => {
107
    const [yamlMode, setYamlMode] = React.useState(false);
108
    const [explicitPathType, setExplicitPathType] = React.useState<{path: string; type: models.AppSourceType}>(null);
109
    const [destFormat, setDestFormat] = React.useState('URL');
110
    const [retry, setRetry] = React.useState(false);
111
    const app = deepMerge(DEFAULT_APP, props.app || {});
112

113
    React.useEffect(() => {
114
        if (app?.spec?.destination?.name && app.spec.destination.name !== '') {
115
            setDestFormat('NAME');
116
        } else {
117
            setDestFormat('URL');
118
        }
119
    }, []);
120

121
    function normalizeTypeFields(formApi: FormApi, type: models.AppSourceType) {
122
        const appToNormalize = formApi.getFormState().values;
123
        for (const item of appTypes) {
124
            if (item.type !== type) {
125
                delete appToNormalize.spec.source[item.field];
126
            }
127
        }
128
        formApi.setAllValues(appToNormalize);
129
    }
130

131
    return (
132
        <React.Fragment>
133
            <DataLoader
134
                key='creation-deps'
135
                load={() =>
136
                    Promise.all([
137
                        services.projects.list('items.metadata.name').then(projects => projects.map(proj => proj.metadata.name).sort()),
138
                        services.clusters.list().then(clusters => clusters.sort()),
139
                        services.repos.list()
140
                    ]).then(([projects, clusters, reposInfo]) => ({projects, clusters, reposInfo}))
141
                }>
142
                {({projects, clusters, reposInfo}) => {
143
                    const repos = reposInfo.map(info => info.repo).sort();
144
                    const repoInfo = reposInfo.find(info => info.repo === app.spec.source.repoURL);
145
                    if (repoInfo) {
146
                        normalizeAppSource(app, repoInfo.type || 'git');
147
                    }
148
                    return (
149
                        <div className='application-create-panel'>
150
                            {(yamlMode && (
151
                                <YamlEditor
152
                                    minHeight={800}
153
                                    initialEditMode={true}
154
                                    input={app}
155
                                    onCancel={() => setYamlMode(false)}
156
                                    onSave={async patch => {
157
                                        props.onAppChanged(jsonMergePatch.apply(app, JSON.parse(patch)));
158
                                        setYamlMode(false);
159
                                        return true;
160
                                    }}
161
                                />
162
                            )) || (
163
                                <Form
164
                                    validateError={(a: models.Application) => ({
165
                                        'metadata.name': !a.metadata.name && 'Application Name is required',
166
                                        'spec.project': !a.spec.project && 'Project Name is required',
167
                                        'spec.source.repoURL': !a.spec.source.repoURL && 'Repository URL is required',
168
                                        'spec.source.targetRevision': !a.spec.source.targetRevision && a.spec.source.hasOwnProperty('chart') && 'Version is required',
169
                                        'spec.source.path': !a.spec.source.path && !a.spec.source.chart && 'Path is required',
170
                                        'spec.source.chart': !a.spec.source.path && !a.spec.source.chart && 'Chart is required',
171
                                        // Verify cluster URL when there is no cluster name field or the name value is empty
172
                                        'spec.destination.server':
173
                                            !a.spec.destination.server &&
174
                                            (!a.spec.destination.hasOwnProperty('name') || a.spec.destination.name === '') &&
175
                                            'Cluster URL is required',
176
                                        // Verify cluster name when there is no cluster URL field or the URL value is empty
177
                                        'spec.destination.name':
178
                                            !a.spec.destination.name &&
179
                                            (!a.spec.destination.hasOwnProperty('server') || a.spec.destination.server === '') &&
180
                                            'Cluster name is required'
181
                                    })}
182
                                    defaultValues={app}
183
                                    formDidUpdate={state => props.onAppChanged(state.values as any)}
184
                                    onSubmit={props.createApp}
185
                                    getApi={props.getFormApi}>
186
                                    {api => {
187
                                        const generalPanel = () => (
188
                                            <div className='white-box'>
189
                                                <p>GENERAL</p>
190
                                                {/*
191
                                                    Need to specify "type='button'" because the default type 'submit'
192
                                                    will activate yaml mode whenever enter is pressed while in the panel.
193
                                                    This causes problems with some entry fields that require enter to be
194
                                                    pressed for the value to be accepted.
195

196
                                                    See https://github.com/argoproj/argo-cd/issues/4576
197
                                                */}
198
                                                {!yamlMode && (
199
                                                    <button
200
                                                        type='button'
201
                                                        className='argo-button argo-button--base application-create-panel__yaml-button'
202
                                                        onClick={() => setYamlMode(true)}>
203
                                                        Edit as YAML
204
                                                    </button>
205
                                                )}
206
                                                <div className='argo-form-row'>
207
                                                    <FormField
208
                                                        formApi={api}
209
                                                        label='Application Name'
210
                                                        qeId='application-create-field-app-name'
211
                                                        field='metadata.name'
212
                                                        component={Text}
213
                                                    />
214
                                                </div>
215
                                                <div className='argo-form-row'>
216
                                                    <FormField
217
                                                        formApi={api}
218
                                                        label='Project Name'
219
                                                        qeId='application-create-field-project'
220
                                                        field='spec.project'
221
                                                        component={AutocompleteField}
222
                                                        componentProps={{
223
                                                            items: projects,
224
                                                            filterSuggestions: true
225
                                                        }}
226
                                                    />
227
                                                </div>
228
                                                <div className='argo-form-row'>
229
                                                    <FormField
230
                                                        formApi={api}
231
                                                        field='spec.syncPolicy.automated'
232
                                                        qeId='application-create-field-sync-policy'
233
                                                        component={AutoSyncFormField}
234
                                                    />
235
                                                </div>
236
                                                <div className='argo-form-row'>
237
                                                    <FormField formApi={api} field='metadata.finalizers' component={SetFinalizerOnApplication} />
238
                                                </div>
239
                                                <div className='argo-form-row'>
240
                                                    <label>Sync Options</label>
241
                                                    <FormField formApi={api} field='spec.syncPolicy.syncOptions' component={ApplicationSyncOptionsField} />
242
                                                    <ApplicationRetryOptions
243
                                                        formApi={api}
244
                                                        field='spec.syncPolicy.retry'
245
                                                        retry={retry || (api.getFormState().values.spec.syncPolicy && api.getFormState().values.spec.syncPolicy.retry)}
246
                                                        setRetry={setRetry}
247
                                                        initValues={api.getFormState().values.spec.syncPolicy ? api.getFormState().values.spec.syncPolicy.retry : null}
248
                                                    />
249
                                                </div>
250
                                            </div>
251
                                        );
252

253
                                        const repoType = (api.getFormState().values.spec.source.hasOwnProperty('chart') && 'helm') || 'git';
254
                                        const sourcePanel = () => (
255
                                            <div className='white-box'>
256
                                                <p>SOURCE</p>
257
                                                <div className='row argo-form-row'>
258
                                                    <div className='columns small-10'>
259
                                                        <FormField
260
                                                            formApi={api}
261
                                                            label='Repository URL'
262
                                                            qeId='application-create-field-repository-url'
263
                                                            field='spec.source.repoURL'
264
                                                            component={AutocompleteField}
265
                                                            componentProps={{items: repos}}
266
                                                        />
267
                                                    </div>
268
                                                    <div className='columns small-2'>
269
                                                        <div style={{paddingTop: '1.5em'}}>
270
                                                            {(repoInfo && (
271
                                                                <React.Fragment>
272
                                                                    <span>{(repoInfo.type || 'git').toUpperCase()}</span> <i className='fa fa-check' />
273
                                                                </React.Fragment>
274
                                                            )) || (
275
                                                                <DropDownMenu
276
                                                                    anchor={() => (
277
                                                                        <p>
278
                                                                            {repoType.toUpperCase()} <i className='fa fa-caret-down' />
279
                                                                        </p>
280
                                                                    )}
281
                                                                    qeId='application-create-dropdown-source-repository'
282
                                                                    items={['git', 'helm'].map((type: 'git' | 'helm') => ({
283
                                                                        title: type.toUpperCase(),
284
                                                                        action: () => {
285
                                                                            if (repoType !== type) {
286
                                                                                const updatedApp = api.getFormState().values as models.Application;
287
                                                                                if (normalizeAppSource(updatedApp, type)) {
288
                                                                                    api.setAllValues(updatedApp);
289
                                                                                }
290
                                                                            }
291
                                                                        }
292
                                                                    }))}
293
                                                                />
294
                                                            )}
295
                                                        </div>
296
                                                    </div>
297
                                                </div>
298
                                                {(repoType === 'git' && (
299
                                                    <React.Fragment>
300
                                                        <RevisionFormField formApi={api} helpIconTop={'2.5em'} repoURL={app.spec.source.repoURL} />
301
                                                        <div className='argo-form-row'>
302
                                                            <DataLoader
303
                                                                input={{repoURL: app.spec.source.repoURL, revision: app.spec.source.targetRevision}}
304
                                                                load={async src =>
305
                                                                    (src.repoURL &&
306
                                                                        services.repos
307
                                                                            .apps(src.repoURL, src.revision, app.metadata.name, app.spec.project)
308
                                                                            .then(apps => Array.from(new Set(apps.map(item => item.path))).sort())
309
                                                                            .catch(() => new Array<string>())) ||
310
                                                                    new Array<string>()
311
                                                                }>
312
                                                                {(apps: string[]) => (
313
                                                                    <FormField
314
                                                                        formApi={api}
315
                                                                        label='Path'
316
                                                                        qeId='application-create-field-path'
317
                                                                        field='spec.source.path'
318
                                                                        component={AutocompleteField}
319
                                                                        componentProps={{
320
                                                                            items: apps,
321
                                                                            filterSuggestions: true
322
                                                                        }}
323
                                                                    />
324
                                                                )}
325
                                                            </DataLoader>
326
                                                        </div>
327
                                                    </React.Fragment>
328
                                                )) || (
329
                                                    <DataLoader
330
                                                        input={{repoURL: app.spec.source.repoURL}}
331
                                                        load={async src =>
332
                                                            (src.repoURL && services.repos.charts(src.repoURL).catch(() => new Array<models.HelmChart>())) ||
333
                                                            new Array<models.HelmChart>()
334
                                                        }>
335
                                                        {(charts: models.HelmChart[]) => {
336
                                                            const selectedChart = charts.find(chart => chart.name === api.getFormState().values.spec.source.chart);
337
                                                            return (
338
                                                                <div className='row argo-form-row'>
339
                                                                    <div className='columns small-10'>
340
                                                                        <FormField
341
                                                                            formApi={api}
342
                                                                            label='Chart'
343
                                                                            field='spec.source.chart'
344
                                                                            component={AutocompleteField}
345
                                                                            componentProps={{
346
                                                                                items: charts.map(chart => chart.name),
347
                                                                                filterSuggestions: true
348
                                                                            }}
349
                                                                        />
350
                                                                    </div>
351
                                                                    <div className='columns small-2'>
352
                                                                        <FormField
353
                                                                            formApi={api}
354
                                                                            field='spec.source.targetRevision'
355
                                                                            component={AutocompleteField}
356
                                                                            componentProps={{
357
                                                                                items: (selectedChart && selectedChart.versions) || []
358
                                                                            }}
359
                                                                        />
360
                                                                        <RevisionHelpIcon type='helm' />
361
                                                                    </div>
362
                                                                </div>
363
                                                            );
364
                                                        }}
365
                                                    </DataLoader>
366
                                                )}
367
                                            </div>
368
                                        );
369
                                        const destinationPanel = () => (
370
                                            <div className='white-box'>
371
                                                <p>DESTINATION</p>
372
                                                <div className='row argo-form-row'>
373
                                                    {(destFormat.toUpperCase() === 'URL' && (
374
                                                        <div className='columns small-10'>
375
                                                            <FormField
376
                                                                formApi={api}
377
                                                                label='Cluster URL'
378
                                                                qeId='application-create-field-cluster-url'
379
                                                                field='spec.destination.server'
380
                                                                componentProps={{items: clusters.map(cluster => cluster.server)}}
381
                                                                component={AutocompleteField}
382
                                                            />
383
                                                        </div>
384
                                                    )) || (
385
                                                        <div className='columns small-10'>
386
                                                            <FormField
387
                                                                formApi={api}
388
                                                                label='Cluster Name'
389
                                                                qeId='application-create-field-cluster-name'
390
                                                                field='spec.destination.name'
391
                                                                componentProps={{items: clusters.map(cluster => cluster.name)}}
392
                                                                component={AutocompleteField}
393
                                                            />
394
                                                        </div>
395
                                                    )}
396
                                                    <div className='columns small-2'>
397
                                                        <div style={{paddingTop: '1.5em'}}>
398
                                                            <DropDownMenu
399
                                                                anchor={() => (
400
                                                                    <p>
401
                                                                        {destFormat} <i className='fa fa-caret-down' />
402
                                                                    </p>
403
                                                                )}
404
                                                                qeId='application-create-dropdown-destination'
405
                                                                items={['URL', 'NAME'].map((type: 'URL' | 'NAME') => ({
406
                                                                    title: type,
407
                                                                    action: () => {
408
                                                                        if (destFormat !== type) {
409
                                                                            const updatedApp = api.getFormState().values as models.Application;
410
                                                                            if (type === 'URL') {
411
                                                                                delete updatedApp.spec.destination.name;
412
                                                                            } else {
413
                                                                                delete updatedApp.spec.destination.server;
414
                                                                            }
415
                                                                            api.setAllValues(updatedApp);
416
                                                                            setDestFormat(type);
417
                                                                        }
418
                                                                    }
419
                                                                }))}
420
                                                            />
421
                                                        </div>
422
                                                    </div>
423
                                                </div>
424
                                                <div className='argo-form-row'>
425
                                                    <FormField
426
                                                        qeId='application-create-field-namespace'
427
                                                        formApi={api}
428
                                                        label='Namespace'
429
                                                        field='spec.destination.namespace'
430
                                                        component={Text}
431
                                                    />
432
                                                </div>
433
                                            </div>
434
                                        );
435

436
                                        const typePanel = () => (
437
                                            <DataLoader
438
                                                input={{
439
                                                    repoURL: app.spec.source.repoURL,
440
                                                    path: app.spec.source.path,
441
                                                    chart: app.spec.source.chart,
442
                                                    targetRevision: app.spec.source.targetRevision,
443
                                                    appName: app.metadata.name
444
                                                }}
445
                                                load={async src => {
446
                                                    if (src.repoURL && src.targetRevision && (src.path || src.chart)) {
447
                                                        return services.repos.appDetails(src, src.appName, app.spec.project).catch(() => ({
448
                                                            type: 'Directory',
449
                                                            details: {}
450
                                                        }));
451
                                                    } else {
452
                                                        return {
453
                                                            type: 'Directory',
454
                                                            details: {}
455
                                                        };
456
                                                    }
457
                                                }}>
458
                                                {(details: models.RepoAppDetails) => {
459
                                                    const type = (explicitPathType && explicitPathType.path === app.spec.source.path && explicitPathType.type) || details.type;
460
                                                    if (details.type !== type) {
461
                                                        switch (type) {
462
                                                            case 'Helm':
463
                                                                details = {
464
                                                                    type,
465
                                                                    path: details.path,
466
                                                                    helm: {name: '', valueFiles: [], path: '', parameters: [], fileParameters: []}
467
                                                                };
468
                                                                break;
469
                                                            case 'Kustomize':
470
                                                                details = {type, path: details.path, kustomize: {path: ''}};
471
                                                                break;
472
                                                            case 'Plugin':
473
                                                                details = {type, path: details.path, plugin: {name: '', env: []}};
474
                                                                break;
475
                                                            // Directory
476
                                                            default:
477
                                                                details = {type, path: details.path, directory: {}};
478
                                                                break;
479
                                                        }
480
                                                    }
481
                                                    return (
482
                                                        <React.Fragment>
483
                                                            <DropDownMenu
484
                                                                anchor={() => (
485
                                                                    <p>
486
                                                                        {type} <i className='fa fa-caret-down' />
487
                                                                    </p>
488
                                                                )}
489
                                                                qeId='application-create-dropdown-source'
490
                                                                items={appTypes.map(item => ({
491
                                                                    title: item.type,
492
                                                                    action: () => {
493
                                                                        setExplicitPathType({type: item.type, path: app.spec.source.path});
494
                                                                        normalizeTypeFields(api, item.type);
495
                                                                    }
496
                                                                }))}
497
                                                            />
498
                                                            <ApplicationParameters
499
                                                                noReadonlyMode={true}
500
                                                                application={app}
501
                                                                details={details}
502
                                                                save={async updatedApp => {
503
                                                                    api.setAllValues(updatedApp);
504
                                                                }}
505
                                                            />
506
                                                        </React.Fragment>
507
                                                    );
508
                                                }}
509
                                            </DataLoader>
510
                                        );
511

512
                                        return (
513
                                            <form onSubmit={api.submitForm} role='form' className='width-control'>
514
                                                {generalPanel()}
515

516
                                                {sourcePanel()}
517

518
                                                {destinationPanel()}
519

520
                                                {typePanel()}
521
                                            </form>
522
                                        );
523
                                    }}
524
                                </Form>
525
                            )}
526
                        </div>
527
                    );
528
                }}
529
            </DataLoader>
530
        </React.Fragment>
531
    );
532
};
533

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.