@rancher/shell 3.0.12-rc.3 → 3.0.12-rc.4

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 (258) hide show
  1. package/assets/styles/global/_layout.scss +4 -0
  2. package/assets/translations/en-us.yaml +144 -41
  3. package/assets/translations/zh-hans.yaml +1 -7
  4. package/chart/monitoring/ClusterSelector.vue +0 -21
  5. package/chart/monitoring/prometheus/index.vue +6 -3
  6. package/components/CruResource.vue +161 -14
  7. package/components/ExplorerMembers.vue +8 -4
  8. package/components/ExplorerProjectsNamespaces.vue +10 -6
  9. package/components/GrowlManager.vue +4 -0
  10. package/components/MgmtNodeList.vue +184 -0
  11. package/components/Resource/Detail/Card/StateCard/__tests__/composables.test.ts +90 -1
  12. package/components/Resource/Detail/Card/StateCard/composables.ts +57 -87
  13. package/components/Resource/Detail/Card/StatusCard/__tests__/StatusCard.test.ts +61 -0
  14. package/components/Resource/Detail/Card/StatusCard/index.vue +61 -15
  15. package/components/Resource/Detail/Metadata/IdentifyingInformation/index.vue +2 -0
  16. package/components/Resource/Detail/Metadata/KeyValue.vue +5 -2
  17. package/components/Resource/Detail/Metadata/KeyValueRow.vue +2 -6
  18. package/components/ResourceDetail/index.vue +1 -1
  19. package/components/ResourceList/Masthead.vue +7 -1
  20. package/components/ResourceList/index.vue +82 -1
  21. package/components/RichTranslation.vue +5 -2
  22. package/components/Setting.vue +1 -0
  23. package/components/SubtleLink.vue +31 -6
  24. package/components/Tabbed/Tab.vue +29 -3
  25. package/components/Tabbed/index.vue +25 -3
  26. package/components/TableOfContents/TableOfContents.vue +109 -0
  27. package/components/TableOfContents/composables.ts +258 -0
  28. package/components/Window/ContainerShell.vue +21 -11
  29. package/components/Window/__tests__/ContainerShell.test.ts +107 -37
  30. package/components/Wizard.vue +9 -4
  31. package/components/fleet/AppCoChartGrid.vue +401 -0
  32. package/components/fleet/AppCoEmptyState.vue +127 -0
  33. package/components/fleet/AppCoPageHeader.vue +119 -0
  34. package/components/fleet/AppCoVersionSelect.vue +70 -0
  35. package/components/fleet/FleetClusterTargets/ClusterSelectionFields.vue +217 -0
  36. package/components/fleet/FleetClusterTargets/TargetsList.vue +123 -35
  37. package/components/fleet/FleetClusterTargets/index.vue +189 -146
  38. package/components/fleet/FleetIntro.vue +7 -3
  39. package/components/fleet/FleetNoWorkspaces.vue +7 -3
  40. package/components/fleet/FleetSecretSelector.vue +5 -3
  41. package/components/fleet/FleetValuesFrom.vue +8 -2
  42. package/components/fleet/GitRepoTargetTab.vue +0 -2
  43. package/components/fleet/HelmOpAdvancedTab.vue +19 -53
  44. package/components/fleet/HelmOpAppCoConfigTab.vue +593 -0
  45. package/components/fleet/HelmOpAppCoResourcesSection.vue +162 -0
  46. package/components/fleet/HelmOpResourcesSection.vue +82 -0
  47. package/components/fleet/HelmOpTargetOptionsSection.vue +89 -0
  48. package/components/fleet/HelmOpTargetTab.vue +64 -60
  49. package/components/fleet/HelmOpValuesTab.vue +129 -105
  50. package/components/fleet/__tests__/AppCoEmptyState.test.ts +71 -0
  51. package/components/fleet/__tests__/AppCoVersionSelect.test.ts +36 -0
  52. package/components/fleet/__tests__/ClusterSelectionFields.test.ts +62 -0
  53. package/components/fleet/__tests__/FleetClusterTargets.test.ts +253 -0
  54. package/components/fleet/__tests__/FleetSecretSelector.test.ts +16 -0
  55. package/components/fleet/__tests__/FleetValuesFrom.test.ts +44 -0
  56. package/components/fleet/__tests__/HelmOpAppCoConfigTab.test.ts +59 -0
  57. package/components/fleet/__tests__/HelmOpAppCoResourcesSection.test.ts +62 -0
  58. package/components/fleet/__tests__/HelmOpResourcesSection.test.ts +43 -0
  59. package/components/fleet/__tests__/HelmOpTargetOptionsSection.test.ts +34 -0
  60. package/components/fleet/__tests__/HelmOpValuesTab.test.ts +39 -0
  61. package/components/fleet/__tests__/__snapshots__/AppCoEmptyState.test.ts.snap +97 -0
  62. package/components/fleet/__tests__/__snapshots__/AppCoVersionSelect.test.ts.snap +30 -0
  63. package/components/fleet/__tests__/__snapshots__/ClusterSelectionFields.test.ts.snap +209 -0
  64. package/components/fleet/__tests__/__snapshots__/HelmOpTargetOptionsSection.test.ts.snap +140 -0
  65. package/components/fleet/dashboard/Empty.vue +8 -4
  66. package/components/fleet/dashboard/ResourceCard.vue +28 -0
  67. package/components/fleet/dashboard/ResourceDetails.vue +28 -0
  68. package/components/fleet/dashboard/__tests__/ResourceCard.test.ts +87 -0
  69. package/components/form/ArrayList.vue +61 -4
  70. package/components/form/KeyValue.vue +23 -2
  71. package/components/form/LabeledSelect.vue +39 -1
  72. package/components/form/Labels.vue +22 -3
  73. package/components/form/NameNsDescription.vue +13 -5
  74. package/components/form/ResourceTabs/index.vue +1 -0
  75. package/components/form/__tests__/NameNsDescription.test.ts +75 -0
  76. package/components/formatter/InternalExternalIP.vue +10 -4
  77. package/components/formatter/ServiceTargets.vue +26 -7
  78. package/components/formatter/__tests__/InternalExternalIP.test.ts +132 -0
  79. package/components/formatter/__tests__/ServiceTargets.test.ts +412 -0
  80. package/components/nav/Header.vue +4 -0
  81. package/components/nav/TopLevelMenu.vue +7 -2
  82. package/components/nav/__tests__/Header.test.ts +15 -0
  83. package/components/nav/__tests__/TopLevelMenu.test.ts +120 -2
  84. package/components/templates/default.vue +9 -4
  85. package/components/templates/home.vue +9 -4
  86. package/components/templates/plain.vue +9 -4
  87. package/composables/useHelmOpResources.test.ts +56 -0
  88. package/composables/useHelmOpResources.ts +32 -0
  89. package/composables/useStateColor.test.ts +325 -0
  90. package/composables/useStateColor.ts +128 -0
  91. package/config/home-links.js +1 -1
  92. package/config/labels-annotations.js +1 -0
  93. package/config/product/explorer.js +17 -4
  94. package/config/product/manager.js +2 -0
  95. package/config/router/index.js +16 -0
  96. package/config/router/navigation-guards/__tests__/authentication.test.ts +130 -0
  97. package/config/router/navigation-guards/authentication.js +10 -4
  98. package/config/router/routes.js +20 -6
  99. package/config/settings.ts +0 -2
  100. package/config/table-headers.js +3 -4
  101. package/config/types.js +9 -0
  102. package/core/plugin-products-base.ts +3 -3
  103. package/core/plugin-types.ts +83 -30
  104. package/core/plugin.ts +3 -0
  105. package/core/types-provisioning.ts +34 -1
  106. package/core/types.ts +15 -2
  107. package/detail/__tests__/provisioning.cattle.io.cluster.test.ts +114 -0
  108. package/detail/__tests__/workload.test.ts +3 -152
  109. package/detail/catalog.cattle.io.clusterrepo.vue +1 -1
  110. package/detail/provisioning.cattle.io.cluster.vue +30 -4
  111. package/detail/workload/index.vue +12 -55
  112. package/edit/__tests__/catalog.cattle.io.clusterrepo.test.ts +248 -0
  113. package/edit/__tests__/fleet.cattle.io.helmop.test.ts +105 -0
  114. package/edit/auditlog.cattle.io.auditpolicy/__tests__/__snapshots__/General.test.ts.snap +6 -0
  115. package/edit/auditlog.cattle.io.auditpolicy/__tests__/__snapshots__/index.test.ts.snap +1 -0
  116. package/edit/auth/__tests__/azuread.test.ts +34 -9
  117. package/edit/auth/__tests__/github.test.ts +234 -0
  118. package/edit/auth/__tests__/oidc.test.ts +26 -6
  119. package/edit/auth/__tests__/saml.test.ts +196 -0
  120. package/edit/auth/azuread.vue +128 -95
  121. package/edit/auth/github.vue +72 -13
  122. package/edit/auth/ldap/__tests__/index.test.ts +206 -0
  123. package/edit/auth/ldap/config.vue +8 -0
  124. package/edit/auth/ldap/index.vue +75 -1
  125. package/edit/auth/oidc.vue +119 -73
  126. package/edit/auth/saml.vue +76 -12
  127. package/edit/catalog.cattle.io.clusterrepo.vue +140 -32
  128. package/edit/fleet.cattle.io.helmop.vue +491 -136
  129. package/edit/management.cattle.io.user.vue +5 -2
  130. package/edit/provisioning.cattle.io.cluster/rke2.vue +84 -10
  131. package/edit/provisioning.cattle.io.cluster/tabs/MachinePool.vue +11 -0
  132. package/list/group.principal.vue +5 -4
  133. package/list/harvesterhci.io.management.cluster.vue +8 -9
  134. package/list/management.cattle.io.user.vue +12 -9
  135. package/list/provisioning.cattle.io.cluster.vue +16 -10
  136. package/mixins/__tests__/auth-config.test.ts +90 -0
  137. package/mixins/__tests__/chart.test.ts +94 -0
  138. package/mixins/__tests__/resource-fetch-api-pagination.test.ts +48 -0
  139. package/mixins/auth-config.js +7 -0
  140. package/mixins/chart.js +11 -2
  141. package/mixins/child-hook.js +12 -6
  142. package/mixins/create-edit-view/impl.js +5 -3
  143. package/mixins/resource-fetch-api-pagination.js +21 -1
  144. package/models/__tests__/catalog.cattle.io.clusterrepo.test.ts +57 -0
  145. package/models/__tests__/compliance.cattle.io.clusterscan.test.ts +144 -0
  146. package/models/__tests__/fleet-application.test.ts +175 -0
  147. package/models/__tests__/fleet.cattle.io.bundle.test.ts +169 -0
  148. package/models/__tests__/fleet.cattle.io.helmop.test.ts +84 -0
  149. package/models/__tests__/management.cattle.io.node.ts +22 -0
  150. package/models/__tests__/namespace.test.ts +36 -0
  151. package/models/__tests__/provisioning.cattle.io.cluster.test.ts +49 -0
  152. package/models/__tests__/workload.test.ts +401 -26
  153. package/models/catalog.cattle.io.clusterrepo.js +28 -4
  154. package/models/compliance.cattle.io.clusterscan.js +39 -4
  155. package/models/fleet-application.js +4 -0
  156. package/models/fleet.cattle.io.helmop.js +20 -1
  157. package/models/management.cattle.io.cluster.js +18 -2
  158. package/models/management.cattle.io.node.js +44 -3
  159. package/models/namespace.js +1 -1
  160. package/models/pod.js +33 -1
  161. package/models/provisioning.cattle.io.cluster.js +5 -5
  162. package/models/workload.js +108 -13
  163. package/models/workload.service.js +5 -0
  164. package/package.json +14 -13
  165. package/pages/about.vue +5 -6
  166. package/pages/auth/login.vue +0 -35
  167. package/pages/auth/setup.vue +11 -0
  168. package/pages/c/_cluster/apps/charts/AppChartCardFooter.vue +2 -2
  169. package/pages/c/_cluster/apps/charts/AppChartCardSubHeader.vue +10 -1
  170. package/pages/c/_cluster/apps/charts/__tests__/index.test.ts +93 -0
  171. package/pages/c/_cluster/apps/charts/chart.vue +2 -1
  172. package/pages/c/_cluster/apps/charts/index.vue +48 -10
  173. package/pages/c/_cluster/apps/charts/install.vue +122 -116
  174. package/pages/c/_cluster/auth/roles/index.vue +5 -4
  175. package/pages/c/_cluster/explorer/workload-dashboard/ByNamespaceSection.vue +31 -0
  176. package/pages/c/_cluster/explorer/workload-dashboard/ByStateSection.vue +138 -0
  177. package/pages/c/_cluster/explorer/workload-dashboard/ByTypeSection.vue +30 -0
  178. package/pages/c/_cluster/explorer/workload-dashboard/WorkloadCard.vue +155 -0
  179. package/pages/c/_cluster/explorer/workload-dashboard/WorkloadNamespaceCard.vue +142 -0
  180. package/pages/c/_cluster/explorer/workload-dashboard/WorkloadTypeCard.vue +159 -0
  181. package/pages/c/_cluster/explorer/workload-dashboard/__tests__/composable.test.ts +561 -0
  182. package/pages/c/_cluster/explorer/workload-dashboard/composable.ts +440 -0
  183. package/pages/c/_cluster/explorer/workload-dashboard/index.vue +187 -0
  184. package/pages/c/_cluster/explorer/workload-dashboard/types.ts +80 -0
  185. package/pages/c/_cluster/fleet/application/create.vue +187 -136
  186. package/pages/c/_cluster/fleet/application/index.vue +5 -3
  187. package/pages/c/_cluster/fleet/application/suse-app-collection/ChartDetailBody.vue +338 -0
  188. package/pages/c/_cluster/fleet/application/suse-app-collection/ChartDetailHeader.vue +121 -0
  189. package/pages/c/_cluster/fleet/application/suse-app-collection/chart.vue +369 -0
  190. package/pages/c/_cluster/fleet/application/suse-app-collection/charts.vue +248 -0
  191. package/pages/c/_cluster/fleet/application/suse-app-collection/credentials.vue +310 -0
  192. package/pages/c/_cluster/fleet/index.vue +2 -2
  193. package/pages/c/_cluster/uiplugins/__tests__/index.test.ts +96 -0
  194. package/pages/c/_cluster/uiplugins/index.vue +15 -0
  195. package/pages/fail-whale.vue +16 -11
  196. package/pages/home.vue +16 -46
  197. package/plugins/clean-html.d.ts +9 -0
  198. package/plugins/dashboard-store/__tests__/resource-class.test.ts +93 -0
  199. package/plugins/dashboard-store/resource-class.js +62 -7
  200. package/plugins/steve/__tests__/actions.test.ts +212 -0
  201. package/plugins/steve/actions.js +96 -0
  202. package/plugins/steve/steve-pagination-utils.ts +1 -1
  203. package/rancher-components/Accordion/Accordion.vue +53 -9
  204. package/rancher-components/Form/Checkbox/Checkbox.vue +14 -0
  205. package/rancher-components/Form/Radio/RadioButton.vue +17 -1
  206. package/rancher-components/Form/Radio/RadioGroup.vue +10 -0
  207. package/rancher-components/Pill/RcTag/RcTag.vue +3 -2
  208. package/rancher-components/RcButton/RcButton.test.ts +103 -0
  209. package/rancher-components/RcButton/RcButton.vue +94 -15
  210. package/rancher-components/RcButton/types.ts +3 -0
  211. package/rancher-components/RcItemCard/RcItemCard.test.ts +18 -0
  212. package/rancher-components/RcItemCard/RcItemCard.vue +2 -2
  213. package/rancher-components/RcSection/RcSection.vue +28 -3
  214. package/scripts/extension/helm/package/Dockerfile +1 -1
  215. package/scripts/test-plugins-build.sh +2 -1
  216. package/store/__tests__/notifications.test.ts +434 -0
  217. package/store/catalog.js +57 -0
  218. package/store/plugins.js +7 -4
  219. package/types/components/buttonGroup.ts +5 -0
  220. package/types/shell/index.d.ts +104 -70
  221. package/utils/__tests__/auth.test.ts +273 -0
  222. package/utils/__tests__/computed.test.ts +193 -0
  223. package/utils/__tests__/cspAdaptor.test.ts +163 -0
  224. package/utils/__tests__/dom.test.ts +81 -0
  225. package/utils/__tests__/duration.test.ts +37 -1
  226. package/utils/__tests__/dynamic-importer.test.ts +102 -0
  227. package/utils/__tests__/fleet-appco.test.ts +312 -0
  228. package/utils/__tests__/monitoring.test.ts +130 -0
  229. package/utils/__tests__/object.test.ts +22 -0
  230. package/utils/__tests__/platform.test.ts +91 -0
  231. package/utils/__tests__/position.test.ts +237 -0
  232. package/utils/__tests__/provider.test.ts +51 -1
  233. package/utils/__tests__/queue.test.ts +232 -0
  234. package/utils/__tests__/release-notes.test.ts +221 -0
  235. package/utils/__tests__/router.test.js +254 -1
  236. package/utils/__tests__/select.test.ts +208 -0
  237. package/utils/__tests__/time.test.ts +265 -1
  238. package/utils/__tests__/title.test.ts +47 -0
  239. package/utils/__tests__/width.test.ts +53 -0
  240. package/utils/__tests__/window.test.ts +158 -0
  241. package/utils/__tests__/xccdf.test.ts +126 -6
  242. package/utils/crypto/__tests__/browserHashUtils.test.ts +98 -0
  243. package/utils/crypto/__tests__/index.test.ts +144 -0
  244. package/utils/duration.ts +104 -0
  245. package/utils/dynamic-content/__tests__/notification-handler.test.ts +196 -0
  246. package/utils/dynamic-content/info.ts +2 -1
  247. package/utils/error.js +13 -0
  248. package/utils/fleet-appco.ts +323 -0
  249. package/utils/object.js +22 -2
  250. package/utils/provider.ts +12 -0
  251. package/utils/validators/__tests__/container-images.test.ts +104 -0
  252. package/utils/validators/__tests__/flow-output.test.ts +91 -0
  253. package/utils/validators/__tests__/logging-outputs.test.ts +58 -0
  254. package/utils/validators/__tests__/monitoring-route.test.ts +119 -0
  255. package/utils/xccdf.ts +39 -42
  256. package/vue.config.js +1 -1
  257. package/pages/support/index.vue +0 -264
  258. package/utils/duration.js +0 -43
