mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
feat: add deployment_id to the ui and licenses (#13096)
* feat: expose `deployment_id` in the user dropdown * feat: add license deployment_id verification * Ignore wireguard.com from mlc config
This commit is contained in:
@@ -17,6 +17,9 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"pattern": "tailscale.com"
|
"pattern": "tailscale.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pattern": "wireguard.com"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"aliveStatusCodes": [200, 0]
|
"aliveStatusCodes": [200, 0]
|
||||||
|
|||||||
Generated
+4
@@ -8541,6 +8541,10 @@ const docTemplate = `{
|
|||||||
"description": "DashboardURL is the URL to hit the deployment's dashboard.\nFor external workspace proxies, this is the coderd they are connected\nto.",
|
"description": "DashboardURL is the URL to hit the deployment's dashboard.\nFor external workspace proxies, this is the coderd they are connected\nto.",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"deployment_id": {
|
||||||
|
"description": "DeploymentID is the unique identifier for this deployment.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"external_url": {
|
"external_url": {
|
||||||
"description": "ExternalURL references the current Coder version.\nFor production builds, this will link directly to a release. For development builds, this will link to a commit.",
|
"description": "ExternalURL references the current Coder version.\nFor production builds, this will link directly to a release. For development builds, this will link to a commit.",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
|||||||
Generated
+4
@@ -7599,6 +7599,10 @@
|
|||||||
"description": "DashboardURL is the URL to hit the deployment's dashboard.\nFor external workspace proxies, this is the coderd they are connected\nto.",
|
"description": "DashboardURL is the URL to hit the deployment's dashboard.\nFor external workspace proxies, this is the coderd they are connected\nto.",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"deployment_id": {
|
||||||
|
"description": "DeploymentID is the unique identifier for this deployment.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"external_url": {
|
"external_url": {
|
||||||
"description": "ExternalURL references the current Coder version.\nFor production builds, this will link directly to a release. For development builds, this will link to a commit.",
|
"description": "ExternalURL references the current Coder version.\nFor production builds, this will link directly to a release. For development builds, this will link to a commit.",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
|||||||
+1
-1
@@ -735,7 +735,7 @@ func New(options *Options) *API {
|
|||||||
// All CSP errors will be logged
|
// All CSP errors will be logged
|
||||||
r.Post("/csp/reports", api.logReportCSPViolations)
|
r.Post("/csp/reports", api.logReportCSPViolations)
|
||||||
|
|
||||||
r.Get("/buildinfo", buildInfo(api.AccessURL, api.DeploymentValues.CLIUpgradeMessage.String()))
|
r.Get("/buildinfo", buildInfo(api.AccessURL, api.DeploymentValues.CLIUpgradeMessage.String(), api.DeploymentID))
|
||||||
// /regions is overridden in the enterprise version
|
// /regions is overridden in the enterprise version
|
||||||
r.Group(func(r chi.Router) {
|
r.Group(func(r chi.Router) {
|
||||||
r.Use(apiKeyMiddleware)
|
r.Use(apiKeyMiddleware)
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ func (api *API) deploymentStats(rw http.ResponseWriter, r *http.Request) {
|
|||||||
// @Tags General
|
// @Tags General
|
||||||
// @Success 200 {object} codersdk.BuildInfoResponse
|
// @Success 200 {object} codersdk.BuildInfoResponse
|
||||||
// @Router /buildinfo [get]
|
// @Router /buildinfo [get]
|
||||||
func buildInfo(accessURL *url.URL, upgradeMessage string) http.HandlerFunc {
|
func buildInfo(accessURL *url.URL, upgradeMessage, deploymentID string) http.HandlerFunc {
|
||||||
return func(rw http.ResponseWriter, r *http.Request) {
|
return func(rw http.ResponseWriter, r *http.Request) {
|
||||||
httpapi.Write(r.Context(), rw, http.StatusOK, codersdk.BuildInfoResponse{
|
httpapi.Write(r.Context(), rw, http.StatusOK, codersdk.BuildInfoResponse{
|
||||||
ExternalURL: buildinfo.ExternalURL(),
|
ExternalURL: buildinfo.ExternalURL(),
|
||||||
@@ -77,6 +77,7 @@ func buildInfo(accessURL *url.URL, upgradeMessage string) http.HandlerFunc {
|
|||||||
DashboardURL: accessURL.String(),
|
DashboardURL: accessURL.String(),
|
||||||
WorkspaceProxy: false,
|
WorkspaceProxy: false,
|
||||||
UpgradeMessage: upgradeMessage,
|
UpgradeMessage: upgradeMessage,
|
||||||
|
DeploymentID: deploymentID,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2151,6 +2151,9 @@ type BuildInfoResponse struct {
|
|||||||
// UpgradeMessage is the message displayed to users when an outdated client
|
// UpgradeMessage is the message displayed to users when an outdated client
|
||||||
// is detected.
|
// is detected.
|
||||||
UpgradeMessage string `json:"upgrade_message"`
|
UpgradeMessage string `json:"upgrade_message"`
|
||||||
|
|
||||||
|
// DeploymentID is the unique identifier for this deployment.
|
||||||
|
DeploymentID string `json:"deployment_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type WorkspaceProxyBuildInfo struct {
|
type WorkspaceProxyBuildInfo struct {
|
||||||
|
|||||||
Generated
+1
@@ -55,6 +55,7 @@ curl -X GET http://coder-server:8080/api/v2/buildinfo \
|
|||||||
{
|
{
|
||||||
"agent_api_version": "string",
|
"agent_api_version": "string",
|
||||||
"dashboard_url": "string",
|
"dashboard_url": "string",
|
||||||
|
"deployment_id": "string",
|
||||||
"external_url": "string",
|
"external_url": "string",
|
||||||
"upgrade_message": "string",
|
"upgrade_message": "string",
|
||||||
"version": "string",
|
"version": "string",
|
||||||
|
|||||||
Generated
+2
@@ -1178,6 +1178,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
|
|||||||
{
|
{
|
||||||
"agent_api_version": "string",
|
"agent_api_version": "string",
|
||||||
"dashboard_url": "string",
|
"dashboard_url": "string",
|
||||||
|
"deployment_id": "string",
|
||||||
"external_url": "string",
|
"external_url": "string",
|
||||||
"upgrade_message": "string",
|
"upgrade_message": "string",
|
||||||
"version": "string",
|
"version": "string",
|
||||||
@@ -1191,6 +1192,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
|
|||||||
| ------------------- | ------- | -------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| ------------------- | ------- | -------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `agent_api_version` | string | false | | Agent api version is the current version of the Agent API (back versions MAY still be supported). |
|
| `agent_api_version` | string | false | | Agent api version is the current version of the Agent API (back versions MAY still be supported). |
|
||||||
| `dashboard_url` | string | false | | Dashboard URL is the URL to hit the deployment's dashboard. For external workspace proxies, this is the coderd they are connected to. |
|
| `dashboard_url` | string | false | | Dashboard URL is the URL to hit the deployment's dashboard. For external workspace proxies, this is the coderd they are connected to. |
|
||||||
|
| `deployment_id` | string | false | | Deployment ID is the unique identifier for this deployment. |
|
||||||
| `external_url` | string | false | | External URL references the current Coder version. For production builds, this will link directly to a release. For development builds, this will link to a commit. |
|
| `external_url` | string | false | | External URL references the current Coder version. For production builds, this will link directly to a release. For development builds, this will link to a commit. |
|
||||||
| `upgrade_message` | string | false | | Upgrade message is the message displayed to users when an outdated client is detected. |
|
| `upgrade_message` | string | false | | Upgrade message is the message displayed to users when an outdated client is detected. |
|
||||||
| `version` | string | false | | Version returns the semantic version of the build. |
|
| `version` | string | false | | Version returns the semantic version of the build. |
|
||||||
|
|||||||
@@ -147,13 +147,14 @@ func NewWithAPI(t *testing.T, options *Options) (
|
|||||||
}
|
}
|
||||||
|
|
||||||
type LicenseOptions struct {
|
type LicenseOptions struct {
|
||||||
AccountType string
|
AccountType string
|
||||||
AccountID string
|
AccountID string
|
||||||
Trial bool
|
DeploymentIDs []string
|
||||||
AllFeatures bool
|
Trial bool
|
||||||
GraceAt time.Time
|
AllFeatures bool
|
||||||
ExpiresAt time.Time
|
GraceAt time.Time
|
||||||
Features license.Features
|
ExpiresAt time.Time
|
||||||
|
Features license.Features
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddFullLicense generates a license with all features enabled.
|
// AddFullLicense generates a license with all features enabled.
|
||||||
@@ -190,6 +191,7 @@ func GenerateLicense(t *testing.T, options LicenseOptions) string {
|
|||||||
LicenseExpires: jwt.NewNumericDate(options.GraceAt),
|
LicenseExpires: jwt.NewNumericDate(options.GraceAt),
|
||||||
AccountType: options.AccountType,
|
AccountType: options.AccountType,
|
||||||
AccountID: options.AccountID,
|
AccountID: options.AccountID,
|
||||||
|
DeploymentIDs: options.DeploymentIDs,
|
||||||
Trial: options.Trial,
|
Trial: options.Trial,
|
||||||
Version: license.CurrentVersion,
|
Version: license.CurrentVersion,
|
||||||
AllFeatures: options.AllFeatures,
|
AllFeatures: options.AllFeatures,
|
||||||
|
|||||||
@@ -257,14 +257,16 @@ type Claims struct {
|
|||||||
// the end of the grace period (identical to LicenseExpires if there is no grace period).
|
// the end of the grace period (identical to LicenseExpires if there is no grace period).
|
||||||
// The reason we use the standard claim for the end of the grace period is that we want JWT
|
// The reason we use the standard claim for the end of the grace period is that we want JWT
|
||||||
// processing libraries to consider the token "valid" until then.
|
// processing libraries to consider the token "valid" until then.
|
||||||
LicenseExpires *jwt.NumericDate `json:"license_expires,omitempty"`
|
LicenseExpires *jwt.NumericDate `json:"license_expires,omitempty"`
|
||||||
AccountType string `json:"account_type,omitempty"`
|
AccountType string `json:"account_type,omitempty"`
|
||||||
AccountID string `json:"account_id,omitempty"`
|
AccountID string `json:"account_id,omitempty"`
|
||||||
Trial bool `json:"trial"`
|
// DeploymentIDs enforces the license can only be used on a set of deployments.
|
||||||
AllFeatures bool `json:"all_features"`
|
DeploymentIDs []string `json:"deployment_ids,omitempty"`
|
||||||
Version uint64 `json:"version"`
|
Trial bool `json:"trial"`
|
||||||
Features Features `json:"features"`
|
AllFeatures bool `json:"all_features"`
|
||||||
RequireTelemetry bool `json:"require_telemetry,omitempty"`
|
Version uint64 `json:"version"`
|
||||||
|
Features Features `json:"features"`
|
||||||
|
RequireTelemetry bool `json:"require_telemetry,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseRaw consumes a license and returns the claims.
|
// ParseRaw consumes a license and returns the claims.
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -120,6 +121,15 @@ func (api *API) postLicense(rw http.ResponseWriter, r *http.Request) {
|
|||||||
// old licenses with a uuid.
|
// old licenses with a uuid.
|
||||||
id = uuid.New()
|
id = uuid.New()
|
||||||
}
|
}
|
||||||
|
if len(claims.DeploymentIDs) > 0 && !slices.Contains(claims.DeploymentIDs, api.AGPL.DeploymentID) {
|
||||||
|
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||||
|
Message: "License cannot be used on this deployment!",
|
||||||
|
Detail: fmt.Sprintf("The provided license is locked to the following deployments: %q. "+
|
||||||
|
"Your deployment identifier is %q. Please contact sales.", claims.DeploymentIDs, api.AGPL.DeploymentID),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
dl, err := api.Database.InsertLicense(ctx, database.InsertLicenseParams{
|
dl, err := api.Database.InsertLicense(ctx, database.InsertLicenseParams{
|
||||||
UploadedAt: dbtime.Now(),
|
UploadedAt: dbtime.Now(),
|
||||||
JWT: addLicense.License,
|
JWT: addLicense.License,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"golang.org/x/xerrors"
|
"golang.org/x/xerrors"
|
||||||
@@ -36,6 +37,22 @@ func TestPostLicense(t *testing.T) {
|
|||||||
assert.EqualValues(t, 1, features[codersdk.FeatureAuditLog])
|
assert.EqualValues(t, 1, features[codersdk.FeatureAuditLog])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("InvalidDeploymentID", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
// The generated deployment will start out with a different deployment ID.
|
||||||
|
client, _ := coderdenttest.New(t, &coderdenttest.Options{DontAddLicense: true})
|
||||||
|
license := coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{
|
||||||
|
DeploymentIDs: []string{uuid.NewString()},
|
||||||
|
})
|
||||||
|
_, err := client.AddLicense(context.Background(), codersdk.AddLicenseRequest{
|
||||||
|
License: license,
|
||||||
|
})
|
||||||
|
errResp := &codersdk.Error{}
|
||||||
|
require.ErrorAs(t, err, &errResp)
|
||||||
|
require.Equal(t, http.StatusBadRequest, errResp.StatusCode())
|
||||||
|
require.Contains(t, errResp.Message, "License cannot be used on this deployment!")
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("Unauthorized", func(t *testing.T) {
|
t.Run("Unauthorized", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
client, _ := coderdenttest.New(t, &coderdenttest.Options{DontAddLicense: true})
|
client, _ := coderdenttest.New(t, &coderdenttest.Options{DontAddLicense: true})
|
||||||
|
|||||||
Generated
+1
@@ -165,6 +165,7 @@ export interface BuildInfoResponse {
|
|||||||
readonly workspace_proxy: boolean;
|
readonly workspace_proxy: boolean;
|
||||||
readonly agent_api_version: string;
|
readonly agent_api_version: string;
|
||||||
readonly upgrade_message: string;
|
readonly upgrade_message: string;
|
||||||
|
readonly deployment_id: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// From codersdk/insights.go
|
// From codersdk/insights.go
|
||||||
|
|||||||
@@ -12,9 +12,11 @@ import LaunchIcon from "@mui/icons-material/LaunchOutlined";
|
|||||||
import DocsIcon from "@mui/icons-material/MenuBook";
|
import DocsIcon from "@mui/icons-material/MenuBook";
|
||||||
import Divider from "@mui/material/Divider";
|
import Divider from "@mui/material/Divider";
|
||||||
import MenuItem from "@mui/material/MenuItem";
|
import MenuItem from "@mui/material/MenuItem";
|
||||||
|
import Tooltip from "@mui/material/Tooltip";
|
||||||
import type { FC } from "react";
|
import type { FC } from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import type * as TypesGen from "api/typesGenerated";
|
import type * as TypesGen from "api/typesGenerated";
|
||||||
|
import { CopyButton } from "components/CopyButton/CopyButton";
|
||||||
import { ExternalImage } from "components/ExternalImage/ExternalImage";
|
import { ExternalImage } from "components/ExternalImage/ExternalImage";
|
||||||
import { usePopover } from "components/Popover/Popover";
|
import { usePopover } from "components/Popover/Popover";
|
||||||
import { Stack } from "components/Stack/Stack";
|
import { Stack } from "components/Stack/Stack";
|
||||||
@@ -161,15 +163,51 @@ export const UserDropdownContent: FC<UserDropdownContentProps> = ({
|
|||||||
<Divider css={{ marginBottom: "0 !important" }} />
|
<Divider css={{ marginBottom: "0 !important" }} />
|
||||||
|
|
||||||
<Stack css={styles.info} spacing={0}>
|
<Stack css={styles.info} spacing={0}>
|
||||||
<a
|
<Tooltip title="Coder Version">
|
||||||
title="Browse Source Code"
|
<a
|
||||||
css={[styles.footerText, styles.buildInfo]}
|
title="Browse Source Code"
|
||||||
href={buildInfo?.external_url}
|
css={[styles.footerText, styles.buildInfo]}
|
||||||
target="_blank"
|
href={buildInfo?.external_url}
|
||||||
rel="noreferrer"
|
target="_blank"
|
||||||
>
|
rel="noreferrer"
|
||||||
{buildInfo?.version} <LaunchIcon />
|
>
|
||||||
</a>
|
{buildInfo?.version} <LaunchIcon />
|
||||||
|
</a>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
{Boolean(buildInfo?.deployment_id) && (
|
||||||
|
<div
|
||||||
|
css={css`
|
||||||
|
font-size: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<Tooltip title="Deployment Identifier">
|
||||||
|
<div
|
||||||
|
css={css`
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{buildInfo?.deployment_id}
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
<CopyButton
|
||||||
|
text={buildInfo!.deployment_id}
|
||||||
|
buttonStyles={css`
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div css={styles.footerText}>{Language.copyrightText}</div>
|
<div css={styles.footerText}>{Language.copyrightText}</div>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -201,6 +201,7 @@ export const MockBuildInfo: TypesGen.BuildInfoResponse = {
|
|||||||
dashboard_url: "https:///mock-url",
|
dashboard_url: "https:///mock-url",
|
||||||
workspace_proxy: false,
|
workspace_proxy: false,
|
||||||
upgrade_message: "My custom upgrade message",
|
upgrade_message: "My custom upgrade message",
|
||||||
|
deployment_id: "510d407f-e521-4180-b559-eab4a6d802b8",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MockSupportLinks: TypesGen.LinkConfig[] = [
|
export const MockSupportLinks: TypesGen.LinkConfig[] = [
|
||||||
|
|||||||
Reference in New Issue
Block a user