Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

working on feature versioning mechanism #169

Merged
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@
"css-loader": "4.3.0"
},
"devDependencies": {
"@types/node": "18.11.9"
"@types/node": "18.11.9",
"@types/semver": "^7.5.8",
"semver": "^7.6.0"
},
"scripts": {
"dev": "NODE_ENV=dev ./node_modules/.bin/vue-cli-service serve",
Expand Down
49 changes: 43 additions & 6 deletions pkg/elemental/components/BuildMedia.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Banner } from '@components/Banner';
import AsyncButton from '@shell/components/AsyncButton';
import { randomStr, CHARSET } from '@shell/utils/string';
import { ELEMENTAL_SCHEMA_IDS } from '../config/elemental-types';
import { semverVersionCheck, getOperatorVersion, getGatedFeature, BUILD_MEDIA_RAW_SUPPORT } from '../utils/feature-versioning';

const MEDIA_TYPES = {
RAW: {
Expand Down Expand Up @@ -38,17 +39,32 @@ export default {
registrationEndpoint: {
type: String,
default: ''
},
resource: {
type: String,
default: ''
},
mode: {
type: String,
default: ''
}
},
async fetch() {
this.seedImagesList = await this.$store.dispatch('management/findAll', { type: ELEMENTAL_SCHEMA_IDS.SEED_IMAGE });
this.managedOsVersions = await this.$store.dispatch('management/findAll', { type: ELEMENTAL_SCHEMA_IDS.MANAGED_OS_VERSIONS });

this.operatorVersion = await getOperatorVersion(this.$store);
this.gatedFeature = getGatedFeature(this.resource, this.mode, BUILD_MEDIA_RAW_SUPPORT);
this.buildMediaGatingVersion = this.gatedFeature?.minOperatorVersion || '';
},
data() {
return {
seedImagesList: [],
managedOsVersions: [],
filteredManagedOsVersions: [],
operatorVersion: '',
gatedFeature: {},
buildMediaGatingVersion: '',
buildMediaOsVersions: [],
buildMediaTypes: [
{ label: MEDIA_TYPES.ISO.label, value: MEDIA_TYPES.ISO.type },
Expand Down Expand Up @@ -79,6 +95,19 @@ export default {
}
},
computed: {
isRawDiskImageBuildSupported() {
if (this.operatorVersion && this.buildMediaGatingVersion) {
const check = semverVersionCheck(this.operatorVersion, this.buildMediaGatingVersion);

if (!check) {
this.buildMediaTypeSelected = MEDIA_TYPES.ISO.type; // eslint-disable-line vue/no-side-effects-in-computed-properties
}

return check;
}

return false;
},
registrationEndpointsOptions() {
const activeRegEndpoints = this.registrationEndpointList.filter(item => item.state === 'active');

Expand All @@ -91,10 +120,10 @@ export default {
},
isBuildMediaBtnEnabled() {
if (this.displayRegEndpoints) {
return this.registrationEndpointSelected && this.buildMediaOsVersionSelected && this.buildMediaTypeSelected;
return this.isRawDiskImageBuildSupported ? this.registrationEndpointSelected && this.buildMediaOsVersionSelected && this.buildMediaTypeSelected : this.registrationEndpointSelected && this.buildMediaOsVersionSelected;
}

return this.buildMediaOsVersionSelected && this.buildMediaTypeSelected;
return this.isRawDiskImageBuildSupported ? this.buildMediaOsVersionSelected && this.buildMediaTypeSelected : this.buildMediaOsVersionSelected;
},
seedImageFound() {
if (this.seedImage) {
Expand Down Expand Up @@ -146,21 +175,26 @@ export default {
const machineRegName = this.displayRegEndpoints ? this.registrationEndpointSelected.split('/')[1] : this.registrationEndpoint.split('/')[1];
const machineRegNamespace = this.displayRegEndpoints ? this.registrationEndpointSelected.split('/')[0] : this.registrationEndpoint.split('/')[0];

const seedImageModel = await this.$store.dispatch('management/create', {
const seedImageObject = {
metadata: {
name: `media-image-reg-${ machineRegName }-${ randomStr(8, CHARSET.ALPHA_LOWER ) }`,
namespace: 'fleet-default'
},
spec: {
type: this.buildMediaTypeSelected,
baseImage: this.buildMediaOsVersionSelected,
registrationRef: {
name: machineRegName,
namespace: machineRegNamespace
}
},
type: ELEMENTAL_SCHEMA_IDS.SEED_IMAGE,
});
};

if (this.isRawDiskImageBuildSupported) {
seedImageObject.spec.type = this.buildMediaTypeSelected;
}

const seedImageModel = await this.$store.dispatch('management/create', seedImageObject);

try {
this.seedImage = await seedImageModel.save({ url: `/v1/${ ELEMENTAL_SCHEMA_IDS.SEED_IMAGE }`, method: 'POST' });
Expand Down Expand Up @@ -207,7 +241,10 @@ export default {
:options="registrationEndpointsOptions"
/>
</div>
<div class="col span-2">
<div
v-if="isRawDiskImageBuildSupported"
class="col span-2"
>
<LabeledSelect
v-model="buildMediaTypeSelected"
class="mr-20"
Expand Down
8 changes: 7 additions & 1 deletion pkg/elemental/components/DashboardView.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<script>
import { allHash } from '@shell/utils/promise';
import { CAPI, CATALOG } from '@shell/config/types';
import { _VIEW } from '@shell/config/query-params';
import { NAME } from '@shell/config/table-headers';
import ResourceTable from '@shell/components/ResourceTable';
import PercentageBar from '@shell/components/PercentageBar';
Expand All @@ -10,6 +11,7 @@ import {
ELEMENTAL_CLUSTER_PROVIDER,
KIND
} from '../config/elemental-types';
import { ELEMENTAL_TYPES } from '../types';
import { createElementalRoute } from '../utils/custom-routing';
import { filterForElementalClusters } from '../utils/elemental-utils';
import BuildMedia from './BuildMedia';
Expand Down Expand Up @@ -54,7 +56,7 @@ export default {
// we need to check for the length of the response
// due to some issue with a standard-user, which can list apps
// but the list comes up empty []
const isElementalOperatorNotInstalledOnApps = allDispatches.installedApps && allDispatches.installedApps.length && !allDispatches.installedApps.find(item => item.id.includes('elemental-operator'));
const isElementalOperatorNotInstalledOnApps = allDispatches.installedApps && allDispatches.installedApps.length && !allDispatches.installedApps.find(item => item.id.includes('elemental-operator') && !item.id.includes('elemental-operator-crds'));

// check if CRD is there but operator isn't
if (allDispatches.elementalSchema && isElementalOperatorNotInstalledOnApps) {
Expand All @@ -65,6 +67,8 @@ export default {
return {
ELEMENTAL_CLUSTERS: 'elementalClusters',
isElementalOpNotInstalledAndHasSchema: false,
resource: ELEMENTAL_TYPES.DASHBOARD,
mode: _VIEW,
resourcesData: {
[`${ ELEMENTAL_SCHEMA_IDS.MACHINE_REGISTRATIONS }`]: [],
[`${ ELEMENTAL_SCHEMA_IDS.MACHINE_INVENTORIES }`]: [],
Expand Down Expand Up @@ -283,6 +287,8 @@ export default {
<div class="mt-20 mb-20">
<BuildMedia
:registration-endpoint-list="registrationEndpoints"
:resource="resource"
:mode="mode"
/>
</div>
<!-- Tables -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ export default {
type: String,
required: true
},
resource: {
type: String,
required: true
},
},
data() {
return {
Expand Down Expand Up @@ -115,6 +119,8 @@ export default {
<BuildMedia
:display-reg-endpoints="false"
:registration-endpoint="`${value.metadata.namespace}/${value.metadata.name}`"
:resource="resource"
:mode="mode"
/>
</div>
<div
Expand Down
34 changes: 31 additions & 3 deletions pkg/elemental/edit/elemental.cattle.io.machineregistration.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ import { exceptionToErrorsArray } from '@shell/utils/error';
import Tabbed from '@shell/components/Tabbed/index.vue';
import Tab from '@shell/components/Tabbed/Tab.vue';

import { semverVersionCheck, getOperatorVersion, getGatedFeature, MACH_REG_CONFIG_DEFAULTS } from '../utils/feature-versioning';
import { OLD_DEFAULT_CREATION_YAML, DEFAULT_CREATION_YAML } from '../models/elemental.cattle.io.machineregistration';

export default {
name: 'MachineRegistrationEditView',
components: {
Expand All @@ -39,12 +42,36 @@ export default {
type: String,
required: true
},
resource: {
type: String,
required: true
},
},
async fetch() {
// in CREATE mode, since YAMLEditor doesn't live update, we need to force a re-render of the component for it to update
if (this.mode === _CREATE) {
const operatorVersion = await getOperatorVersion(this.$store);
const gatedFeature = getGatedFeature(this.resource, this.mode, MACH_REG_CONFIG_DEFAULTS);
const minOperatorVersion = gatedFeature?.minOperatorVersion || '';

this.newCloudConfigcompatibilityCheck = semverVersionCheck(operatorVersion, minOperatorVersion);

if (!this.value.spec) {
this.value.spec = this.newCloudConfigcompatibilityCheck ? DEFAULT_CREATION_YAML : OLD_DEFAULT_CREATION_YAML;
}

this.cloudConfig = typeof this.value.spec === 'string' ? this.value.spec : saferDump(this.value.spec);
this.rerender = true;
}
},
data() {
return {
cloudConfig: typeof this.value.spec === 'string' ? this.value.spec : saferDump(this.value.spec),
yamlErrors: null,
isFormValid: true
rerender: false,
cloudConfig: typeof this.value.spec === 'string' ? this.value.spec : saferDump(this.value.spec),
newCloudConfigcompatibilityCheck: false,
yamlErrors: null,
isFormValid: true,
gatedFeature: {}
};
},
watch: {
Expand Down Expand Up @@ -195,6 +222,7 @@ export default {
<div class="col span-6">
<h3>{{ t('elemental.machineRegistration.create.cloudConfiguration') }}</h3>
<YamlEditor
:key="rerender"
ref="yamleditor"
v-model="cloudConfig"
class="mb-20"
Expand Down
16 changes: 13 additions & 3 deletions pkg/elemental/models/elemental.cattle.io.machineregistration.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,17 @@ import { downloadFile } from '@shell/utils/download';
import { ELEMENTAL_DEFAULT_NAMESPACE } from '../types';
import ElementalResource from './elemental-resource';

const DEFAULT_CREATION_YAML = `config:
export const OLD_DEFAULT_CREATION_YAML = `config:
cloud-config:
users:
- name: root
passwd: root
elemental:
install:
poweroff: true
device: /dev/nvme0n1`;

export const DEFAULT_CREATION_YAML = `config:
cloud-config:
users:
- name: root
Expand All @@ -32,8 +42,8 @@ const DEFAULT_CREATION_YAML = `config:

export default class MachineRegistration extends ElementalResource {
applyDefaults(vm, mode) {
if ( !this.spec ) {
Vue.set(this, 'spec', DEFAULT_CREATION_YAML);
if ( !this.spec || mode === _CREATE ) {
Vue.set(this, 'spec', {});
}
if ( !this.metadata || mode === _CREATE ) {
Vue.set(this, 'metadata', { namespace: ELEMENTAL_DEFAULT_NAMESPACE });
Expand Down
5 changes: 3 additions & 2 deletions pkg/elemental/pages/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,18 @@ export default {
// this covers scenario where Elemental Operator is deleted from Apps and we lose the Elemental Admin role for Standard Users...
if (this.$store.getters['management/canList'](ELEMENTAL_SCHEMA_IDS.MACHINE_REGISTRATIONS)) {
let installedApps;

// needed to check if operator is installed
if (this.$store.getters['management/canList'](CATALOG.APP)) {
installedApps = await this.$store.dispatch('management/findAll', { type: CATALOG.APP });
}

const elementalSchema = this.$store.getters['management/schemaFor'](ELEMENTAL_SCHEMA_IDS.MACHINE_INVENTORIES)
const elementalSchema = this.$store.getters['management/schemaFor'](ELEMENTAL_SCHEMA_IDS.MACHINE_INVENTORIES);

// we need to check for the length of the response
// due to some issue with a standard-user, which can list apps
// but the list comes up empty []
const isElementalOperatorNotInstalledOnApps = installedApps?.length && !installedApps?.find(item => item.id.includes('elemental-operator'));
const isElementalOperatorNotInstalledOnApps = installedApps?.length && !installedApps?.find(item => item.id.includes('elemental-operator') && !item.id.includes('elemental-operator-crds'));

// check if operator is installed
if (!elementalSchema || isElementalOperatorNotInstalledOnApps) {
Expand Down
79 changes: 79 additions & 0 deletions pkg/elemental/utils/feature-versioning.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import semver from 'semver';

import { _CREATE, _VIEW } from '@shell/config/query-params';
import { WORKLOAD_TYPES } from '@shell/config/types';
import { ELEMENTAL_SCHEMA_IDS } from '../config/elemental-types';
import { ELEMENTAL_TYPES } from '../types';

interface FeaturesGatingConfig {
area: string,
mode: string[],
minOperatorVersion: string,
features: string[],
}

export const MACH_REG_CONFIG_DEFAULTS:string = 'machine-reg-config-defaults';
export const BUILD_MEDIA_RAW_SUPPORT:string = 'build-media-raw-support';

const FEATURES_GATING:FeaturesGatingConfig[] = [
{
area: ELEMENTAL_SCHEMA_IDS.MACHINE_REGISTRATIONS,
mode: [_CREATE],
minOperatorVersion: '1.6.0',
features: [MACH_REG_CONFIG_DEFAULTS]
},
{
area: ELEMENTAL_TYPES.DASHBOARD,
mode: [_VIEW],
minOperatorVersion: '1.6.0',
features: [BUILD_MEDIA_RAW_SUPPORT]
},
{
area: ELEMENTAL_SCHEMA_IDS.MACHINE_REGISTRATIONS,
mode: [_VIEW],
minOperatorVersion: '1.6.0',
features: [BUILD_MEDIA_RAW_SUPPORT]
}
];

/**
* Get the current elemental-operator version
* @param any store
* @param any alreadyInstalledApps
* @returns Promise<string | void>
*/
export async function getOperatorVersion(store: any): Promise<string | void> {
aalves08 marked this conversation as resolved.
Show resolved Hide resolved
// needed to check operator version installed (on the deployment)
if (store.getters['management/canList'](WORKLOAD_TYPES.DEPLOYMENT)) {
const elementalOperatorDeployment = await store.dispatch('management/find', { type: WORKLOAD_TYPES.DEPLOYMENT, id: 'cattle-elemental-system/elemental-operator' });

return elementalOperatorDeployment?.metadata?.labels?.['app.kubernetes.io/version'] || '0.1.0';
}

return '0.1.0';
}

/**
* Get the gated feature based on resource + mode + string
* @param string
aalves08 marked this conversation as resolved.
Show resolved Hide resolved
* @param string
* @param string
* @returns FeaturesGatingConfig | {} | void
*/
export function getGatedFeature(resource: string, mode: string, feature: string): FeaturesGatingConfig | {} | void {
if (resource && mode) {
return FEATURES_GATING.find(feat => feat.area === resource && feat.mode.includes(mode) && feat.features.includes(feature));
}

return {};
}

/**
* Determines if a given feature is enabled by doing a semver version comparison
* @param string
* @param string
* @returns Boolean | void
*/
export function semverVersionCheck(operatorVersion: string, operatorMinVersion: string): Boolean | void {
return semver.gte(operatorVersion, operatorMinVersion);
}
aalves08 marked this conversation as resolved.
Show resolved Hide resolved
Loading