Merge pull request #13372 from torchiaf/7824-expose-polling

Fleet: expose Polling Interval for GitRepos
This commit is contained in:
Francesco Torchia 2025-02-21 20:53:16 +01:00 committed by GitHub
commit 8a0e784f72
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 302 additions and 27 deletions

View File

@ -64,7 +64,7 @@
"@aws-sdk/client-kms": "3.8.1",
"@novnc/novnc": "1.2.0",
"@popperjs/core": "2.11.8",
"@rancher/icons": "2.0.29",
"@rancher/icons": "2.0.32",
"ansi_up": "5.0.0",
"axios": "0.21.4",
"axios-retry": "3.1.9",

View File

@ -2479,6 +2479,14 @@ fleet:
keepResourcesTooltip: When enabled, resources will be kept when deleting a GitRepo or Bundle - only Helm release secrets will be deleted.
correctDrift: Enable Self-Healing
correctDriftTooltip: When enabled, Fleet will ensure that the cluster resources are kept in sync with the git repository. All resource changes made on the cluster will be lost.
polling:
label: Polling
enable: Enable Polling
pollingInterval:
label: Polling Interval
tooltip: Polling Interval is the time between a push to the Repository and Fleet's reaction to it.
webhookWarning: Warning, a webhook is configured in Rancher to reconcile Git updates and GitRepo resources. The Polling Interval will automatically be adjusted to 1 hour.
minimumValuewarning: Warning, the recommended minimum Polling Interval is 15 seconds. To avoid performance issues with GitRepos that contain a large amount of resources please consider increasing this value.
add:
steps:
repoInfo:

View File

