From 5edfccf30dcf459072393f11031228bc603cebde Mon Sep 17 00:00:00 2001
From: Stephen Kirby <58410745+stirby@users.noreply.github.com>
Date: Thu, 18 Jul 2024 16:18:09 -0500
Subject: [PATCH 1/6] chore: patch 2.13.1 (#13927)
* fix: let workspace pages download partial logs for unhealthy workspaces (#13761)
* fix: get basic fix in for preventing download logs from blowing up UI
* fix: make sure blob units can't go out of bounds
* fix: make sure timeout is cleared on component unmount
* fix: reduce risk of shared cache state breaking useAgentLogs
* fix: allow partial downloading of logs
* fix: make sure useMemo cache is used properly
* wip: commit current progress on updated logs functionality
* docs: rewrite comment for clarity
* refactor: clean up current code
* fix: update styles for unavailable logs
* fix: resolve linter violations
* fix: update type signature of getErrorDetail
* fix: revert log/enabled logic for useAgentLogs
* fix: remove memoization from DownloadLogsDialog
* fix: update name of timeout state
* refactor: make log web sockets logic more clear
* docs: reword comment for clarity
* fix: commit current style update progress
* fix: finish style updates
(cherry picked from commit 940afa1ab1d273cddaef52c1a27616ce71d018bb)
* fix(site): enable dormant workspace to be deleted (#13850)
(cherry picked from commit 01b30eaa324df69b82ad4c6570dc721f10751de6)
* chore: remove `organizationIds` from `AuthProvider` (#13917)
(cherry picked from commit 80cbffe8435d56ceef8d8d75bf3df2f62f8cef91)
---------
Co-authored-by: Michael Smith
Co-authored-by: Bruno Quaresma
Co-authored-by: Kayla Washburn-Love
---
site/src/api/errors.ts | 7 +-
site/src/contexts/auth/AuthProvider.tsx | 2 -
site/src/contexts/auth/RequireAuth.test.tsx | 1 -
site/src/contexts/auth/RequireAuth.tsx | 6 +-
.../modules/dashboard/DashboardProvider.tsx | 35 +--
.../resources/AgentLogs/useAgentLogs.ts | 31 +-
.../OrganizationSettingsLayout.tsx | 22 +-
.../OrganizationSettingsPage.tsx | 2 +-
.../WorkspaceActions/DownloadLogsDialog.tsx | 265 ++++++++++++++----
.../WorkspaceActions.stories.tsx | 12 +
.../WorkspaceActions/constants.ts | 2 +-
site/src/testHelpers/storybook.tsx | 1 -
12 files changed, 271 insertions(+), 115 deletions(-)
diff --git a/site/src/api/errors.ts b/site/src/api/errors.ts
index ada591d754fb2..621b19856601b 100644
--- a/site/src/api/errors.ts
+++ b/site/src/api/errors.ts
@@ -110,15 +110,18 @@ export const getValidationErrorMessage = (error: unknown): string => {
return validationErrors.map((error) => error.detail).join("\n");
};
-export const getErrorDetail = (error: unknown): string | undefined | null => {
+export const getErrorDetail = (error: unknown): string | undefined => {
if (error instanceof Error) {
return "Please check the developer console for more details.";
}
+
if (isApiError(error)) {
return error.response.data.detail;
}
+
if (isApiErrorResponse(error)) {
return error.detail;
}
- return null;
+
+ return undefined;
};
diff --git a/site/src/contexts/auth/AuthProvider.tsx b/site/src/contexts/auth/AuthProvider.tsx
index 2925ac095aadd..d71ece0ae4bc7 100644
--- a/site/src/contexts/auth/AuthProvider.tsx
+++ b/site/src/contexts/auth/AuthProvider.tsx
@@ -30,7 +30,6 @@ export type AuthContextValue = {
isUpdatingProfile: boolean;
user: User | undefined;
permissions: Permissions | undefined;
- organizationIds: readonly string[] | undefined;
signInError: unknown;
updateProfileError: unknown;
signOut: () => void;
@@ -119,7 +118,6 @@ export const AuthProvider: FC = ({ children }) => {
permissions: permissionsQuery.data as Permissions | undefined,
signInError: loginMutation.error,
updateProfileError: updateProfileMutation.error,
- organizationIds: userQuery.data?.organization_ids,
}}
>
{children}
diff --git a/site/src/contexts/auth/RequireAuth.test.tsx b/site/src/contexts/auth/RequireAuth.test.tsx
index e1194cb601cbc..29cd9b6c54f96 100644
--- a/site/src/contexts/auth/RequireAuth.test.tsx
+++ b/site/src/contexts/auth/RequireAuth.test.tsx
@@ -95,7 +95,6 @@ describe("useAuthenticated", () => {
wrapper: createAuthWrapper({
user: MockUser,
permissions: MockPermissions,
- organizationIds: [],
}),
});
}).not.toThrow();
diff --git a/site/src/contexts/auth/RequireAuth.tsx b/site/src/contexts/auth/RequireAuth.tsx
index 6172ba8212ac5..d00b52dcd05d1 100644
--- a/site/src/contexts/auth/RequireAuth.tsx
+++ b/site/src/contexts/auth/RequireAuth.tsx
@@ -74,7 +74,7 @@ type RequireKeys = Omit & {
// values are not undefined when authenticated
type AuthenticatedAuthContextValue = RequireKeys<
AuthContextValue,
- "user" | "permissions" | "organizationIds"
+ "user" | "permissions"
>;
export const useAuthenticated = (): AuthenticatedAuthContextValue => {
@@ -88,9 +88,5 @@ export const useAuthenticated = (): AuthenticatedAuthContextValue => {
throw new Error("Permissions are not available.");
}
- if (!auth.organizationIds) {
- throw new Error("Organization ID is not available.");
- }
-
return auth as AuthenticatedAuthContextValue;
};
diff --git a/site/src/modules/dashboard/DashboardProvider.tsx b/site/src/modules/dashboard/DashboardProvider.tsx
index a44a162b994dd..2f3f16252887d 100644
--- a/site/src/modules/dashboard/DashboardProvider.tsx
+++ b/site/src/modules/dashboard/DashboardProvider.tsx
@@ -1,9 +1,4 @@
-import {
- createContext,
- type FC,
- type PropsWithChildren,
- useState,
-} from "react";
+import { createContext, type FC, type PropsWithChildren } from "react";
import { useQuery } from "react-query";
import { appearance } from "api/queries/appearance";
import { entitlements } from "api/queries/entitlements";
@@ -15,12 +10,14 @@ import type {
} from "api/typesGenerated";
import { Loader } from "components/Loader/Loader";
import { useAuthenticated } from "contexts/auth/RequireAuth";
-import { useEffectEvent } from "hooks/hookPolyfills";
import { useEmbeddedMetadata } from "hooks/useEmbeddedMetadata";
export interface DashboardValue {
+ /**
+ * @deprecated Do not add new usage of this value. It is being removed as part
+ * of the multi-org work.
+ */
organizationId: string;
- setOrganizationId: (id: string) => void;
entitlements: Entitlements;
experiments: Experiments;
appearance: AppearanceConfig;
@@ -32,7 +29,7 @@ export const DashboardContext = createContext(
export const DashboardProvider: FC = ({ children }) => {
const { metadata } = useEmbeddedMetadata();
- const { user, organizationIds } = useAuthenticated();
+ const { user } = useAuthenticated();
const entitlementsQuery = useQuery(entitlements(metadata.entitlements));
const experimentsQuery = useQuery(experiments(metadata.experiments));
const appearanceQuery = useQuery(appearance(metadata.appearance));
@@ -40,23 +37,6 @@ export const DashboardProvider: FC = ({ children }) => {
const isLoading =
!entitlementsQuery.data || !appearanceQuery.data || !experimentsQuery.data;
- const lastUsedOrganizationId = localStorage.getItem(
- `user:${user.id}.lastUsedOrganizationId`,
- );
- const [activeOrganizationId, setActiveOrganizationId] = useState(() =>
- lastUsedOrganizationId && organizationIds.includes(lastUsedOrganizationId)
- ? lastUsedOrganizationId
- : organizationIds[0],
- );
-
- const setOrganizationId = useEffectEvent((id: string) => {
- if (!organizationIds.includes(id)) {
- throw new ReferenceError("Invalid organization ID");
- }
- localStorage.setItem(`user:${user.id}.lastUsedOrganizationId`, id);
- setActiveOrganizationId(id);
- });
-
if (isLoading) {
return ;
}
@@ -64,8 +44,7 @@ export const DashboardProvider: FC = ({ children }) => {
return (
;
+/**
+ * Defines a custom hook that gives you all workspace agent logs for a given
+ * workspace.
+ *
+ * Depending on the status of the workspace, all logs may or may not be
+ * available.
+ */
export function useAgentLogs(
options: UseAgentLogsOptions,
): readonly WorkspaceAgentLog[] | undefined {
const { workspaceId, agentId, agentLifeCycleState, enabled = true } = options;
+
const queryClient = useQueryClient();
const queryOptions = agentLogs(workspaceId, agentId);
- const query = useQuery({
- ...queryOptions,
- enabled,
- });
- const logs = query.data;
+ const { data: logs, isFetched } = useQuery({ ...queryOptions, enabled });
+ // Track the ID of the last log received when the initial logs response comes
+ // back. If the logs are not complete, the ID will mark the start point of the
+ // Web sockets response so that the remaining logs can be received over time
const lastQueriedLogId = useRef(0);
useEffect(() => {
- if (logs && lastQueriedLogId.current === 0) {
- lastQueriedLogId.current = logs[logs.length - 1].id;
+ const isAlreadyTracking = lastQueriedLogId.current !== 0;
+ if (isAlreadyTracking) {
+ return;
+ }
+
+ const lastLog = logs?.at(-1);
+ if (lastLog !== undefined) {
+ lastQueriedLogId.current = lastLog.id;
}
}, [logs]);
@@ -42,7 +55,7 @@ export function useAgentLogs(
});
useEffect(() => {
- if (agentLifeCycleState !== "starting" || !query.isFetched) {
+ if (agentLifeCycleState !== "starting" || !isFetched) {
return;
}
@@ -69,7 +82,7 @@ export function useAgentLogs(
return () => {
socket.close();
};
- }, [addLogs, agentId, agentLifeCycleState, query.isFetched]);
+ }, [addLogs, agentId, agentLifeCycleState, isFetched]);
return logs;
}
diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationSettingsLayout.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationSettingsLayout.tsx
index ae278b053428a..fad703671ca37 100644
--- a/site/src/pages/OrganizationSettingsPage/OrganizationSettingsLayout.tsx
+++ b/site/src/pages/OrganizationSettingsPage/OrganizationSettingsLayout.tsx
@@ -1,6 +1,6 @@
import { createContext, type FC, Suspense, useContext } from "react";
import { useQuery } from "react-query";
-import { Outlet, useParams } from "react-router-dom";
+import { Outlet, useLocation, useParams } from "react-router-dom";
import { myOrganizations } from "api/queries/users";
import type { Organization } from "api/typesGenerated";
import { Loader } from "components/Loader/Loader";
@@ -13,7 +13,7 @@ import NotFoundPage from "pages/404Page/404Page";
import { Sidebar } from "./Sidebar";
type OrganizationSettingsContextValue = {
- currentOrganizationId: string;
+ currentOrganizationId?: string;
organizations: Organization[];
};
@@ -32,13 +32,18 @@ export const useOrganizationSettings = (): OrganizationSettingsContextValue => {
};
export const OrganizationSettingsLayout: FC = () => {
- const { permissions, organizationIds } = useAuthenticated();
+ const location = useLocation();
+ const { permissions } = useAuthenticated();
const { experiments } = useDashboard();
const { organization } = useParams() as { organization: string };
const organizationsQuery = useQuery(myOrganizations());
const multiOrgExperimentEnabled = experiments.includes("multi-organization");
+ const inOrganizationSettings =
+ location.pathname.startsWith("/organizations") &&
+ location.pathname !== "/organizations/new";
+
if (!multiOrgExperimentEnabled) {
return ;
}
@@ -50,10 +55,13 @@ export const OrganizationSettingsLayout: FC = () => {
{organizationsQuery.data ? (
org.name === organization,
- )?.id ?? organizationIds[0],
+ currentOrganizationId: !inOrganizationSettings
+ ? undefined
+ : !organization
+ ? organizationsQuery.data[0]?.id
+ : organizationsQuery.data.find(
+ (org) => org.name === organization,
+ )?.id,
organizations: organizationsQuery.data,
}}
>
diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.tsx
index bc278b79c7e42..02ed8d1de9767 100644
--- a/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.tsx
+++ b/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPage.tsx
@@ -140,7 +140,7 @@ const OrganizationSettingsPage: FC = () => {
css={styles.dangerButton}
variant="contained"
onClick={() =>
- deleteOrganizationMutation.mutate(currentOrganizationId)
+ deleteOrganizationMutation.mutate(currentOrganizationId!)
}
>
Delete this organization
diff --git a/site/src/pages/WorkspacePage/WorkspaceActions/DownloadLogsDialog.tsx b/site/src/pages/WorkspacePage/WorkspaceActions/DownloadLogsDialog.tsx
index aefd4d7d6b9e1..ab1ee817a9de7 100644
--- a/site/src/pages/WorkspacePage/WorkspaceActions/DownloadLogsDialog.tsx
+++ b/site/src/pages/WorkspacePage/WorkspaceActions/DownloadLogsDialog.tsx
@@ -2,10 +2,11 @@ import { useTheme, type Interpolation, type Theme } from "@emotion/react";
import Skeleton from "@mui/material/Skeleton";
import { saveAs } from "file-saver";
import JSZip from "jszip";
-import { useMemo, useState, type FC } from "react";
+import { type FC, useMemo, useState, useRef, useEffect } from "react";
import { useQueries, useQuery } from "react-query";
import { agentLogs, buildLogs } from "api/queries/workspaces";
import type { Workspace, WorkspaceAgent } from "api/typesGenerated";
+import { Alert } from "components/Alert/Alert";
import {
ConfirmDialog,
type ConfirmDialogProps,
@@ -28,70 +29,107 @@ type DownloadableFile = {
export const DownloadLogsDialog: FC = ({
workspace,
+ open,
+ onClose,
download = saveAs,
- ...dialogProps
}) => {
const theme = useTheme();
- const agents = selectAgents(workspace);
- const agentLogResults = useQueries({
- queries: agents.map((a) => ({
- ...agentLogs(workspace.id, a.id),
- enabled: dialogProps.open,
- })),
- });
+
const buildLogsQuery = useQuery({
...buildLogs(workspace),
- enabled: dialogProps.open,
+ enabled: open,
});
- const downloadableFiles: DownloadableFile[] = useMemo(() => {
- const files: DownloadableFile[] = [
- {
- name: `${workspace.name}-build-logs.txt`,
- blob: buildLogsQuery.data
- ? new Blob([buildLogsQuery.data.map((l) => l.output).join("\n")], {
- type: "text/plain",
- })
- : undefined,
- },
- ];
-
- agents.forEach((a, i) => {
+
+ const allUniqueAgents = useMemo(() => {
+ const allAgents = workspace.latest_build.resources.flatMap(
+ (resource) => resource.agents ?? [],
+ );
+
+ // Can't use the "new Set()" trick because we're not dealing with primitives
+ const uniqueAgents = new Map(allAgents.map((agent) => [agent.id, agent]));
+ const iterable = [...uniqueAgents.values()];
+ return iterable;
+ }, [workspace.latest_build.resources]);
+
+ const agentLogQueries = useQueries({
+ queries: allUniqueAgents.map((agent) => ({
+ ...agentLogs(workspace.id, agent.id),
+ enabled: open,
+ })),
+ });
+
+ // Note: trying to memoize this via useMemo got really clunky. Removing all
+ // memoization for now, but if we get to a point where performance matters,
+ // we should make it so that this state doesn't even begin to mount until the
+ // user decides to open the Logs dropdown
+ const allFiles: readonly DownloadableFile[] = (() => {
+ const files = allUniqueAgents.map((a, i) => {
const name = `${a.name}-logs.txt`;
- const logs = agentLogResults[i].data;
- const txt = logs?.map((l) => l.output).join("\n");
+ const txt = agentLogQueries[i]?.data?.map((l) => l.output).join("\n");
+
let blob: Blob | undefined;
if (txt) {
blob = new Blob([txt], { type: "text/plain" });
}
- files.push({ name, blob });
+
+ return { name, blob };
});
+ const buildLogsFile = {
+ name: `${workspace.name}-build-logs.txt`,
+ blob: buildLogsQuery.data
+ ? new Blob([buildLogsQuery.data.map((l) => l.output).join("\n")], {
+ type: "text/plain",
+ })
+ : undefined,
+ };
+
+ files.unshift(buildLogsFile);
return files;
- }, [agentLogResults, agents, buildLogsQuery.data, workspace.name]);
- const isLoadingFiles = downloadableFiles.some((f) => f.blob === undefined);
+ })();
+
const [isDownloading, setIsDownloading] = useState(false);
+ const isWorkspaceHealthy = workspace.health.healthy;
+ const isLoadingFiles = allFiles.some((f) => f.blob === undefined);
+
+ const downloadTimeoutIdRef = useRef(undefined);
+ useEffect(() => {
+ const clearTimeoutOnUnmount = () => {
+ window.clearTimeout(downloadTimeoutIdRef.current);
+ };
+
+ return clearTimeoutOnUnmount;
+ }, []);
return (
{
+ setIsDownloading(true);
+ const zip = new JSZip();
+ allFiles.forEach((f) => {
+ if (f.blob) {
+ zip.file(f.name, f.blob);
+ }
+ });
+
try {
- setIsDownloading(true);
- const zip = new JSZip();
- downloadableFiles.forEach((f) => {
- if (f.blob) {
- zip.file(f.name, f.blob);
- }
- });
const content = await zip.generateAsync({ type: "blob" });
download(content, `${workspace.name}-logs.zip`);
- dialogProps.onClose();
- setTimeout(() => {
+ onClose();
+
+ downloadTimeoutIdRef.current = window.setTimeout(() => {
setIsDownloading(false);
}, theme.transitions.duration.leavingScreen);
} catch (error) {
@@ -106,18 +144,21 @@ export const DownloadLogsDialog: FC = ({
Downloading logs will create a zip file containing all logs from all
jobs in this workspace. This may take a while.
+
+ {!isWorkspaceHealthy && isLoadingFiles && (
+
+ Your workspace is unhealthy. Some logs may be unavailable for
+ download.
+
+ )}
+
- {downloadableFiles.map((f) => (
- -
- {f.name}
-
- {f.blob ? (
- humanBlobSize(f.blob.size)
- ) : (
-
- )}
-
-
+ {allFiles.map((f) => (
+
))}
@@ -126,20 +167,98 @@ export const DownloadLogsDialog: FC = ({
);
};
+type DownloadingItemProps = Readonly<{
+ // A value of undefined indicates that the component will wait forever
+ giveUpTimeMs?: number;
+ file: DownloadableFile;
+}>;
+
+const DownloadingItem: FC = ({ file, giveUpTimeMs }) => {
+ const theme = useTheme();
+ const [isWaiting, setIsWaiting] = useState(true);
+
+ useEffect(() => {
+ if (giveUpTimeMs === undefined || file.blob !== undefined) {
+ setIsWaiting(true);
+ return;
+ }
+
+ const timeoutId = window.setTimeout(
+ () => setIsWaiting(false),
+ giveUpTimeMs,
+ );
+
+ return () => window.clearTimeout(timeoutId);
+ }, [giveUpTimeMs, file]);
+
+ const { baseName, fileExtension } = extractFileNameInfo(file.name);
+
+ return (
+
+
+ {baseName}
+ .{fileExtension}
+
+
+
+ {file.blob ? (
+ humanBlobSize(file.blob.size)
+ ) : isWaiting ? (
+
+ ) : (
+ Not available
+ )}
+
+
+ );
+};
+
function humanBlobSize(size: number) {
- const units = ["B", "KB", "MB", "GB", "TB"];
+ const BLOB_SIZE_UNITS = ["B", "KB", "MB", "GB", "TB"] as const;
let i = 0;
- while (size > 1024 && i < units.length) {
+ while (size > 1024 && i < BLOB_SIZE_UNITS.length) {
size /= 1024;
i++;
}
- return `${size.toFixed(2)} ${units[i]}`;
+
+ // The condition for the while loop above means that over time, we could break
+ // out of the loop because we accidentally shot past the array bounds and i
+ // is at index (BLOB_SIZE_UNITS.length). Adding a lot of redundant checks to
+ // make sure we always have a usable unit
+ const finalUnit = BLOB_SIZE_UNITS[i] ?? BLOB_SIZE_UNITS.at(-1) ?? "TB";
+ return `${size.toFixed(2)} ${finalUnit}`;
}
-function selectAgents(workspace: Workspace): WorkspaceAgent[] {
- return workspace.latest_build.resources
- .flatMap((r) => r.agents)
- .filter((a) => a !== undefined) as WorkspaceAgent[];
+type FileNameInfo = Readonly<{
+ baseName: string;
+ fileExtension: string | undefined;
+}>;
+
+function extractFileNameInfo(filename: string): FileNameInfo {
+ if (filename.length === 0) {
+ return {
+ baseName: "",
+ fileExtension: undefined,
+ };
+ }
+
+ const periodIndex = filename.lastIndexOf(".");
+ if (periodIndex === -1) {
+ return {
+ baseName: filename,
+ fileExtension: undefined,
+ };
+ }
+
+ return {
+ baseName: filename.slice(0, periodIndex),
+ fileExtension: filename.slice(periodIndex + 1),
+ };
}
const styles = {
@@ -151,16 +270,46 @@ const styles = {
flexDirection: "column",
gap: 8,
},
+
listItem: {
+ width: "100%",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
+ columnGap: "32px",
},
+
listItemPrimary: (theme) => ({
fontWeight: 500,
color: theme.palette.text.primary,
+ display: "flex",
+ flexFlow: "row nowrap",
+ columnGap: 0,
+ overflow: "hidden",
}),
+
+ listItemPrimaryBaseName: {
+ minWidth: 0,
+ flexShrink: 1,
+ overflow: "hidden",
+ textOverflow: "ellipsis",
+ },
+
+ listItemPrimaryFileExtension: {
+ flexShrink: 0,
+ },
+
listItemSecondary: {
+ flexShrink: 0,
fontSize: 14,
+ whiteSpace: "nowrap",
},
+
+ notAvailableText: (theme) => ({
+ display: "flex",
+ flexFlow: "row nowrap",
+ alignItems: "center",
+ columnGap: "4px",
+ color: theme.palette.text.disabled,
+ }),
} satisfies Record>;
diff --git a/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.stories.tsx b/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.stories.tsx
index b42dbd418a04f..c50f1ac8dfffe 100644
--- a/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.stories.tsx
+++ b/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.stories.tsx
@@ -206,6 +206,18 @@ export const OpenDownloadLogs: Story = {
},
};
+export const CanDeleteDormantWorkspace: Story = {
+ args: {
+ workspace: Mocks.MockDormantWorkspace,
+ },
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement);
+ await userEvent.click(canvas.getByRole("button", { name: "More options" }));
+ const deleteButton = canvas.getByText("Deleteā¦");
+ await expect(deleteButton).toBeEnabled();
+ },
+};
+
function generateLogs(count: number) {
return Array.from({ length: count }, (_, i) => ({
output: `log ${i + 1}`,
diff --git a/site/src/pages/WorkspacePage/WorkspaceActions/constants.ts b/site/src/pages/WorkspacePage/WorkspaceActions/constants.ts
index f6d9f8f1cfa20..329a958ee12a8 100644
--- a/site/src/pages/WorkspacePage/WorkspaceActions/constants.ts
+++ b/site/src/pages/WorkspacePage/WorkspaceActions/constants.ts
@@ -48,7 +48,7 @@ export const abilitiesByWorkspaceStatus = (
return {
actions: ["activate"],
canCancel: false,
- canAcceptJobs: false,
+ canAcceptJobs: true,
};
}
diff --git a/site/src/testHelpers/storybook.tsx b/site/src/testHelpers/storybook.tsx
index af1ba691bf826..d795af5f1818d 100644
--- a/site/src/testHelpers/storybook.tsx
+++ b/site/src/testHelpers/storybook.tsx
@@ -27,7 +27,6 @@ export const withDashboardProvider = (
{},
entitlements,
experiments,
appearance: MockAppearanceConfig,
From 5b120203b27fa75d7e971a32a2c4626837fe962f Mon Sep 17 00:00:00 2001
From: Stephen Kirby <58410745+stirby@users.noreply.github.com>
Date: Mon, 22 Jul 2024 16:22:01 -0500
Subject: [PATCH 2/6] chore: keep active users active in scim (#13955) (#13973)
* chore: scim should keep active users active
* chore: add a unit test to excercise dormancy bug
* audit log should not be dropped when there is no change
* add ability to cancel audit log
(cherry picked from commit 03c5d42233c23cdf8ab85fce3e7c00f7cf4f4108)
Co-authored-by: Steven Masley
---
coderd/audit/request.go | 20 +++++++++
enterprise/coderd/scim.go | 45 +++++++++++++------
enterprise/coderd/scim_test.go | 79 ++++++++++++++++++++++++++++++++++
3 files changed, 130 insertions(+), 14 deletions(-)
diff --git a/coderd/audit/request.go b/coderd/audit/request.go
index 1c027fc85527f..8801bdf9c998d 100644
--- a/coderd/audit/request.go
+++ b/coderd/audit/request.go
@@ -257,6 +257,26 @@ func requireOrgID[T Auditable](ctx context.Context, id uuid.UUID, log slog.Logge
return id
}
+// InitRequestWithCancel returns a commit function with a boolean arg.
+// If the arg is false, future calls to commit() will not create an audit log
+// entry.
+func InitRequestWithCancel[T Auditable](w http.ResponseWriter, p *RequestParams) (*Request[T], func(commit bool)) {
+ req, commitF := InitRequest[T](w, p)
+ cancelled := false
+ return req, func(commit bool) {
+ // Once 'commit=false' is called, block
+ // any future commit attempts.
+ if !commit {
+ cancelled = true
+ return
+ }
+ // If it was ever cancelled, block any commits
+ if !cancelled {
+ commitF()
+ }
+ }
+}
+
// InitRequest initializes an audit log for a request. It returns a function
// that should be deferred, causing the audit log to be committed when the
// handler returns.
diff --git a/enterprise/coderd/scim.go b/enterprise/coderd/scim.go
index 2e638e667e9a1..b7f1bc8d106c4 100644
--- a/enterprise/coderd/scim.go
+++ b/enterprise/coderd/scim.go
@@ -272,13 +272,14 @@ func (api *API) scimPatchUser(rw http.ResponseWriter, r *http.Request) {
}
auditor := *api.AGPL.Auditor.Load()
- aReq, commitAudit := audit.InitRequest[database.User](rw, &audit.RequestParams{
+ aReq, commitAudit := audit.InitRequestWithCancel[database.User](rw, &audit.RequestParams{
Audit: auditor,
Log: api.Logger,
Request: r,
Action: database.AuditActionWrite,
})
- defer commitAudit()
+
+ defer commitAudit(true)
id := chi.URLParam(r, "id")
@@ -307,23 +308,39 @@ func (api *API) scimPatchUser(rw http.ResponseWriter, r *http.Request) {
var status database.UserStatus
if sUser.Active {
- // The user will get transitioned to Active after logging in.
- status = database.UserStatusDormant
+ switch dbUser.Status {
+ case database.UserStatusActive:
+ // Keep the user active
+ status = database.UserStatusActive
+ case database.UserStatusDormant, database.UserStatusSuspended:
+ // Move (or keep) as dormant
+ status = database.UserStatusDormant
+ default:
+ // If the status is unknown, just move them to dormant.
+ // The user will get transitioned to Active after logging in.
+ status = database.UserStatusDormant
+ }
} else {
status = database.UserStatusSuspended
}
- //nolint:gocritic // needed for SCIM
- userNew, err := api.Database.UpdateUserStatus(dbauthz.AsSystemRestricted(r.Context()), database.UpdateUserStatusParams{
- ID: dbUser.ID,
- Status: status,
- UpdatedAt: dbtime.Now(),
- })
- if err != nil {
- _ = handlerutil.WriteError(rw, err)
- return
+ if dbUser.Status != status {
+ //nolint:gocritic // needed for SCIM
+ userNew, err := api.Database.UpdateUserStatus(dbauthz.AsSystemRestricted(r.Context()), database.UpdateUserStatusParams{
+ ID: dbUser.ID,
+ Status: status,
+ UpdatedAt: dbtime.Now(),
+ })
+ if err != nil {
+ _ = handlerutil.WriteError(rw, err)
+ return
+ }
+ dbUser = userNew
+ } else {
+ // Do not push an audit log if there is no change.
+ commitAudit(false)
}
- aReq.New = userNew
+ aReq.New = dbUser
httpapi.Write(ctx, rw, http.StatusOK, sUser)
}
diff --git a/enterprise/coderd/scim_test.go b/enterprise/coderd/scim_test.go
index 237d53983a1a3..9421c6cf5b785 100644
--- a/enterprise/coderd/scim_test.go
+++ b/enterprise/coderd/scim_test.go
@@ -8,11 +8,13 @@ import (
"net/http"
"testing"
+ "github.com/golang-jwt/jwt/v4"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/coderd/audit"
"github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/coderd/coderdtest/oidctest"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/cryptorand"
@@ -364,5 +366,82 @@ func TestScim(t *testing.T) {
require.Len(t, userRes.Users, 1)
assert.Equal(t, codersdk.UserStatusSuspended, userRes.Users[0].Status)
})
+
+ // Create a user via SCIM, which starts as dormant.
+ // Log in as the user, making them active.
+ // Then patch the user again and the user should still be active.
+ t.Run("ActiveIsActive", func(t *testing.T) {
+ t.Parallel()
+
+ ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
+ defer cancel()
+
+ scimAPIKey := []byte("hi")
+
+ mockAudit := audit.NewMock()
+ fake := oidctest.NewFakeIDP(t, oidctest.WithServing())
+ client, _ := coderdenttest.New(t, &coderdenttest.Options{
+ Options: &coderdtest.Options{
+ Auditor: mockAudit,
+ OIDCConfig: fake.OIDCConfig(t, []string{}),
+ },
+ SCIMAPIKey: scimAPIKey,
+ AuditLogging: true,
+ LicenseOptions: &coderdenttest.LicenseOptions{
+ AccountID: "coolin",
+ Features: license.Features{
+ codersdk.FeatureSCIM: 1,
+ codersdk.FeatureAuditLog: 1,
+ },
+ },
+ })
+ mockAudit.ResetLogs()
+
+ // User is dormant on create
+ sUser := makeScimUser(t)
+ res, err := client.Request(ctx, "POST", "/scim/v2/Users", sUser, setScimAuth(scimAPIKey))
+ require.NoError(t, err)
+ defer res.Body.Close()
+ assert.Equal(t, http.StatusOK, res.StatusCode)
+
+ err = json.NewDecoder(res.Body).Decode(&sUser)
+ require.NoError(t, err)
+
+ // Check the audit log
+ aLogs := mockAudit.AuditLogs()
+ require.Len(t, aLogs, 1)
+ assert.Equal(t, database.AuditActionCreate, aLogs[0].Action)
+
+ // Verify the user is dormant
+ scimUser, err := client.User(ctx, sUser.UserName)
+ require.NoError(t, err)
+ require.Equal(t, codersdk.UserStatusDormant, scimUser.Status, "user starts as dormant")
+
+ // Log in as the user, making them active
+ //nolint:bodyclose
+ scimUserClient, _ := fake.Login(t, client, jwt.MapClaims{
+ "email": sUser.Emails[0].Value,
+ })
+ scimUser, err = scimUserClient.User(ctx, codersdk.Me)
+ require.NoError(t, err)
+ require.Equal(t, codersdk.UserStatusActive, scimUser.Status, "user should now be active")
+
+ // Patch the user
+ mockAudit.ResetLogs()
+ res, err = client.Request(ctx, "PATCH", "/scim/v2/Users/"+sUser.ID, sUser, setScimAuth(scimAPIKey))
+ require.NoError(t, err)
+ _, _ = io.Copy(io.Discard, res.Body)
+ _ = res.Body.Close()
+ assert.Equal(t, http.StatusOK, res.StatusCode)
+
+ // Should be no audit logs since there is no diff
+ aLogs = mockAudit.AuditLogs()
+ require.Len(t, aLogs, 0)
+
+ // Verify the user is still active.
+ scimUser, err = client.User(ctx, sUser.UserName)
+ require.NoError(t, err)
+ require.Equal(t, codersdk.UserStatusActive, scimUser.Status, "user is still active")
+ })
})
}
From 48c485994231e849e2d217152be47e200bd809ad Mon Sep 17 00:00:00 2001
From: Stephen Kirby <58410745+stirby@users.noreply.github.com>
Date: Tue, 23 Jul 2024 15:08:32 -0500
Subject: [PATCH 3/6] chore(scripts): fix cherry-pick check in
`check_commit_metadata.sh` (#13980) (#13994)
(cherry picked from commit 5a4dbcfc02d7acc78be1e8e4312eec7b07808ba8)
Co-authored-by: Mathias Fredriksson
---
scripts/release/check_commit_metadata.sh | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/scripts/release/check_commit_metadata.sh b/scripts/release/check_commit_metadata.sh
index 507e8d8d797a7..dff4cb1c738fc 100755
--- a/scripts/release/check_commit_metadata.sh
+++ b/scripts/release/check_commit_metadata.sh
@@ -126,6 +126,7 @@ main() {
log "Found renamed cherry-pick commit ${commit1} -> ${renamed}"
renamed_cherry_pick_commits[${commit1}]=${renamed}
renamed_cherry_pick_commits[${renamed}]=${commit1}
+ i=$((i - 1))
continue
fi
@@ -147,6 +148,11 @@ main() {
log "Found matching cherry-pick commit ${commit} -> ${renamed_cherry_pick_commits[${commit}]}"
done
+ # Merge the two maps.
+ for commit in "${!renamed_cherry_pick_commits[@]}"; do
+ cherry_pick_commits[${commit}]=${renamed_cherry_pick_commits[${commit}]}
+ done
+
# Get abbreviated and full commit hashes and titles for each commit.
git_log_out="$(git log --no-merges --left-right --pretty=format:"%m %h %H %s" "${range}")"
if [[ -z ${git_log_out} ]]; then
From d4f8a6481a87927e66becb51a79f170e1dc96449 Mon Sep 17 00:00:00 2001
From: Stephen Kirby <58410745+stirby@users.noreply.github.com>
Date: Thu, 1 Aug 2024 12:34:10 -0500
Subject: [PATCH 4/6] fix: change time format string from 15:40 to 15:04
(#14033) (#14072)
* Change string format to constant value
(cherry picked from commit eacdfb9f9ce20528b2a6d0bb0a35f26a036354c5)
Co-authored-by: Charlie Voiselle <464492+angrycub@users.noreply.github.com>
---
enterprise/coderd/users.go | 6 ++++--
enterprise/coderd/users_test.go | 17 +++++++++++------
2 files changed, 15 insertions(+), 8 deletions(-)
diff --git a/enterprise/coderd/users.go b/enterprise/coderd/users.go
index 935eeb8f6e689..07e66708b1713 100644
--- a/enterprise/coderd/users.go
+++ b/enterprise/coderd/users.go
@@ -14,6 +14,8 @@ import (
"github.com/coder/coder/v2/codersdk"
)
+const TimeFormatHHMM = "15:04"
+
func (api *API) autostopRequirementEnabledMW(next http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
// Entitlement must be enabled.
@@ -66,7 +68,7 @@ func (api *API) userQuietHoursSchedule(rw http.ResponseWriter, r *http.Request)
RawSchedule: opts.Schedule.String(),
UserSet: opts.UserSet,
UserCanSet: opts.UserCanSet,
- Time: opts.Schedule.TimeParsed().Format("15:40"),
+ Time: opts.Schedule.TimeParsed().Format(TimeFormatHHMM),
Timezone: opts.Schedule.Location().String(),
Next: opts.Schedule.Next(time.Now().In(opts.Schedule.Location())),
})
@@ -118,7 +120,7 @@ func (api *API) putUserQuietHoursSchedule(rw http.ResponseWriter, r *http.Reques
RawSchedule: opts.Schedule.String(),
UserSet: opts.UserSet,
UserCanSet: opts.UserCanSet,
- Time: opts.Schedule.TimeParsed().Format("15:40"),
+ Time: opts.Schedule.TimeParsed().Format(TimeFormatHHMM),
Timezone: opts.Schedule.Location().String(),
Next: opts.Schedule.Next(time.Now().In(opts.Schedule.Location())),
})
diff --git a/enterprise/coderd/users_test.go b/enterprise/coderd/users_test.go
index 4f55859cd9e4d..00bb9e9d74240 100644
--- a/enterprise/coderd/users_test.go
+++ b/enterprise/coderd/users_test.go
@@ -12,11 +12,14 @@ import (
"github.com/coder/coder/v2/coderd/rbac"
"github.com/coder/coder/v2/coderd/schedule/cron"
"github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/enterprise/coderd"
"github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
"github.com/coder/coder/v2/enterprise/coderd/license"
"github.com/coder/coder/v2/testutil"
)
+const TimeFormatHHMM = coderd.TimeFormatHHMM
+
func TestUserQuietHours(t *testing.T) {
t.Parallel()
@@ -42,15 +45,17 @@ func TestUserQuietHours(t *testing.T) {
t.Run("OK", func(t *testing.T) {
t.Parallel()
-
- defaultQuietHoursSchedule := "CRON_TZ=America/Chicago 0 1 * * *"
+ // Using 10 for minutes lets us test a format bug in which values greater
+ // than 5 were causing the API to explode because the time was returned
+ // incorrectly
+ defaultQuietHoursSchedule := "CRON_TZ=America/Chicago 10 1 * * *"
defaultScheduleParsed, err := cron.Daily(defaultQuietHoursSchedule)
require.NoError(t, err)
nextTime := defaultScheduleParsed.Next(time.Now().In(defaultScheduleParsed.Location()))
if time.Until(nextTime) < time.Hour {
// Use a different default schedule instead, because we want to avoid
// the schedule "ticking over" during this test run.
- defaultQuietHoursSchedule = "CRON_TZ=America/Chicago 0 13 * * *"
+ defaultQuietHoursSchedule = "CRON_TZ=America/Chicago 10 13 * * *"
defaultScheduleParsed, err = cron.Daily(defaultQuietHoursSchedule)
require.NoError(t, err)
}
@@ -79,7 +84,7 @@ func TestUserQuietHours(t *testing.T) {
require.NoError(t, err)
require.Equal(t, defaultScheduleParsed.String(), sched1.RawSchedule)
require.False(t, sched1.UserSet)
- require.Equal(t, defaultScheduleParsed.TimeParsed().Format("15:40"), sched1.Time)
+ require.Equal(t, defaultScheduleParsed.TimeParsed().Format(TimeFormatHHMM), sched1.Time)
require.Equal(t, defaultScheduleParsed.Location().String(), sched1.Timezone)
require.WithinDuration(t, defaultScheduleParsed.Next(time.Now()), sched1.Next, 15*time.Second)
@@ -102,7 +107,7 @@ func TestUserQuietHours(t *testing.T) {
require.NoError(t, err)
require.Equal(t, customScheduleParsed.String(), sched2.RawSchedule)
require.True(t, sched2.UserSet)
- require.Equal(t, customScheduleParsed.TimeParsed().Format("15:40"), sched2.Time)
+ require.Equal(t, customScheduleParsed.TimeParsed().Format(TimeFormatHHMM), sched2.Time)
require.Equal(t, customScheduleParsed.Location().String(), sched2.Timezone)
require.WithinDuration(t, customScheduleParsed.Next(time.Now()), sched2.Next, 15*time.Second)
@@ -111,7 +116,7 @@ func TestUserQuietHours(t *testing.T) {
require.NoError(t, err)
require.Equal(t, customScheduleParsed.String(), sched3.RawSchedule)
require.True(t, sched3.UserSet)
- require.Equal(t, customScheduleParsed.TimeParsed().Format("15:40"), sched3.Time)
+ require.Equal(t, customScheduleParsed.TimeParsed().Format(TimeFormatHHMM), sched3.Time)
require.Equal(t, customScheduleParsed.Location().String(), sched3.Timezone)
require.WithinDuration(t, customScheduleParsed.Next(time.Now()), sched3.Next, 15*time.Second)
From bddf0bf85a9d95725f5c95da308fff40592f5a02 Mon Sep 17 00:00:00 2001
From: Stephen Kirby <58410745+stirby@users.noreply.github.com>
Date: Tue, 6 Aug 2024 11:58:37 -0500
Subject: [PATCH 5/6] chore: update emoji-mart data (#13746) (#14189)
(cherry picked from commit 4a0fd7466c88f0c5eae88dd06fb50c91d2271f51)
Co-authored-by: Kayla Washburn-Love
---
site/package.json | 2 +-
site/pnpm-lock.yaml | 8 +-
site/static/emojis/LICENSE-GRAPHICS | 393 ++++++++++++++++++++++++++++
3 files changed, 398 insertions(+), 5 deletions(-)
create mode 100644 site/static/emojis/LICENSE-GRAPHICS
diff --git a/site/package.json b/site/package.json
index 27a12d56b32d7..9f228df04abed 100644
--- a/site/package.json
+++ b/site/package.json
@@ -30,7 +30,7 @@
"deadcode": "ts-prune | grep -v \".stories\\|.config\\|e2e\\|__mocks__\\|used in module\\|testHelpers\\|typesGenerated\" || echo \"No deadcode found.\""
},
"dependencies": {
- "@emoji-mart/data": "1.1.2",
+ "@emoji-mart/data": "1.2.1",
"@emoji-mart/react": "1.1.1",
"@emotion/css": "11.11.2",
"@emotion/react": "11.11.1",
diff --git a/site/pnpm-lock.yaml b/site/pnpm-lock.yaml
index 287960052dfbe..c9245710c9920 100644
--- a/site/pnpm-lock.yaml
+++ b/site/pnpm-lock.yaml
@@ -10,8 +10,8 @@ overrides:
dependencies:
'@emoji-mart/data':
- specifier: 1.1.2
- version: 1.1.2
+ specifier: 1.2.1
+ version: 1.2.1
'@emoji-mart/react':
specifier: 1.1.1
version: 1.1.1(emoji-mart@5.6.0)(react@18.2.0)
@@ -2130,8 +2130,8 @@ packages:
engines: {node: '>=10.0.0'}
dev: true
- /@emoji-mart/data@1.1.2:
- resolution: {integrity: sha512-1HP8BxD2azjqWJvxIaWAMyTySeZY0Osr83ukYjltPVkNXeJvTz7yDrPLBtnrD5uqJ3tg4CcLuuBW09wahqL/fg==}
+ /@emoji-mart/data@1.2.1:
+ resolution: {integrity: sha512-no2pQMWiBy6gpBEiqGeU77/bFejDqUTRY7KX+0+iur13op3bqUsXdnwoZs6Xb1zbv0gAj5VvS1PWoUUckSr5Dw==}
dev: false
/@emoji-mart/react@1.1.1(emoji-mart@5.6.0)(react@18.2.0):
diff --git a/site/static/emojis/LICENSE-GRAPHICS b/site/static/emojis/LICENSE-GRAPHICS
new file mode 100644
index 0000000000000..230507dedabfb
--- /dev/null
+++ b/site/static/emojis/LICENSE-GRAPHICS
@@ -0,0 +1,393 @@
+Attribution 4.0 International
+
+=======================================================================
+
+Creative Commons Corporation ("Creative Commons") is not a law firm and
+does not provide legal services or legal advice. Distribution of
+Creative Commons public licenses does not create a lawyer-client or
+other relationship. Creative Commons makes its licenses and related
+information available on an "as-is" basis. Creative Commons gives no
+warranties regarding its licenses, any material licensed under their
+terms and conditions, or any related information. Creative Commons
+disclaims all liability for damages resulting from their use to the
+fullest extent possible.
+
+Using Creative Commons Public Licenses
+
+Creative Commons public licenses provide a standard set of terms and
+conditions that creators and other rights holders may use to share
+original works of authorship and other material subject to copyright
+and certain other rights specified in the public license below. The
+following considerations are for informational purposes only, are not
+exhaustive, and do not form part of our licenses.
+
+ Considerations for licensors: Our public licenses are
+ intended for use by those authorized to give the public
+ permission to use material in ways otherwise restricted by
+ copyright and certain other rights. Our licenses are
+ irrevocable. Licensors should read and understand the terms
+ and conditions of the license they choose before applying it.
+ Licensors should also secure all rights necessary before
+ applying our licenses so that the public can reuse the
+ material as expected. Licensors should clearly mark any
+ material not subject to the license. This includes other CC-
+ licensed material, or material used under an exception or
+ limitation to copyright. More considerations for licensors:
+ wiki.creativecommons.org/Considerations_for_licensors
+
+ Considerations for the public: By using one of our public
+ licenses, a licensor grants the public permission to use the
+ licensed material under specified terms and conditions. If
+ the licensor's permission is not necessary for any reason--for
+ example, because of any applicable exception or limitation to
+ copyright--then that use is not regulated by the license. Our
+ licenses grant only permissions under copyright and certain
+ other rights that a licensor has authority to grant. Use of
+ the licensed material may still be restricted for other
+ reasons, including because others have copyright or other
+ rights in the material. A licensor may make special requests,
+ such as asking that all changes be marked or described.
+ Although not required by our licenses, you are encouraged to
+ respect those requests where reasonable. More_considerations
+ for the public:
+ wiki.creativecommons.org/Considerations_for_licensees
+
+=======================================================================
+
+Creative Commons Attribution 4.0 International Public License
+
+By exercising the Licensed Rights (defined below), You accept and agree
+to be bound by the terms and conditions of this Creative Commons
+Attribution 4.0 International Public License ("Public License"). To the
+extent this Public License may be interpreted as a contract, You are
+granted the Licensed Rights in consideration of Your acceptance of
+these terms and conditions, and the Licensor grants You such rights in
+consideration of benefits the Licensor receives from making the
+Licensed Material available under these terms and conditions.
+
+
+Section 1 -- Definitions.
+
+ a. Adapted Material means material subject to Copyright and Similar
+ Rights that is derived from or based upon the Licensed Material
+ and in which the Licensed Material is translated, altered,
+ arranged, transformed, or otherwise modified in a manner requiring
+ permission under the Copyright and Similar Rights held by the
+ Licensor. For purposes of this Public License, where the Licensed
+ Material is a musical work, performance, or sound recording,
+ Adapted Material is always produced where the Licensed Material is
+ synched in timed relation with a moving image.
+
+ b. Adapter's License means the license You apply to Your Copyright
+ and Similar Rights in Your contributions to Adapted Material in
+ accordance with the terms and conditions of this Public License.
+
+ c. Copyright and Similar Rights means copyright and/or similar rights
+ closely related to copyright including, without limitation,
+ performance, broadcast, sound recording, and Sui Generis Database
+ Rights, without regard to how the rights are labeled or
+ categorized. For purposes of this Public License, the rights
+ specified in Section 2(b)(1)-(2) are not Copyright and Similar
+ Rights.
+
+ d. Effective Technological Measures means those measures that, in the
+ absence of proper authority, may not be circumvented under laws
+ fulfilling obligations under Article 11 of the WIPO Copyright
+ Treaty adopted on December 20, 1996, and/or similar international
+ agreements.
+
+ e. Exceptions and Limitations means fair use, fair dealing, and/or
+ any other exception or limitation to Copyright and Similar Rights
+ that applies to Your use of the Licensed Material.
+
+ f. Licensed Material means the artistic or literary work, database,
+ or other material to which the Licensor applied this Public
+ License.
+
+ g. Licensed Rights means the rights granted to You subject to the
+ terms and conditions of this Public License, which are limited to
+ all Copyright and Similar Rights that apply to Your use of the
+ Licensed Material and that the Licensor has authority to license.
+
+ h. Licensor means the individual(s) or entity(ies) granting rights
+ under this Public License.
+
+ i. Share means to provide material to the public by any means or
+ process that requires permission under the Licensed Rights, such
+ as reproduction, public display, public performance, distribution,
+ dissemination, communication, or importation, and to make material
+ available to the public including in ways that members of the
+ public may access the material from a place and at a time
+ individually chosen by them.
+
+ j. Sui Generis Database Rights means rights other than copyright
+ resulting from Directive 96/9/EC of the European Parliament and of
+ the Council of 11 March 1996 on the legal protection of databases,
+ as amended and/or succeeded, as well as other essentially
+ equivalent rights anywhere in the world.
+
+ k. You means the individual or entity exercising the Licensed Rights
+ under this Public License. Your has a corresponding meaning.
+
+
+Section 2 -- Scope.
+
+ a. License grant.
+
+ 1. Subject to the terms and conditions of this Public License,
+ the Licensor hereby grants You a worldwide, royalty-free,
+ non-sublicensable, non-exclusive, irrevocable license to
+ exercise the Licensed Rights in the Licensed Material to:
+
+ a. reproduce and Share the Licensed Material, in whole or
+ in part; and
+
+ b. produce, reproduce, and Share Adapted Material.
+
+ 2. Exceptions and Limitations. For the avoidance of doubt, where
+ Exceptions and Limitations apply to Your use, this Public
+ License does not apply, and You do not need to comply with
+ its terms and conditions.
+
+ 3. Term. The term of this Public License is specified in Section
+ 6(a).
+
+ 4. Media and formats; technical modifications allowed. The
+ Licensor authorizes You to exercise the Licensed Rights in
+ all media and formats whether now known or hereafter created,
+ and to make technical modifications necessary to do so. The
+ Licensor waives and/or agrees not to assert any right or
+ authority to forbid You from making technical modifications
+ necessary to exercise the Licensed Rights, including
+ technical modifications necessary to circumvent Effective
+ Technological Measures. For purposes of this Public License,
+ simply making modifications authorized by this Section 2(a)
+ (4) never produces Adapted Material.
+
+ 5. Downstream recipients.
+
+ a. Offer from the Licensor -- Licensed Material. Every
+ recipient of the Licensed Material automatically
+ receives an offer from the Licensor to exercise the
+ Licensed Rights under the terms and conditions of this
+ Public License.
+
+ b. No downstream restrictions. You may not offer or impose
+ any additional or different terms or conditions on, or
+ apply any Effective Technological Measures to, the
+ Licensed Material if doing so restricts exercise of the
+ Licensed Rights by any recipient of the Licensed
+ Material.
+
+ 6. No endorsement. Nothing in this Public License constitutes or
+ may be construed as permission to assert or imply that You
+ are, or that Your use of the Licensed Material is, connected
+ with, or sponsored, endorsed, or granted official status by,
+ the Licensor or others designated to receive attribution as
+ provided in Section 3(a)(1)(A)(i).
+
+ b. Other rights.
+
+ 1. Moral rights, such as the right of integrity, are not
+ licensed under this Public License, nor are publicity,
+ privacy, and/or other similar personality rights; however, to
+ the extent possible, the Licensor waives and/or agrees not to
+ assert any such rights held by the Licensor to the limited
+ extent necessary to allow You to exercise the Licensed
+ Rights, but not otherwise.
+
+ 2. Patent and trademark rights are not licensed under this
+ Public License.
+
+ 3. To the extent possible, the Licensor waives any right to
+ collect royalties from You for the exercise of the Licensed
+ Rights, whether directly or through a collecting society
+ under any voluntary or waivable statutory or compulsory
+ licensing scheme. In all other cases the Licensor expressly
+ reserves any right to collect such royalties.
+
+
+Section 3 -- License Conditions.
+
+Your exercise of the Licensed Rights is expressly made subject to the
+following conditions.
+
+ a. Attribution.
+
+ 1. If You Share the Licensed Material (including in modified
+ form), You must:
+
+ a. retain the following if it is supplied by the Licensor
+ with the Licensed Material:
+
+ i. identification of the creator(s) of the Licensed
+ Material and any others designated to receive
+ attribution, in any reasonable manner requested by
+ the Licensor (including by pseudonym if
+ designated);
+
+ ii. a copyright notice;
+
+ iii. a notice that refers to this Public License;
+
+ iv. a notice that refers to the disclaimer of
+ warranties;
+
+ v. a URI or hyperlink to the Licensed Material to the
+ extent reasonably practicable;
+
+ b. indicate if You modified the Licensed Material and
+ retain an indication of any previous modifications; and
+
+ c. indicate the Licensed Material is licensed under this
+ Public License, and include the text of, or the URI or
+ hyperlink to, this Public License.
+
+ 2. You may satisfy the conditions in Section 3(a)(1) in any
+ reasonable manner based on the medium, means, and context in
+ which You Share the Licensed Material. For example, it may be
+ reasonable to satisfy the conditions by providing a URI or
+ hyperlink to a resource that includes the required
+ information.
+
+ 3. If requested by the Licensor, You must remove any of the
+ information required by Section 3(a)(1)(A) to the extent
+ reasonably practicable.
+
+ 4. If You Share Adapted Material You produce, the Adapter's
+ License You apply must not prevent recipients of the Adapted
+ Material from complying with this Public License.
+
+
+Section 4 -- Sui Generis Database Rights.
+
+Where the Licensed Rights include Sui Generis Database Rights that
+apply to Your use of the Licensed Material:
+
+ a. for the avoidance of doubt, Section 2(a)(1) grants You the right
+ to extract, reuse, reproduce, and Share all or a substantial
+ portion of the contents of the database;
+
+ b. if You include all or a substantial portion of the database
+ contents in a database in which You have Sui Generis Database
+ Rights, then the database in which You have Sui Generis Database
+ Rights (but not its individual contents) is Adapted Material; and
+
+ c. You must comply with the conditions in Section 3(a) if You Share
+ all or a substantial portion of the contents of the database.
+
+For the avoidance of doubt, this Section 4 supplements and does not
+replace Your obligations under this Public License where the Licensed
+Rights include other Copyright and Similar Rights.
+
+
+Section 5 -- Disclaimer of Warranties and Limitation of Liability.
+
+ a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE
+ EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS
+ AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF
+ ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,
+ IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,
+ WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR
+ PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,
+ ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT
+ KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT
+ ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.
+
+ b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE
+ TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,
+ NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,
+ INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,
+ COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR
+ USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN
+ ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR
+ DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR
+ IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.
+
+ c. The disclaimer of warranties and limitation of liability provided
+ above shall be interpreted in a manner that, to the extent
+ possible, most closely approximates an absolute disclaimer and
+ waiver of all liability.
+
+
+Section 6 -- Term and Termination.
+
+ a. This Public License applies for the term of the Copyright and
+ Similar Rights licensed here. However, if You fail to comply with
+ this Public License, then Your rights under this Public License
+ terminate automatically.
+
+ b. Where Your right to use the Licensed Material has terminated under
+ Section 6(a), it reinstates:
+
+ 1. automatically as of the date the violation is cured, provided
+ it is cured within 30 days of Your discovery of the
+ violation; or
+
+ 2. upon express reinstatement by the Licensor.
+
+ For the avoidance of doubt, this Section 6(b) does not affect any
+ right the Licensor may have to seek remedies for Your violations
+ of this Public License.
+
+ c. For the avoidance of doubt, the Licensor may also offer the
+ Licensed Material under separate terms or conditions or stop
+ distributing the Licensed Material at any time; however, doing so
+ will not terminate this Public License.
+
+ d. Sections 1, 5, 6, 7, and 8 survive termination of this Public
+ License.
+
+
+Section 7 -- Other Terms and Conditions.
+
+ a. The Licensor shall not be bound by any additional or different
+ terms or conditions communicated by You unless expressly agreed.
+
+ b. Any arrangements, understandings, or agreements regarding the
+ Licensed Material not stated herein are separate from and
+ independent of the terms and conditions of this Public License.
+
+
+Section 8 -- Interpretation.
+
+ a. For the avoidance of doubt, this Public License does not, and
+ shall not be interpreted to, reduce, limit, restrict, or impose
+ conditions on any use of the Licensed Material that could lawfully
+ be made without permission under this Public License.
+
+ b. To the extent possible, if any provision of this Public License is
+ deemed unenforceable, it shall be automatically reformed to the
+ minimum extent necessary to make it enforceable. If the provision
+ cannot be reformed, it shall be severed from this Public License
+ without affecting the enforceability of the remaining terms and
+ conditions.
+
+ c. No term or condition of this Public License will be waived and no
+ failure to comply consented to unless expressly agreed to by the
+ Licensor.
+
+ d. Nothing in this Public License constitutes or may be interpreted
+ as a limitation upon, or waiver of, any privileges and immunities
+ that apply to the Licensor or You, including from the legal
+ processes of any jurisdiction or authority.
+
+
+=======================================================================
+
+Creative Commons is not a party to its public licenses.
+Notwithstanding, Creative Commons may elect to apply one of its public
+licenses to material it publishes and in those instances will be
+considered the "Licensor." Except for the limited purpose of indicating
+that material is shared under a Creative Commons public license or as
+otherwise permitted by the Creative Commons policies published at
+creativecommons.org/policies, Creative Commons does not authorize the
+use of the trademark "Creative Commons" or any other trademark or logo
+of Creative Commons without its prior written consent including,
+without limitation, in connection with any unauthorized modifications
+to any of its public licenses or any other arrangements,
+understandings, or agreements concerning use of licensed material. For
+the avoidance of doubt, this paragraph does not form part of the public
+licenses.
+
+Creative Commons may be contacted at creativecommons.org.
From 1a027b07746dadb2c23d01d6d7a0607bb2b5a1a3 Mon Sep 17 00:00:00 2001
From: Stephen Kirby <58410745+stirby@users.noreply.github.com>
Date: Tue, 20 Aug 2024 13:20:54 -0500
Subject: [PATCH 6/6] chore: sign the windows installer (#14353) (#14368)
(cherry picked from commit 6f9b3c1592bca6412ef01fb98758c068902757b8)
Co-authored-by: Kyle Carberry
---
scripts/build_windows_installer.sh | 10 ++++++++++
1 file changed, 10 insertions(+)
diff --git a/scripts/build_windows_installer.sh b/scripts/build_windows_installer.sh
index 3b4d15a3cee9c..1a20a2cca3fb3 100755
--- a/scripts/build_windows_installer.sh
+++ b/scripts/build_windows_installer.sh
@@ -19,6 +19,7 @@ source "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
agpl="${CODER_BUILD_AGPL:-0}"
output_path=""
version=""
+sign_windows="${CODER_SIGN_WINDOWS:-0}"
args="$(getopt -o "" -l agpl,output:,version: -- "$@")"
eval set -- "$args"
@@ -51,6 +52,11 @@ if [[ "$output_path" == "" ]]; then
error "--output is a required parameter"
fi
+if [[ "$sign_windows" == 1 ]]; then
+ dependencies java
+ requiredenvs JSIGN_PATH EV_KEYSTORE EV_KEY EV_CERTIFICATE_PATH EV_TSA_URL GCLOUD_ACCESS_TOKEN
+fi
+
if [[ "$#" != 1 ]]; then
error "Exactly one argument must be provided to this script, $# were supplied"
fi
@@ -125,3 +131,7 @@ popd
cp "$temp_dir/installer.exe" "$output_path"
rm -rf "$temp_dir"
+
+if [[ "$sign_windows" == 1 ]]; then
+ execrelative ./sign_windows.sh "$output_path" 1>&2
+fi