Skip to content

Commit bc9132e

Browse files
authored
Merge pull request #169 from aalves08/gate-disk-image-build-to-operator-version
working on feature versioning mechanism
2 parents 48de581 + b449e8a commit bc9132e

File tree

9 files changed

+215
-23
lines changed

9 files changed

+215
-23
lines changed

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@
1414
"css-loader": "4.3.0"
1515
},
1616
"devDependencies": {
17-
"@types/node": "18.11.9"
17+
"@types/node": "18.11.9",
18+
"@types/semver": "^7.5.8",
19+
"semver": "^7.6.0"
1820
},
1921
"scripts": {
2022
"dev": "NODE_ENV=dev ./node_modules/.bin/vue-cli-service serve",

pkg/elemental/components/BuildMedia.vue

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Banner } from '@components/Banner';
44
import AsyncButton from '@shell/components/AsyncButton';
55
import { randomStr, CHARSET } from '@shell/utils/string';
66
import { ELEMENTAL_SCHEMA_IDS } from '../config/elemental-types';
7+
import { getOperatorVersion, checkGatedFeatureCompatibility, BUILD_MEDIA_RAW_SUPPORT } from '../utils/feature-versioning';
78
89
const MEDIA_TYPES = {
910
RAW: {
@@ -38,17 +39,28 @@ export default {
3839
registrationEndpoint: {
3940
type: String,
4041
default: ''
42+
},
43+
resource: {
44+
type: String,
45+
default: ''
46+
},
47+
mode: {
48+
type: String,
49+
default: ''
4150
}
4251
},
4352
async fetch() {
4453
this.seedImagesList = await this.$store.dispatch('management/findAll', { type: ELEMENTAL_SCHEMA_IDS.SEED_IMAGE });
4554
this.managedOsVersions = await this.$store.dispatch('management/findAll', { type: ELEMENTAL_SCHEMA_IDS.MANAGED_OS_VERSIONS });
55+
56+
this.operatorVersion = await getOperatorVersion(this.$store);
4657
},
4758
data() {
4859
return {
4960
seedImagesList: [],
5061
managedOsVersions: [],
5162
filteredManagedOsVersions: [],
63+
operatorVersion: '',
5264
buildMediaOsVersions: [],
5365
buildMediaTypes: [
5466
{ label: MEDIA_TYPES.ISO.label, value: MEDIA_TYPES.ISO.type },
@@ -79,6 +91,15 @@ export default {
7991
}
8092
},
8193
computed: {
94+
isRawDiskImageBuildSupported() {
95+
const check = checkGatedFeatureCompatibility(this.resource, this.mode, BUILD_MEDIA_RAW_SUPPORT, this.operatorVersion);
96+
97+
if (!check) {
98+
this.buildMediaTypeSelected = MEDIA_TYPES.ISO.type; // eslint-disable-line vue/no-side-effects-in-computed-properties
99+
}
100+
101+
return check;
102+
},
82103
registrationEndpointsOptions() {
83104
const activeRegEndpoints = this.registrationEndpointList.filter(item => item.state === 'active');
84105
@@ -91,10 +112,10 @@ export default {
91112
},
92113
isBuildMediaBtnEnabled() {
93114
if (this.displayRegEndpoints) {
94-
return this.registrationEndpointSelected && this.buildMediaOsVersionSelected && this.buildMediaTypeSelected;
115+
return this.isRawDiskImageBuildSupported ? this.registrationEndpointSelected && this.buildMediaOsVersionSelected && this.buildMediaTypeSelected : this.registrationEndpointSelected && this.buildMediaOsVersionSelected;
95116
}
96117
97-
return this.buildMediaOsVersionSelected && this.buildMediaTypeSelected;
118+
return this.isRawDiskImageBuildSupported ? this.buildMediaOsVersionSelected && this.buildMediaTypeSelected : this.buildMediaOsVersionSelected;
98119
},
99120
seedImageFound() {
100121
if (this.seedImage) {
@@ -146,21 +167,26 @@ export default {
146167
const machineRegName = this.displayRegEndpoints ? this.registrationEndpointSelected.split('/')[1] : this.registrationEndpoint.split('/')[1];
147168
const machineRegNamespace = this.displayRegEndpoints ? this.registrationEndpointSelected.split('/')[0] : this.registrationEndpoint.split('/')[0];
148169
149-
const seedImageModel = await this.$store.dispatch('management/create', {
170+
const seedImageObject = {
150171
metadata: {
151172
name: `media-image-reg-${ machineRegName }-${ randomStr(8, CHARSET.ALPHA_LOWER ) }`,
152173
namespace: 'fleet-default'
153174
},
154175
spec: {
155-
type: this.buildMediaTypeSelected,
156176
baseImage: this.buildMediaOsVersionSelected,
157177
registrationRef: {
158178
name: machineRegName,
159179
namespace: machineRegNamespace
160180
}
161181
},
162182
type: ELEMENTAL_SCHEMA_IDS.SEED_IMAGE,
163-
});
183+
};
184+
185+
if (this.isRawDiskImageBuildSupported) {
186+
seedImageObject.spec.type = this.buildMediaTypeSelected;
187+
}
188+
189+
const seedImageModel = await this.$store.dispatch('management/create', seedImageObject);
164190
165191
try {
166192
this.seedImage = await seedImageModel.save({ url: `/v1/${ ELEMENTAL_SCHEMA_IDS.SEED_IMAGE }`, method: 'POST' });
@@ -207,7 +233,10 @@ export default {
207233
:options="registrationEndpointsOptions"
208234
/>
209235
</div>
210-
<div class="col span-2">
236+
<div
237+
v-if="isRawDiskImageBuildSupported"
238+
class="col span-2"
239+
>
211240
<LabeledSelect
212241
v-model="buildMediaTypeSelected"
213242
class="mr-20"

pkg/elemental/components/DashboardView.vue

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<script>
22
import { allHash } from '@shell/utils/promise';
33
import { CAPI, CATALOG } from '@shell/config/types';
4+
import { _VIEW } from '@shell/config/query-params';
45
import { NAME } from '@shell/config/table-headers';
56
import ResourceTable from '@shell/components/ResourceTable';
67
import PercentageBar from '@shell/components/PercentageBar';
@@ -10,6 +11,7 @@ import {
1011
ELEMENTAL_CLUSTER_PROVIDER,
1112
KIND
1213
} from '../config/elemental-types';
14+
import { ELEMENTAL_TYPES } from '../types';
1315
import { createElementalRoute } from '../utils/custom-routing';
1416
import { filterForElementalClusters } from '../utils/elemental-utils';
1517
import BuildMedia from './BuildMedia';
@@ -54,7 +56,7 @@ export default {
5456
// we need to check for the length of the response
5557
// due to some issue with a standard-user, which can list apps
5658
// but the list comes up empty []
57-
const isElementalOperatorNotInstalledOnApps = allDispatches.installedApps && allDispatches.installedApps.length && !allDispatches.installedApps.find(item => item.id.includes('elemental-operator'));
59+
const isElementalOperatorNotInstalledOnApps = allDispatches.installedApps && allDispatches.installedApps.length && !allDispatches.installedApps.find(item => item.id.includes('elemental-operator') && !item.id.includes('elemental-operator-crds'));
5860
5961
// check if CRD is there but operator isn't
6062
if (allDispatches.elementalSchema && isElementalOperatorNotInstalledOnApps) {
@@ -65,6 +67,8 @@ export default {
6567
return {
6668
ELEMENTAL_CLUSTERS: 'elementalClusters',
6769
isElementalOpNotInstalledAndHasSchema: false,
70+
resource: ELEMENTAL_TYPES.DASHBOARD,
71+
mode: _VIEW,
6872
resourcesData: {
6973
[`${ ELEMENTAL_SCHEMA_IDS.MACHINE_REGISTRATIONS }`]: [],
7074
[`${ ELEMENTAL_SCHEMA_IDS.MACHINE_INVENTORIES }`]: [],
@@ -283,6 +287,8 @@ export default {
283287
<div class="mt-20 mb-20">
284288
<BuildMedia
285289
:registration-endpoint-list="registrationEndpoints"
290+
:resource="resource"
291+
:mode="mode"
286292
/>
287293
</div>
288294
<!-- Tables -->

pkg/elemental/detail/elemental.cattle.io.machineregistration.vue

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ export default {
3232
type: String,
3333
required: true
3434
},
35+
resource: {
36+
type: String,
37+
required: true
38+
},
3539
},
3640
data() {
3741
return {
@@ -115,6 +119,8 @@ export default {
115119
<BuildMedia
116120
:display-reg-endpoints="false"
117121
:registration-endpoint="`${value.metadata.namespace}/${value.metadata.name}`"
122+
:resource="resource"
123+
:mode="mode"
118124
/>
119125
</div>
120126
<div

pkg/elemental/edit/elemental.cattle.io.machineregistration.vue

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ import { exceptionToErrorsArray } from '@shell/utils/error';
1616
import Tabbed from '@shell/components/Tabbed/index.vue';
1717
import Tab from '@shell/components/Tabbed/Tab.vue';
1818
19+
import { getOperatorVersion, checkGatedFeatureCompatibility, MACH_REG_CONFIG_DEFAULTS } from '../utils/feature-versioning';
20+
import { OLD_DEFAULT_CREATION_YAML, DEFAULT_CREATION_YAML } from '../models/elemental.cattle.io.machineregistration';
21+
1922
export default {
2023
name: 'MachineRegistrationEditView',
2124
components: {
@@ -39,12 +42,33 @@ export default {
3942
type: String,
4043
required: true
4144
},
45+
resource: {
46+
type: String,
47+
required: true
48+
},
49+
},
50+
async fetch() {
51+
// in CREATE mode, since YAMLEditor doesn't live update, we need to force a re-render of the component for it to update
52+
if (this.mode === _CREATE) {
53+
const operatorVersion = await getOperatorVersion(this.$store);
54+
55+
this.newCloudConfigcompatibilityCheck = checkGatedFeatureCompatibility(this.resource, this.mode, MACH_REG_CONFIG_DEFAULTS, operatorVersion);
56+
57+
if (!this.value.spec) {
58+
this.value.spec = this.newCloudConfigcompatibilityCheck ? DEFAULT_CREATION_YAML : OLD_DEFAULT_CREATION_YAML;
59+
}
60+
61+
this.cloudConfig = typeof this.value.spec === 'string' ? this.value.spec : saferDump(this.value.spec);
62+
this.rerender = true;
63+
}
4264
},
4365
data() {
4466
return {
45-
cloudConfig: typeof this.value.spec === 'string' ? this.value.spec : saferDump(this.value.spec),
46-
yamlErrors: null,
47-
isFormValid: true
67+
rerender: false,
68+
cloudConfig: typeof this.value.spec === 'string' ? this.value.spec : saferDump(this.value.spec),
69+
newCloudConfigcompatibilityCheck: false,
70+
yamlErrors: null,
71+
isFormValid: true
4872
};
4973
},
5074
watch: {
@@ -195,6 +219,7 @@ export default {
195219
<div class="col span-6">
196220
<h3>{{ t('elemental.machineRegistration.create.cloudConfiguration') }}</h3>
197221
<YamlEditor
222+
:key="rerender"
198223
ref="yamleditor"
199224
v-model="cloudConfig"
200225
class="mb-20"

pkg/elemental/models/elemental.cattle.io.machineregistration.js

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,17 @@ import { downloadFile } from '@shell/utils/download';
88
import { ELEMENTAL_DEFAULT_NAMESPACE } from '../types';
99
import ElementalResource from './elemental-resource';
1010

11-
const DEFAULT_CREATION_YAML = `config:
11+
export const OLD_DEFAULT_CREATION_YAML = `config:
12+
cloud-config:
13+
users:
14+
- name: root
15+
passwd: root
16+
elemental:
17+
install:
18+
poweroff: true
19+
device: /dev/nvme0n1`;
20+
21+
export const DEFAULT_CREATION_YAML = `config:
1222
cloud-config:
1323
users:
1424
- name: root
@@ -32,8 +42,8 @@ const DEFAULT_CREATION_YAML = `config:
3242

3343
export default class MachineRegistration extends ElementalResource {
3444
applyDefaults(vm, mode) {
35-
if ( !this.spec ) {
36-
Vue.set(this, 'spec', DEFAULT_CREATION_YAML);
45+
if ( !this.spec || mode === _CREATE ) {
46+
Vue.set(this, 'spec', {});
3747
}
3848
if ( !this.metadata || mode === _CREATE ) {
3949
Vue.set(this, 'metadata', { namespace: ELEMENTAL_DEFAULT_NAMESPACE });

pkg/elemental/pages/index.vue

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,18 @@ export default {
1616
// this covers scenario where Elemental Operator is deleted from Apps and we lose the Elemental Admin role for Standard Users...
1717
if (this.$store.getters['management/canList'](ELEMENTAL_SCHEMA_IDS.MACHINE_REGISTRATIONS)) {
1818
let installedApps;
19+
1920
// needed to check if operator is installed
2021
if (this.$store.getters['management/canList'](CATALOG.APP)) {
2122
installedApps = await this.$store.dispatch('management/findAll', { type: CATALOG.APP });
2223
}
2324
24-
const elementalSchema = this.$store.getters['management/schemaFor'](ELEMENTAL_SCHEMA_IDS.MACHINE_INVENTORIES)
25+
const elementalSchema = this.$store.getters['management/schemaFor'](ELEMENTAL_SCHEMA_IDS.MACHINE_INVENTORIES);
2526
2627
// we need to check for the length of the response
2728
// due to some issue with a standard-user, which can list apps
2829
// but the list comes up empty []
29-
const isElementalOperatorNotInstalledOnApps = installedApps?.length && !installedApps?.find(item => item.id.includes('elemental-operator'));
30+
const isElementalOperatorNotInstalledOnApps = installedApps?.length && !installedApps?.find(item => item.id.includes('elemental-operator') && !item.id.includes('elemental-operator-crds'));
3031
3132
// check if operator is installed
3233
if (!elementalSchema || isElementalOperatorNotInstalledOnApps) {
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import semver from 'semver';
2+
3+
import { _CREATE, _VIEW } from '@shell/config/query-params';
4+
import { WORKLOAD_TYPES } from '@shell/config/types';
5+
import { ELEMENTAL_SCHEMA_IDS } from '../config/elemental-types';
6+
import { ELEMENTAL_TYPES } from '../types';
7+
8+
interface FeaturesGatingConfig {
9+
area: string,
10+
mode: string[],
11+
minOperatorVersion: string,
12+
features: string[],
13+
}
14+
15+
// features to be gated to specific operator versions
16+
export const MACH_REG_CONFIG_DEFAULTS:string = 'machine-reg-config-defaults';
17+
export const BUILD_MEDIA_RAW_SUPPORT:string = 'build-media-raw-support';
18+
19+
const FEATURES_GATING:FeaturesGatingConfig[] = [
20+
{
21+
area: ELEMENTAL_SCHEMA_IDS.MACHINE_REGISTRATIONS,
22+
mode: [_CREATE],
23+
minOperatorVersion: '1.6.0',
24+
features: [MACH_REG_CONFIG_DEFAULTS]
25+
},
26+
{
27+
area: ELEMENTAL_TYPES.DASHBOARD,
28+
mode: [_VIEW],
29+
minOperatorVersion: '1.6.0',
30+
features: [BUILD_MEDIA_RAW_SUPPORT]
31+
},
32+
{
33+
area: ELEMENTAL_SCHEMA_IDS.MACHINE_REGISTRATIONS,
34+
mode: [_VIEW],
35+
minOperatorVersion: '1.6.0',
36+
features: [BUILD_MEDIA_RAW_SUPPORT]
37+
}
38+
];
39+
40+
/**
41+
* Get the current elemental-operator version
42+
* @param any Vue store
43+
* @returns Promise<string | void>
44+
*/
45+
export async function getOperatorVersion(store: any): Promise<string | void> {
46+
// needed to check operator version installed (on the deployment)
47+
if (store.getters['management/canList'](WORKLOAD_TYPES.DEPLOYMENT)) {
48+
const elementalOperatorDeployment = await store.dispatch('management/find', { type: WORKLOAD_TYPES.DEPLOYMENT, id: 'cattle-elemental-system/elemental-operator' });
49+
50+
return elementalOperatorDeployment?.metadata?.labels?.['app.kubernetes.io/version'] || '0.1.0';
51+
}
52+
53+
return '0.1.0';
54+
}
55+
56+
/**
57+
* Check the gated feature compatibility with the current Elemental Operator version installed
58+
* @param string resource type (ex: Deployment)
59+
* @param string UI mode (ex: edit, create, view)
60+
* @param string Elemental feature (ex: Build media, cloud config)
61+
* @param string Elemental Operator version
62+
* @returns Boolean
63+
*/
64+
export function checkGatedFeatureCompatibility(resource: string, mode: string, feature: string, operatorVersion: string): Boolean {
65+
if (resource && mode && feature) {
66+
const gatedFeature = FEATURES_GATING.find(feat => feat.area === resource && feat.mode.includes(mode) && feat.features.includes(feature));
67+
68+
if (!gatedFeature?.minOperatorVersion || !operatorVersion) {
69+
return false;
70+
}
71+
72+
return semver.gte(operatorVersion, gatedFeature?.minOperatorVersion);
73+
}
74+
75+
return false;
76+
}

0 commit comments

Comments
 (0)