diff --git a/modules/web/src/app/cluster/details/cluster/edit-cluster/component.ts b/modules/web/src/app/cluster/details/cluster/edit-cluster/component.ts
index bacd3ca3a2..8c77dc6d25 100644
--- a/modules/web/src/app/cluster/details/cluster/edit-cluster/component.ts
+++ b/modules/web/src/app/cluster/details/cluster/edit-cluster/component.ts
@@ -52,8 +52,11 @@ import {
CLUSTER_DEFAULT_NODE_SELECTOR_TOOLTIP,
generateEncryptionKey,
} from '@shared/utils/cluster';
-import {getEditionVersion} from '@shared/utils/common';
+import {getEditionVersion, tooltipWithAdminNote} from '@shared/utils/common';
import {
+ ADMIN_ENFORCED_IN_DATACENTER_NOTE,
+ ADMIN_ENFORCED_NOTE,
+ CLUSTER_OPTION_TOOLTIPS,
KUBERNETES_DASHBOARD_DEPRECATED_MESSAGE,
NODE_EGRESS_PROXY_TOOLTIP,
OPA_DEPRECATED_MESSAGE,
@@ -148,6 +151,10 @@ export class EditClusterComponent implements OnInit, OnDestroy {
readonly KUBERNETES_DASHBOARD_DEPRECATED_MESSAGE = KUBERNETES_DASHBOARD_DEPRECATED_MESSAGE;
readonly OPA_DEPRECATED_MESSAGE = OPA_DEPRECATED_MESSAGE;
readonly NODE_EGRESS_PROXY_TOOLTIP = NODE_EGRESS_PROXY_TOOLTIP;
+ readonly Tooltips = CLUSTER_OPTION_TOOLTIPS;
+ readonly AdminEnforcedNote = ADMIN_ENFORCED_NOTE;
+ readonly AdminEnforcedInDatacenterNote = ADMIN_ENFORCED_IN_DATACENTER_NOTE;
+ readonly tooltipWithAdminNote = tooltipWithAdminNote;
private readonly _nameMinLen = 3;
private readonly ENCRYPTION_KEY_ANNOTATION = 'kubermatic.io/encryption-key';
private _settings: AdminSettings;
diff --git a/modules/web/src/app/cluster/details/cluster/edit-cluster/template.html b/modules/web/src/app/cluster/details/cluster/edit-cluster/template.html
index 61ef84c4ef..3d319c60c5 100644
--- a/modules/web/src/app/cluster/details/cluster/edit-cluster/template.html
+++ b/modules/web/src/app/cluster/details/cluster/edit-cluster/template.html
@@ -50,7 +50,7 @@
class="km-select-ellipsis"
disableOptionCentering
disabled
- matTooltip="Containerd is the only supported container runtime"
+ [matTooltip]="Tooltips.containerRuntime"
required
kmValueChangedIndicator>
containerd
@@ -72,7 +72,7 @@
{{getPluginName(admissionPlugin)}}
@if (admissionPlugin === admissionPlugin.PodSecurityPolicy) {
+ [matTooltip]="Tooltips.podSecurityPolicy">
}
}
@@ -164,46 +164,46 @@
@if (!!form.get(Controls.Konnectivity).value && form.get(Controls.Konnectivity).disabled) {
+ [matTooltip]="Tooltips.konnectivity">
}
Audit Logging
+ kmValueChangedIndicator>
+ Audit Logging
+
+
@if (!!form.get(Controls.AuditLogging).value) {
+ [matTooltip]="Tooltips.auditPolicyCustom">
custom
+ [matTooltip]="Tooltips.auditPolicyMetadata">
metadata
+ [matTooltip]="Tooltips.auditPolicyMinimal">
minimal
+ [matTooltip]="Tooltips.auditPolicyRecommended">
recommended
}
- @if (!!datacenter.spec.enforceAuditLogging) {
-
- }
@if (!!form.get(Controls.AuditLogging).value && !isAuditWebhookBackendHidden) {
Audit Webhook Backend
+ [matTooltip]="Tooltips.auditWebhookBackend">
@if (!!form.get(Controls.AuditWebhookBackend).value) {
@@ -248,7 +248,7 @@
OPA Integration
@if (isEnforced(Controls.OPAIntegration)) {
+ [matTooltip]="form.get(Controls.OPAIntegration).value ? Tooltips.opaEnforced : Tooltips.opaDisabled">
}
@@ -260,7 +260,7 @@
kmValueChangedIndicator>
Kyverno Policy Management
+ [matTooltip]="Tooltips.kyverno">
}
@@ -268,10 +268,8 @@
User Cluster Logging
- @if (isEnforced(Controls.MLALogging)) {
- }
+ [matTooltip]="tooltipWithAdminNote(Tooltips.mlaLogging, AdminEnforcedNote, isEnforced(Controls.MLALogging))">
}
@@ -279,10 +277,8 @@
User Cluster Monitoring
- @if (isEnforced(Controls.MLAMonitoring)) {
- }
+ [matTooltip]="tooltipWithAdminNote(Tooltips.mlaMonitoring, AdminEnforcedNote, isEnforced(Controls.MLAMonitoring))">
}
@@ -303,7 +299,7 @@
@if (isKubeLBEnabled || isKubeLBEnabledForCluster()) {
@if (!isKubeLBEnforced) {
+ [matTooltip]="Tooltips.kubeLB">
}
}
@@ -321,14 +317,14 @@
Use LoadBalancer Class
+ [matTooltip]="Tooltips.kubeLBLoadBalancerClass">
Enable Gateway API
+ [matTooltip]="Tooltips.gatewayAPI">
}
@@ -339,7 +335,7 @@
kmValueChangedIndicator>
Disable CSI Driver
+ [matTooltip]="tooltipWithAdminNote(Tooltips.disableCSIDriver, Tooltips.csiDriverDisabledByAdmin, isCSIDriverDisabled)">
Encryption at Rest
+ [matTooltip]="Tooltips.encryptionAtRest">
@@ -365,7 +361,7 @@
type="button"
matSuffix
class="km-randomize-btn"
- matTooltip="Generate encryption key"
+ [matTooltip]="Tooltips.generateEncryptionKey"
(click)="generateEncryptionKey()">
@@ -387,7 +383,7 @@
kmValueChangedIndicator>
Cluster Backup
+ [matTooltip]="Tooltips.clusterBackup">
}
diff --git a/modules/web/src/app/shared/constants/common.ts b/modules/web/src/app/shared/constants/common.ts
index 63b95c5f9b..fbce7af4c3 100644
--- a/modules/web/src/app/shared/constants/common.ts
+++ b/modules/web/src/app/shared/constants/common.ts
@@ -54,6 +54,65 @@ export const DELETE_SELECTED_TOOLTIP = 'Delete selected';
export const CLOSE_PANEL_TOOLTIP = 'Close panel';
export const GO_BACK_TO_CLUSTER_LIST_TOOLTIP = 'Go back to the cluster list';
+// Cluster option tooltips
+// Shared by the cluster wizard and the edit cluster dialog so both describe an option the same way.
+// Each entry says what the option actually does; when an admin locks the option, one of the notes
+// below is prefixed so the reason the control is disabled is the first thing the user reads.
+export const ADMIN_ENFORCED_NOTE = 'Enforced by your admin and cannot be changed.';
+export const ADMIN_ENFORCED_IN_DATACENTER_NOTE =
+ 'Enforced by your admin in the chosen datacenter and cannot be changed.';
+export const ADMIN_DISABLED_IN_DATACENTER_NOTE =
+ 'Disabled by your admin in the chosen datacenter and cannot be changed.';
+
+export const CLUSTER_OPTION_TOOLTIPS = {
+ containerRuntime: 'Containerd is the only supported container runtime',
+ dualStack:
+ 'Dual Stack is a technology preview feature, some limitations may apply depending on the chosen provider. Please see the KKP documentation for more details.',
+ podSecurityPolicy: 'Pod Security Policies allow detailed authorization of pod creation and updates.',
+ konnectivity: 'OpenVPN support is deprecated, hence Konnectivity can no longer be disabled.',
+ ciliumIngress: 'Enable Cilium kubernetes ingress support',
+
+ auditLogging:
+ 'Records requests received by the Kubernetes API of this cluster. Logs are collected by a fluent-bit sidecar in the control plane.',
+ auditPolicyCustom:
+ 'Sets up cluster with a metadata audit policy that can be edited after the cluster has been created.',
+ auditPolicyMetadata: 'Logs metadata for all requests received by the Kubernetes API.',
+ auditPolicyMinimal:
+ 'Logs extended information about key security concerns like workload modifications and access to sensitive information.',
+ auditPolicyRecommended:
+ 'Logs extended information about key security concerns and metadata for all other requests. Recommended for best security coverage.',
+ auditWebhookBackend: 'Ships audit logs to an external webhook backend, configured from a secret in this cluster.',
+
+ opaEnforced: 'OPA Integration is enforced by your admin.',
+ opaDisabled: 'OPA Integration is disabled by your admin.',
+ kyverno:
+ 'Deploys Kyverno for policy management. Its controllers run in the cluster control plane and register admission webhooks that validate and mutate resources in this user cluster.',
+
+ mlaLogging: 'Collects logs from all pods in this user cluster and ships them to the central Grafana Loki store.',
+ mlaMonitoring:
+ 'Scrapes metrics from this user cluster and writes them to the central metrics store for viewing in Grafana.',
+
+ kubeLB:
+ 'Enable to use Kubermatic KubeLB for managing load balancers in your cluster. This allows automatic provisioning and management of load balancers for your services.',
+ kubeLBEnforced: 'Kubermatic KubeLB is enforced by your admin in the chosen datacenter and cannot be disabled.',
+ kubeLBLoadBalancerClass:
+ 'Enable to limit KubeLB to only process services with Kubernetes LoadBalancer Class named `kubelb`. When disabled, KubeLB will manage all services of type `LoadBalancer`',
+ gatewayAPI: 'Enable to use Gateway APIs. KKP will install the Gateway API CRDs in this cluster.',
+
+ disableCSIDriver:
+ "Skips installation of the provider's default CSI driver, leaving the cluster without dynamic volume provisioning or snapshots. It cannot be turned on while existing volumes still use the driver.",
+ csiDriverDisabledByAdmin: 'The CSI driver is disabled by your admin in the chosen datacenter and cannot be enabled.',
+
+ encryptionAtRest:
+ 'Encrypts Kubernetes secrets at rest in etcd with a secretbox key. Enabling it or changing the key runs a job that re-encrypts all affected resources.',
+ generateEncryptionKey: 'Generate encryption key',
+
+ clusterBackup:
+ 'Installs Velero in this cluster so cluster resources and volume data can be backed up to the selected backup storage location.',
+ userSSHKeyAgent:
+ 'Enable to deploy User SSH Key Agent to the cluster. It cannot be changed once the cluster is created.',
+} as const;
+
// Per-cluster proxy tooltips
export const PROXY_MODE_HINT = 'kube-proxy mode for in-cluster service routing.';
export const NODE_EGRESS_PROXY_TOOLTIP =
diff --git a/modules/web/src/app/shared/utils/common.ts b/modules/web/src/app/shared/utils/common.ts
index 28de500fac..73a734a25d 100644
--- a/modules/web/src/app/shared/utils/common.ts
+++ b/modules/web/src/app/shared/utils/common.ts
@@ -100,6 +100,12 @@ export function getPercentage(total: number, used: number, maxUsage = maxUsageDe
return Math.round(((used / total) * maxUsage + Number.EPSILON) * maxUsage) / maxUsage;
}
+// Prefixes an option's description with the reason it is locked, so that a checkbox disabled by an
+// admin explains both why it cannot be changed and what it does.
+export function tooltipWithAdminNote(description: string, note: string, locked: boolean): string {
+ return locked ? `${note} ${description}` : description;
+}
+
export function getEditionVersion(): string {
return `v${version.semver?.major}.${version.semver?.minor}`;
}
diff --git a/modules/web/src/app/wizard/step/cluster/component.ts b/modules/web/src/app/wizard/step/cluster/component.ts
index 8ad0037266..78945f815c 100644
--- a/modules/web/src/app/wizard/step/cluster/component.ts
+++ b/modules/web/src/app/wizard/step/cluster/component.ts
@@ -77,8 +77,11 @@ import {
CLUSTER_DEFAULT_NODE_SELECTOR_TOOLTIP,
generateEncryptionKey,
} from '@shared/utils/cluster';
-import {getEditionVersion} from '@shared/utils/common';
+import {getEditionVersion, tooltipWithAdminNote} from '@shared/utils/common';
import {
+ ADMIN_ENFORCED_IN_DATACENTER_NOTE,
+ ADMIN_ENFORCED_NOTE,
+ CLUSTER_OPTION_TOOLTIPS,
GENERATE_NAME_TOOLTIP,
KUBERNETES_DASHBOARD_DEPRECATED_MESSAGE,
NODE_EGRESS_PROXY_TOOLTIP,
@@ -226,6 +229,10 @@ export class ClusterStepComponent extends StepBase implements OnInit, ControlVal
readonly NODE_EGRESS_PROXY_TOOLTIP = NODE_EGRESS_PROXY_TOOLTIP;
readonly PROXY_MODE_HINT = PROXY_MODE_HINT;
readonly GENERATE_NAME_TOOLTIP = GENERATE_NAME_TOOLTIP;
+ readonly Tooltips = CLUSTER_OPTION_TOOLTIPS;
+ readonly AdminEnforcedNote = ADMIN_ENFORCED_NOTE;
+ readonly AdminEnforcedInDatacenterNote = ADMIN_ENFORCED_IN_DATACENTER_NOTE;
+ readonly tooltipWithAdminNote = tooltipWithAdminNote;
private _datacenterSpec: Datacenter;
private _seedSettings: SeedSettings;
private _settings: AdminSettings;
diff --git a/modules/web/src/app/wizard/step/cluster/template.html b/modules/web/src/app/wizard/step/cluster/template.html
index e2bd9f5599..fd0a308490 100644
--- a/modules/web/src/app/wizard/step/cluster/template.html
+++ b/modules/web/src/app/wizard/step/cluster/template.html
@@ -107,7 +107,7 @@
fxLayoutAlign=" center">
IPv4 and IPv6 (Dual Stack)
+ [matTooltip]="Tooltips.dualStack">
@@ -295,12 +295,12 @@ IPv6
@if (!!control(Controls.Konnectivity).value && control(Controls.Konnectivity).disabled) {
+ [matTooltip]="Tooltips.konnectivity">
}
@if (isCiliumSelected()) {
Ingress
+ [matTooltip]="Tooltips.ciliumIngress">
}
@if (canEditCNIValues) {
@@ -379,7 +379,7 @@
IPv6
class="km-select-ellipsis"
disableOptionCentering
disabled
- matTooltip="Containerd is the only supported container runtime"
+ [matTooltip]="Tooltips.containerRuntime"
required>
containerd
@@ -401,7 +401,7 @@
IPv6
{{getPluginName(admissionPlugin)}}
@if (admissionPlugin === admissionPlugin.PodSecurityPolicy) {
+ [matTooltip]="Tooltips.podSecurityPolicy">
}
}
@@ -456,38 +456,38 @@
IPv6
fxFlex="100"
fxLayoutGap="10px">
- Audit Logging
+
+ Audit Logging
+
+
@if (!!controlValue(Controls.AuditLogging)) {
+ [matTooltip]="Tooltips.auditPolicyCustom">
custom
+ [matTooltip]="Tooltips.auditPolicyMetadata">
metadata
+ [matTooltip]="Tooltips.auditPolicyMinimal">
minimal
+ [matTooltip]="Tooltips.auditPolicyRecommended">
recommended
}
- @if (isEnforced(Controls.AuditLogging)) {
-
- }
@if (!!controlValue(Controls.AuditLogging) && !isAuditWebhookBackendHidden) {
Audit Webhook Backend
+ [matTooltip]="tooltipWithAdminNote(Tooltips.auditWebhookBackend, AdminEnforcedInDatacenterNote, !!enforcedAuditWebhookSettings)">
@if (!!controlValue(Controls.AuditWebhookBackend)) {
@@ -528,7 +528,7 @@ IPv6
Cluster Backup
+ [matTooltip]="Tooltips.clusterBackup">
}
@if (!!controlValue(Controls.ClusterBackup) && isclusterBackupEnabled) {
@@ -552,7 +552,7 @@ IPv6
[disabled]="isCSIDriverDisabled">
Disable CSI Driver
+ [matTooltip]="tooltipWithAdminNote(Tooltips.disableCSIDriver, Tooltips.csiDriverDisabledByAdmin, isCSIDriverDisabled)">
@if (isEncryptionAtRestVisible()) {
@@ -561,7 +561,7 @@ IPv6
fxLayoutGap="8px">
Encryption at Rest
+ [matTooltip]="Tooltips.encryptionAtRest">
}
@@ -577,7 +577,7 @@ IPv6
type="button"
matSuffix
class="km-randomize-btn"
- matTooltip="Generate encryption key"
+ [matTooltip]="Tooltips.generateEncryptionKey"
(click)="generateEncryptionKey()">
@@ -598,7 +598,7 @@ IPv6
@if (isKubeLBEnabled) {
@@ -606,7 +606,7 @@ IPv6
@if (!isKubeLBEnforced) {
+ [matTooltip]="Tooltips.kubeLB">
}
}
@@ -615,14 +615,14 @@ IPv6
Use LoadBalancer Class
+ [matTooltip]="Tooltips.kubeLBLoadBalancerClass">
Enable Gateway API
+ [matTooltip]="Tooltips.gatewayAPI">
}
@@ -645,7 +645,7 @@ IPv6
OPA Integration
@if (isEnforced(Controls.OPAIntegration)) {
+ [matTooltip]="form.get(Controls.OPAIntegration).value ? Tooltips.opaEnforced : Tooltips.opaDisabled">
}
@@ -655,30 +655,26 @@ IPv6
Kyverno Policy Management
+ [matTooltip]="Tooltips.kyverno">
}
@if (isMLAEnabled()) {
User Cluster Logging
- @if (isEnforced(Controls.MLALogging)) {
- }
+ [matTooltip]="tooltipWithAdminNote(Tooltips.mlaLogging, AdminEnforcedNote, isEnforced(Controls.MLALogging))">
User Cluster Monitoring
- @if (isEnforced(Controls.MLAMonitoring)) {
- }
+ [matTooltip]="tooltipWithAdminNote(Tooltips.mlaMonitoring, AdminEnforcedNote, isEnforced(Controls.MLAMonitoring))">
}
@if (isUserSshKeyEnabled) {
User SSH Key Agent
+ [matTooltip]="Tooltips.userSSHKeyAgent">
}
@if (form.get(Controls.RouterReconciliation)) {
diff --git a/modules/web/src/assets/css/material/_main.scss b/modules/web/src/assets/css/material/_main.scss
index 4b45146abc..3e8d09a127 100644
--- a/modules/web/src/assets/css/material/_main.scss
+++ b/modules/web/src/assets/css/material/_main.scss
@@ -594,6 +594,13 @@ button {
}
}
+// Material puts `.mdc-checkbox--disabled { pointer-events: none; }` on the checkbox host, so a
+// disabled checkbox also swallows hover on its projected label content. Info icons there usually
+// explain *why* the option is disabled, so their tooltips must stay reachable.
+.mat-mdc-checkbox.mat-mdc-checkbox-disabled .mdc-label .km-icon-info {
+ pointer-events: auto;
+}
+
.mat-button-toggle-group {
&.mat-button-toggle-group-appearance-standard {
border: none;