@ -40,7 +40,7 @@ export default {
},
computed: {
gitRepoHasClusters() {
return this.value.status.desiredReadyClusters;
return this.value.status?.desiredReadyClusters;
},
clusterSchema() {
return this.$store.getters['management/schemaFor'](FLEET.CLUSTER);

View File

@ -1,7 +1,20 @@
import { mount } from '@vue/test-utils';
import { _CREATE, _EDIT } from '@shell/config/query-params';
import GitRepo from '@shell/edit/fleet.cattle.io.gitrepo.vue';
describe('view: fleet.cattle.io.gitrepo should', () => {
const values = {
metadata: { namespace: 'test' },
spec: {
template: {},
correctDrift: { enabled: false },
},
targetInfo: { mode: 'all' },
};
describe.each([
_CREATE,
_EDIT
])('view: fleet.cattle.io.gitrepo, mode: %p - should', (mode) => {
const mockStore = {
dispatch: jest.fn(),
getters: {
@ -25,35 +38,32 @@ describe('view: fleet.cattle.io.gitrepo should', () => {
}
},
};
const values = {
metadata: { namespace: 'test' }, spec: { template: {}, correctDrift: { enabled: false } }, targetInfo: { mode: 'all' },
};
const wrapper = mount(GitRepo, {
props: { value: values },
props: { value: values, mode },
global: { mocks }
});
it('should have self-healing checkbox and tooltip', () => {
const correctDriftCheckbox = wrapper.find('[data-testid="GitRepo-correctDrift-checkbox"]');
const tooltip = wrapper.find('[data-testid="GitRepo-correctDrift-checkbox"]');
it('have self-healing checkbox and tooltip', () => {
const correctDriftCheckbox = wrapper.find('[data-testid="gitRepo-correctDrift-checkbox"]');
const tooltip = wrapper.find('[data-testid="gitRepo-correctDrift-checkbox"]');
expect(tooltip.element.classList).toContain('v-popper--has-tooltip');
expect(correctDriftCheckbox.exists()).toBeTruthy();
expect(correctDriftCheckbox.attributes().value).toBeFalsy();
});
it('should have keep-resources checkbox and tooltip', () => {
const correctDriftCheckbox = wrapper.find('[data-testid="GitRepo-keepResources-checkbox"]');
const tooltip = wrapper.find('[data-testid="GitRepo-keepResources-checkbox"]');
it('have keep-resources checkbox and tooltip', () => {
const correctDriftCheckbox = wrapper.find('[data-testid="gitRepo-keepResources-checkbox"]');
const tooltip = wrapper.find('[data-testid="gitRepo-keepResources-checkbox"]');
expect(tooltip.element.classList).toContain('v-popper--has-tooltip');
expect(correctDriftCheckbox.exists()).toBeTruthy();
expect(correctDriftCheckbox.attributes().value).toBeFalsy();
});
it('should enable drift if self-healing is checked', async() => {
const correctDriftCheckbox = wrapper.findComponent('[data-testid="GitRepo-correctDrift-checkbox"]');
const correctDriftContainer = wrapper.find('[data-testid="GitRepo-correctDrift-checkbox"] .checkbox-container');
it('enable drift if self-healing is checked', async() => {
const correctDriftCheckbox = wrapper.findComponent('[data-testid="gitRepo-correctDrift-checkbox"]');
const correctDriftContainer = wrapper.find('[data-testid="gitRepo-correctDrift-checkbox"] .checkbox-container');
expect(correctDriftContainer.exists()).toBeTruthy();
@ -63,4 +73,129 @@ describe('view: fleet.cattle.io.gitrepo should', () => {
expect(correctDriftCheckbox.emitted('update:value')![0][0]).toBe(true);
expect(correctDriftCheckbox.props().value).toBeTruthy();
});
it.each([
['show Polling Interval and warnings', 'enabled', undefined, true],
['show Polling Interval and warnings', 'enabled', false, true],
['hide Polling Interval and warnings', 'disabled', true, false],
])('show Enable Polling checkbox and %p if %p, with spec.disablePolling: %p', (
descr1,
descr2,
disablePolling,
enabled
) => {
const wrapper = mount(GitRepo, {
props: {
value: {
...values,
spec: {
disablePolling,
pollingInterval: 10
},
status: { webhookCommit: 'sha' },
},
realMode: mode
},
global: { mocks },
});
const pollingCheckbox = wrapper.findComponent('[data-testid="gitRepo-enablePolling-checkbox"]') as any;
const pollingIntervalInput = wrapper.find('[data-testid="gitRepo-pollingInterval-input"]');
const pollingIntervalMinimumValueWarning = wrapper.find('[data-testid="gitRepo-pollingInterval-minimumValueWarning"]');
const pollingIntervalWebhookWarning = wrapper.find('[data-testid="gitRepo-pollingInterval-webhookWarning"]');
expect(pollingIntervalMinimumValueWarning.exists()).toBe(enabled);
expect(pollingIntervalWebhookWarning.exists()).toBe(enabled);
expect(pollingCheckbox.exists()).toBeTruthy();
expect(pollingCheckbox.vm.value).toBe(enabled);
expect(pollingIntervalInput.exists()).toBe(enabled);
});
const defaultPollingInterval = mode === _CREATE ? '60' : '15';
it.each([
['null', `default ${ defaultPollingInterval } seconds`, null, defaultPollingInterval],
['0', `default ${ defaultPollingInterval } seconds`, 0, defaultPollingInterval],
['1', 'custom 1 second', 1, '1'],
['60', 'custom 60 seconds', 60, '60'],
['15', 'custom 15 seconds', 15, '15'],
])('show Polling Interval input with source: %p, value: %p', async(
descr1,
descr2,
pollingInterval,
unitValue,
) => {
const wrapper = mount(GitRepo, {
props: {
value: {
...values,
spec: { pollingInterval }
},
realMode: mode
},
global: { mocks },
});
const pollingIntervalInput = wrapper.find('[data-testid="gitRepo-pollingInterval-input"]').element as any;
expect(pollingIntervalInput).toBeDefined();
expect(pollingIntervalInput.value).toBe(unitValue);
});
it.each([
['hide', 'source: null, value: equal to 60', null, false],
['hide', 'source: 0, value: equal to 60', 0, false],
['hide', 'source: 15, value: equal to 15', 15, false],
['hide', 'source: 60, value: equal to 60', 60, false],
['hide', 'source: 16, value: higher than 15', 16, false],
['show', 'source: 1, value: lower than 15', 1, true],
])('%p Polling Interval warning if %p', async(
descr1,
descr2,
pollingInterval,
visible,
) => {
const wrapper = mount(GitRepo, {
props: {
value: {
...values,
spec: { pollingInterval }
},
realMode: mode
},
global: { mocks },
});
const pollingIntervalMinimumValueWarning = wrapper.find('[data-testid="gitRepo-pollingInterval-minimumValueWarning"]');
expect(pollingIntervalMinimumValueWarning.exists()).toBe(visible);
});
it.each([
['hide', 'disabled', null, false],
['hide', 'disabled', false, false],
['hide', 'disabled', '', false],
['show', 'enabled', 'sha', true],
])('%p Webhook configured warning if webhook is %p', (
descr1,
descr2,
webhookCommit,
visible
) => {
const wrapper = mount(GitRepo, {
props: {
value: {
...values,
spec: { pollingInterval: 60 },
status: { webhookCommit },
},
realMode: mode
},
global: { mocks },
});
const pollingIntervalWebhookWarning = wrapper.find('[data-testid="gitRepo-pollingInterval-webhookWarning"]');
expect(pollingIntervalWebhookWarning.exists()).toBe(visible);
});
});

View File

@ -19,13 +19,17 @@ import NameNsDescription from '@shell/components/form/NameNsDescription';
import YamlEditor from '@shell/components/YamlEditor';
import { base64Decode, base64Encode } from '@shell/utils/crypto';
import SelectOrCreateAuthSecret from '@shell/components/form/SelectOrCreateAuthSecret';
import { _CREATE } from '@shell/config/query-params';
import { _CREATE, _EDIT, _VIEW } from '@shell/config/query-params';
import { isHarvesterCluster } from '@shell/utils/cluster';
import { CAPI, CATALOG, FLEET as FLEET_LABELS } from '@shell/config/labels-annotations';
import { SECRET_TYPES } from '@shell/config/secret';
import { checkSchemasForFindAllHash } from '@shell/utils/auth';
import Checkbox from '@components/Form/Checkbox/Checkbox.vue';
import FormValidation from '@shell/mixins/form-validation';
import UnitInput from '@shell/components/form/UnitInput';
const MINIMUM_POLLING_INTERVAL = 15;
const DEFAULT_POLLING_INTERVAL = 60;
const _VERIFY = 'verify';
const _SKIP = 'skip';
@ -51,6 +55,7 @@ export default {
NameNsDescription,
YamlEditor,
SelectOrCreateAuthSecret,
UnitInput,
},
mixins: [CreateEditView, FormValidation],
@ -92,6 +97,16 @@ export default {
},
data() {
let pollingInterval = this.value.spec.pollingInterval;
if (!pollingInterval) {
if (this.realMode === _CREATE) {
pollingInterval = DEFAULT_POLLING_INTERVAL;
} else if (this.realMode === _EDIT || this.realMode === _VIEW) {
pollingInterval = MINIMUM_POLLING_INTERVAL;
}
}
const targetInfo = this.value.targetInfo;
const targetCluster = targetInfo.cluster;
const targetClusterGroup = targetInfo.clusterGroup;
@ -144,6 +159,7 @@ export default {
correctDriftEnabled: false,
targetAdvancedErrors: null,
matchingClusters: null,
pollingInterval,
ref,
refValue,
targetMode,
@ -185,6 +201,14 @@ export default {
return !(this.value?.spec?.repo || '').startsWith('http://');
},
isPollingEnabled() {
return !this.value.spec.disablePolling;
},
isWebhookConfigured() {
return !!this.value.status?.webhookCommit;
},
targetOptions() {
const out = [
{
@ -265,6 +289,10 @@ export default {
];
},
showPollingIntervalWarning() {
return !this.isView && this.isPollingEnabled && this.pollingInterval < MINIMUM_POLLING_INTERVAL;
},
stepOneRequires() {
return !!this.value.metadata.name && !!this.refValue && !!this.fvFormIsValid;
},
@ -512,6 +540,35 @@ export default {
}
},
enablePolling(value) {
if (value) {
delete this.value.spec.disablePolling;
} else {
this.value.spec.disablePolling = true;
}
},
updatePollingInterval(value) {
if (!value) {
this.pollingInterval = DEFAULT_POLLING_INTERVAL;
this.value.spec.pollingInterval = DEFAULT_POLLING_INTERVAL.toString();
} else if (value === MINIMUM_POLLING_INTERVAL) {
delete this.value.spec.pollingInterval;
} else {
this.value.spec.pollingInterval = value.toString();
}
},
scrollToBottom() {
this.$nextTick(() => {
const scrollable = document.getElementsByTagName('main')[0];
if (scrollable) {
scrollable.scrollTop = scrollable.scrollHeight;
}
});
},
onSave() {
this.value.spec['correctDrift'] = { enabled: this.correctDriftEnabled };
@ -669,7 +726,7 @@ export default {
<Checkbox
v-model:value="correctDriftEnabled"
:tooltip="t('fleet.gitRepo.resources.correctDriftTooltip')"
data-testid="GitRepo-correctDrift-checkbox"
data-testid="gitRepo-correctDrift-checkbox"
class="check"
type="checkbox"
label-key="fleet.gitRepo.resources.correctDrift"
@ -678,7 +735,7 @@ export default {
<Checkbox
v-model:value="value.spec.keepResources"
:tooltip="t('fleet.gitRepo.resources.keepResourcesTooltip')"
data-testid="GitRepo-keepResources-checkbox"
data-testid="gitRepo-keepResources-checkbox"
class="check"
type="checkbox"
label-key="fleet.gitRepo.resources.keepResources"
@ -696,6 +753,50 @@ export default {
:add-label="t('fleet.gitRepo.paths.addLabel')"
:protip="t('fleet.gitRepo.paths.empty')"
/>
<div class="spacer" />
<h2 v-t="'fleet.gitRepo.polling.label'" />
<div class="row polling">
<div class="col span-6">
<Checkbox
v-model:value="isPollingEnabled"
data-testid="gitRepo-enablePolling-checkbox"
class="check"
type="checkbox"
label-key="fleet.gitRepo.polling.enable"
:mode="mode"
@update:value="enablePolling"
/>
</div>
<template v-if="isPollingEnabled">
<div class="col">
<Banner
v-if="showPollingIntervalWarning"
color="warning"
label-key="fleet.gitRepo.polling.pollingInterval.minimumValuewarning"
data-testid="gitRepo-pollingInterval-minimumValueWarning"
/>
<Banner
v-if="isWebhookConfigured"
color="warning"
label-key="fleet.gitRepo.polling.pollingInterval.webhookWarning"
data-testid="gitRepo-pollingInterval-webhookWarning"
/>
</div>
<div class="col span-6">
<UnitInput
v-model:value="pollingInterval"
data-testid="gitRepo-pollingInterval-input"
min="1"
:suffix="t('suffix.seconds', { count: pollingInterval })"
:label="t('fleet.gitRepo.polling.pollingInterval.label')"
:mode="mode"
tooltip-key="fleet.gitRepo.polling.pollingInterval.tooltip"
@update:value="updatePollingInterval"
/>
</div>
</template>
</div>
</template>
<template #stepTargetInfo>
<h2 v-t="isLocal ? 'fleet.gitRepo.target.labelLocal' : 'fleet.gitRepo.target.label'" />
@ -781,6 +882,11 @@ export default {
.resource-handling {
display: flex;
flex-direction: column;
gap: 5px
gap: 5px;
}
.polling {
display: flex;
flex-direction: column;
gap: 5px;
}
</style>

View File

@ -254,7 +254,7 @@ export default {
>
<UnitInput
v-model:value="terminationGracePeriodSeconds"
:suffix="terminationGracePeriodSeconds == 1 ? 'Second' : 'Seconds'"
:suffix="t('suffix.seconds', { count: terminationGracePeriodSeconds })"
:label="t('workload.upgrading.activeDeadlineSeconds.label')"
:mode="mode"
@input="update"
@ -316,7 +316,7 @@ export default {
>
<UnitInput
v-model:value="terminationGracePeriodSeconds"
:suffix="terminationGracePeriodSeconds == 1 ? 'Second' : 'Seconds'"
:suffix="t('suffix.seconds', { count: terminationGracePeriodSeconds })"
:label="t('workload.upgrading.activeDeadlineSeconds.label')"
:mode="mode"
>

View File

@ -83,6 +83,22 @@ export default class GitRepo extends SteveModel {
});
insertAt(out, 2, {
action: 'enablePolling',
label: 'Enable Polling',
icon: 'icon icon-endpoints_connected',
bulkable: true,
enabled: !!this.links.update && !!this.spec?.disablePolling
});
insertAt(out, 3, {
action: 'disablePolling',
label: 'Disable Polling',
icon: 'icon icon-endpoints_disconnected',
bulkable: true,
enabled: !!this.links.update && !this.spec?.disablePolling
});
insertAt(out, 4, {
action: 'forceUpdate',
label: 'Force Update',
icon: 'icon icon-refresh',
@ -90,7 +106,7 @@ export default class GitRepo extends SteveModel {
enabled: !!this.links.update
});
insertAt(out, 3, { divider: true });
insertAt(out, 5, { divider: true });
return out;
}
@ -105,6 +121,16 @@ export default class GitRepo extends SteveModel {
this.save();
}
enablePolling() {
this.spec.disablePolling = false;
this.save();
}
disablePolling() {
this.spec.disablePolling = true;
this.save();
}
forceUpdate() {
const now = this.spec.forceSyncGeneration || 1;

View File

@ -3320,10 +3320,10 @@
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f"
integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==
"@rancher/icons@2.0.29":
version "2.0.29"
resolved "https://registry.npmjs.org/@rancher/icons/-/icons-2.0.29.tgz#6546d69768c706bebd66bfa73b36aef3d105987a"
integrity sha512-qBBqfazS9y5VjV7fJDPNXmxd9AP/2uiE05mKFWP41kpbO+tEb62RnUBXCm14XLeScDZQcOuiAKVHMmvCFzF0BA==
"@rancher/icons@2.0.32":
version "2.0.32"
resolved "https://registry.yarnpkg.com/@rancher/icons/-/icons-2.0.32.tgz#c6b18c81b87bc9439b1e449b2f6bd859d76c0bbe"
integrity sha512-afZlIHPboVgQIwnMuaQT7LTPoWuCIgyqD2Ch277lEAoFIzuU+NwHzpEAvX5xcOpVXYAFlvDE/VlJ6FmYaQBBig==
"@rtsao/scc@^1.1.0":
version "1.1.0"