Skip to content
Merged
Show file tree
Hide file tree
Changes from 44 commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
8c527c7
wip cluster loading
mantis-toboggan-md Sep 10, 2025
13567c8
wip extension page is reloading
mantis-toboggan-md Sep 16, 2025
07c13d1
loadCluster looks working
mantis-toboggan-md Sep 22, 2025
340a51e
wip vpc edit
mantis-toboggan-md Sep 29, 2025
f97f557
wip policy and fix catalog script
mantis-toboggan-md Sep 29, 2025
72879ca
loading
mantis-toboggan-md Sep 30, 2025
c33c572
ns saving is slow
mantis-toboggan-md Oct 2, 2025
49b9ad9
wip table status
mantis-toboggan-md Oct 3, 2025
3e74070
been a while since I commited
mantis-toboggan-md Oct 7, 2025
49dbd04
vcp working mostly
mantis-toboggan-md Oct 8, 2025
412be42
landing page
mantis-toboggan-md Oct 8, 2025
c82b1c7
try again button is buggy
mantis-toboggan-md Oct 9, 2025
07d9504
prefeedback
mantis-toboggan-md Oct 9, 2025
93cf5ee
only show partially assigned and deselected projects in inline status…
mantis-toboggan-md Oct 9, 2025
74b0861
modal open no dynamic
mantis-toboggan-md Oct 10, 2025
348846d
custom quota component; fix some input events
mantis-toboggan-md Oct 13, 2025
c40e4b0
reactive modal
mantis-toboggan-md Oct 13, 2025
fc7cc89
modal style
mantis-toboggan-md Oct 13, 2025
17a90c0
landing page
mantis-toboggan-md Oct 13, 2025
779484b
oops delete build files
mantis-toboggan-md Oct 13, 2025
34eb72e
clean up some style and translations in project statrus table; fix qu…
mantis-toboggan-md Oct 15, 2025
60661fe
filter projects already assigned to policies; fix table not rendering…
mantis-toboggan-md Oct 16, 2025
905d1b3
add name validation
mantis-toboggan-md Oct 16, 2025
7cd2103
remove namespace file
mantis-toboggan-md Oct 16, 2025
022631b
fix cancel button
mantis-toboggan-md Oct 16, 2025
7768d98
exclude projects with no ns
mantis-toboggan-md Oct 16, 2025
f15e257
add project assignment column to policy list view
mantis-toboggan-md Oct 16, 2025
24934dc
add psa none option, update some text
mantis-toboggan-md Oct 17, 2025
424950b
style and text tweaks
mantis-toboggan-md Oct 17, 2025
d871adb
get policies directly; find policy on edit
mantis-toboggan-md Oct 17, 2025
ea9f10d
simplify notification code
mantis-toboggan-md Oct 17, 2025
156a04f
clean up comments
mantis-toboggan-md Oct 20, 2025
d69496b
revert version change
mantis-toboggan-md Oct 20, 2025
586b92f
remove test component
mantis-toboggan-md Oct 20, 2025
9b5b650
use label; set mode and nodeselector from policy
mantis-toboggan-md Oct 21, 2025
ab70068
prompt remove modal; error text color
mantis-toboggan-md Oct 21, 2025
9d36f25
fix policy dropdown none option
mantis-toboggan-md Oct 21, 2025
927a541
feedback, clean up
mantis-toboggan-md Oct 21, 2025
1c1c7f2
loading behavior in policy dropdown
mantis-toboggan-md Oct 21, 2025
66cfedb
remove commented code
mantis-toboggan-md Oct 21, 2025
88bf0a8
pin version
mantis-toboggan-md Oct 21, 2025
4431b51
errors questionmark
mantis-toboggan-md Oct 21, 2025
def33ce
remove unused props
mantis-toboggan-md Oct 21, 2025
4ba0732
error banners
mantis-toboggan-md Oct 21, 2025
ba6c003
recheck 500 errors
mantis-toboggan-md Oct 23, 2025
fa46dfc
refactor stupid async code
mantis-toboggan-md Oct 24, 2025
091c0bc
feedback
mantis-toboggan-md Oct 24, 2025
c71a980
filter policies with no namespaces
mantis-toboggan-md Oct 24, 2025
dfbda46
fix namespace loading when no k3k installation detected
mantis-toboggan-md Oct 24, 2025
f062334
translation capitalization
mantis-toboggan-md Oct 24, 2025
c5784ff
remove unneeded watcher
mantis-toboggan-md Oct 24, 2025
f7f6762
fix formatter comment
mantis-toboggan-md Oct 24, 2025
24ac759
pr feedback
mantis-toboggan-md Oct 28, 2025
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
1 change: 1 addition & 0 deletions .github/workflows/build--extension-catalog.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,6 @@ jobs:
with:
registry_target: ghcr.io
registry_user: ${{ github.actor }}
tagged_release: ${{ github.ref_name }}
secrets:
registry_token: ${{ secrets.GITHUB_TOKEN }}
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"typescript.tsdk": "node_modules/typescript/lib"
}
29 changes: 9 additions & 20 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,43 +1,32 @@
{
"name": "virtual-clusters",
"version": "0.1.2",
"version": "0.1.3",
"private": false,
"engines": {
"node": ">=20.0.0"
},
"dependencies": {
"@babel/plugin-transform-class-static-block": "7.28.3",
"@rancher/components": "^0.3.0-alpha.1",
"@rancher/shell": "3.0.4",
"@types/lodash": "4.14.184",
"@rancher/shell": "3.0.6",
"@types/lodash": "4.14.196",
"cache-loader": "4.1.0",
"core-js": "3.21.1",
"css-loader": "6.7.3",
"focus-trap": "7.6.4"
"node-polyfill-webpack-plugin": "3.0.0"
},
"devDependencies": {
"@types/jest": "29.5.12",
"@types/lodash": "4.14.184",
"@types/node": "20.10.8",
"eslint": "7.32.0",
"eslint-config-standard": "16.0.3",
"eslint-import-resolver-node": "0.3.4",
"eslint-module-utils": "2.6.1",
"eslint-plugin-cypress": "2.12.1",
"eslint-plugin-import": "2.31.0",
"eslint-plugin-jest": "24.4.0",
"eslint-plugin-local-rules": "link:./eslint-plugin-local-rules",
"eslint-plugin-node": "11.1.0",
"eslint-plugin-promise": "7.2.1",
"eslint-plugin-vue": "9.10.0",
"file-loader": "6.2.0",
"node-polyfill-webpack-plugin": "3.0.0",
"raw-loader": "4.0.2",
"url-loader": "4.1.1"
"eslint-plugin-vue": "9.10.0"
},
"resolutions": {
"**/webpack": "5",
"@types/node": "~20.10.0",
"glob": "7.2.3",
"@types/lodash": "4.17.5",
"html-webpack-plugin": "5.6.3"
"glob": "7.2.3"
},
"scripts": {
"dev": "NODE_ENV=dev ./node_modules/.bin/vue-cli-service serve",
Expand Down
10 changes: 9 additions & 1 deletion pkg/virtual-clusters/babel.config.js
Original file line number Diff line number Diff line change
@@ -1 +1,9 @@
module.exports = require('./.shell/pkg/babel.config.js');
const baseConfig = require('@rancher/shell/pkg/babel.config');

module.exports = {
...baseConfig,
plugins: [
...(baseConfig.plugins || []),
'@babel/plugin-transform-class-static-block'
]
};
264 changes: 264 additions & 0 deletions pkg/virtual-clusters/components/CruK3KCluster/ClusterPolicy.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,264 @@
<script>
import { _CREATE } from '@shell/config/query-params';

import LabeledSelect from '@shell/components/form/LabeledSelect';
import { LABELS, K3K } from '../../types';
import { NAMESPACE } from '@shell/config/types';
import { Banner } from '@rancher/components';

import isEmpty from 'lodash/isEmpty';

export default {
name: 'K3kPolicySelector',

emits: ['update:policy', 'update:targetNamespace'],

components: { LabeledSelect, Banner },

props: {

mode: {
type: String,
default: _CREATE
},

targetNamespace: {
type: String,
default: ''
},

hostCluster: {
type: Object,
default: () => {
return {};
}
},

k3kInstalled: {
type: Boolean,
default: false
},

policy: {
type: Object,
default: () => {
return {};
}
},
},

async fetch() {
this.fetchPolicies();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we do not need to await for it?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah, fair point. We don't really need to await this in create mode; the component wont break if it tries to render without policies loaded, and the dropdowns have their own load spinner independent of $fetchState.pending. But if not in create mode, findSelectedPolicy is likely going to call fetchPolicies over again (and await it).

if (this.mode !== _CREATE) {
await this.findSelectedPolicy();
}
},

data() {
return {
policies: [],
namespaces: [],
loadingPolicies: false,
loadingNamespaces: false,
namespaceError: false,
policyError: false
};
},

watch: {
hostClusterId(neu, old) {
this.$emit('update:policy', {});
this.$emit('update:targetNamespace', '');
if (neu && this.k3kInstalled) {
this.fetchPolicies();
}
},

k3kInstalled(neu) {
if (neu) {
this.fetchPolicies();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here with await, sorry, I thought I mentioned it in the previous review. It's okay if this intentionally doesn't wait

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one is intentional

}
},

policyOptions(neu = []) {
const policyOpt = neu.find((p) => !!p?.value ) ;

if (this.mode === _CREATE) {
this.$emit('update:policy', policyOpt?.value || null);
this.$emit('update:targetNamespace', '');
}
},

namespaceOptions(neu = []) {
if (this.mode === _CREATE && !neu.includes(this.targetNamespace)) {
this.$emit('update:targetNamespace', neu[0] || '');
}
}
},

methods: {
async fetchPolicies() {
if (this.hostClusterId) {
this.loadingPolicies = true;

this.policyError = false;

this.policies = [];

try {
const res = await this.$store.dispatch('management/request', {
url: `/k8s/clusters/${ this.hostClusterId }/v1/${ K3K.POLICY }`,
method: 'GET'
});

this.policies = res.data || [];
} catch (err) {
this.policies = [];
this.policyError = true;
}

this.loadingPolicies = false;

return await this.fetchNamespaces();
}
},

async fetchNamespaces() {
this.loadingNamespaces = true;
this.namespaceError = false;

try {
const res = await this.$store.dispatch('management/request', {
url: `/k8s/clusters/${ this.hostClusterId }/v1/${ NAMESPACE }`,
method: 'GET'
});

this.namespaces = res.data || [];
} catch (e) {
this.namespaces = [];
this.namespaceError = true;
}

this.loadingNamespaces = false;
},

// we show policies in this form but they are not saved as part of the k3k cluster spec
// get the namespace the k3k cluster is in and check its labels to work out which policy the cluster falls under
async findSelectedPolicy() {
if (!this.policies.length) {
await this.fetchPolicies();
}

const nsObject = this.namespaces.find((ns) => ns.id === this.targetNamespace);

const policyName = nsObject?.metadata?.labels?.[LABELS.POLICY] || '';

// if we can't find the policy name, the namespace may be labeled with a policy that has since been deleted
// we should show 'none' in that case
const policyObject = this.policies.find((p) => p?.metadata?.name === policyName);

if (policyObject) {
this.$emit('update:policy', policyObject);
}

return '';
},

isEmpty
},

computed: {
isCreate() {
return this.mode === _CREATE;
},

hostClusterId() {
const mgmt = this.hostCluster?.mgmt;

return mgmt?.id;
},

policyOptions() {
return [{ label: this.t('generic.none'), value: null }, ...this.policies.map((p) => {
return { label: p?.metadata?.name, value: p };
})];
},

namespaceOptions() {
// if "no policy" is selected, show all NS without policy label
if ( !this.policy) {
return (this.namespaces || []).reduce((all, ns) => {
if (!ns?.metadata?.labels?.[LABELS.POLICY]) {
all.push(ns.id);
}

return all;
}, []);
}

return (this.namespaces || []).reduce((all, ns) => {
if (ns?.metadata?.labels?.[LABELS.POLICY] === this.policy?.metadata?.name) {
all.push(ns.id);
}

return all;
}, []);
}
},
};

</script>

<template>
<Banner
v-if="namespaceError"
color="error"
:label="t('k3k.errors.loadingNamespaces', {cluster:hostCluster?.displayName || hostCluster?.metadata?.name || '' })"
/>
<Banner
v-if="policyError && k3kInstalled"
color="error"
:label="t('k3k.errors.loadingPolicies', {cluster:hostCluster?.displayName || hostCluster?.metadata?.name || '' })"
/>
<div class="row mb-20">
<div
class="col span-6"
>
<LabeledSelect
:value="policy && !isEmpty(policy) ? policy : t('generic.none')"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't value be null instead of t('generic.none') ?

Copy link
Member Author

@mantis-toboggan-md mantis-toboggan-md Oct 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is making the LabeledSelect component display "None" when the value of policy in ClusterPolicy.vue is null

:loading="loadingPolicies || $fetchState.pending"
:disabled="!hostClusterId || !k3kInstalled || !isCreate"
:mode="mode"
:label="t('k3k.policy.label')"
:options="policyOptions"
:hover-tooltip="false"
@selecting="e=>$emit('update:policy', e)"
/>
<span
v-if="!policy && !loadingPolicies && k3kInstalled && !$fetchState.pending"
class="nonepolicy-warning text-muted"
><i class="icon icon-warning" />{{ t('k3k.policy.noneWarning') }}</span>
</div>
<div class="col span-6">
<LabeledSelect
:value="targetNamespace"
:loading="loadingNamespaces || loadingPolicies"
:mode="mode"
:disabled="!isCreate"
:label="t('k3k.targetNamespace.label')"
:options="namespaceOptions"
@selecting="e=>$emit('update:targetNamespace', e)"
/>
</div>
</div>
</template>

<style lang="scss">
.nonepolicy-warning {
margin: 3px;
display: flex;
& i {
margin-right: 3px;
}
}
</style>
17 changes: 14 additions & 3 deletions pkg/virtual-clusters/components/CruK3KCluster/HostCluster.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ const DOWNLOAD_MAX_RETRIES = 10;
const RETRY_WAIT = 1000;
const INCLUDE_LOCAL = process.env.dev;

export const K3K_CHART_NAME = 'k3k';
export const K3K_CHART_NAMESPACE = 'k3k-system';

export const K3K_REPO_NAME = 'k3k';
export const K3K_REPO_URL = 'https://rancher.github.io/k3k';

export default {
name: 'K3kHostCluster',

Expand Down Expand Up @@ -69,6 +75,10 @@ export default {
computed: {
...mapGetters({ t: 'i18n/withFallback' }),

isCreate() {
return this.mode === _CREATE;
},

parentClusterOptions() {
const out = this.clusters.reduce((opts, cluster) => {
if (!cluster?.metadata?.annotations?.['ui.rancher/parent-cluster'] && !(!INCLUDE_LOCAL && cluster.name === 'local') && cluster.mgmt.isReady) {
Expand Down Expand Up @@ -107,7 +117,7 @@ export default {
const mgmtCluster = cluster.mgmt;

await this.$store.dispatch('management/request', {
url: `/k8s/clusters/${ mgmtCluster.id }/v1/catalog.cattle.io.app/k3k-system/k3k`,
url: `/k8s/clusters/${ mgmtCluster.id }/v1/catalog.cattle.io.app/${ K3K_CHART_NAMESPACE }/${ K3K_CHART_NAME }`,
method: 'GET',
});

Expand All @@ -125,8 +135,8 @@ export default {
const repo = {
apiVersion: 'catalog.cattle.io/v1',
kind: 'ClusterRepo',
metadata: { name: 'k3k' },
spec: { url: 'https://rancher.github.io/k3k' }
metadata: { name: K3K_REPO_NAME },
spec: { url: K3K_REPO_URL }
};

const cluster = this.parentCluster;
Expand Down Expand Up @@ -254,6 +264,7 @@ export default {
v-model:value="selectedParentOption"
label-key="k3k.hostCluster.label"
:mode="mode"
:disabled="!isCreate"
:options="parentClusterOptions"
/>
</div>
Expand Down
Loading