@rancher/shell 3.0.12-rc.4 → 3.0.12-rc.5
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/styles/global/_button.scss +1 -1
- package/assets/translations/en-us.yaml +39 -10
- package/components/ActionDropdownShell.vue +5 -3
- package/components/ButtonGroup.vue +26 -1
- package/components/CruResource.vue +51 -2
- package/components/PromptRestore.vue +93 -32
- package/components/Questions/index.vue +1 -0
- package/components/ResourceTable.vue +1 -0
- package/components/SortableTable/index.vue +4 -3
- package/components/Wizard.vue +14 -1
- package/components/__tests__/ButtonGroup.test.ts +56 -0
- package/components/__tests__/PromptRestore.test.ts +169 -19
- package/components/fleet/GitRepoAdvancedTab.vue +1 -0
- package/components/fleet/GitRepoMetadataTab.vue +5 -0
- package/components/fleet/HelmOpAppCoConfigTab.vue +4 -0
- package/components/fleet/HelmOpMetadataTab.vue +5 -0
- package/components/form/FileSelector.vue +39 -1
- package/components/form/PrivateRegistry.constants.ts +7 -0
- package/components/form/PrivateRegistry.vue +253 -18
- package/components/form/SelectOrCreateAuthSecret.vue +140 -17
- package/components/form/__tests__/FileSelector.test.ts +23 -0
- package/components/form/__tests__/PrivateRegistry.test.ts +463 -73
- package/components/form/__tests__/SelectOrCreateAuthSecret.test.ts +122 -0
- package/components/formatter/EtcdSnapshotName.vue +73 -0
- package/components/nav/Header.vue +8 -1
- package/components/templates/default.vue +7 -0
- package/config/features.js +1 -0
- package/config/labels-annotations.js +2 -0
- package/config/product/manager.js +6 -0
- package/config/secret.ts +10 -0
- package/config/settings.ts +6 -2
- package/config/types.js +7 -0
- package/detail/provisioning.cattle.io.cluster.vue +79 -3
- package/dialog/RotateEncryptionKeyDialog.vue +33 -9
- package/dialog/__tests__/RotateEncryptionKeyDialog.test.ts +78 -0
- package/edit/__tests__/fleet.cattle.io.gitrepo.test.ts +92 -0
- package/edit/__tests__/fleet.cattle.io.helmop.test.ts +101 -0
- package/edit/__tests__/management.cattle.io.setting.test.ts +2 -1
- package/edit/compliance.cattle.io.clusterscanprofile.vue +39 -41
- package/edit/fleet.cattle.io.gitrepo.vue +70 -16
- package/edit/fleet.cattle.io.helmop.vue +51 -5
- package/edit/helm.cattle.io.projecthelmchart.vue +1 -0
- package/edit/{management.cattle.io.setting.vue → management.cattle.io.setting/index.vue} +32 -9
- package/edit/management.cattle.io.setting/system-default-registry-pull-secrets.vue +81 -0
- package/edit/provisioning.cattle.io.cluster/SelectCredential.vue +3 -12
- package/edit/provisioning.cattle.io.cluster/__tests__/rke2.test.ts +18 -0
- package/edit/provisioning.cattle.io.cluster/rke2.vue +5 -1
- package/edit/provisioning.cattle.io.cluster/tabs/etcd/index.vue +0 -1
- package/edit/provisioning.cattle.io.cluster/tabs/registries/index.vue +14 -55
- package/models/__tests__/provisioning.cattle.io.cluster.test.ts +156 -0
- package/models/__tests__/secret.test.ts +68 -1
- package/models/management.cattle.io.cluster.js +21 -3
- package/models/pod.js +13 -2
- package/models/provisioning.cattle.io.cluster.js +59 -9
- package/models/rke.cattle.io.etcdsnapshot.js +17 -9
- package/models/secret.js +19 -0
- package/models/workload.js +12 -7
- package/package.json +1 -1
- package/pages/c/_cluster/apps/charts/__tests__/install.test.ts +485 -107
- package/pages/c/_cluster/apps/charts/install.vue +114 -28
- package/pkg/require-asset.lib.js +25 -0
- package/pkg/vue.config.js +7 -0
- package/plugins/dashboard-store/__tests__/resource-class.test.ts +84 -0
- package/plugins/dashboard-store/getters.js +0 -1
- package/plugins/dashboard-store/resource-class.js +52 -12
- package/rancher-components/Form/TextArea/TextAreaAutoGrow.vue +30 -0
- package/rancher-components/Form/TextArea/__tests__/TextAreaAutoGrow.test.ts +95 -0
- package/rancher-components/RcButton/index.ts +1 -1
- package/rancher-components/RcDropdown/RcDropdownTrigger.vue +6 -1
- package/store/__tests__/features.test.ts +131 -0
- package/store/__tests__/growl.test.ts +374 -0
- package/store/__tests__/modal.test.ts +131 -0
- package/store/__tests__/slideInPanel.test.ts +88 -0
- package/store/__tests__/type-map.utils.test.ts +433 -0
- package/store/features.js +4 -0
- package/types/shell/index.d.ts +62 -0
- package/utils/__tests__/operation-cr.test.ts +34 -0
- package/utils/operation-cr.js +19 -0
- package/utils/require-asset.ts +7 -0
- package/utils/validators/__tests__/private-registry.test.ts +27 -15
- package/utils/validators/private-registry.ts +15 -4
|
@@ -1,24 +1,185 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { ref, watch } from 'vue';
|
|
2
|
+
import { ref, watch, onMounted, computed } from 'vue';
|
|
3
|
+
import { useStore } from 'vuex';
|
|
4
|
+
import { useI18n } from '@shell/composables/useI18n';
|
|
3
5
|
import Banner from '@components/Banner/Banner.vue';
|
|
4
6
|
import { Checkbox } from '@components/Form/Checkbox';
|
|
5
7
|
import LabeledInput from '@components/Form/LabeledInput/LabeledInput.vue';
|
|
8
|
+
import SelectOrCreateAuthSecret from '@shell/components/form/SelectOrCreateAuthSecret.vue';
|
|
9
|
+
import { MANAGEMENT } from '@shell/config/types';
|
|
10
|
+
import { SETTING } from '@shell/config/settings';
|
|
11
|
+
import { PRIVATE_REGISTRY_CONTEXT } from '@shell/components/form/PrivateRegistry.constants';
|
|
12
|
+
import type { PrivateRegistryContext } from '@shell/components/form/PrivateRegistry.constants';
|
|
6
13
|
|
|
7
|
-
const props = defineProps<{
|
|
14
|
+
const props = withDefaults(defineProps<{
|
|
8
15
|
value?: string | null;
|
|
9
16
|
enabled?: boolean;
|
|
10
17
|
mode?: string;
|
|
11
18
|
rules?: Function[];
|
|
12
19
|
checkboxTestId?: string;
|
|
13
20
|
inputTestId?: string;
|
|
14
|
-
|
|
21
|
+
pullSecret?: string;
|
|
22
|
+
registerBeforeHook?: Function;
|
|
23
|
+
context?: PrivateRegistryContext;
|
|
24
|
+
defaultRegistry?: string;
|
|
25
|
+
namespace?: string;
|
|
26
|
+
inStore?: string;
|
|
27
|
+
showPullSecrets?: boolean;
|
|
28
|
+
noneLabel?: string | null;
|
|
29
|
+
skipPullSecrets?: boolean;
|
|
30
|
+
repoDefaultPullSecrets?: string[];
|
|
31
|
+
existingValuesPullSecrets?: string[];
|
|
32
|
+
}>(), {
|
|
33
|
+
value: undefined,
|
|
34
|
+
enabled: false,
|
|
35
|
+
mode: 'edit',
|
|
36
|
+
rules: () => [],
|
|
37
|
+
checkboxTestId: undefined,
|
|
38
|
+
inputTestId: undefined,
|
|
39
|
+
pullSecret: undefined,
|
|
40
|
+
registerBeforeHook: undefined,
|
|
41
|
+
context: PRIVATE_REGISTRY_CONTEXT.PROVISIONING,
|
|
42
|
+
defaultRegistry: undefined,
|
|
43
|
+
namespace: 'fleet-default',
|
|
44
|
+
inStore: 'management',
|
|
45
|
+
showPullSecrets: true,
|
|
46
|
+
noneLabel: undefined,
|
|
47
|
+
skipPullSecrets: false,
|
|
48
|
+
repoDefaultPullSecrets: () => [],
|
|
49
|
+
existingValuesPullSecrets: () => [],
|
|
50
|
+
});
|
|
15
51
|
|
|
16
52
|
const emit = defineEmits<{
|
|
17
|
-
'update:value': [val: string |
|
|
53
|
+
'update:value': [val: string | undefined];
|
|
18
54
|
'update:enabled': [val: boolean];
|
|
55
|
+
'update:pullSecret': [val: string | undefined];
|
|
56
|
+
'update:skipPullSecrets': [val: boolean];
|
|
19
57
|
}>();
|
|
20
58
|
|
|
59
|
+
const store = useStore();
|
|
60
|
+
const { t } = useI18n(store);
|
|
61
|
+
|
|
21
62
|
const showInput = ref(!!props.value);
|
|
63
|
+
const globalRegistry = ref('');
|
|
64
|
+
const defaultPullSecrets = ref<string[]>([]);
|
|
65
|
+
const localSkipPullSecrets = ref(props.skipPullSecrets);
|
|
66
|
+
|
|
67
|
+
const isCharts = computed(() => props.context === PRIVATE_REGISTRY_CONTEXT.CHARTS);
|
|
68
|
+
const isImporting = computed(() => props.context === PRIVATE_REGISTRY_CONTEXT.IMPORTING);
|
|
69
|
+
|
|
70
|
+
const descriptionKey = computed(() => {
|
|
71
|
+
if (isCharts.value) {
|
|
72
|
+
return 'catalog.chart.registry.tooltip';
|
|
73
|
+
}
|
|
74
|
+
if (isImporting.value) {
|
|
75
|
+
return 'cluster.privateRegistry.importedDescription';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return 'cluster.privateRegistry.description';
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const checkboxLabelKey = computed(() => {
|
|
82
|
+
if (isCharts.value) {
|
|
83
|
+
return 'catalog.chart.registry.custom.checkBoxLabel';
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return 'cluster.privateRegistry.label';
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const checkboxTooltipKey = computed(() => {
|
|
90
|
+
if (isCharts.value) {
|
|
91
|
+
return 'catalog.chart.registry.tooltip';
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return undefined;
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
onMounted(() => {
|
|
98
|
+
if (props.defaultRegistry) {
|
|
99
|
+
globalRegistry.value = props.defaultRegistry;
|
|
100
|
+
} else {
|
|
101
|
+
const registrySetting = store.getters['management/byId'](MANAGEMENT.SETTING, SETTING.SYSTEM_DEFAULT_REGISTRY);
|
|
102
|
+
|
|
103
|
+
globalRegistry.value = registrySetting?.value || registrySetting?.defaultValue;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const pullSecretsSetting = store.getters['management/byId'](MANAGEMENT.SETTING, SETTING.SYSTEM_DEFAULT_REGISTRY_PULL_SECRETS);
|
|
107
|
+
|
|
108
|
+
if (props.repoDefaultPullSecrets?.length) {
|
|
109
|
+
defaultPullSecrets.value = props.repoDefaultPullSecrets;
|
|
110
|
+
} else if (pullSecretsSetting?.value ) {
|
|
111
|
+
defaultPullSecrets.value = pullSecretsSetting?.value.split(',').map((s: string) => s.trim());
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const hasMultipleDefaultSecrets = computed(() => {
|
|
116
|
+
return defaultPullSecrets.value?.length > 1;
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* On upgrade, if the chart values already contain multiple imagePullSecrets,
|
|
121
|
+
* the dropdown cannot represent them. Show a banner instead and let the
|
|
122
|
+
* user edit the values YAML directly.
|
|
123
|
+
*/
|
|
124
|
+
const hasMultipleExistingPullSecrets = computed(() => {
|
|
125
|
+
return (props.existingValuesPullSecrets?.length ?? 0) > 1;
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Format a list of names with commas and "and" before the last item.
|
|
130
|
+
* e.g. ["a"] => "<code>a</code>"
|
|
131
|
+
* ["a", "b"] => "<code>a</code> and <code>b</code>"
|
|
132
|
+
* ["a", "b", "c"] => "<code>a</code>, <code>b</code>, and <code>c</code>"
|
|
133
|
+
*/
|
|
134
|
+
function formatSecretList(names: string[]): string {
|
|
135
|
+
const wrapped = names.map((n) => `<code>${ n }</code>`);
|
|
136
|
+
|
|
137
|
+
const and = t('generic.and');
|
|
138
|
+
|
|
139
|
+
if (wrapped.length <= 1) {
|
|
140
|
+
return wrapped[0] || '';
|
|
141
|
+
}
|
|
142
|
+
if (wrapped.length === 2) {
|
|
143
|
+
return `${ wrapped[0] }${ and }${ wrapped[1] }`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return `${ wrapped.slice(0, -1).join(', ') },${ and }${ wrapped[wrapped.length - 1] }`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* If there is exactly one default pull secret configured, we can show that it is the default in the secret dropdown.
|
|
151
|
+
* If there is more than one, UI can't be certain which secret will be used
|
|
152
|
+
* so a banner listing all secrets is shown instead.
|
|
153
|
+
*/
|
|
154
|
+
const defaultPullSecretLabel = computed(() => {
|
|
155
|
+
if (props.noneLabel) {
|
|
156
|
+
return props.noneLabel;
|
|
157
|
+
}
|
|
158
|
+
if (!defaultPullSecrets.value?.length) {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
if (defaultPullSecrets.value?.length === 1) {
|
|
162
|
+
return t('catalog.chart.registry.pullSecret.defaultLabel', { name: defaultPullSecrets.value[0] });
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return t('catalog.chart.registry.pullSecret.defaultLabelGeneric');
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const existingValuesBannerHtml = computed(() => {
|
|
169
|
+
if (!props.existingValuesPullSecrets?.length) {
|
|
170
|
+
return '';
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return t('catalog.chart.registry.pullSecret.existingValuesBanner', { secrets: formatSecretList(props.existingValuesPullSecrets) }, true);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const defaultSecretsBannerHtml = computed(() => {
|
|
177
|
+
if (!defaultPullSecrets.value?.length) {
|
|
178
|
+
return '';
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return t('catalog.chart.registry.pullSecret.defaultSecretsBanner', { secrets: formatSecretList(defaultPullSecrets.value) }, true);
|
|
182
|
+
});
|
|
22
183
|
|
|
23
184
|
watch(() => props.enabled, (neu) => {
|
|
24
185
|
if (typeof neu === 'boolean' && neu !== showInput.value) {
|
|
@@ -31,7 +192,8 @@ watch(showInput, (neu, old) => {
|
|
|
31
192
|
emit('update:enabled', neu);
|
|
32
193
|
}
|
|
33
194
|
if (!neu && old && props.value) {
|
|
34
|
-
emit('update:value',
|
|
195
|
+
emit('update:value', undefined);
|
|
196
|
+
emit('update:pullSecret', undefined);
|
|
35
197
|
}
|
|
36
198
|
});
|
|
37
199
|
|
|
@@ -40,30 +202,103 @@ watch(() => props.value, (neu) => {
|
|
|
40
202
|
showInput.value = true;
|
|
41
203
|
}
|
|
42
204
|
});
|
|
205
|
+
|
|
206
|
+
watch(() => props.pullSecret, (neu, old) => {
|
|
207
|
+
if (neu && !props.value) {
|
|
208
|
+
emit('update:value', globalRegistry.value);
|
|
209
|
+
}
|
|
210
|
+
if (!neu && old && props.value === globalRegistry.value) {
|
|
211
|
+
emit('update:value', undefined);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
watch(localSkipPullSecrets, (neu) => {
|
|
216
|
+
emit('update:skipPullSecrets', neu);
|
|
217
|
+
if (neu) {
|
|
218
|
+
emit('update:pullSecret', undefined);
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
watch(() => props.skipPullSecrets, (neu) => {
|
|
223
|
+
if (neu !== localSkipPullSecrets.value) {
|
|
224
|
+
localSkipPullSecrets.value = neu;
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
|
|
43
228
|
</script>
|
|
44
229
|
|
|
45
230
|
<template>
|
|
46
231
|
<Banner
|
|
47
232
|
color="info"
|
|
48
233
|
class="mt-0"
|
|
49
|
-
label-key="
|
|
234
|
+
:label-key="descriptionKey"
|
|
50
235
|
/>
|
|
51
236
|
<Checkbox
|
|
52
237
|
v-model:value="showInput"
|
|
53
238
|
class="mb-20"
|
|
54
239
|
:mode="mode"
|
|
55
|
-
:label="t(
|
|
240
|
+
:label="t(checkboxLabelKey)"
|
|
241
|
+
:tooltip="checkboxTooltipKey ? t(checkboxTooltipKey) : undefined"
|
|
56
242
|
:data-testid="checkboxTestId"
|
|
57
243
|
/>
|
|
58
|
-
<
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
244
|
+
<template v-if="showInput">
|
|
245
|
+
<div class="row">
|
|
246
|
+
<div class="col span-6">
|
|
247
|
+
<LabeledInput
|
|
248
|
+
:value="value || globalRegistry"
|
|
249
|
+
:mode="mode"
|
|
250
|
+
:rules="rules"
|
|
251
|
+
:required="!globalRegistry"
|
|
252
|
+
label-key="catalog.chart.registry.custom.inputLabel"
|
|
253
|
+
:data-testid="inputTestId"
|
|
254
|
+
:placeholder="t('catalog.chart.registry.custom.placeholder')"
|
|
255
|
+
@update:value="(val) => emit('update:value', val)"
|
|
256
|
+
/>
|
|
257
|
+
</div>
|
|
258
|
+
</div>
|
|
259
|
+
<template v-if="showPullSecrets">
|
|
260
|
+
<Checkbox
|
|
261
|
+
v-if="isCharts"
|
|
262
|
+
v-model:value="localSkipPullSecrets"
|
|
263
|
+
class="mb-10 mt-10"
|
|
264
|
+
:mode="mode"
|
|
265
|
+
:label="t('catalog.chart.registry.pullSecret.skipOption')"
|
|
266
|
+
data-testid="registry-skip-pull-secrets-checkbox"
|
|
267
|
+
/>
|
|
268
|
+
<Banner
|
|
269
|
+
v-if="hasMultipleExistingPullSecrets && !localSkipPullSecrets"
|
|
270
|
+
color="info"
|
|
271
|
+
>
|
|
272
|
+
<span v-clean-html="existingValuesBannerHtml" />
|
|
273
|
+
</Banner>
|
|
274
|
+
<Banner
|
|
275
|
+
v-if="!hasMultipleExistingPullSecrets && hasMultipleDefaultSecrets && !localSkipPullSecrets"
|
|
276
|
+
color="info"
|
|
277
|
+
>
|
|
278
|
+
<span v-clean-html="defaultSecretsBannerHtml" />
|
|
279
|
+
</Banner>
|
|
280
|
+
<div :class="{'col span-6': !isCharts}">
|
|
281
|
+
<SelectOrCreateAuthSecret
|
|
282
|
+
v-if="!localSkipPullSecrets && !hasMultipleExistingPullSecrets"
|
|
283
|
+
:value="pullSecret"
|
|
284
|
+
:namespace="namespace"
|
|
285
|
+
:allow-rke="!isCharts"
|
|
286
|
+
:vertical="!isCharts"
|
|
287
|
+
:in-store="inStore"
|
|
288
|
+
limit-to-namespace
|
|
289
|
+
fixed-image-pull-secret
|
|
290
|
+
:none-label="defaultPullSecretLabel"
|
|
291
|
+
:image-pull-secret-docker-json-url-config="value || globalRegistry"
|
|
292
|
+
:register-before-hook="registerBeforeHook"
|
|
293
|
+
@update:value="(val) => emit('update:pullSecret', val)"
|
|
294
|
+
/>
|
|
295
|
+
</div>
|
|
296
|
+
</template>
|
|
297
|
+
</template>
|
|
69
298
|
</template>
|
|
299
|
+
|
|
300
|
+
<style lang="scss" scoped>
|
|
301
|
+
:deep(.banner__content) {
|
|
302
|
+
display: block;
|
|
303
|
+
}
|
|
304
|
+
</style>
|
|
@@ -4,8 +4,9 @@ import { Banner } from '@components/Banner';
|
|
|
4
4
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
|
5
5
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
|
6
6
|
import SSHKnownHosts from '@shell/components/form/SSHKnownHosts';
|
|
7
|
+
import FileSelector from '@shell/components/form/FileSelector';
|
|
7
8
|
import { AUTH_TYPE, NORMAN, SECRET } from '@shell/config/types';
|
|
8
|
-
import { SECRET_TYPES } from '@shell/config/secret';
|
|
9
|
+
import { SECRET_TYPES, GITHUB_APP_SECRET_KEYS } from '@shell/config/secret';
|
|
9
10
|
import { base64Encode } from '@shell/utils/crypto';
|
|
10
11
|
import { addObjects, insertAt } from '@shell/utils/array';
|
|
11
12
|
import { sortBy } from '@shell/utils/sort';
|
|
@@ -15,6 +16,16 @@ import {
|
|
|
15
16
|
PaginationParamFilter,
|
|
16
17
|
} from '@shell/types/store/pagination.types';
|
|
17
18
|
|
|
19
|
+
// Auth types for which this component renders inline fields and can create a secret/credential
|
|
20
|
+
const CREATABLE_AUTH_TYPES = [
|
|
21
|
+
AUTH_TYPE._SSH,
|
|
22
|
+
AUTH_TYPE._BASIC,
|
|
23
|
+
AUTH_TYPE._S3,
|
|
24
|
+
AUTH_TYPE._RKE,
|
|
25
|
+
AUTH_TYPE._IMAGE_PULL_SECRET,
|
|
26
|
+
AUTH_TYPE._GITHUB_APP,
|
|
27
|
+
];
|
|
28
|
+
|
|
18
29
|
export default {
|
|
19
30
|
name: 'SelectOrCreateAuthSecret',
|
|
20
31
|
|
|
@@ -25,6 +36,7 @@ export default {
|
|
|
25
36
|
LabeledInput,
|
|
26
37
|
LabeledSelect,
|
|
27
38
|
SSHKnownHosts,
|
|
39
|
+
FileSelector,
|
|
28
40
|
},
|
|
29
41
|
|
|
30
42
|
props: {
|
|
@@ -101,6 +113,11 @@ export default {
|
|
|
101
113
|
default: false,
|
|
102
114
|
},
|
|
103
115
|
|
|
116
|
+
allowGithubApp: {
|
|
117
|
+
type: Boolean,
|
|
118
|
+
default: false,
|
|
119
|
+
},
|
|
120
|
+
|
|
104
121
|
registerBeforeHook: {
|
|
105
122
|
type: Function,
|
|
106
123
|
required: true,
|
|
@@ -195,10 +212,16 @@ export default {
|
|
|
195
212
|
type: Boolean,
|
|
196
213
|
default: false,
|
|
197
214
|
},
|
|
215
|
+
|
|
216
|
+
// Overwrite the default label for "None" option ('generic.none')
|
|
217
|
+
noneLabel: {
|
|
218
|
+
type: [String, null],
|
|
219
|
+
default: null
|
|
220
|
+
}
|
|
198
221
|
},
|
|
199
222
|
|
|
200
223
|
async fetch() {
|
|
201
|
-
if ( (this.allowSsh || this.allowBasic || this.allowRke || this.fixedImagePullSecret) && this.$store.getters[`${ this.inStore }/schemaFor`](SECRET) ) {
|
|
224
|
+
if ( (this.allowSsh || this.allowBasic || this.allowRke || this.allowGithubApp || this.fixedImagePullSecret) && this.$store.getters[`${ this.inStore }/schemaFor`](SECRET) ) {
|
|
202
225
|
if (this.$store.getters[`${ this.inStore }/paginationEnabled`](SECRET)) {
|
|
203
226
|
// Filter results via api (because we shouldn't be fetching them all...)
|
|
204
227
|
this.filteredSecrets = await this.filterSecretsByApi();
|
|
@@ -248,13 +271,19 @@ export default {
|
|
|
248
271
|
publicKey: '',
|
|
249
272
|
privateKey: '',
|
|
250
273
|
sshKnownHosts: '',
|
|
251
|
-
|
|
274
|
+
|
|
275
|
+
githubAppId: '',
|
|
276
|
+
githubAppInstallationId: '',
|
|
277
|
+
githubAppPrivateKey: '',
|
|
278
|
+
|
|
279
|
+
uniqueId: new Date().getTime(), // Allows form state to be individually tracked if the form is in a list
|
|
252
280
|
|
|
253
281
|
SSH: AUTH_TYPE._SSH,
|
|
254
282
|
BASIC: AUTH_TYPE._BASIC,
|
|
255
283
|
IMAGE_PULL_SECRET: AUTH_TYPE._IMAGE_PULL_SECRET,
|
|
256
284
|
S3: AUTH_TYPE._S3,
|
|
257
285
|
RKE: AUTH_TYPE._RKE,
|
|
286
|
+
GITHUB_APP: AUTH_TYPE._GITHUB_APP,
|
|
258
287
|
};
|
|
259
288
|
},
|
|
260
289
|
|
|
@@ -280,6 +309,12 @@ export default {
|
|
|
280
309
|
types.push(SECRET_TYPES.RKE_AUTH_CONFIG);
|
|
281
310
|
}
|
|
282
311
|
|
|
312
|
+
// GitHub App secrets are stored as Opaque; they're narrowed down to actual
|
|
313
|
+
// GitHub App secrets via the data keys in `options`.
|
|
314
|
+
if ( this.allowGithubApp ) {
|
|
315
|
+
types.push(SECRET_TYPES.OPAQUE);
|
|
316
|
+
}
|
|
317
|
+
|
|
283
318
|
return types;
|
|
284
319
|
},
|
|
285
320
|
|
|
@@ -311,6 +346,12 @@ export default {
|
|
|
311
346
|
filteredSecrets = this.filteredSecrets;
|
|
312
347
|
}
|
|
313
348
|
|
|
349
|
+
// GitHub App secrets are fetched as Opaque (broad). Keep only the Opaque
|
|
350
|
+
// secrets that actually hold the GitHub App data keys.
|
|
351
|
+
if (this.allowGithubApp) {
|
|
352
|
+
filteredSecrets = filteredSecrets.filter((x) => x._type !== SECRET_TYPES.OPAQUE || x.isGithubApp);
|
|
353
|
+
}
|
|
354
|
+
|
|
314
355
|
let out = filteredSecrets.map((x) => {
|
|
315
356
|
const {
|
|
316
357
|
dataPreview, subTypeDisplay, metadata, id
|
|
@@ -370,12 +411,12 @@ export default {
|
|
|
370
411
|
}
|
|
371
412
|
if ( this.allowNone ) {
|
|
372
413
|
out.unshift({
|
|
373
|
-
label: this.t('generic.none'),
|
|
414
|
+
label: this.noneLabel || this.t('generic.none'),
|
|
374
415
|
value: AUTH_TYPE._NONE,
|
|
375
416
|
});
|
|
376
417
|
}
|
|
377
418
|
|
|
378
|
-
if (this.allowSsh || this.allowS3 || this.allowBasic || this.allowRke || this.fixedImagePullSecret) {
|
|
419
|
+
if (this.allowSsh || this.allowS3 || this.allowBasic || this.allowRke || this.allowGithubApp || this.fixedImagePullSecret) {
|
|
379
420
|
out.unshift({
|
|
380
421
|
label: 'divider',
|
|
381
422
|
disabled: true,
|
|
@@ -383,6 +424,14 @@ export default {
|
|
|
383
424
|
});
|
|
384
425
|
}
|
|
385
426
|
|
|
427
|
+
if ( this.allowGithubApp ) {
|
|
428
|
+
out.unshift({
|
|
429
|
+
label: this.t('selectOrCreateAuthSecret.createGithubApp'),
|
|
430
|
+
value: AUTH_TYPE._GITHUB_APP,
|
|
431
|
+
kind: 'highlighted'
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
|
|
386
435
|
if ( this.allowSsh ) {
|
|
387
436
|
out.unshift({
|
|
388
437
|
label: this.t('selectOrCreateAuthSecret.createSsh'),
|
|
@@ -453,12 +502,15 @@ export default {
|
|
|
453
502
|
},
|
|
454
503
|
|
|
455
504
|
watch: {
|
|
456
|
-
selected:
|
|
457
|
-
publicKey:
|
|
458
|
-
privateKey:
|
|
459
|
-
sshKnownHosts:
|
|
460
|
-
|
|
461
|
-
|
|
505
|
+
selected: 'update',
|
|
506
|
+
publicKey: 'updateKeyVal',
|
|
507
|
+
privateKey: 'updateKeyVal',
|
|
508
|
+
sshKnownHosts: 'updateKeyVal',
|
|
509
|
+
githubAppId: 'updateKeyVal',
|
|
510
|
+
githubAppInstallationId: 'updateKeyVal',
|
|
511
|
+
githubAppPrivateKey: 'updateKeyVal',
|
|
512
|
+
preSelect: 'updateSelectedFromValue',
|
|
513
|
+
value: 'updateSelectedFromValue',
|
|
462
514
|
|
|
463
515
|
async namespace(ns) {
|
|
464
516
|
if (ns && !this.selected.startsWith(`${ ns }/`)) {
|
|
@@ -541,10 +593,13 @@ export default {
|
|
|
541
593
|
},
|
|
542
594
|
|
|
543
595
|
updateKeyVal() {
|
|
544
|
-
if ( !
|
|
596
|
+
if ( !CREATABLE_AUTH_TYPES.includes(this.selected) ) {
|
|
545
597
|
this.privateKey = '';
|
|
546
598
|
this.publicKey = '';
|
|
547
599
|
this.sshKnownHosts = '';
|
|
600
|
+
this.githubAppId = '';
|
|
601
|
+
this.githubAppInstallationId = '';
|
|
602
|
+
this.githubAppPrivateKey = '';
|
|
548
603
|
}
|
|
549
604
|
|
|
550
605
|
const value = {
|
|
@@ -557,11 +612,17 @@ export default {
|
|
|
557
612
|
value.sshKnownHosts = this.sshKnownHosts;
|
|
558
613
|
}
|
|
559
614
|
|
|
615
|
+
if (this.selected === AUTH_TYPE._GITHUB_APP) {
|
|
616
|
+
value.githubAppId = this.githubAppId;
|
|
617
|
+
value.githubAppInstallationId = this.githubAppInstallationId;
|
|
618
|
+
value.githubAppPrivateKey = this.githubAppPrivateKey;
|
|
619
|
+
}
|
|
620
|
+
|
|
560
621
|
this.$emit('inputauthval', value);
|
|
561
622
|
},
|
|
562
623
|
|
|
563
624
|
update() {
|
|
564
|
-
if ( (!this.selected ||
|
|
625
|
+
if ( (!this.selected || this.selected === AUTH_TYPE._NONE || CREATABLE_AUTH_TYPES.includes(this.selected))) {
|
|
565
626
|
this.$emit('update:value', null);
|
|
566
627
|
} else if ( this.selected.includes(':')) {
|
|
567
628
|
// Cloud creds
|
|
@@ -585,7 +646,7 @@ export default {
|
|
|
585
646
|
},
|
|
586
647
|
|
|
587
648
|
async doCreate() {
|
|
588
|
-
if ( !
|
|
649
|
+
if ( !CREATABLE_AUTH_TYPES.includes(this.selected) || this.delegateCreateToParent ) {
|
|
589
650
|
return;
|
|
590
651
|
}
|
|
591
652
|
|
|
@@ -636,6 +697,14 @@ export default {
|
|
|
636
697
|
// Set the 'auth' key to be the base64 of the username and password concatenated with a ':' character
|
|
637
698
|
secret.data = { auth: base64Encode(`${ this.publicKey }:${ this.privateKey }`) };
|
|
638
699
|
break;
|
|
700
|
+
case AUTH_TYPE._GITHUB_APP:
|
|
701
|
+
type = SECRET_TYPES.OPAQUE;
|
|
702
|
+
secret.data = {
|
|
703
|
+
[GITHUB_APP_SECRET_KEYS.APP_ID]: base64Encode(this.githubAppId),
|
|
704
|
+
[GITHUB_APP_SECRET_KEYS.INSTALLATION_ID]: base64Encode(this.githubAppInstallationId),
|
|
705
|
+
[GITHUB_APP_SECRET_KEYS.PRIVATE_KEY]: base64Encode(this.githubAppPrivateKey),
|
|
706
|
+
};
|
|
707
|
+
break;
|
|
639
708
|
default:
|
|
640
709
|
throw new Error('Unknown type');
|
|
641
710
|
}
|
|
@@ -655,8 +724,15 @@ export default {
|
|
|
655
724
|
secret.data.known_hosts = base64Encode(this.sshKnownHosts || '');
|
|
656
725
|
}
|
|
657
726
|
|
|
727
|
+
// Components passing imagePullSecretDockerJsonUrlConfig are responsible for validating that a valid hostname or URL is provided
|
|
658
728
|
if (this.selected === AUTH_TYPE._IMAGE_PULL_SECRET && this.imagePullSecretDockerJsonUrlConfig) {
|
|
659
|
-
|
|
729
|
+
let registryHost;
|
|
730
|
+
|
|
731
|
+
try {
|
|
732
|
+
registryHost = new URL(this.imagePullSecretDockerJsonUrlConfig).host;
|
|
733
|
+
} catch {
|
|
734
|
+
registryHost = this.imagePullSecretDockerJsonUrlConfig;
|
|
735
|
+
}
|
|
660
736
|
|
|
661
737
|
const config = {
|
|
662
738
|
auths: {
|
|
@@ -672,13 +748,14 @@ export default {
|
|
|
672
748
|
}
|
|
673
749
|
}
|
|
674
750
|
}
|
|
675
|
-
|
|
676
751
|
await secret.save();
|
|
677
752
|
|
|
678
753
|
await this.$nextTick(() => {
|
|
679
754
|
this.selected = secret.id;
|
|
680
755
|
});
|
|
681
756
|
|
|
757
|
+
this.update();
|
|
758
|
+
|
|
682
759
|
return secret;
|
|
683
760
|
},
|
|
684
761
|
},
|
|
@@ -780,11 +857,57 @@ export default {
|
|
|
780
857
|
</div>
|
|
781
858
|
</template>
|
|
782
859
|
</div>
|
|
860
|
+
<div
|
|
861
|
+
v-if="selected === GITHUB_APP"
|
|
862
|
+
class="mt-20"
|
|
863
|
+
:class="{'row': !vertical}"
|
|
864
|
+
>
|
|
865
|
+
<div :class="vertical ? 'mt-20' : 'col span-3'">
|
|
866
|
+
<LabeledInput
|
|
867
|
+
v-model:value="githubAppId"
|
|
868
|
+
data-testid="auth-secret-github-app-id"
|
|
869
|
+
:mode="mode"
|
|
870
|
+
label-key="selectOrCreateAuthSecret.githubApp.appId"
|
|
871
|
+
/>
|
|
872
|
+
</div>
|
|
873
|
+
<div :class="vertical ? 'mt-20' : 'col span-3'">
|
|
874
|
+
<LabeledInput
|
|
875
|
+
v-model:value="githubAppInstallationId"
|
|
876
|
+
data-testid="auth-secret-github-app-installation-id"
|
|
877
|
+
:mode="mode"
|
|
878
|
+
label-key="selectOrCreateAuthSecret.githubApp.installationId"
|
|
879
|
+
/>
|
|
880
|
+
</div>
|
|
881
|
+
<div :class="vertical ? 'mt-20' : 'col span-6 gap'">
|
|
882
|
+
<LabeledInput
|
|
883
|
+
v-model:value="githubAppPrivateKey"
|
|
884
|
+
data-testid="auth-secret-github-app-private-key"
|
|
885
|
+
:mode="mode"
|
|
886
|
+
type="multiline"
|
|
887
|
+
:max-height="1000"
|
|
888
|
+
:resize-on-value-change-and-resize-window="true"
|
|
889
|
+
label-key="selectOrCreateAuthSecret.githubApp.privateKey"
|
|
890
|
+
/>
|
|
891
|
+
<FileSelector
|
|
892
|
+
:as-rc-button="true"
|
|
893
|
+
:mode="mode"
|
|
894
|
+
:label="t('generic.readFromFile')"
|
|
895
|
+
data-testid="auth-secret-github-app-private-key-file"
|
|
896
|
+
@selected="githubAppPrivateKey = $event"
|
|
897
|
+
/>
|
|
898
|
+
</div>
|
|
899
|
+
</div>
|
|
783
900
|
</div>
|
|
784
901
|
</template>
|
|
785
902
|
|
|
786
|
-
<style lang="scss">
|
|
903
|
+
<style scoped lang="scss">
|
|
787
904
|
.select-or-create-auth-secret div.labeled-select {
|
|
788
905
|
min-height: $input-height;
|
|
789
906
|
}
|
|
907
|
+
|
|
908
|
+
.gap {
|
|
909
|
+
display: flex;
|
|
910
|
+
flex-flow: row wrap;
|
|
911
|
+
gap: var(--gap);
|
|
912
|
+
}
|
|
790
913
|
</style>
|
|
@@ -30,6 +30,29 @@ describe('component: FileSelector', () => {
|
|
|
30
30
|
expect(uploadButton.exists()).toBeTruthy();
|
|
31
31
|
});
|
|
32
32
|
|
|
33
|
+
it('should render a small, secondary RcButton when asRcButton is set', () => {
|
|
34
|
+
wrapper = mount(FileSelector, {
|
|
35
|
+
props: { label: 'upload', asRcButton: true },
|
|
36
|
+
global: { mocks: {} },
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const rcButton = wrapper.findComponent({ name: 'RcButton' });
|
|
40
|
+
|
|
41
|
+
expect(rcButton.exists()).toBe(true);
|
|
42
|
+
expect(rcButton.props('variant')).toBe('secondary');
|
|
43
|
+
expect(rcButton.props('size')).toBe('small');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should render a plain button by default (asRcButton not set)', () => {
|
|
47
|
+
wrapper = mount(FileSelector, {
|
|
48
|
+
props: { label: 'upload' },
|
|
49
|
+
global: { mocks: {} },
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
expect(wrapper.findComponent({ name: 'RcButton' }).exists()).toBe(false);
|
|
53
|
+
expect(wrapper.find('[data-testid="file-selector__uploader-button"]').exists()).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
|
|
33
56
|
it('should succeed when loading an image', async() => {
|
|
34
57
|
wrapper = mount(FileSelector, {
|
|
35
58
|
props: { label: 'upload', accept: 'image/jpeg,image/png,image/svg+xml' },
|