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;