@@ -0,0 +1,323 @@
1
+ import { base64Encode } from '@shell/utils/crypto';
2
+ import { CATALOG as CATALOG_TYPES, SECRET } from '@shell/config/types';
3
+ import { CATALOG, DESCRIPTION, FLEET as FLEET_LABELS } from '@shell/config/labels-annotations';
4
+ import { SECRET_TYPES } from '@shell/config/secret';
5
+
6
+ export const SUSE_APP_COLLECTION_REPO_URL = 'oci://dp.apps.rancher.io/charts';
7
+ export const FLEET_APPCO_AUTH_GENERATE_NAME = 'fleet-appco-auth-';
8
+ export const IMAGE_PULL_SECRET_SUFFIX = '-image-pull-secret';
9
+ export const SUSE_APPCO_DISPLAY_NAME = 'SUSE AppCo';
10
+
11
+ // Used when the Rancher version can't be parsed (e.g. dev builds), so we point
12
+ // at the latest, unversioned Fleet docs.
13
+ export const FLEET_DOWNSTREAM_RESOURCES_DOCS_FALLBACK_URL = 'https://fleet.rancher.io/next/downstream-resources';
14
+
15
+ /**
16
+ * Build the URL to the Fleet "downstream resources" docs for the running Rancher version.
17
+ *
18
+ * Rancher `2.X.0` (for X >= 15) ships with Fleet `0.(X+1)`, whose docs are published at
19
+ * `https://fleet.rancher.io/0.<minor + 1>/downstream-resources`. For anything older or
20
+ * unparseable we fall back to the unversioned `next` docs.
21
+ */
22
+ export function getDownstreamResourcesDocsUrl(rancherVersion?: string): string {
23
+ // Harcoded to 2.X.0, it is fragile because if the version changes it will break.
24
+ // Ideally it would require a correlation between versions, but we have the fallback in place.
25
+ const match = /^v?2\.(\d+)/.exec(rancherVersion || '');
26
+ const minor = match ? parseInt(match[1], 10) : NaN;
27
+
28
+ // It should only exists after version 0.15.0, which will be fleet 0.16.0.
29
+ if (!isNaN(minor) && minor >= 15) {
30
+ return `https://fleet.rancher.io/0.${ minor + 1 }/downstream-resources`;
31
+ }
32
+
33
+ return FLEET_DOWNSTREAM_RESOURCES_DOCS_FALLBACK_URL;
34
+ }
35
+
36
+ interface AuthCredentials {
37
+ publicKey: string;
38
+ privateKey: string;
39
+ }
40
+
41
+ export interface RepoState {
42
+ repoName: string;
43
+ stateDisplay: string;
44
+ stateBackground: string;
45
+ transitioning: boolean;
46
+ error: boolean;
47
+ errorMessage: string;
48
+ }
49
+
50
+ interface WaitResult {
51
+ repo: any;
52
+ state: RepoState | null;
53
+ // True only when the repo definitively does not exist (404), as opposed to the
54
+ // lookup failing for another reason (network/API error). Distinguishes "repo
55
+ // absent, safe to create" from "couldn't reach the repo".
56
+ notFound?: boolean;
57
+ }
58
+
59
+ interface VuexStore {
60
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
61
+ dispatch: (action: string, payload?: any) => Promise<any>;
62
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
63
+ getters: Record<string, any>;
64
+ }
65
+
66
+ export async function createAppCoAuthSecret(store: VuexStore, credentials: AuthCredentials, namespace: string) {
67
+ const { publicKey, privateKey } = credentials;
68
+
69
+ const secret = await store.dispatch(`${ CATALOG._MANAGEMENT }/create`, {
70
+ type: SECRET,
71
+ metadata: {
72
+ namespace,
73
+ generateName: FLEET_APPCO_AUTH_GENERATE_NAME,
74
+ labels: { [FLEET_LABELS.MANAGED]: 'true' }
75
+ }
76
+ });
77
+
78
+ secret._type = SECRET_TYPES.BASIC;
79
+ secret.data = {
80
+ username: base64Encode(publicKey),
81
+ password: base64Encode(privateKey),
82
+ };
83
+
84
+ await secret.save();
85
+
86
+ return secret;
87
+ }
88
+
89
+ export async function ensureAppCoImagePullSecret(store: VuexStore, authSecretName: string, namespace: string): Promise<string | undefined> {
90
+ const imagePullSecretName = `${ authSecretName }${ IMAGE_PULL_SECRET_SUFFIX }`;
91
+
92
+ let imagePullSecret = store.getters[`${ CATALOG._MANAGEMENT }/byId`](SECRET, `${ namespace }/${ imagePullSecretName }`);
93
+
94
+ if (!imagePullSecret) {
95
+ try {
96
+ imagePullSecret = await store.dispatch(`${ CATALOG._MANAGEMENT }/find`, { type: SECRET, id: `${ namespace }/${ imagePullSecretName }` });
97
+ } catch (e) {
98
+ let authSecret;
99
+
100
+ try {
101
+ authSecret = await store.dispatch(`${ CATALOG._MANAGEMENT }/find`, { type: SECRET, id: `${ namespace }/${ authSecretName }` });
102
+ } catch (_) {
103
+ console.warn(`AppCo: auth secret "${ authSecretName }" not found in namespace "${ namespace }", skipping image-pull-secret creation`); // eslint-disable-line no-console
104
+
105
+ return;
106
+ }
107
+
108
+ const registryHost = new URL(SUSE_APP_COLLECTION_REPO_URL.replace('oci://', 'https://')).host;
109
+ const username = authSecret.decodedData?.username || '';
110
+ const password = authSecret.decodedData?.password || '';
111
+ const config = { auths: { [registryHost]: { username, password } } };
112
+
113
+ const newSecret = await store.dispatch(`${ CATALOG._MANAGEMENT }/create`, {
114
+ type: SECRET,
115
+ _type: SECRET_TYPES.DOCKER_JSON,
116
+ metadata: {
117
+ name: imagePullSecretName,
118
+ namespace,
119
+ labels: { [FLEET_LABELS.MANAGED]: 'true' }
120
+ }
121
+ });
122
+
123
+ newSecret.setData('.dockerconfigjson', JSON.stringify(config));
124
+ await newSecret.save();
125
+ }
126
+ }
127
+
128
+ return imagePullSecretName;
129
+ }
130
+
131
+ export async function ensureAppCoClusterRepo(store: VuexStore, authSecretName: string, namespace: string, t: (key: string) => string): Promise<string> {
132
+ const repoName = deriveRepoName(authSecretName);
133
+ let repo = store.getters[`${ CATALOG._MANAGEMENT }/byId`](CATALOG_TYPES.CLUSTER_REPO, repoName);
134
+
135
+ if (!repo) {
136
+ try {
137
+ repo = await store.dispatch(`${ CATALOG._MANAGEMENT }/find`, { type: CATALOG_TYPES.CLUSTER_REPO, id: repoName });
138
+ } catch (e) {
139
+ try {
140
+ repo = await store.dispatch(`${ CATALOG._MANAGEMENT }/create`, {
141
+ type: CATALOG_TYPES.CLUSTER_REPO,
142
+ metadata: {
143
+ name: repoName,
144
+ annotations: {
145
+ [DESCRIPTION]: t('catalog.repo.target.suseAppCollection.description'),
146
+ [CATALOG.SUSE_APP_COLLECTION]: 'true',
147
+ },
148
+ },
149
+ spec: {
150
+ url: SUSE_APP_COLLECTION_REPO_URL,
151
+ clientSecret: {
152
+ namespace,
153
+ name: authSecretName,
154
+ },
155
+ },
156
+ });
157
+
158
+ await repo.save();
159
+ } catch (err: any) {
160
+ if (err.status === 409) {
161
+ return repoName;
162
+ }
163
+
164
+ throw err;
165
+ }
166
+ }
167
+ }
168
+
169
+ return repoName;
170
+ }
171
+
172
+ /**
173
+ * Verify the auth secret exists, then ensure its image-pull secret and ClusterRepo
174
+ * exist. Returns false (and creates nothing) if the secret cannot be found.
175
+ */
176
+ export async function ensureAppCoResources(
177
+ store: VuexStore,
178
+ authSecretName: string,
179
+ namespace: string,
180
+ t: (key: string) => string
181
+ ): Promise<boolean> {
182
+ const secretId = `${ namespace }/${ authSecretName }`;
183
+ let authSecret = store.getters[`${ CATALOG._MANAGEMENT }/byId`](SECRET, secretId);
184
+
185
+ if (!authSecret) {
186
+ try {
187
+ authSecret = await store.dispatch(`${ CATALOG._MANAGEMENT }/find`, { type: SECRET, id: secretId });
188
+ } catch (e) {
189
+ return false;
190
+ }
191
+ }
192
+
193
+ await Promise.all([
194
+ ensureAppCoImagePullSecret(store, authSecretName, namespace),
195
+ ensureAppCoClusterRepo(store, authSecretName, namespace, t),
196
+ ]);
197
+
198
+ return true;
199
+ }
200
+
201
+ const REPO_WAIT_TIMEOUT_MS = 90000;
202
+ const REPO_WAIT_INTERVAL_MS = 3000;
203
+
204
+ function getRepoState(repo: any, repoName: string): { state: RepoState; isReady: boolean; hasError: boolean } {
205
+ const state = repo.metadata?.state;
206
+ const conditions = repo.status?.conditions || [];
207
+ const ociCondition = conditions.find((c: any) => c.type === 'OCIDownloaded');
208
+ const isReady = ociCondition?.status === 'True';
209
+ const hasError = !!(state?.error || ociCondition?.error);
210
+
211
+ const repoState: RepoState = {
212
+ repoName,
213
+ stateDisplay: repo.stateDisplay,
214
+ stateBackground: repo.stateBackground,
215
+ transitioning: !isReady && !hasError,
216
+ error: hasError,
217
+ errorMessage: state?.message || ociCondition?.message || '',
218
+ };
219
+
220
+ return {
221
+ state: repoState, isReady, hasError
222
+ };
223
+ }
224
+
225
+ async function waitForRepoReady(
226
+ store: VuexStore,
227
+ repoName: string,
228
+ { onStateChange, signal }: { onStateChange?: (state: RepoState) => void; signal?: AbortSignal } = {}
229
+ ): Promise<WaitResult> {
230
+ let repo;
231
+
232
+ // `find` with `force: true` re-fetches and registers a watch, so the store's
233
+ // cached resource is kept up to date via subscription while we wait below.
234
+ try {
235
+ repo = await store.dispatch(`${ CATALOG._MANAGEMENT }/find`, {
236
+ type: CATALOG_TYPES.CLUSTER_REPO,
237
+ id: repoName,
238
+ opt: { force: true },
239
+ });
240
+ } catch (e: any) {
241
+ // A 404 means the repo simply doesn't exist yet; any other error means the
242
+ // lookup itself failed (network/API), which callers must not treat as "absent".
243
+ return {
244
+ repo: null, state: null, notFound: e?.status === 404
245
+ };
246
+ }
247
+
248
+ let result: WaitResult = { repo: null, state: null };
249
+
250
+ try {
251
+ await repo.waitForTestFn(() => {
252
+ if (signal?.aborted) {
253
+ return true;
254
+ }
255
+
256
+ // Read the latest resource from the store, kept fresh by the watch above.
257
+ const current = store.getters[`${ CATALOG._MANAGEMENT }/byId`](CATALOG_TYPES.CLUSTER_REPO, repoName);
258
+
259
+ if (!current) {
260
+ return true;
261
+ }
262
+
263
+ const { state, isReady, hasError } = getRepoState(current, repoName);
264
+
265
+ onStateChange?.(state);
266
+
267
+ if (hasError) {
268
+ result = { repo: null, state };
269
+
270
+ return true;
271
+ }
272
+
273
+ if (isReady) {
274
+ result = { repo: current, state };
275
+
276
+ return true;
277
+ }
278
+
279
+ return false;
280
+ }, `appco repo ${ repoName } ready`, REPO_WAIT_TIMEOUT_MS, REPO_WAIT_INTERVAL_MS);
281
+ } catch (e) {
282
+ // Timed out waiting for the repo to become ready
283
+ return result;
284
+ }
285
+
286
+ if (signal?.aborted) {
287
+ return { repo: null, state: null };
288
+ }
289
+
290
+ return result;
291
+ }
292
+
293
+ interface FetchChartsResult {
294
+ entries: Record<string, any[]> | null;
295
+ repoState: RepoState | null;
296
+ // True only when the repo does not exist (404); see WaitResult.notFound.
297
+ notFound?: boolean;
298
+ }
299
+
300
+ export async function fetchAppCoCharts(
301
+ store: VuexStore,
302
+ repoName: string,
303
+ onStateChange?: (state: RepoState) => void,
304
+ // Used to stop on unmount or when repoName changes
305
+ signal?: AbortSignal
306
+ ): Promise<FetchChartsResult> {
307
+ const { repo, state: repoState, notFound } = await waitForRepoReady(store, repoName, { onStateChange, signal });
308
+
309
+ if (!repo) {
310
+ return {
311
+ entries: null, repoState, notFound
312
+ };
313
+ }
314
+
315
+ const index = await repo.followLink('index');
316
+ const entries = index?.entries || {};
317
+
318
+ return { entries, repoState };
319
+ }
320
+
321
+ export function deriveRepoName(secretName: string): string {
322
+ return secretName ? secretName.replace('auth', 'repo') : '';
323
+ }
package/utils/object.js CHANGED
@@ -163,6 +163,9 @@ returns an object with no key/value pairs (including nested) where the value is:
163
163
  undefined
