fix: ensure the web UI doesn't break when license telemetry required check fails (#16667)

Addresses https://github.com/coder/coder/issues/16455.

## Changes

- Initialize default entitlements in a Set to include all features
- Initialize entitlements' `Warnings` and `Errors` fields to arrays
rather than `nil`s.
- Minor changes in formatting on the frontend

## Reasoning

I had to change how entitlements are initialized to match the `codersdk`
[generated
types](https://github.com/coder/coder/blob/33d62619225702257fa2542f40ecc26bfd0d1fa6/site/src/api/typesGenerated.ts#L727),
which the frontend assumes are correct, and doesn't run additional
checks on.

- `features: Record<FeatureName, Feature>`: this type signifies that
every `FeatureName` is present in the record, but on `main`, that's not
true if there's a telemetry required error
- `warnings: readonly string[];` and `errors: readonly string[];`: these
types mean that the fields are not `null`, but that's not always true

With a valid license, the [`LicensesEntitlements`
function](https://github.com/coder/coder/blob/33d62619225702257fa2542f40ecc26bfd0d1fa6/enterprise/coderd/license/license.go#L92)
ensures that all features are present in the entitlements. It's called
by the [`Entitlements`
function](https://github.com/coder/coder/blob/33d62619225702257fa2542f40ecc26bfd0d1fa6/enterprise/coderd/license/license.go#L42),
which is called by
[`api.updateEnittlements`](https://github.com/coder/coder/blob/33d62619225702257fa2542f40ecc26bfd0d1fa6/enterprise/coderd/coderd.go#L687).
However, when a license requires telemetry and telemetry is disabled,
the entitlements with all features [are
discarded](https://github.com/coder/coder/blob/33d62619225702257fa2542f40ecc26bfd0d1fa6/enterprise/coderd/coderd.go#L704)
in an early exit from the same function. By initializing entitlements
with all the features from the get go, we avoid this problem.

## License issue banner after the changes

<img width="1512" alt="Screenshot 2025-02-23 at 20 25 42"
src="https://github.com/user-attachments/assets/ee0134b3-f745-45d9-8333-bfa1661e33d2"
/>
This commit is contained in:
Hugo Dutka
2025-02-24 16:02:33 +01:00
committed by GitHub
parent bebf2d5eb8
commit ac88c9ba17
4 changed files with 32 additions and 7 deletions
+11 -3
View File
@@ -30,8 +30,8 @@ func New() *Set {
// These will be updated when coderd is initialized.
entitlements: codersdk.Entitlements{
Features: map[codersdk.FeatureName]codersdk.Feature{},
Warnings: nil,
Errors: nil,
Warnings: []string{},
Errors: []string{},
HasLicense: false,
Trial: false,
RequireTelemetry: false,
@@ -39,13 +39,21 @@ func New() *Set {
},
right2Update: make(chan struct{}, 1),
}
// Ensure all features are present in the entitlements. Our frontend
// expects this.
for _, featureName := range codersdk.FeatureNames {
s.entitlements.AddFeature(featureName, codersdk.Feature{
Entitlement: codersdk.EntitlementNotEntitled,
Enabled: false,
})
}
s.right2Update <- struct{}{} // one token, serialized updates
return s
}
// ErrLicenseRequiresTelemetry is an error returned by a fetch passed to Update to indicate that the
// fetched license cannot be used because it requires telemetry.
var ErrLicenseRequiresTelemetry = xerrors.New("License requires telemetry but telemetry is disabled")
var ErrLicenseRequiresTelemetry = xerrors.New(codersdk.LicenseTelemetryRequiredErrorText)
func (l *Set) Update(ctx context.Context, fetch func(context.Context) (codersdk.Entitlements, error)) error {
select {
+2 -1
View File
@@ -12,7 +12,8 @@ import (
)
const (
LicenseExpiryClaim = "license_expires"
LicenseExpiryClaim = "license_expires"
LicenseTelemetryRequiredErrorText = "License requires telemetry but telemetry is disabled"
)
type AddLicenseRequest struct {
+4
View File
@@ -1116,6 +1116,10 @@ export interface License {
// From codersdk/licenses.go
export const LicenseExpiryClaim = "license_expires";
// From codersdk/licenses.go
export const LicenseTelemetryRequiredErrorText =
"License requires telemetry but telemetry is disabled";
// From codersdk/deployment.go
export interface LinkConfig {
readonly name: string;
@@ -6,6 +6,7 @@ import {
useTheme,
} from "@emotion/react";
import Link from "@mui/material/Link";
import { LicenseTelemetryRequiredErrorText } from "api/typesGenerated";
import { Expander } from "components/Expander/Expander";
import { Pill } from "components/Pill/Pill";
import { type FC, useState } from "react";
@@ -14,6 +15,7 @@ export const Language = {
licenseIssue: "License Issue",
licenseIssues: (num: number): string => `${num} License Issues`,
upgrade: "Contact sales@coder.com.",
exception: "Contact sales@coder.com if you need an exception.",
exceeded: "It looks like you've exceeded some limits of your license.",
lessDetails: "Less",
moreDetails: "More",
@@ -26,6 +28,14 @@ const styles = {
},
} satisfies Record<string, Interpolation<Theme>>;
const formatMessage = (message: string) => {
// If the message ends with an alphanumeric character, add a period.
if (/[a-z0-9]$/i.test(message)) {
return `${message}.`;
}
return message;
};
export interface LicenseBannerViewProps {
errors: readonly string[];
warnings: readonly string[];
@@ -57,14 +67,16 @@ export const LicenseBannerView: FC<LicenseBannerViewProps> = ({
<div css={containerStyles}>
<Pill type={type}>{Language.licenseIssue}</Pill>
<div css={styles.leftContent}>
<span>{messages[0]}</span>
<span>{formatMessage(messages[0])}</span>
&nbsp;
<Link
color={textColor}
fontWeight="medium"
href="mailto:sales@coder.com"
>
{Language.upgrade}
{messages[0] === LicenseTelemetryRequiredErrorText
? Language.exception
: Language.upgrade}
</Link>
</div>
</div>
@@ -90,7 +102,7 @@ export const LicenseBannerView: FC<LicenseBannerViewProps> = ({
<ul css={{ padding: 8, margin: 0 }}>
{messages.map((message) => (
<li css={{ margin: 4 }} key={message}>
{message}
{formatMessage(message)}
</li>
))}
</ul>