@rancher/shell 3.0.9-rc.3 → 3.0.9-rc.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/assets/brand/suse/metadata.json +2 -1
  2. package/assets/translations/en-us.yaml +105 -5
  3. package/components/ActionMenuShell.vue +1 -1
  4. package/components/Inactivity.vue +2 -2
  5. package/components/Resource/Detail/Card/ExtrasCard.vue +49 -15
  6. package/components/Resource/Detail/Card/__tests__/ExtrasCard.test.ts +111 -0
  7. package/components/Resource/Detail/Masthead/__tests__/index.test.ts +0 -17
  8. package/components/Resource/Detail/Masthead/index.vue +11 -4
  9. package/components/Resource/Detail/Metadata/IdentifyingInformation/index.vue +3 -1
  10. package/components/Resource/Detail/Metadata/index.vue +1 -1
  11. package/components/Resource/Detail/ResourceRow.vue +1 -1
  12. package/components/ResourceDetail/Masthead/latest.vue +12 -2
  13. package/components/ResourceList/index.vue +9 -0
  14. package/components/ResourceTable.vue +38 -4
  15. package/components/Tabbed/Tab.vue +4 -0
  16. package/components/Tabbed/index.vue +4 -1
  17. package/components/__tests__/ProjectRow.test.ts +60 -0
  18. package/components/form/ChangePassword.vue +41 -35
  19. package/components/form/ResourceQuota/Project.vue +42 -1
  20. package/components/form/ResourceQuota/ProjectRow.vue +71 -4
  21. package/components/form/ResourceQuota/__tests__/Project.test.ts +63 -0
  22. package/components/form/SelectOrCreateAuthSecret.vue +6 -1
  23. package/components/form/__tests__/SelectOrCreateAuthSecret.test.ts +35 -0
  24. package/components/formatter/KubeconfigClusters.vue +74 -0
  25. package/components/formatter/MachineSummaryGraph.vue +10 -2
  26. package/components/formatter/__tests__/KubeconfigClusters.test.ts +125 -0
  27. package/components/nav/TopLevelMenu.helper.ts +50 -2
  28. package/components/nav/TopLevelMenu.vue +14 -0
  29. package/components/nav/Type.vue +5 -0
  30. package/components/nav/__tests__/TopLevelMenu.test.ts +3 -3
  31. package/components/nav/__tests__/Type.test.ts +6 -4
  32. package/config/product/explorer.js +4 -3
  33. package/config/product/manager.js +47 -3
  34. package/config/router/navigation-guards/authentication.js +8 -9
  35. package/config/router/routes.js +4 -1
  36. package/config/types.js +10 -2
  37. package/detail/auditlog.cattle.io.auditpolicy.vue +19 -0
  38. package/detail/management.cattle.io.user.vue +1 -2
  39. package/detail/node.vue +0 -1
  40. package/detail/provisioning.cattle.io.cluster.vue +2 -1
  41. package/dialog/ChangePasswordDialog.vue +8 -0
  42. package/dialog/GenericPrompt.vue +20 -3
  43. package/dialog/ScaleMachineDownDialog.vue +65 -15
  44. package/dialog/SearchDialog.vue +10 -2
  45. package/dialog/__tests__/ScaleMachineDownDialog.test.ts +184 -0
  46. package/edit/__tests__/fleet.cattle.io.gitrepo.test.ts +89 -0
  47. package/edit/__tests__/management.cattle.io.project.test.js +56 -1
  48. package/edit/auditlog.cattle.io.auditpolicy/AdditionalRedactions.vue +114 -0
  49. package/edit/auditlog.cattle.io.auditpolicy/Filters.vue +119 -0
  50. package/edit/auditlog.cattle.io.auditpolicy/General.vue +180 -0
  51. package/edit/auditlog.cattle.io.auditpolicy/__tests__/AdditionalRedactions.test.ts +327 -0
  52. package/edit/auditlog.cattle.io.auditpolicy/__tests__/Filters.test.ts +449 -0
  53. package/edit/auditlog.cattle.io.auditpolicy/__tests__/General.test.ts +472 -0
  54. package/edit/auditlog.cattle.io.auditpolicy/__tests__/__snapshots__/AdditionalRedactions.test.ts.snap +27 -0
  55. package/edit/auditlog.cattle.io.auditpolicy/__tests__/__snapshots__/Filters.test.ts.snap +39 -0
  56. package/edit/auditlog.cattle.io.auditpolicy/__tests__/__snapshots__/General.test.ts.snap +174 -0
  57. package/edit/auditlog.cattle.io.auditpolicy/__tests__/__snapshots__/index.test.ts.snap +29 -0
  58. package/edit/auditlog.cattle.io.auditpolicy/__tests__/index.test.ts +215 -0
  59. package/edit/auditlog.cattle.io.auditpolicy/index.vue +104 -0
  60. package/edit/auditlog.cattle.io.auditpolicy/types.ts +28 -0
  61. package/edit/fleet.cattle.io.gitrepo.vue +16 -1
  62. package/edit/management.cattle.io.project.vue +8 -2
  63. package/edit/management.cattle.io.user.vue +29 -34
  64. package/edit/provisioning.cattle.io.cluster/__tests__/rke2.test.ts +178 -0
  65. package/edit/provisioning.cattle.io.cluster/rke2.vue +22 -2
  66. package/edit/provisioning.cattle.io.cluster/shared.ts +4 -0
  67. package/edit/provisioning.cattle.io.cluster/tabs/Basics.vue +1 -0
  68. package/edit/provisioning.cattle.io.cluster/tabs/etcd/S3Config.vue +57 -2
  69. package/edit/provisioning.cattle.io.cluster/tabs/etcd/__tests__/S3Config.test.ts +109 -0
  70. package/edit/provisioning.cattle.io.cluster/tabs/etcd/index.vue +1 -0
  71. package/list/auditlog.cattle.io.auditpolicy.vue +63 -0
  72. package/list/ext.cattle.io.kubeconfig.vue +118 -0
  73. package/list/group.principal.vue +11 -15
  74. package/list/management.cattle.io.user.vue +11 -21
  75. package/machine-config/azure.vue +14 -0
  76. package/mixins/__tests__/chart.test.ts +147 -0
  77. package/mixins/browser-tab-visibility.js +5 -4
  78. package/mixins/chart.js +10 -8
  79. package/mixins/fetch.client.js +6 -0
  80. package/models/__tests__/auditlog.cattle.io.auditpolicy.test.ts +117 -0
  81. package/models/__tests__/ext.cattle.io.kubeconfig.test.ts +364 -0
  82. package/models/__tests__/secret.test.ts +55 -0
  83. package/models/__tests__/workload.test.ts +49 -6
  84. package/models/auditlog.cattle.io.auditpolicy.js +46 -0
  85. package/models/cluster.x-k8s.io.machine.js +1 -1
  86. package/models/cluster.x-k8s.io.machinedeployment.js +5 -5
  87. package/models/event.js +5 -0
  88. package/models/ext.cattle.io.groupmembershiprefreshrequest.js +15 -0
  89. package/models/ext.cattle.io.kubeconfig.ts +97 -0
  90. package/models/ext.cattle.io.passwordchangerequest.js +15 -0
  91. package/models/ext.cattle.io.selfuser.js +15 -0
  92. package/models/fleet-application.js +17 -7
  93. package/models/management.cattle.io.user.js +28 -31
  94. package/models/schema.js +18 -0
  95. package/models/secret.js +28 -25
  96. package/models/steve-schema.ts +39 -2
  97. package/models/workload.js +3 -2
  98. package/package.json +2 -2
  99. package/pages/about.vue +3 -2
  100. package/pages/account/index.vue +23 -16
  101. package/pages/auth/login.vue +15 -8
  102. package/pages/auth/setup.vue +52 -15
  103. package/pages/c/_cluster/apps/charts/AppChartCardFooter.vue +38 -14
  104. package/pages/c/_cluster/apps/charts/index.vue +1 -0
  105. package/pages/home.vue +9 -3
  106. package/plugins/dashboard-store/__tests__/resource-class.test.ts +1 -3
  107. package/plugins/dashboard-store/actions.js +7 -0
  108. package/plugins/dashboard-store/getters.js +23 -1
  109. package/plugins/dashboard-store/index.js +3 -2
  110. package/plugins/dashboard-store/mutations.js +4 -0
  111. package/plugins/dashboard-store/resource-class.js +12 -5
  112. package/plugins/steve/__tests__/steve-class.test.ts +167 -0
  113. package/plugins/steve/schema.d.ts +5 -0
  114. package/plugins/steve/steve-class.js +19 -0
  115. package/plugins/steve/steve-pagination-utils.ts +2 -1
  116. package/rancher-components/RcItemCard/RcItemCard.test.ts +4 -2
  117. package/rancher-components/RcItemCard/RcItemCard.vue +27 -10
  118. package/store/auth.js +57 -19
  119. package/store/notifications.ts +1 -1
  120. package/store/type-map.js +12 -1
  121. package/types/shell/index.d.ts +24 -15
  122. package/types/store/dashboard-store.types.ts +7 -0
  123. package/utils/__tests__/chart.test.ts +96 -0
  124. package/utils/__tests__/version.test.ts +1 -19
  125. package/utils/chart.js +64 -0
  126. package/utils/pagination-wrapper.ts +11 -3
  127. package/utils/version.js +5 -17
  128. package/vue.config.js +26 -13