164
164
  */
165
165
  export function cleanUp(obj) {
166
+ if ( !obj || typeof obj !== 'object') {
167
+ return obj;
168
+ }
166
169
  Object.keys(obj).map((key) => {
167
170
  const val = obj[key];
168
171
 
@@ -267,7 +270,7 @@ export function diff(from, to, preventNull = false) {
267
270
  }
268
271
 
269
272
  if (preventNull) {
270
- // keys that come from "definedKeys" method are strings with "" chars inside... We need to clean them up
273
+ // keys that come from "definedKeys" method are strings with "" chars inside... We need to clean them up
271
274
  // so that we can access the value of the obj property
272
275
  let key = k;
273
276
 
@@ -282,7 +285,24 @@ export function diff(from, to, preventNull = false) {
282
285
  set(out, key, null);
283
286
  }
284
287
  } else {
285
- set(out, k, null);
288
+ const parts = splitObjectPath(k);
289
+
290
+ // Skip any missing nested key whose parent path in out is already a
291
+ // non-object. We don't want to attempt to null out the key that appeared
292
+ // in the diff when a pre-defined key
293
+ // (githubConfigSecret.github_token: '') gets updated to
294
+ // (githubConfigSecret: 'preexisting-secret')
295
+ const skip = parts.some((part) => {
296
+ const existingVal = out?.[part];
297
+
298
+ if (existingVal !== undefined && !isObject(existingVal)) {
299
+ return true;
300
+ }
301
+ });
302
+
303
+ if (!skip) {
304
+ set(out, k, null);
305
+ }
286
306
  }
287
307
  }
288
308
 
package/utils/provider.ts CHANGED
@@ -4,6 +4,10 @@ export function getHostedProviders(context: ClusterProvisionerContext) {
4
4
  return context?.$extension?.getProviders(context)?.filter((p: IClusterProvisioner) => p.group === 'hosted') || [];
5
5
  }
6
6
 
7
+ export function getCAPIProviders(context: ClusterProvisionerContext) {
8
+ return context?.$extension?.getProviders(context)?.filter((p: IClusterProvisioner) => p.group === 'capi') || [];
9
+ }
10
+
7
11
  export function isHostedProvider(context: ClusterProvisionerContext, provisioner: string) {
8
12
  if (!provisioner) {
9
13
  return false;
@@ -12,3 +16,11 @@ export function isHostedProvider(context: ClusterProvisionerContext, provisioner
12
16
 
13
17
  return provisioners.has(provisioner.toLowerCase());
14
18
  }
19
+ export function isCAPIProvider(context: ClusterProvisionerContext, provisioner: string) {
20
+ if (!provisioner) {
21
+ return false;
22
+ }
23
+ const provisioners = new Set(getCAPIProviders(context).map((p: IClusterProvisioner) => p.id.toLowerCase()));
24
+
25
+ return provisioners.has(provisioner.toLowerCase());
26
+ }
@@ -0,0 +1,104 @@
1
+ import { containerImages } from '@shell/utils/validators/container-images';
2
+
3
+ const mockGetters = {
4
+ 'i18n/t': (key: string, args?: object) => (args ? `${ key }:${ JSON.stringify(args) }` : key),
5
+ 'i18n/exists': () => false,
6
+ };
7
+
8
+ describe('validators/container-images', () => {
9
+ describe('containerImages', () => {
10
+ it('adds required error when containers array is empty (regular workload)', () => {
11
+ const errors: string[] = [];
12
+ const spec = { template: { spec: { containers: [] } } };
13
+
14
+ containerImages(spec, mockGetters, errors);
15
+
16
+ expect(errors).toStrictEqual(['validation.required:{"key":"workload.container.titles.containers"}']);
17
+ });
18
+
19
+ it('adds required error when containers is missing (regular workload)', () => {
20
+ const errors: string[] = [];
21
+ const spec = { template: { spec: {} } };
22
+
23
+ containerImages(spec, mockGetters, errors);
24
+
25
+ expect(errors).toStrictEqual(['validation.required:{"key":"workload.container.titles.containers"}']);
26
+ });
27
+
28
+ it('adds no errors when all containers have images (regular workload)', () => {
29
+ const errors: string[] = [];
30
+ const spec = {
31
+ template: {
32
+ spec: {
33
+ containers: [
34
+ { name: 'web', image: 'nginx:latest' },
35
+ { name: 'sidecar', image: 'busybox:1.36' },
36
+ ],
37
+ },
38
+ },
39
+ };
40
+
41
+ containerImages(spec, mockGetters, errors);
42
+
43
+ expect(errors).toStrictEqual([]);
44
+ });
45
+
46
+ it('adds image error for each container missing an image (regular workload)', () => {
47
+ const errors: string[] = [];
48
+ const spec = {
49
+ template: {
50
+ spec: {
51
+ containers: [
52
+ { name: 'web' },
53
+ { name: 'sidecar', image: 'busybox' },
54
+ { name: 'proxy' },
55
+ ],
56
+ },
57
+ },
58
+ };
59
+
60
+ containerImages(spec, mockGetters, errors);
61
+
62
+ expect(errors).toStrictEqual([
63
+ 'workload.validation.containerImage:{"name":"web"}',
64
+ 'workload.validation.containerImage:{"name":"proxy"}',
65
+ ]);
66
+ });
67
+
68
+ it('adds required error when containers array is empty (cronjob)', () => {
69
+ const errors: string[] = [];
70
+ const spec = { jobTemplate: { spec: { template: { spec: { containers: [] } } } } };
71
+
72
+ containerImages(spec, mockGetters, errors);
73
+
74
+ expect(errors).toStrictEqual(['validation.required:{"key":"workload.container.titles.containers"}']);
75
+ });
76
+
77
+ it('adds no errors when all containers have images (cronjob)', () => {
78
+ const errors: string[] = [];
79
+ const spec = { jobTemplate: { spec: { template: { spec: { containers: [{ name: 'job', image: 'alpine:3' }] } } } } };
80
+
81
+ containerImages(spec, mockGetters, errors);
82
+
83
+ expect(errors).toStrictEqual([]);
84
+ });
85
+
86
+ it('adds image error for container missing image (cronjob)', () => {
87
+ const errors: string[] = [];
88
+ const spec = { jobTemplate: { spec: { template: { spec: { containers: [{ name: 'batch' }] } } } } };
89
+
90
+ containerImages(spec, mockGetters, errors);
91
+
92
+ expect(errors).toStrictEqual(['workload.validation.containerImage:{"name":"batch"}']);
93
+ });
94
+
95
+ it('does not add error when container is null-like in the array', () => {
96
+ const errors: string[] = [];
97
+ const spec = { template: { spec: { containers: [null] } } };
98
+
99
+ containerImages(spec, mockGetters, errors);
100
+
101
+ expect(errors).toStrictEqual([]);
102
+ });
103
+ });
104
+ });
@@ -0,0 +1,91 @@
1
+ import { flowOutput } from '@shell/utils/validators/flow-output';
2
+
3
+ const mockGetters = {
4
+ 'i18n/t': (key: string, args?: object) => (args ? `${ key }:${ JSON.stringify(args) }` : key),
5
+ 'i18n/exists': () => false,
6
+ };
7
+
8
+ describe('validators/flow-output', () => {
9
+ describe('flowOutput', () => {
10
+ describe('when verifyLocal is not in validatorArgs', () => {
11
+ it('adds global error when globalOutputRefs is empty', () => {
12
+ const errors: string[] = [];
13
+
14
+ flowOutput({ globalOutputRefs: [] }, mockGetters, errors, []);
15
+
16
+ expect(errors).toStrictEqual(['validation.flowOutput.global']);
17
+ });
18
+
19
+ it('adds global error when globalOutputRefs is missing', () => {
20
+ const errors: string[] = [];
21
+
22
+ flowOutput({}, mockGetters, errors, []);
23
+
24
+ expect(errors).toStrictEqual(['validation.flowOutput.global']);
25
+ });
26
+
27
+ it('adds no error when globalOutputRefs is non-empty', () => {
28
+ const errors: string[] = [];
29
+
30
+ flowOutput({ globalOutputRefs: ['output-1'] }, mockGetters, errors, []);
31
+
32
+ expect(errors).toStrictEqual([]);
33
+ });
34
+
35
+ it('does not check localOutputRefs', () => {
36
+ const errors: string[] = [];
37
+
38
+ flowOutput({ localOutputRefs: ['local-1'], globalOutputRefs: [] }, mockGetters, errors, []);
39
+
40
+ expect(errors).toStrictEqual(['validation.flowOutput.global']);
41
+ });
42
+ });
43
+
44
+ describe('when verifyLocal is in validatorArgs', () => {
45
+ it('adds both error when both localOutputRefs and globalOutputRefs are empty', () => {
46
+ const errors: string[] = [];
47
+
48
+ flowOutput({ localOutputRefs: [], globalOutputRefs: [] }, mockGetters, errors, ['verifyLocal']);
49
+
50
+ expect(errors).toStrictEqual(['validation.flowOutput.both']);
51
+ });
52
+
53
+ it('adds both error when both refs are missing', () => {
54
+ const errors: string[] = [];
55
+
56
+ flowOutput({}, mockGetters, errors, ['verifyLocal']);
57
+
58
+ expect(errors).toStrictEqual(['validation.flowOutput.both']);
59
+ });
60
+
61
+ it('adds no error when localOutputRefs is non-empty', () => {
62
+ const errors: string[] = [];
63
+
64
+ flowOutput({ localOutputRefs: ['local-1'], globalOutputRefs: [] }, mockGetters, errors, ['verifyLocal']);
65
+
66
+ expect(errors).toStrictEqual([]);
67
+ });
68
+
69
+ it('adds no error when globalOutputRefs is non-empty', () => {
70
+ const errors: string[] = [];
71
+
72
+ flowOutput({ localOutputRefs: [], globalOutputRefs: ['global-1'] }, mockGetters, errors, ['verifyLocal']);
73
+
74
+ expect(errors).toStrictEqual([]);
75
+ });
76
+
77
+ it('adds no error when both refs are non-empty', () => {
78
+ const errors: string[] = [];
79
+
80
+ flowOutput(
81
+ { localOutputRefs: ['local-1'], globalOutputRefs: ['global-1'] },
82
+ mockGetters,
83
+ errors,
84
+ ['verifyLocal']
85
+ );
86
+
87
+ expect(errors).toStrictEqual([]);
88
+ });
89
+ });
90
+ });
91
+ });
@@ -0,0 +1,58 @@
1
+ import { logdna } from '@shell/utils/validators/logging-outputs';
2
+
3
+ const mockGetters = {
4
+ 'i18n/t': (key: string, args?: object) => (args ? `${ key }:${ JSON.stringify(args) }` : key),
5
+ 'i18n/exists': () => false,
6
+ };
7
+
8
+ describe('validators/logging-outputs', () => {
9
+ describe('logdna', () => {
10
+ it('adds no error when value is empty object', () => {
11
+ const errors: string[] = [];
12
+
13
+ logdna({}, mockGetters, errors, []);
14
+
15
+ expect(errors).toStrictEqual([]);
16
+ });
17
+
18
+ it('adds no error when value is null', () => {
19
+ const errors: string[] = [];
20
+
21
+ logdna(null, mockGetters, errors, []);
22
+
23
+ expect(errors).toStrictEqual([]);
24
+ });
25
+
26
+ it('adds no error when value is undefined', () => {
27
+ const errors: string[] = [];
28
+
29
+ logdna(undefined, mockGetters, errors, []);
30
+
31
+ expect(errors).toStrictEqual([]);
32
+ });
33
+
34
+ it('adds no error when api_key is present', () => {
35
+ const errors: string[] = [];
36
+
37
+ logdna({ api_key: 'my-secret-key' }, mockGetters, errors, []);
38
+
39
+ expect(errors).toStrictEqual([]);
40
+ });
41
+
42
+ it('adds apiKey error when api_key is missing', () => {
43
+ const errors: string[] = [];
44
+
45
+ logdna({ host: 'logs.example.com' }, mockGetters, errors, []);
46
+
47
+ expect(errors).toStrictEqual(['validation.output.logdna.apiKey']);
48
+ });
49
+
50
+ it('adds apiKey error when api_key is empty string', () => {
51
+ const errors: string[] = [];
52
+
53
+ logdna({ api_key: '' }, mockGetters, errors, []);
54
+
55
+ expect(errors).toStrictEqual(['validation.output.logdna.apiKey']);
56
+ });
57
+ });
58
+ });