|
| 1 | +<script> |
| 2 | +import CruResource from '@shell/components/CruResource'; |
| 3 | +import Tabbed from '@shell/components/Tabbed'; |
| 4 | +import Tab from '@shell/components/Tabbed/Tab'; |
| 5 | +import { LabeledInput } from '@components/Form/LabeledInput'; |
| 6 | +import LabeledSelect from '@shell/components/form/LabeledSelect'; |
| 7 | +import NameNsDescription from '@shell/components/form/NameNsDescription'; |
| 8 | +import { RadioGroup } from '@components/Form/Radio'; |
| 9 | +import CreateEditView from '@shell/mixins/create-edit-view'; |
| 10 | +import FormValidation from '@shell/mixins/form-validation'; |
| 11 | +import { SECRET } from '@shell/config/types'; |
| 12 | +import { randomStr } from '@shell/utils/string'; |
| 13 | +import { mapGetters } from 'vuex'; |
| 14 | +
|
| 15 | +export default { |
| 16 | + name: 'EditKVMSource', |
| 17 | +
|
| 18 | + emits: ['update:value'], |
| 19 | +
|
| 20 | + components: { |
| 21 | + CruResource, |
| 22 | + Tabbed, |
| 23 | + Tab, |
| 24 | + LabeledInput, |
| 25 | + LabeledSelect, |
| 26 | + NameNsDescription, |
| 27 | + RadioGroup, |
| 28 | + }, |
| 29 | +
|
| 30 | + mixins: [CreateEditView, FormValidation], |
| 31 | +
|
| 32 | + inheritAttrs: false, |
| 33 | +
|
| 34 | + props: { |
| 35 | + value: { |
| 36 | + type: Object, |
| 37 | + required: true, |
| 38 | + }, |
| 39 | + mode: { |
| 40 | + type: String, |
| 41 | + required: true, |
| 42 | + }, |
| 43 | + }, |
| 44 | +
|
| 45 | + async fetch() { |
| 46 | + const inStore = this.$store.getters['currentProduct'].inStore; |
| 47 | +
|
| 48 | + this.allSecrets = await this.$store.dispatch(`${ inStore }/findAll`, { type: SECRET }); |
| 49 | + }, |
| 50 | +
|
| 51 | + data() { |
| 52 | + if (!this.value.spec) this.value.spec = {}; |
| 53 | + if (!this.value.spec.credentials) this.value.spec.credentials = {}; |
| 54 | +
|
| 55 | + const initialMode = this.value.spec.credentials.name ? 'existing' : 'new'; |
| 56 | +
|
| 57 | + return { |
| 58 | + allSecrets: [], |
| 59 | + authMode: initialMode, |
| 60 | + newUsername: '', |
| 61 | + newPassword: '', |
| 62 | + newPrivateKey: '', |
| 63 | +
|
| 64 | + fvFormRuleSets: [ |
| 65 | + { path: 'metadata.name', rules: ['nameRequired'] }, |
| 66 | + { path: 'spec.libvirtURI', rules: ['uriRequired'] }, |
| 67 | + ], |
| 68 | + }; |
| 69 | + }, |
| 70 | +
|
| 71 | + computed: { |
| 72 | + ...mapGetters({ t: 'i18n/t' }), |
| 73 | +
|
| 74 | + authModeOptions() { |
| 75 | + return [ |
| 76 | + { label: this.t('harvester.addons.vmImport.fields.createSecret'), value: 'new' }, |
| 77 | + { label: this.t('harvester.addons.vmImport.fields.useSecret'), value: 'existing' } |
| 78 | + ]; |
| 79 | + }, |
| 80 | +
|
| 81 | + secretOptions() { |
| 82 | + const currentNamespace = this.value.metadata.namespace || 'default'; |
| 83 | +
|
| 84 | + return this.allSecrets |
| 85 | + .filter((s) => s.metadata.namespace === currentNamespace) |
| 86 | + .map((s) => ({ |
| 87 | + label: s.nameDisplay, |
| 88 | + value: s.metadata.name |
| 89 | + })); |
| 90 | + }, |
| 91 | +
|
| 92 | + fvExtraRules() { |
| 93 | + return { |
| 94 | + nameRequired: (val) => !val ? this.t('validation.required', { key: this.t('harvester.fields.name') }) : undefined, |
| 95 | + uriRequired: (val) => !val ? this.t('validation.required', { key: this.t('harvester.addons.vmImport.kvm.fields.uri') }) : undefined, |
| 96 | + }; |
| 97 | + }, |
| 98 | +
|
| 99 | + isFormValid() { |
| 100 | + if (!this.fvFormIsValid) { |
| 101 | + return false; |
| 102 | + } |
| 103 | +
|
| 104 | + if (this.authMode === 'new') { |
| 105 | + if (!this.newUsername || !this.newPassword) return false; |
| 106 | + } else { |
| 107 | + if (!this.value.spec.credentials.name) return false; |
| 108 | + } |
| 109 | +
|
| 110 | + return true; |
| 111 | + } |
| 112 | + }, |
| 113 | +
|
| 114 | + methods: { |
| 115 | + usernameRule(val) { |
| 116 | + return !val ? this.t('validation.required', { key: this.t('harvester.addons.vmImport.fields.username') }) : undefined; |
| 117 | + }, |
| 118 | + secretRule(val) { |
| 119 | + return !val ? this.t('validation.required', { key: this.t('harvester.addons.vmImport.fields.selectSecret') }) : undefined; |
| 120 | + }, |
| 121 | +
|
| 122 | + async saveSource(buttonCb) { |
| 123 | + const inStore = this.$store.getters['currentProduct'].inStore; |
| 124 | +
|
| 125 | + try { |
| 126 | + if (this.authMode === 'new') { |
| 127 | + const secretName = `${ this.value.metadata.name }-creds-${ randomStr(4).toLowerCase() }`; |
| 128 | + const namespace = this.value.metadata.namespace || 'default'; |
| 129 | +
|
| 130 | + // Create the model with the correct Schema ID (SECRET) |
| 131 | + const newSecret = await this.$store.dispatch(`${ inStore }/create`, { |
| 132 | + type: SECRET, |
| 133 | + metadata: { |
| 134 | + name: secretName, |
| 135 | + namespace |
| 136 | + } |
| 137 | + }); |
| 138 | +
|
| 139 | + // Use '_type' to set the Kubernetes 'type' field. |
| 140 | + newSecret['_type'] = 'Opaque'; |
| 141 | +
|
| 142 | + // base64 encode the data |
| 143 | + newSecret['data'] = { |
| 144 | + username: btoa(this.newUsername), |
| 145 | + password: btoa(this.newPassword), |
| 146 | + privateKey: this.newPrivateKey ? btoa(this.newPrivateKey) : undefined |
| 147 | + }; |
| 148 | +
|
| 149 | + await newSecret.save(); |
| 150 | +
|
| 151 | + this.value.spec.credentials = { |
| 152 | + name: secretName, |
| 153 | + namespace |
| 154 | + }; |
| 155 | + } |
| 156 | +
|
| 157 | + await this.save(buttonCb); |
| 158 | + } catch (err) { |
| 159 | + this.errors = [err]; |
| 160 | + buttonCb(false); |
| 161 | + } |
| 162 | + } |
| 163 | + } |
| 164 | +}; |
| 165 | +</script> |
| 166 | + |
| 167 | +<template> |
| 168 | + <CruResource |
| 169 | + :done-route="doneRoute" |
| 170 | + :resource="value" |
| 171 | + :mode="mode" |
| 172 | + :errors="errors" |
| 173 | + :apply-hooks="applyHooks" |
| 174 | + :validation-passed="isFormValid" |
| 175 | + @finish="saveSource" |
| 176 | + @error="e=>errors=e" |
| 177 | + > |
| 178 | + <NameNsDescription |
| 179 | + :value="value" |
| 180 | + :mode="mode" |
| 181 | + :rules="{ name: fvGetAndReportPathRules('metadata.name') }" |
| 182 | + @update:value="$emit('update:value', $event)" |
| 183 | + /> |
| 184 | + |
| 185 | + <Tabbed |
| 186 | + v-bind="$attrs" |
| 187 | + class="mt-15" |
| 188 | + :side-tabs="true" |
| 189 | + > |
| 190 | + <Tab |
| 191 | + name="basic" |
| 192 | + :label="t('harvester.addons.vmImport.titles.basic')" |
| 193 | + :weight="2" |
| 194 | + > |
| 195 | + <div class="row mb-20"> |
| 196 | + <div class="col span-12"> |
| 197 | + <LabeledInput |
| 198 | + v-model:value="value.spec.libvirtURI" |
| 199 | + :label="t('harvester.addons.vmImport.kvm.fields.uri')" |
| 200 | + :placeholder="t('harvester.addons.vmImport.kvm.placeholders.uri')" |
| 201 | + :mode="mode" |
| 202 | + :rules="fvGetAndReportPathRules('spec.libvirtURI')" |
| 203 | + required |
| 204 | + /> |
| 205 | + </div> |
| 206 | + </div> |
| 207 | + </Tab> |
| 208 | + |
| 209 | + <Tab |
| 210 | + name="auth" |
| 211 | + :label="t('harvester.addons.vmImport.titles.auth')" |
| 212 | + :weight="1" |
| 213 | + > |
| 214 | + <div class="row mb-20"> |
| 215 | + <div class="col span-12"> |
| 216 | + <RadioGroup |
| 217 | + v-model:value="authMode" |
| 218 | + name="authMode" |
| 219 | + :options="authModeOptions" |
| 220 | + :mode="mode" |
| 221 | + /> |
| 222 | + </div> |
| 223 | + </div> |
| 224 | + |
| 225 | + <div v-if="authMode === 'new'"> |
| 226 | + <div class="row mb-20"> |
| 227 | + <div class="col span-6"> |
| 228 | + <LabeledInput |
| 229 | + v-model:value="newUsername" |
| 230 | + :label="t('harvester.addons.vmImport.fields.username')" |
| 231 | + :mode="mode" |
| 232 | + :rules="[usernameRule]" |
| 233 | + required |
| 234 | + /> |
| 235 | + </div> |
| 236 | + <div class="col span-6"> |
| 237 | + <LabeledInput |
| 238 | + v-model:value="newPassword" |
| 239 | + type="password" |
| 240 | + :label="t('harvester.addons.vmImport.fields.password')" |
| 241 | + placeholder="(Optional)" |
| 242 | + :mode="mode" |
| 243 | + /> |
| 244 | + </div> |
| 245 | + </div> |
| 246 | + <div class="row mb-20"> |
| 247 | + <div class="col span-12"> |
| 248 | + <LabeledInput |
| 249 | + v-model:value="newPrivateKey" |
| 250 | + type="multiline" |
| 251 | + :label="t('harvester.addons.vmImport.kvm.fields.privateKey')" |
| 252 | + :placeholder="t('harvester.addons.vmImport.kvm.placeholders.privateKey')" |
| 253 | + :min-height="100" |
| 254 | + :mode="mode" |
| 255 | + /> |
| 256 | + </div> |
| 257 | + </div> |
| 258 | + |
| 259 | + <div class="text-muted"> |
| 260 | + Note: A new Kubernetes Secret will be created to store these credentials. |
| 261 | + </div> |
| 262 | + </div> |
| 263 | + |
| 264 | + <div v-if="authMode === 'existing'"> |
| 265 | + <div class="row mb-20"> |
| 266 | + <div class="col span-6"> |
| 267 | + <LabeledSelect |
| 268 | + v-model:value="value.spec.credentials.name" |
| 269 | + :options="secretOptions" |
| 270 | + :label="t('harvester.addons.vmImport.fields.selectSecret')" |
| 271 | + :mode="mode" |
| 272 | + :rules="[secretRule]" |
| 273 | + required |
| 274 | + /> |
| 275 | + </div> |
| 276 | + </div> |
| 277 | + </div> |
| 278 | + </Tab> |
| 279 | + </Tabbed> |
| 280 | + </CruResource> |
| 281 | +</template> |
0 commit comments