@@ -1,5 +1,5 @@
1
1
  <script>
2
- import { MANAGEMENT, NORMAN } from '@shell/config/types';
2
+ import { MANAGEMENT, SECRET } from '@shell/config/types';
3
3
  import CreateEditView from '@shell/mixins/create-edit-view';
4
4
  import GlobalRoleBindings from '@shell/components/GlobalRoleBindings.vue';
5
5
  import ChangePassword from '@shell/components/form/ChangePassword';
@@ -9,6 +9,7 @@ import { exceptionToErrorsArray } from '@shell/utils/error';
9
9
  import { _CREATE, _EDIT } from '@shell/config/query-params';
10
10
  import Loading from '@shell/components/Loading';
11
11
  import { wait } from '@shell/utils/async';
12
+ import { base64Encode } from '@shell/utils/crypto';
12
13
 
13
14
  export default {
14
15
  components: {
@@ -128,26 +129,33 @@ export default {
128
129
  },
129
130
 
130
131
  async createUser() {
131
- // Ensure username is unique (this does not happen in the backend)
132
- const users = await this.$store.dispatch('management/findAll', { type: MANAGEMENT.USER });
133
-
134
- if (users.find((u) => u.username === this.form.username)) {
135
- throw new Error(this.t('user.edit.credentials.username.exists'));
136
- }
137
-
138
- const user = await this.$store.dispatch('rancher/create', {
139
- type: NORMAN.USER,
132
+ // never use "password" as it's deprecated!
133
+ const user = await this.$store.dispatch('management/create', {
134
+ type: MANAGEMENT.USER,
140
135
  description: this.form.description,
141
136
  enabled: true,
142
137
  mustChangePassword: this.form.password.userChangeOnLogin,
143
- name: this.form.displayName,
144
- password: this.form.password.password,
138
+ displayName: this.form.displayName,
145
139
  username: this.form.username
146
140
  });
147
141
 
148
- const newNormanUser = await user.save();
142
+ const userSaved = await user.save();
143
+
144
+ if (this.form.password.password) {
145
+ // create secret to hold user password
146
+ const secret = await this.$store.dispatch('management/create', {
147
+ type: SECRET,
148
+ metadata: {
149
+ namespace: 'cattle-local-user-passwords',
150
+ name: userSaved.id
151
+ },
152
+ data: { password: base64Encode(this.form.password.password) }
153
+ });
154
+
155
+ await secret.save();
156
+ }
149
157
 
150
- return this.$store.dispatch('management/find', { type: MANAGEMENT.USER, id: newNormanUser.id });
158
+ return userSaved;
151
159
  },
152
160
 
153
161
  async editUser() {
@@ -155,16 +163,13 @@ export default {
155
163
  return;
156
164
  }
157
165
 
158
- const normanUser = await this.$store.dispatch('rancher/find', {
159
- type: NORMAN.USER,
160
- id: this.value.id || this.user?.id,
161
- });
166
+ // never use this.value.password as it's deprecated!
167
+ this.value.description = this.form.description;
168
+ this.value.displayName = this.form.displayName;
169
+ this.value.mustChangePassword = this.form.password.userChangeOnLogin;
162
170
 
163
- // Save change of password
164
- // - Password must be changed before editing mustChangePassword (setpassword action sets this to false)
165
171
  if (this.form.password.password) {
166
- await this.$refs.changePassword.save(normanUser);
167
-
172
+ await this.$refs.changePassword.save(this.value);
168
173
  // Why the wait? Without these the user updates below are ignored
169
174
  // - The update request succeeds and shows the correct values in it's response.
170
175
  // - Fetching the norman user again sometimes shows the correct value, sometimes not
@@ -173,18 +178,7 @@ export default {
173
178
  await wait(5000);
174
179
  }
175
180
 
176
- // Save user updates
177
- normanUser.description = this.form.description;
178
- normanUser._name = this.form.displayName;
179
- normanUser.mustChangePassword = this.form.password.userChangeOnLogin;
180
-
181
- await normanUser.save();
182
-
183
- return await this.$store.dispatch('management/find', {
184
- type: MANAGEMENT.USER,
185
- id: this.value.id,
186
- opt: { force: true }
187
- });
181
+ this.value.save();
188
182
  },
189
183
 
190
184
  async updateRoles(userId) {
@@ -267,6 +261,7 @@ export default {
267
261
  v-model:value="form.password"
268
262
  :mode="mode"
269
263
  :must-change-password="value.mustChangePassword"
264
+ :user="value"
270
265
  @valid="validation.password = $event"
271
266
  />
272
267
  </div>
@@ -4,6 +4,7 @@ import { _CREATE } from '@shell/config/query-params';
4
4
  import rke2 from '@shell/edit/provisioning.cattle.io.cluster/rke2.vue';
5
5
  import { get } from '@shell/utils/object';
6
6
  import { rke2TestTable } from './utils/rke2-test-data';
7
+ import { NGINX_SUPPORTED, INGRESS_CONTROLLER, INGRESS_NGINX } from '@shell/edit/provisioning.cattle.io.cluster/shared';
7
8
 
8
9
  /**
9
10
  * DISCLAIMER ***************************************************************************************
@@ -612,4 +613,181 @@ describe('component: rke2', () => {
612
613
 
613
614
  expect(wrapper.vm.value.spec.rkeConfig.chartValues).toStrictEqual(expected);
614
615
  });
616
+
617
+ describe('should correctly update NGINX configuration', () => {
618
+ const k8sVersion = 'v1.25.0+rke2r1';
619
+ const createWrapper = () => {
620
+ return shallowMount(rke2, {
621
+ props: {
622
+ mode: _CREATE,
623
+ value: {
624
+ spec: {
625
+ ...defaultSpec,
626
+ rkeConfig: {
627
+ machineGlobalConfig: {},
628
+ chartValues: {},
629
+ upgradeStrategy: {},
630
+ dataDirectories: {},
631
+ machineSelectorConfig: []
632
+ },
633
+ kubernetesVersion: k8sVersion,
634
+ },
635
+ agentConfig: {}
636
+ },
637
+ provider: 'custom',
638
+ },
639
+ global: {
640
+ mocks: {
641
+ ...defaultMocks,
642
+ $store: { dispatch: () => jest.fn(), getters: defaultGetters },
643
+ $extension: { getDynamic: jest.fn(() => undefined ) },
644
+ },
645
+ stubs: defaultStubs,
646
+ },
647
+ });
648
+ };
649
+
650
+ it('should set ingress-controller to ingress-nginx by default when nginx is supported and not disabled', async() => {
651
+ const wrapper = createWrapper();
652
+
653
+ await wrapper.setData({
654
+ rke2Versions: [{
655
+ id: k8sVersion,
656
+ version: k8sVersion,
657
+ serverArgs: { disable: { options: [NGINX_SUPPORTED] } }
658
+ }]
659
+ });
660
+
661
+ expect(wrapper.vm.value.spec.rkeConfig.machineGlobalConfig[INGRESS_CONTROLLER]).toBe(INGRESS_NGINX);
662
+ });
663
+
664
+ it('should set ingress-controller to undefined by default when nginx is not supported', async() => {
665
+ const wrapper = createWrapper();
666
+
667
+ await wrapper.setData({
668
+ rke2Versions: [{
669
+ id: k8sVersion,
670
+ version: k8sVersion,
671
+ serverArgs: { disable: { options: [] } }
672
+ }]
673
+ });
674
+
675
+ expect(wrapper.vm.value.spec.rkeConfig.machineGlobalConfig[INGRESS_CONTROLLER]).toBeUndefined();
676
+ });
677
+
678
+ it('should set ingress-controller to ingress-nginx on change when nginx is supported and not disabled', () => {
679
+ const wrapper = createWrapper();
680
+
681
+ wrapper.setData({
682
+ rke2Versions: [{
683
+ id: k8sVersion,
684
+ version: k8sVersion,
685
+ serverArgs: { disable: { options: [NGINX_SUPPORTED] } }
686
+ }]
687
+ });
688
+
689
+ (wrapper.vm as any).handleEnabledSystemServicesChanged([]);
690
+
691
+ expect(wrapper.vm.value.spec.rkeConfig.machineGlobalConfig[INGRESS_CONTROLLER]).toBe(INGRESS_NGINX);
692
+ });
693
+
694
+ it('should set ingress-controller to undefined when nginx is supported but disabled', () => {
695
+ const wrapper = createWrapper();
696
+
697
+ wrapper.setData({
698
+ rke2Versions: [{
699
+ id: k8sVersion,
700
+ version: k8sVersion,
701
+ serverArgs: { disable: { options: [NGINX_SUPPORTED] } }
702
+ }]
703
+ });
704
+
705
+ (wrapper.vm as any).handleEnabledSystemServicesChanged([NGINX_SUPPORTED]);
706
+
707
+ expect(wrapper.vm.value.spec.rkeConfig.machineGlobalConfig[INGRESS_CONTROLLER]).toBeUndefined();
708
+ });
709
+
710
+ it('should set ingress-controller to undefined when nginx is not supported', () => {
711
+ const wrapper = createWrapper();
712
+
713
+ wrapper.setData({
714
+ rke2Versions: [{
715
+ id: k8sVersion,
716
+ version: k8sVersion,
717
+ serverArgs: { disable: { options: [] } }
718
+ }]
719
+ });
720
+
721
+ (wrapper.vm as any).handleEnabledSystemServicesChanged([]);
722
+
723
+ expect(wrapper.vm.value.spec.rkeConfig.machineGlobalConfig[INGRESS_CONTROLLER]).toBeUndefined();
724
+ });
725
+
726
+ it('should correctly update disable list in serverConfig', () => {
727
+ const wrapper = createWrapper();
728
+
729
+ wrapper.setData({
730
+ rke2Versions: [{
731
+ id: k8sVersion,
732
+ version: k8sVersion,
733
+ serverArgs: { disable: { options: [NGINX_SUPPORTED] } }
734
+ }]
735
+ });
736
+ const disabledServices = ['other-service'];
737
+
738
+ (wrapper.vm as any).handleEnabledSystemServicesChanged(disabledServices);
739
+
740
+ expect(wrapper.vm.value.spec.rkeConfig.machineGlobalConfig.disable).toStrictEqual(disabledServices);
741
+ });
742
+
743
+ it('should set ingress-controller to ingress-nginx on version change when nginx is supported', async() => {
744
+ const wrapper = createWrapper();
745
+ const newVersion = 'v1.26.0+rke2r1';
746
+
747
+ await wrapper.setData({
748
+ rke2Versions: [
749
+ {
750
+ id: k8sVersion,
751
+ version: k8sVersion,
752
+ serverArgs: { disable: { options: [NGINX_SUPPORTED] } }
753
+ },
754
+ {
755
+ id: newVersion,
756
+ version: newVersion,
757
+ serverArgs: { disable: { options: [NGINX_SUPPORTED] } }
758
+ }
759
+ ]
760
+ });
761
+
762
+ wrapper.vm.value.spec.kubernetesVersion = newVersion;
763
+ (wrapper.vm as any).handleKubernetesChange(newVersion);
764
+
765
+ expect(wrapper.vm.value.spec.rkeConfig.machineGlobalConfig[INGRESS_CONTROLLER]).toBe(INGRESS_NGINX);
766
+ });
767
+
768
+ it('should not set ingress-controller to ingress-nginx on version change when nginx is not supported', async() => {
769
+ const wrapper = createWrapper();
770
+ const newVersion = 'v1.26.0+rke2r1';
771
+
772
+ await wrapper.setData({
773
+ k3sVersions: [
774
+ {
775
+ id: k8sVersion,
776
+ version: k8sVersion,
777
+ serverArgs: { disable: { options: [] } }
778
+ },
779
+ {
780
+ id: newVersion,
781
+ version: newVersion,
782
+ serverArgs: { disable: { options: [] } }
783
+ }
784
+ ]
785
+ });
786
+
787
+ wrapper.vm.value.spec.kubernetesVersion = newVersion;
788
+ (wrapper.vm as any).handleKubernetesChange(newVersion);
789
+
790
+ expect(wrapper.vm.value.spec.rkeConfig.machineGlobalConfig[INGRESS_CONTROLLER]).toBeUndefined();
791
+ });
792
+ });
615
793
  });
@@ -66,6 +66,7 @@ import { DEFAULT_COMMON_BASE_PATH, DEFAULT_SUBDIRS } from '@shell/edit/provision
66
66
  import ClusterAppearance from '@shell/components/form/ClusterAppearance';
67
67
  import AddOnAdditionalManifest from '@shell/edit/provisioning.cattle.io.cluster/tabs/AddOnAdditionalManifest';
68
68
  import VsphereUtils, { VMWARE_VSPHERE } from '@shell/utils/v-sphere';
69
+ import { RETENTION_DEFAULT, NGINX_SUPPORTED, INGRESS_CONTROLLER, INGRESS_NGINX } from '@shell/edit/provisioning.cattle.io.cluster/shared';
69
70
  import { mapGetters } from 'vuex';
70
71
  const HARVESTER = 'harvester';
71
72
  const GOOGLE = 'google';
@@ -891,7 +892,13 @@ export default {
891
892
  this.fvFormIsValid &&
892
893
  this.etcdConfigValid;
893
894
  },
895
+ nginxSupported() {
896
+ if (this.serverArgs?.disable?.options.includes(NGINX_SUPPORTED)) {
897
+ return true;
898
+ }
894
899
 
900
+ return false;
901
+ },
895
902
  },
896
903
 
897
904
  watch: {
@@ -999,7 +1006,7 @@ export default {
999
1006
 
1000
1007
  this.localValue.spec.rkeConfig.networking.stackPreference = STACK_PREFS.IPV4;
1001
1008
  }
1002
- },
1009
+ }
1003
1010
  },
1004
1011
 
1005
1012
  created() {
@@ -1061,7 +1068,7 @@ export default {
1061
1068
  this.rkeConfig.etcd = {
1062
1069
  disableSnapshots: false,
1063
1070
  s3: null,
1064
- snapshotRetention: 5,
1071
+ snapshotRetention: RETENTION_DEFAULT,
1065
1072
  snapshotScheduleCron: '0 */5 * * *',
1066
1073
  };
1067
1074
  } else if (typeof this.rkeConfig.etcd.disableSnapshots === 'undefined') {
@@ -1882,6 +1889,7 @@ export default {
1882
1889
  if (!this.serverConfig?.profile) {
1883
1890
  this.serverConfig.profile = null;
1884
1891
  }
1892
+ this.updateNginxConfiguration(this.serverConfig?.disable || []);
1885
1893
  },
1886
1894
 
1887
1895
  chartVersionKey(name) {
@@ -2116,6 +2124,8 @@ export default {
2116
2124
  handleKubernetesChange(value, old) {
2117
2125
  if (value) {
2118
2126
  this.togglePsaDefault();
2127
+ // Need to make sure we explicitly set ingress due to a default change
2128
+ this.updateNginxConfiguration(this.serverConfig?.disable || []);
2119
2129
 
2120
2130
  // If Harvester driver, reset cloud provider if not compatible
2121
2131
  if (this.isHarvesterDriver && this.mode === _CREATE && this.isHarvesterIncompatible) {
@@ -2137,8 +2147,18 @@ export default {
2137
2147
  this.machinePoolValidation[id] = value;
2138
2148
  }
2139
2149
  },
2150
+
2151
+ updateNginxConfiguration(val) {
2152
+ if (val.includes(NGINX_SUPPORTED) || !this.nginxSupported) {
2153
+ this.serverConfig[INGRESS_CONTROLLER] = undefined;
2154
+ } else if (this.serverConfig[INGRESS_CONTROLLER] !== INGRESS_NGINX) {
2155
+ this.serverConfig[INGRESS_CONTROLLER] = INGRESS_NGINX;
2156
+ }
2157
+ },
2158
+
2140
2159
  handleEnabledSystemServicesChanged(val) {
2141
2160
  this.serverConfig.disable = val;
2161
+ this.updateNginxConfiguration(val);
2142
2162
  },
2143
2163
 
2144
2164
  handleCiliumValuesChanged(neu) {
@@ -0,0 +1,4 @@
1
+ export const RETENTION_DEFAULT = 5;
2
+ export const NGINX_SUPPORTED = 'rke2-ingress-nginx';
3
+ export const INGRESS_NGINX = 'ingress-nginx';
4
+ export const INGRESS_CONTROLLER = 'ingress-controller';
@@ -580,6 +580,7 @@ export default {
580
580
  <div class="col span-6">
581
581
  <!-- PSA template selector -->
582
582
  <LabeledSelect
583
+ :key="value.isK3s"
583
584
  v-model:value="value.spec.defaultPodSecurityAdmissionConfigurationTemplateName"
584
585
  :mode="mode"
585
586
  data-testid="rke2-custom-edit-psa"
@@ -4,6 +4,10 @@ import { LabeledInput } from '@components/Form/LabeledInput';
4
4
  import SelectOrCreateAuthSecret from '@shell/components/form/SelectOrCreateAuthSecret';
5
5
  import { NORMAN } from '@shell/config/types';
6
6
  import FormValidation from '@shell/mixins/form-validation';
7
+ import UnitInput from '@shell/components/form/UnitInput';
8
+ import RadioGroup from '@components/Form/Radio/RadioGroup.vue';
9
+ import { _CREATE } from '@shell/config/query-params';
10
+ import { RETENTION_DEFAULT } from '@shell/edit/provisioning.cattle.io.cluster/shared';
7
11
 
8
12
  export default {
9
13
  emits: ['update:value', 'validationChanged'],
@@ -12,6 +16,8 @@ export default {
12
16
  LabeledInput,
13
17
  Checkbox,
14
18
  SelectOrCreateAuthSecret,
19
+ UnitInput,
20
+ RadioGroup
15
21
  },
16
22
  mixins: [FormValidation],
17
23
 
@@ -35,6 +41,10 @@ export default {
35
41
  type: Function,
36
42
  required: true,
37
43
  },
44
+ localRetentionCount: {
45
+ type: Number,
46
+ default: null,
47
+ }
38
48
  },
39
49
 
40
50
  data() {
@@ -47,9 +57,11 @@ export default {
47
57
  folder: '',
48
58
  region: '',
49
59
  skipSSLVerify: false,
60
+ retention: null,
50
61
  ...(this.value || {}),
51
62
  },
52
- fvFormRuleSets: [
63
+ differentRetention: false,
64
+ fvFormRuleSets: [
53
65
  {
54
66
  path: 'endpoint', rootObject: this.config, rules: ['awsStyleEndpoint']
55
67
  },
@@ -59,6 +71,9 @@ export default {
59
71
  ]
60
72
  };
61
73
  },
74
+ mounted() {
75
+ this.differentRetention = !(this.mode === _CREATE || this.value?.retention === this.localRetentionCount);
76
+ },
62
77
 
63
78
  computed: {
64
79
 
@@ -73,10 +88,25 @@ export default {
73
88
 
74
89
  return {};
75
90
  },
91
+
92
+ localCountToUse() {
93
+ return this.localRetentionCount === null || this.localRetentionCount === undefined ? RETENTION_DEFAULT : this.localRetentionCount;
94
+ },
95
+ retentionOptionsOptions() {
96
+ return [
97
+ { label: this.t('cluster.rke2.etcd.s3config.snapshotRetention.options.localDefined', { count: this.localCountToUse }), value: false }, { label: this.t('cluster.rke2.etcd.s3config.snapshotRetention.options.manual'), value: true }
98
+ ];
99
+ }
76
100
  },
77
101
  watch: {
78
102
  fvFormIsValid(newValue) {
79
103
  this.$emit('validationChanged', !!newValue);
104
+ },
105
+ localRetentionCount(neu) {
106
+ if (!this.differentRetention) {
107
+ this.config.retention = this.localCountToUse;
108
+ this.update();
109
+ }
80
110
  }
81
111
  },
82
112
 
@@ -86,6 +116,10 @@ export default {
86
116
 
87
117
  this.$emit('update:value', out);
88
118
  },
119
+ resetRetention() {
120
+ this.config.retention = this.localCountToUse;
121
+ this.update();
122
+ }
89
123
  },
90
124
  };
91
125
  </script>
@@ -150,7 +184,6 @@ export default {
150
184
  />
151
185
  </div>
152
186
  </div>
153
-
154
187
  <div
155
188
  v-if="!ccData.defaultSkipSSLVerify"
156
189
  class="mt-20"
@@ -172,5 +205,27 @@ export default {
172
205
  @update:value="update"
173
206
  />
174
207
  </div>
208
+ <div class="row mt-20">
209
+ <div class="col span-6">
210
+ <h4>{{ t('cluster.rke2.etcd.s3config.snapshotRetention.title') }}</h4>
211
+ <RadioGroup
212
+ v-model:value="differentRetention"
213
+ name="s3config-retention"
214
+ :mode="mode"
215
+ :options="retentionOptionsOptions"
216
+ :row="true"
217
+ @update:value="resetRetention"
218
+ />
219
+ <UnitInput
220
+ v-if="differentRetention"
221
+ v-model:value="config.retention"
222
+ :label="t('cluster.rke2.etcd.s3config.snapshotRetention.label')"
223
+ :mode="mode"
224
+ :suffix="t('cluster.rke2.snapshots.s3Suffix')"
225
+ class="mt-10"
226
+ @update:value="update"
227
+ />
228
+ </div>
229
+ </div>
175
230
  </div>
176
231
  </template>
@@ -0,0 +1,109 @@
1
+ import { shallowMount } from '@vue/test-utils';
2
+ import S3Config from '@shell/edit/provisioning.cattle.io.cluster/tabs/etcd/S3Config.vue';
3
+
4
+ describe('s3Config', () => {
5
+ const defaultProps = {
6
+ mode: 'create',
7
+ namespace: 'test-ns',
8
+ registerBeforeHook: jest.fn(),
9
+ localRetentionCount: 5,
10
+ value: {}
11
+ };
12
+
13
+ const mockStore = { getters: { 'rancher/byId': jest.fn() } };
14
+
15
+ const createWrapper = (props = {}) => {
16
+ return shallowMount(S3Config, {
17
+ propsData: {
18
+ ...defaultProps,
19
+ ...props
20
+ },
21
+ mocks: {
22
+ $store: mockStore,
23
+ t: (key) => key,
24
+ },
25
+ stubs: {
26
+ LabeledInput: true,
27
+ Checkbox: true,
28
+ SelectOrCreateAuthSecret: true,
29
+ UnitInput: true,
30
+ RadioGroup: true,
31
+ }
32
+ });
33
+ };
34
+
35
+ it('renders correctly', () => {
36
+ const wrapper = createWrapper();
37
+
38
+ expect(wrapper.exists()).toBe(true);
39
+ });
40
+
41
+ it('initializes config with default values', () => {
42
+ const wrapper = createWrapper();
43
+
44
+ expect(wrapper.vm.config.bucket).toBe('');
45
+ expect(wrapper.vm.config.skipSSLVerify).toBe(false);
46
+ });
47
+
48
+ it('initializes config with provided value prop', () => {
49
+ const value = {
50
+ bucket: 'test',
51
+ region: 'us-east-1',
52
+ retention: 2
53
+ };
54
+ const wrapper = createWrapper({ value });
55
+
56
+ expect(wrapper.vm.config.bucket).toBe('test');
57
+ expect(wrapper.vm.config.region).toBe('us-east-1');
58
+ expect(wrapper.vm.config.retention).toBe(2);
59
+ });
60
+
61
+ describe('retention Logic', () => {
62
+ it('computes localCountToUse correctly', () => {
63
+ const wrapper = createWrapper({ localRetentionCount: 3 });
64
+
65
+ expect(wrapper.vm.localCountToUse).toBe(3);
66
+ });
67
+
68
+ it('uses default retention if localRetentionCount is null', () => {
69
+ const wrapper = createWrapper({ localRetentionCount: null });
70
+
71
+ expect(wrapper.vm.localCountToUse).toBe(5);
72
+ });
73
+
74
+ it('sets differentRetention to false in create mode', () => {
75
+ const wrapper = createWrapper({ mode: 'create' });
76
+
77
+ expect(wrapper.vm.differentRetention).toBe(false);
78
+ });
79
+
80
+ it('sets differentRetention to false in edit mode if retention matches', () => {
81
+ const wrapper = createWrapper({
82
+ mode: 'edit',
83
+ value: { retention: 5 },
84
+ localRetentionCount: 5
85
+ });
86
+
87
+ expect(wrapper.vm.differentRetention).toBe(false);
88
+ });
89
+
90
+ it('sets differentRetention to true in edit mode if retention differs', () => {
91
+ const wrapper = createWrapper({
92
+ mode: 'edit',
93
+ value: { retention: 2 },
94
+ localRetentionCount: 5
95
+ });
96
+
97
+ expect(wrapper.vm.differentRetention).toBe(true);
98
+ });
99
+
100
+ it('updates retention when localRetentionCount changes and differentRetention is false', async() => {
101
+ const wrapper = createWrapper({ localRetentionCount: 5 });
102
+
103
+ // differentRetention is false by default in create mode
104
+ await wrapper.setProps({ localRetentionCount: 10 });
105
+ expect(wrapper.vm.config.retention).toBe(10);
106
+ expect(wrapper.emitted('update:value')).toBeTruthy();
107
+ });
108
+ });
109
+ });
@@ -124,6 +124,7 @@ export default {
124
124
  <S3Config
125
125
  v-if="s3Backup"
126
126
  v-model:value="etcd.s3"
127
+ :local-retention-count="etcd.snapshotRetention"
127
128
  :namespace="value.metadata.namespace"
128
129
  :register-before-hook="registerBeforeHook"
129
130
  :mode="mode"
@@ -0,0 +1,63 @@
1
+ <script lang="ts">
2
+ import { BadgeState } from '@components/BadgeState';
3
+ import { AGE } from '@shell/config/table-headers';
4
+ import ResourceFetch from '@shell/mixins/resource-fetch';
5
+ import ResourceTable from '@shell/components/ResourceTable.vue';
6
+
7
+ export default {
8
+ components: { BadgeState, ResourceTable },
9
+ mixins: [ResourceFetch],
10
+ props: {
11
+ resource: {
12
+ type: String,
13
+ required: true,
14
+ },
15
+
16
+ schema: {
17
+ type: Object,
18
+ required: true,
19
+ },
20
+
21
+ useQueryParamsForSimpleFiltering: {
22
+ type: Boolean,
23
+ default: false
24
+ }
25
+ },
26
+
27
+ async fetch() {
28
+ await this.$fetchType(this.resource);
29
+ },
30
+
31
+ computed: {
32
+ headers() {
33
+ const headersFromSchema = this.$store.getters['type-map/headersFor'](this.schema);
34
+
35
+ const headers = headersFromSchema.filter((h: {name : string}) => h.name !== 'valid' && h.name !== 'active');
36
+
37
+ headers.push(AGE);
38
+
39
+ return headers;
40
+ },
41
+ }
42
+
43
+ };
44
+ </script>
45
+
46
+ <template>
47
+ <ResourceTable
48
+ :schema="schema"
49
+ :headers="headers"
50
+ :rows="rows"
51
+ :loading="loading"
52
+ :use-query-params-for-simple-filtering="useQueryParamsForSimpleFiltering"
53
+ :namespaced="schema.attributes.namespaced"
54
+ fetchSecondaryResources="fetchSecondaryResources"
55
+ >
56
+ <template #cell:enabled="{row}">
57
+ <BadgeState
58
+ :color="row.spec.enabled ? 'bg-success' : 'badge-disabled'"
59
+ :label="row.spec.enabled ? t('generic.enabled') : t('generic.disabled') "
60
+ />
61
+ </template>
62
+ </ResourceTable>
63
+ </template>