diff --git a/components/dashboard/src/data/projects/set-project-env-var-mutation.ts b/components/dashboard/src/data/projects/set-project-env-var-mutation.ts index ad6fbcc530dc16..ebaaf07d759bb1 100644 --- a/components/dashboard/src/data/projects/set-project-env-var-mutation.ts +++ b/components/dashboard/src/data/projects/set-project-env-var-mutation.ts @@ -5,16 +5,22 @@ */ import { useMutation } from "@tanstack/react-query"; -import { getGitpodService } from "../../service/service"; +import { envVarClient } from "../../service/public-api"; +import { EnvironmentVariableAdmission } from "@gitpod/public-api/lib/gitpod/v1/envvar_pb"; type SetProjectEnvVarArgs = { projectId: string; name: string; value: string; - censored: boolean; + admission: EnvironmentVariableAdmission; }; export const useSetProjectEnvVar = () => { - return useMutation(async ({ projectId, name, value, censored }) => { - return getGitpodService().server.setProjectEnvironmentVariable(projectId, name, value, censored); + return useMutation(async ({ projectId, name, value, admission }) => { + await envVarClient.createConfigurationEnvironmentVariable({ + name, + value, + configurationId: projectId, + admission, + }); }); }; diff --git a/components/dashboard/src/data/setup.tsx b/components/dashboard/src/data/setup.tsx index a3180a4caba568..4af7ef0e62642a 100644 --- a/components/dashboard/src/data/setup.tsx +++ b/components/dashboard/src/data/setup.tsx @@ -22,11 +22,12 @@ import * as WorkspaceClasses from "@gitpod/public-api/lib/gitpod/v1/workspace_pb import * as PaginationClasses from "@gitpod/public-api/lib/gitpod/v1/pagination_pb"; import * as ConfigurationClasses from "@gitpod/public-api/lib/gitpod/v1/configuration_pb"; import * as AuthProviderClasses from "@gitpod/public-api/lib/gitpod/v1/authprovider_pb"; +import * as EnvVarClasses from "@gitpod/public-api/lib/gitpod/v1/envvar_pb"; // This is used to version the cache // If data we cache changes in a non-backwards compatible way, increment this version // That will bust any previous cache versions a client may have stored -const CACHE_VERSION = "5"; +const CACHE_VERSION = "6"; export function noPersistence(queryKey: QueryKey): QueryKey { return [...queryKey, "no-persistence"]; @@ -146,6 +147,7 @@ function initializeMessages() { ...Object.values(PaginationClasses), ...Object.values(ConfigurationClasses), ...Object.values(AuthProviderClasses), + ...Object.values(EnvVarClasses), ]; for (const c of constr) { if ((c as any).prototype instanceof Message) { diff --git a/components/dashboard/src/projects/ProjectVariables.tsx b/components/dashboard/src/projects/ProjectVariables.tsx index b4d9aa0c68517a..e992a824d3b220 100644 --- a/components/dashboard/src/projects/ProjectVariables.tsx +++ b/components/dashboard/src/projects/ProjectVariables.tsx @@ -4,7 +4,7 @@ * See License.AGPL.txt in the project root for license information. */ -import { Project, ProjectEnvVar } from "@gitpod/gitpod-protocol"; +import { Project } from "@gitpod/gitpod-protocol"; import { useCallback, useEffect, useState } from "react"; import { Redirect } from "react-router"; import Alert from "../components/Alert"; @@ -13,23 +13,29 @@ import InfoBox from "../components/InfoBox"; import { Item, ItemField, ItemFieldContextMenu, ItemsList } from "../components/ItemsList"; import Modal, { ModalBody, ModalFooter, ModalFooterAlert, ModalHeader } from "../components/Modal"; import { Heading2, Subheading } from "../components/typography/headings"; -import { getGitpodService } from "../service/service"; import { useCurrentProject } from "./project-context"; import { ProjectSettingsPage } from "./ProjectSettings"; import { Button } from "../components/Button"; import { useSetProjectEnvVar } from "../data/projects/set-project-env-var-mutation"; +import { envVarClient } from "../service/public-api"; +import { + ConfigurationEnvironmentVariable, + EnvironmentVariableAdmission, +} from "@gitpod/public-api/lib/gitpod/v1/envvar_pb"; export default function ProjectVariablesPage() { const { project, loading } = useCurrentProject(); - const [envVars, setEnvVars] = useState([]); + const [envVars, setEnvVars] = useState([]); const [showAddVariableModal, setShowAddVariableModal] = useState(false); const updateEnvVars = async () => { if (!project) { return; } - const vars = await getGitpodService().server.getProjectEnvironmentVariables(project.id); - const sortedVars = vars.sort((a, b) => (a.name.toLowerCase() > b.name.toLowerCase() ? 1 : -1)); + const resp = await envVarClient.listConfigurationEnvironmentVariables({ configurationId: project.id }); + const sortedVars = resp.environmentVariables.sort((a, b) => + a.name.toLowerCase() > b.name.toLowerCase() ? 1 : -1, + ); setEnvVars(sortedVars); }; @@ -39,7 +45,7 @@ export default function ProjectVariablesPage() { }, [project]); const deleteEnvVar = async (variableId: string) => { - await getGitpodService().server.deleteProjectEnvironmentVariable(variableId); + await envVarClient.deleteConfigurationEnvironmentVariable({ envVarId: variableId }); updateEnvVars(); }; @@ -85,7 +91,11 @@ export default function ProjectVariablesPage() { return ( {variable.name} - {variable.censored ? "Hidden" : "Visible"} + + {variable.admission === EnvironmentVariableAdmission.PREBUILD + ? "Hidden" + : "Visible"} + void }) { const [name, setName] = useState(""); const [value, setValue] = useState(""); - const [censored, setCensored] = useState(true); + const [admission, setAdmission] = useState(EnvironmentVariableAdmission.PREBUILD); const setProjectEnvVar = useSetProjectEnvVar(); const addVariable = useCallback(async () => { @@ -123,11 +133,11 @@ function AddVariableModal(props: { project?: Project; onClose: () => void }) { projectId: props.project.id, name, value, - censored, + admission, }, { onSuccess: props.onClose }, ); - }, [censored, name, props.onClose, props.project, setProjectEnvVar, value]); + }, [admission, name, props.onClose, props.project, setProjectEnvVar, value]); return ( @@ -164,10 +174,16 @@ function AddVariableModal(props: { project?: Project; onClose: () => void }) { setCensored(!censored)} + checked={admission === EnvironmentVariableAdmission.PREBUILD} + onChange={() => + setAdmission( + admission === EnvironmentVariableAdmission.PREBUILD + ? EnvironmentVariableAdmission.EVERYWHERE + : EnvironmentVariableAdmission.PREBUILD, + ) + } /> - {!censored && ( + {admission === EnvironmentVariableAdmission.EVERYWHERE && (
This variable will be visible to anyone who starts a Gitpod workspace for your repository. diff --git a/components/dashboard/src/service/json-rpc-envvar-client.ts b/components/dashboard/src/service/json-rpc-envvar-client.ts new file mode 100644 index 00000000000000..d70ed5936f26c4 --- /dev/null +++ b/components/dashboard/src/service/json-rpc-envvar-client.ts @@ -0,0 +1,231 @@ +/** + * Copyright (c) 2023 Gitpod GmbH. All rights reserved. + * Licensed under the GNU Affero General Public License (AGPL). + * See License.AGPL.txt in the project root for license information. + */ + +import { PromiseClient } from "@connectrpc/connect"; +import { PartialMessage } from "@bufbuild/protobuf"; +import { EnvironmentVariableService } from "@gitpod/public-api/lib/gitpod/v1/envvar_connect"; +import { + CreateConfigurationEnvironmentVariableRequest, + CreateConfigurationEnvironmentVariableResponse, + CreateUserEnvironmentVariableRequest, + CreateUserEnvironmentVariableResponse, + DeleteConfigurationEnvironmentVariableRequest, + DeleteConfigurationEnvironmentVariableResponse, + DeleteUserEnvironmentVariableRequest, + DeleteUserEnvironmentVariableResponse, + EnvironmentVariableAdmission, + ListConfigurationEnvironmentVariablesRequest, + ListConfigurationEnvironmentVariablesResponse, + ListUserEnvironmentVariablesRequest, + ListUserEnvironmentVariablesResponse, + ResolveWorkspaceEnvironmentVariablesRequest, + ResolveWorkspaceEnvironmentVariablesResponse, + UpdateConfigurationEnvironmentVariableRequest, + UpdateConfigurationEnvironmentVariableResponse, + UpdateUserEnvironmentVariableRequest, + UpdateUserEnvironmentVariableResponse, +} from "@gitpod/public-api/lib/gitpod/v1/envvar_pb"; +import { converter } from "./public-api"; +import { getGitpodService } from "./service"; +import { UserEnvVar, UserEnvVarValue } from "@gitpod/gitpod-protocol"; +import { ApplicationError, ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error"; + +export class JsonRpcEnvvarClient implements PromiseClient { + async listUserEnvironmentVariables( + req: PartialMessage, + ): Promise { + const result = new ListUserEnvironmentVariablesResponse(); + const userEnvVars = await getGitpodService().server.getAllEnvVars(); + result.environmentVariables = userEnvVars.map((i) => converter.toUserEnvironmentVariable(i)); + + return result; + } + + async updateUserEnvironmentVariable( + req: PartialMessage, + ): Promise { + if (!req.envVarId) { + throw new ApplicationError(ErrorCodes.BAD_REQUEST, "envVarId is required"); + } + + const response = new UpdateUserEnvironmentVariableResponse(); + + const userEnvVars = await getGitpodService().server.getAllEnvVars(); + const userEnvVarfound = userEnvVars.find((i) => i.id === req.envVarId); + if (userEnvVarfound) { + const variable: UserEnvVarValue = { + id: req.envVarId, + name: req.name ?? userEnvVarfound.name, + value: req.value ?? userEnvVarfound.value, + repositoryPattern: req.repositoryPattern ?? userEnvVarfound.repositoryPattern, + }; + variable.repositoryPattern = UserEnvVar.normalizeRepoPattern(variable.repositoryPattern); + + await getGitpodService().server.setEnvVar(variable); + + const updatedUserEnvVars = await getGitpodService().server.getAllEnvVars(); + const updatedUserEnvVar = updatedUserEnvVars.find((i) => i.id === req.envVarId); + if (!updatedUserEnvVar) { + throw new ApplicationError(ErrorCodes.INTERNAL_SERVER_ERROR, "could not update env variable"); + } + + response.environmentVariable = converter.toUserEnvironmentVariable(updatedUserEnvVar); + return response; + } + + throw new ApplicationError(ErrorCodes.NOT_FOUND, "env variable not found"); + } + + async createUserEnvironmentVariable( + req: PartialMessage, + ): Promise { + if (!req.name || !req.value || !req.repositoryPattern) { + throw new ApplicationError(ErrorCodes.BAD_REQUEST, "name, value and repositoryPattern are required"); + } + + const response = new CreateUserEnvironmentVariableResponse(); + + const variable: UserEnvVarValue = { + name: req.name, + value: req.value, + repositoryPattern: req.repositoryPattern, + }; + variable.repositoryPattern = UserEnvVar.normalizeRepoPattern(variable.repositoryPattern); + + await getGitpodService().server.setEnvVar(variable); + + const updatedUserEnvVars = await getGitpodService().server.getAllEnvVars(); + const updatedUserEnvVar = updatedUserEnvVars.find( + (v) => v.name === variable.name && v.repositoryPattern === variable.repositoryPattern, + ); + if (!updatedUserEnvVar) { + throw new ApplicationError(ErrorCodes.INTERNAL_SERVER_ERROR, "could not update env variable"); + } + + response.environmentVariable = converter.toUserEnvironmentVariable(updatedUserEnvVar); + + return response; + } + + async deleteUserEnvironmentVariable( + req: PartialMessage, + ): Promise { + if (!req.envVarId) { + throw new ApplicationError(ErrorCodes.BAD_REQUEST, "envVarId is required"); + } + + const variable: UserEnvVarValue = { + id: req.envVarId, + name: "", + value: "", + repositoryPattern: "", + }; + + await getGitpodService().server.deleteEnvVar(variable); + + const response = new DeleteUserEnvironmentVariableResponse(); + return response; + } + + async listConfigurationEnvironmentVariables( + req: PartialMessage, + ): Promise { + if (!req.configurationId) { + throw new ApplicationError(ErrorCodes.BAD_REQUEST, "configurationId is required"); + } + + const result = new ListConfigurationEnvironmentVariablesResponse(); + const projectEnvVars = await getGitpodService().server.getProjectEnvironmentVariables(req.configurationId); + result.environmentVariables = projectEnvVars.map((i) => converter.toConfigurationEnvironmentVariable(i)); + + return result; + } + + async updateConfigurationEnvironmentVariable( + req: PartialMessage, + ): Promise { + if (!req.envVarId) { + throw new ApplicationError(ErrorCodes.BAD_REQUEST, "envVarId is required"); + } + if (!req.configurationId) { + throw new ApplicationError(ErrorCodes.BAD_REQUEST, "configurationId is required"); + } + + const response = new UpdateConfigurationEnvironmentVariableResponse(); + + const projectEnvVars = await getGitpodService().server.getProjectEnvironmentVariables(req.configurationId); + const projectEnvVarfound = projectEnvVars.find((i) => i.id === req.envVarId); + if (projectEnvVarfound) { + await getGitpodService().server.setProjectEnvironmentVariable( + req.configurationId, + req.name ?? projectEnvVarfound.name, + req.value ?? "", + (req.admission === EnvironmentVariableAdmission.PREBUILD ? true : false) ?? projectEnvVarfound.censored, + ); + + const updatedProjectEnvVars = await getGitpodService().server.getProjectEnvironmentVariables( + req.configurationId, + ); + const updatedProjectEnvVar = updatedProjectEnvVars.find((i) => i.id === req.envVarId); + if (!updatedProjectEnvVar) { + throw new ApplicationError(ErrorCodes.INTERNAL_SERVER_ERROR, "could not update env variable"); + } + + response.environmentVariable = converter.toConfigurationEnvironmentVariable(updatedProjectEnvVar); + return response; + } + + throw new ApplicationError(ErrorCodes.NOT_FOUND, "env variable not found"); + } + + async createConfigurationEnvironmentVariable( + req: PartialMessage, + ): Promise { + if (!req.configurationId || !req.name || !req.value) { + throw new ApplicationError(ErrorCodes.BAD_REQUEST, "configurationId, name and value are required"); + } + + const response = new CreateConfigurationEnvironmentVariableResponse(); + + await getGitpodService().server.setProjectEnvironmentVariable( + req.configurationId, + req.name, + req.value, + req.admission === EnvironmentVariableAdmission.PREBUILD ? true : false, + ); + + const updatedProjectEnvVars = await getGitpodService().server.getProjectEnvironmentVariables( + req.configurationId, + ); + const updatedProjectEnvVar = updatedProjectEnvVars.find((v) => v.name === req.name); + if (!updatedProjectEnvVar) { + throw new ApplicationError(ErrorCodes.INTERNAL_SERVER_ERROR, "could not create env variable"); + } + + response.environmentVariable = converter.toConfigurationEnvironmentVariable(updatedProjectEnvVar); + + return response; + } + + async deleteConfigurationEnvironmentVariable( + req: PartialMessage, + ): Promise { + if (!req.envVarId) { + throw new ApplicationError(ErrorCodes.BAD_REQUEST, "envVarId is required"); + } + + await getGitpodService().server.deleteProjectEnvironmentVariable(req.envVarId); + + const response = new DeleteConfigurationEnvironmentVariableResponse(); + return response; + } + + async resolveWorkspaceEnvironmentVariables( + req: PartialMessage, + ): Promise { + throw new ApplicationError(ErrorCodes.BAD_REQUEST, "Unimplemented"); + } +} diff --git a/components/dashboard/src/service/public-api.ts b/components/dashboard/src/service/public-api.ts index f327a89ad40b20..9a343ccf253e2c 100644 --- a/components/dashboard/src/service/public-api.ts +++ b/components/dashboard/src/service/public-api.ts @@ -25,6 +25,8 @@ import { JsonRpcOrganizationClient } from "./json-rpc-organization-client"; import { JsonRpcWorkspaceClient } from "./json-rpc-workspace-client"; import { JsonRpcAuthProviderClient } from "./json-rpc-authprovider-client"; import { AuthProviderService } from "@gitpod/public-api/lib/gitpod/v1/authprovider_connect"; +import { EnvironmentVariableService } from "@gitpod/public-api/lib/gitpod/v1/envvar_connect"; +import { JsonRpcEnvvarClient } from "./json-rpc-envvar-client"; const transport = createConnectTransport({ baseUrl: `${window.location.protocol}//${window.location.host}/public-api`, @@ -53,6 +55,8 @@ export const configurationClient = createServiceClient(ConfigurationService); export const authProviderClient = createServiceClient(AuthProviderService, new JsonRpcAuthProviderClient()); +export const envVarClient = createServiceClient(EnvironmentVariableService, new JsonRpcEnvvarClient()); + export async function listAllProjects(opts: { orgId: string }): Promise { let pagination = { page: 1, diff --git a/components/dashboard/src/user-settings/EnvironmentVariables.tsx b/components/dashboard/src/user-settings/EnvironmentVariables.tsx index d8fecacd6a0fa8..83da74f0b09968 100644 --- a/components/dashboard/src/user-settings/EnvironmentVariables.tsx +++ b/components/dashboard/src/user-settings/EnvironmentVariables.tsx @@ -9,11 +9,12 @@ import { useCallback, useEffect, useRef, useState } from "react"; import ConfirmationModal from "../components/ConfirmationModal"; import { Item, ItemField, ItemsList } from "../components/ItemsList"; import Modal, { ModalBody, ModalFooter, ModalHeader } from "../components/Modal"; -import { getGitpodService } from "../service/service"; import { PageWithSettingsSubMenu } from "./PageWithSettingsSubMenu"; import { EnvironmentVariableEntry } from "./EnvironmentVariableEntry"; import { Button } from "../components/Button"; import { Heading2, Subheading } from "../components/typography/headings"; +import { envVarClient } from "../service/public-api"; +import { UserEnvironmentVariable } from "@gitpod/public-api/lib/gitpod/v1/envvar_pb"; interface EnvVarModalProps { envVar: UserEnvVarValue; @@ -133,7 +134,7 @@ function DeleteEnvVarModal(p: { variable: UserEnvVarValue; deleteVariable: () => ); } -function sortEnvVars(a: UserEnvVarValue, b: UserEnvVarValue) { +function sortEnvVars(a: UserEnvironmentVariable, b: UserEnvironmentVariable) { if (a.name === b.name) { return a.repositoryPattern > b.repositoryPattern ? 1 : -1; } @@ -143,6 +144,7 @@ function sortEnvVars(a: UserEnvVarValue, b: UserEnvVarValue) { export default function EnvVars() { const [envVars, setEnvVars] = useState([] as UserEnvVarValue[]); const [currentEnvVar, setCurrentEnvVar] = useState({ + id: undefined, name: "", value: "", repositoryPattern: "", @@ -150,9 +152,16 @@ export default function EnvVars() { const [isAddEnvVarModalVisible, setAddEnvVarModalVisible] = useState(false); const [isDeleteEnvVarModalVisible, setDeleteEnvVarModalVisible] = useState(false); const update = async () => { - await getGitpodService() - .server.getAllEnvVars() - .then((r) => setEnvVars(r.sort(sortEnvVars))); + await envVarClient.listUserEnvironmentVariables({}).then((r) => + setEnvVars( + r.environmentVariables.sort(sortEnvVars).map((e) => ({ + id: e.id, + name: e.name, + value: e.value, + repositoryPattern: e.repositoryPattern, + })), + ), + ); }; useEffect(() => { @@ -160,7 +169,7 @@ export default function EnvVars() { }, []); const add = () => { - setCurrentEnvVar({ name: "", value: "", repositoryPattern: "" }); + setCurrentEnvVar({ id: undefined, name: "", value: "", repositoryPattern: "" }); setAddEnvVarModalVisible(true); setDeleteEnvVarModalVisible(false); }; @@ -178,12 +187,28 @@ export default function EnvVars() { }; const save = async (variable: UserEnvVarValue) => { - await getGitpodService().server.setEnvVar(variable); + if (variable.id) { + await envVarClient.updateUserEnvironmentVariable({ + envVarId: variable.id, + name: variable.name, + value: variable.value, + repositoryPattern: variable.repositoryPattern, + }); + } else { + await envVarClient.createUserEnvironmentVariable({ + name: variable.name, + value: variable.value, + repositoryPattern: variable.repositoryPattern, + }); + } + await update(); }; const deleteVariable = async (variable: UserEnvVarValue) => { - await getGitpodService().server.deleteEnvVar(variable); + await envVarClient.deleteUserEnvironmentVariable({ + envVarId: variable.id, + }); await update(); }; diff --git a/components/gitpod-db/src/project-db.ts b/components/gitpod-db/src/project-db.ts index d38e4721ada5b3..17d460d6940e22 100644 --- a/components/gitpod-db/src/project-db.ts +++ b/components/gitpod-db/src/project-db.ts @@ -20,8 +20,11 @@ export interface ProjectDB extends TransactionalDB { projectId: string, envVar: ProjectEnvVarWithValue, ): Promise; - addProjectEnvironmentVariable(projectId: string, envVar: ProjectEnvVarWithValue): Promise; - updateProjectEnvironmentVariable(projectId: string, envVar: Required): Promise; + addProjectEnvironmentVariable(projectId: string, envVar: ProjectEnvVarWithValue): Promise; + updateProjectEnvironmentVariable( + projectId: string, + envVar: Partial, + ): Promise; getProjectEnvironmentVariables(projectId: string): Promise; getProjectEnvironmentVariableById(variableId: string): Promise; deleteProjectEnvironmentVariable(variableId: string): Promise; diff --git a/components/gitpod-db/src/typeorm/project-db-impl.ts b/components/gitpod-db/src/typeorm/project-db-impl.ts index 16262207102d65..2894be8fbb4cd6 100644 --- a/components/gitpod-db/src/typeorm/project-db-impl.ts +++ b/components/gitpod-db/src/typeorm/project-db-impl.ts @@ -16,6 +16,8 @@ import { DBProjectInfo } from "./entity/db-project-info"; import { DBProjectUsage } from "./entity/db-project-usage"; import { TransactionalDBImpl } from "./transactional-db-impl"; import { TypeORM } from "./typeorm"; +import { ApplicationError, ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error"; +import { filter } from "../utils"; function toProjectEnvVar(envVarWithValue: DBProjectEnvVar): ProjectEnvVar { const envVar = { ...envVarWithValue }; @@ -145,9 +147,12 @@ export class ProjectDBImpl extends TransactionalDBImpl implements Pro return envVarRepo.findOne({ projectId, name: envVar.name, deleted: false }); } - public async addProjectEnvironmentVariable(projectId: string, envVar: ProjectEnvVarWithValue): Promise { + public async addProjectEnvironmentVariable( + projectId: string, + envVar: ProjectEnvVarWithValue, + ): Promise { const envVarRepo = await this.getProjectEnvVarRepo(); - await envVarRepo.save({ + const insertedEnvVar = await envVarRepo.save({ id: uuidv4(), projectId, name: envVar.name, @@ -156,14 +161,31 @@ export class ProjectDBImpl extends TransactionalDBImpl implements Pro creationTime: new Date().toISOString(), deleted: false, }); + return toProjectEnvVar(insertedEnvVar); } public async updateProjectEnvironmentVariable( projectId: string, - envVar: Required, - ): Promise { - const envVarRepo = await this.getProjectEnvVarRepo(); - await envVarRepo.update({ id: envVar.id, projectId }, { value: envVar.value, censored: envVar.censored }); + envVar: Partial, + ): Promise { + if (!envVar.id) { + throw new ApplicationError(ErrorCodes.NOT_FOUND, "An environment variable with this ID could not be found"); + } + + return await this.transaction(async (_, ctx) => { + const envVarRepo = ctx.entityManager.getRepository(DBProjectEnvVar); + + await envVarRepo.update( + { id: envVar.id, projectId }, + filter(envVar, (_, v) => v !== null && v !== undefined), + ); + + const found = await envVarRepo.findOne({ id: envVar.id, projectId, deleted: false }); + if (!found) { + return; + } + return toProjectEnvVar(found); + }); } public async getProjectEnvironmentVariables(projectId: string): Promise { diff --git a/components/gitpod-db/src/typeorm/user-db-impl.ts b/components/gitpod-db/src/typeorm/user-db-impl.ts index cb9c3e9b9ad4d8..21cbb0cecffdfe 100644 --- a/components/gitpod-db/src/typeorm/user-db-impl.ts +++ b/components/gitpod-db/src/typeorm/user-db-impl.ts @@ -50,6 +50,7 @@ import { DataCache } from "../data-cache"; import { TransactionalDBImpl } from "./transactional-db-impl"; import { TypeORM } from "./typeorm"; import { ApplicationError, ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error"; +import { filter } from "../utils"; // OAuth token expiry const tokenExpiryInFuture = new DateInterval("7d"); @@ -395,9 +396,9 @@ export class TypeORMUserDBImpl extends TransactionalDBImpl implements Us }); } - public async addEnvVar(userId: string, envVar: UserEnvVarValue): Promise { + public async addEnvVar(userId: string, envVar: UserEnvVarValue): Promise { const repo = await this.getUserEnvVarRepo(); - await repo.save({ + return await repo.save({ id: uuidv4(), userId, name: envVar.name, @@ -406,15 +407,22 @@ export class TypeORMUserDBImpl extends TransactionalDBImpl implements Us }); } - public async updateEnvVar(userId: string, envVar: Required): Promise { - const repo = await this.getUserEnvVarRepo(); - await repo.update( - { - id: envVar.id, - userId: userId, - }, - { name: envVar.name, repositoryPattern: envVar.repositoryPattern, value: envVar.value }, - ); + public async updateEnvVar(userId: string, envVar: Partial): Promise { + if (!envVar.id) { + throw new ApplicationError(ErrorCodes.NOT_FOUND, "An environment variable with this ID could not be found"); + } + + return await this.transaction(async (_, ctx) => { + const envVarRepo = ctx.entityManager.getRepository(DBUserEnvVar); + + await envVarRepo.update( + { id: envVar.id, userId, deleted: false }, + filter(envVar, (_, v) => v !== null && v !== undefined), + ); + + const found = await envVarRepo.findOne({ id: envVar.id, userId, deleted: false }); + return found; + }); } public async getEnvVars(userId: string): Promise { diff --git a/components/gitpod-db/src/user-db.ts b/components/gitpod-db/src/user-db.ts index 522f82ec16b1d4..680c05eced191d 100644 --- a/components/gitpod-db/src/user-db.ts +++ b/components/gitpod-db/src/user-db.ts @@ -114,8 +114,8 @@ export interface UserDB extends OAuthUserRepository, OAuthTokenRepository, Trans findUsersByEmail(email: string): Promise; findEnvVar(userId: string, envVar: UserEnvVarValue): Promise; - addEnvVar(userId: string, envVar: UserEnvVarValue): Promise; - updateEnvVar(userId: string, envVar: UserEnvVarValue): Promise; + addEnvVar(userId: string, envVar: UserEnvVarValue): Promise; + updateEnvVar(userId: string, envVar: Partial): Promise; deleteEnvVar(envVar: UserEnvVar): Promise; getEnvVars(userId: string): Promise; diff --git a/components/gitpod-db/src/utils.ts b/components/gitpod-db/src/utils.ts new file mode 100644 index 00000000000000..ea3f8306d33681 --- /dev/null +++ b/components/gitpod-db/src/utils.ts @@ -0,0 +1,18 @@ +/** + * Copyright (c) 2023 Gitpod GmbH. All rights reserved. + * Licensed under the GNU Affero General Public License (AGPL). + * See License.AGPL.txt in the project root for license information. + */ + +export function filter( + obj: { [key: string]: any }, + predicate: (key: string, value: any) => boolean, +): { [key: string]: any } { + const result = Object.create({}); // typeorm doesn't like Object.create(null) + for (const [key, value] of Object.entries(obj)) { + if (predicate(key, value)) { + result[key] = value; + } + } + return result; +} diff --git a/components/gitpod-protocol/src/public-api-converter.spec.ts b/components/gitpod-protocol/src/public-api-converter.spec.ts index 5cf57f242391e7..62e808a48b6660 100644 --- a/components/gitpod-protocol/src/public-api-converter.spec.ts +++ b/components/gitpod-protocol/src/public-api-converter.spec.ts @@ -8,6 +8,7 @@ import { Timestamp } from "@bufbuild/protobuf"; import { AdmissionLevel, + WorkspaceEnvironmentVariable, WorkspacePhase_Phase, WorkspacePort_Policy, WorkspacePort_Protocol, @@ -21,12 +22,17 @@ import { PrebuildSettings, WorkspaceSettings, } from "@gitpod/public-api/lib/gitpod/v1/configuration_pb"; -import { AuthProviderEntry, AuthProviderInfo } from "./protocol"; +import { AuthProviderEntry, AuthProviderInfo, ProjectEnvVar, UserEnvVarValue, WithEnvvarsContext } from "./protocol"; import { AuthProvider, AuthProviderDescription, AuthProviderType, } from "@gitpod/public-api/lib/gitpod/v1/authprovider_pb"; +import { + ConfigurationEnvironmentVariable, + EnvironmentVariableAdmission, + UserEnvironmentVariable, +} from "@gitpod/public-api/lib/gitpod/v1/envvar_pb"; describe("PublicAPIConverter", () => { const converter = new PublicAPIConverter(); @@ -808,4 +814,59 @@ describe("PublicAPIConverter", () => { } }); }); + + describe("toWorkspaceEnvironmentVariables", () => { + const wsCtx: WithEnvvarsContext = { + title: "title", + envvars: [ + { + name: "FOO", + value: "bar", + }, + ], + }; + const envVars = [new WorkspaceEnvironmentVariable({ name: "FOO", value: "bar" })]; + it("should convert workspace environment variable types", () => { + const result = converter.toWorkspaceEnvironmentVariables(wsCtx); + expect(result).to.deep.equal(envVars); + }); + }); + + describe("toUserEnvironmentVariable", () => { + const envVar: UserEnvVarValue = { + id: "1", + name: "FOO", + value: "bar", + repositoryPattern: "*/*", + }; + const userEnvVar = new UserEnvironmentVariable({ + id: "1", + name: "FOO", + value: "bar", + repositoryPattern: "*/*", + }); + it("should convert user environment variable types", () => { + const result = converter.toUserEnvironmentVariable(envVar); + expect(result).to.deep.equal(userEnvVar); + }); + }); + + describe("toConfigurationEnvironmentVariable", () => { + const envVar: ProjectEnvVar = { + id: "1", + name: "FOO", + censored: true, + projectId: "1", + }; + const userEnvVar = new ConfigurationEnvironmentVariable({ + id: "1", + name: "FOO", + admission: EnvironmentVariableAdmission.PREBUILD, + configurationId: "1", + }); + it("should convert configuration environment variable types", () => { + const result = converter.toConfigurationEnvironmentVariable(envVar); + expect(result).to.deep.equal(userEnvVar); + }); + }); }); diff --git a/components/gitpod-protocol/src/public-api-converter.ts b/components/gitpod-protocol/src/public-api-converter.ts index 512208c3c48d65..ad99e64da1ccfb 100644 --- a/components/gitpod-protocol/src/public-api-converter.ts +++ b/components/gitpod-protocol/src/public-api-converter.ts @@ -38,6 +38,11 @@ import { WorkspacePort_Protocol, WorkspaceStatus, } from "@gitpod/public-api/lib/gitpod/v1/workspace_pb"; +import { + ConfigurationEnvironmentVariable, + EnvironmentVariableAdmission, + UserEnvironmentVariable, +} from "@gitpod/public-api/lib/gitpod/v1/envvar_pb"; import { ContextURL } from "./context-url"; import { ApplicationError, ErrorCode, ErrorCodes } from "./messaging/error"; import { @@ -50,6 +55,8 @@ import { WithPrebuild, WorkspaceContext, WorkspaceInfo, + UserEnvVarValue, + ProjectEnvVar, } from "./protocol"; import { OrgMemberInfo, @@ -104,7 +111,7 @@ export class PublicAPIConverter { status.admission = this.toAdmission(arg.workspace.shareable); status.gitStatus = this.toGitStatus(arg.workspace); workspace.status = status; - workspace.additionalEnvironmentVariables = this.toEnvironmentVariables(arg.workspace.context); + workspace.additionalEnvironmentVariables = this.toWorkspaceEnvironmentVariables(arg.workspace.context); if (arg.latestInstance) { return this.toWorkspace(arg.latestInstance, workspace); @@ -228,17 +235,37 @@ export class PublicAPIConverter { return new ApplicationError(code, reason.message, new TrustedValue(data)); } - toEnvironmentVariables(context: WorkspaceContext): WorkspaceEnvironmentVariable[] { + toWorkspaceEnvironmentVariables(context: WorkspaceContext): WorkspaceEnvironmentVariable[] { if (WithEnvvarsContext.is(context)) { - return context.envvars.map((envvar) => this.toEnvironmentVariable(envvar)); + return context.envvars.map((envvar) => this.toWorkspaceEnvironmentVariable(envvar)); } return []; } - toEnvironmentVariable(envVar: EnvVarWithValue): WorkspaceEnvironmentVariable { + toWorkspaceEnvironmentVariable(envVar: EnvVarWithValue): WorkspaceEnvironmentVariable { const result = new WorkspaceEnvironmentVariable(); result.name = envVar.name; - envVar.value = envVar.value; + result.value = envVar.value; + return result; + } + + toUserEnvironmentVariable(envVar: UserEnvVarValue): UserEnvironmentVariable { + const result = new UserEnvironmentVariable(); + result.id = envVar.id || ""; + result.name = envVar.name; + result.value = envVar.value; + result.repositoryPattern = envVar.repositoryPattern; + return result; + } + + toConfigurationEnvironmentVariable(envVar: ProjectEnvVar): ConfigurationEnvironmentVariable { + const result = new ConfigurationEnvironmentVariable(); + result.id = envVar.id || ""; + result.name = envVar.name; + result.configurationId = envVar.projectId; + result.admission = envVar.censored + ? EnvironmentVariableAdmission.PREBUILD + : EnvironmentVariableAdmission.EVERYWHERE; return result; } diff --git a/components/server/src/api/envvar-service-api.ts b/components/server/src/api/envvar-service-api.ts new file mode 100644 index 00000000000000..33bbbc63c2eb2e --- /dev/null +++ b/components/server/src/api/envvar-service-api.ts @@ -0,0 +1,224 @@ +/** + * Copyright (c) 2023 Gitpod GmbH. All rights reserved. + * Licensed under the GNU Affero General Public License (AGPL). + * See License.AGPL.txt in the project root for license information. + */ + +import { HandlerContext, ServiceImpl } from "@connectrpc/connect"; +import { EnvironmentVariableService as EnvironmentVariableServiceInterface } from "@gitpod/public-api/lib/gitpod/v1/envvar_connect"; +import { + ListUserEnvironmentVariablesRequest, + ListUserEnvironmentVariablesResponse, + UpdateUserEnvironmentVariableRequest, + UpdateUserEnvironmentVariableResponse, + DeleteUserEnvironmentVariableRequest, + DeleteUserEnvironmentVariableResponse, + CreateUserEnvironmentVariableRequest, + CreateUserEnvironmentVariableResponse, + ListConfigurationEnvironmentVariablesRequest, + ListConfigurationEnvironmentVariablesResponse, + UpdateConfigurationEnvironmentVariableRequest, + UpdateConfigurationEnvironmentVariableResponse, + EnvironmentVariableAdmission, + CreateConfigurationEnvironmentVariableRequest, + CreateConfigurationEnvironmentVariableResponse, + DeleteConfigurationEnvironmentVariableRequest, + DeleteConfigurationEnvironmentVariableResponse, + ResolveWorkspaceEnvironmentVariablesResponse, + ResolveWorkspaceEnvironmentVariablesRequest, + ResolveWorkspaceEnvironmentVariablesResponse_EnvironmentVariable, +} from "@gitpod/public-api/lib/gitpod/v1/envvar_pb"; +import { inject, injectable } from "inversify"; +import { EnvVarService } from "../user/env-var-service"; +import { PublicAPIConverter } from "@gitpod/gitpod-protocol/lib/public-api-converter"; +import { ProjectEnvVarWithValue, UserEnvVarValue } from "@gitpod/gitpod-protocol"; +import { WorkspaceService } from "../workspace/workspace-service"; +import { ctxUserId } from "../util/request-context"; +import { validate as uuidValidate } from "uuid"; +import { ApplicationError, ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error"; + +@injectable() +export class EnvironmentVariableServiceAPI implements ServiceImpl { + @inject(EnvVarService) + private readonly envVarService: EnvVarService; + + @inject(WorkspaceService) + private readonly workspaceService: WorkspaceService; + + @inject(PublicAPIConverter) + private readonly apiConverter: PublicAPIConverter; + + async listUserEnvironmentVariables( + req: ListUserEnvironmentVariablesRequest, + _: HandlerContext, + ): Promise { + const response = new ListUserEnvironmentVariablesResponse(); + const userEnvVars = await this.envVarService.listUserEnvVars(ctxUserId(), ctxUserId()); + response.environmentVariables = userEnvVars.map((i) => this.apiConverter.toUserEnvironmentVariable(i)); + + return response; + } + + async updateUserEnvironmentVariable( + req: UpdateUserEnvironmentVariableRequest, + _: HandlerContext, + ): Promise { + if (!uuidValidate(req.envVarId)) { + throw new ApplicationError(ErrorCodes.BAD_REQUEST, "envVarId is required"); + } + + const response = new UpdateUserEnvironmentVariableResponse(); + + const userEnvVars = await this.envVarService.listUserEnvVars(ctxUserId(), ctxUserId()); + const userEnvVarfound = userEnvVars.find((i) => i.id === req.envVarId); + if (userEnvVarfound) { + const variable: UserEnvVarValue = { + id: req.envVarId, + name: req.name ?? userEnvVarfound.name, + value: req.value ?? userEnvVarfound.value, + repositoryPattern: req.repositoryPattern ?? userEnvVarfound.repositoryPattern, + }; + + const updatedUsertEnvVar = await this.envVarService.updateUserEnvVar(ctxUserId(), ctxUserId(), variable); + + response.environmentVariable = this.apiConverter.toUserEnvironmentVariable(updatedUsertEnvVar); + return response; + } + + throw new ApplicationError(ErrorCodes.NOT_FOUND, "env variable not found"); + } + + async createUserEnvironmentVariable( + req: CreateUserEnvironmentVariableRequest, + _: HandlerContext, + ): Promise { + const response = new CreateUserEnvironmentVariableResponse(); + + const variable: UserEnvVarValue = { + name: req.name, + value: req.value, + repositoryPattern: req.repositoryPattern, + }; + + const result = await this.envVarService.addUserEnvVar(ctxUserId(), ctxUserId(), variable); + response.environmentVariable = this.apiConverter.toUserEnvironmentVariable(result); + + return response; + } + + async deleteUserEnvironmentVariable( + req: DeleteUserEnvironmentVariableRequest, + _: HandlerContext, + ): Promise { + if (!uuidValidate(req.envVarId)) { + throw new ApplicationError(ErrorCodes.BAD_REQUEST, "envVarId is required"); + } + + const variable: UserEnvVarValue = { + id: req.envVarId, + name: "", + value: "", + repositoryPattern: "", + }; + + await this.envVarService.deleteUserEnvVar(ctxUserId(), ctxUserId(), variable); + + const response = new DeleteUserEnvironmentVariableResponse(); + return response; + } + + async listConfigurationEnvironmentVariables( + req: ListConfigurationEnvironmentVariablesRequest, + _: HandlerContext, + ): Promise { + if (!uuidValidate(req.configurationId)) { + throw new ApplicationError(ErrorCodes.BAD_REQUEST, "configurationId is required"); + } + + const response = new ListConfigurationEnvironmentVariablesResponse(); + const projectEnvVars = await this.envVarService.listProjectEnvVars(ctxUserId(), req.configurationId); + response.environmentVariables = projectEnvVars.map((i) => + this.apiConverter.toConfigurationEnvironmentVariable(i), + ); + + return response; + } + + async updateConfigurationEnvironmentVariable( + req: UpdateConfigurationEnvironmentVariableRequest, + _: HandlerContext, + ): Promise { + if (!uuidValidate(req.configurationId)) { + throw new ApplicationError(ErrorCodes.BAD_REQUEST, "configurationId is required"); + } + if (!uuidValidate(req.envVarId)) { + throw new ApplicationError(ErrorCodes.BAD_REQUEST, "envVarId is required"); + } + + const updatedProjectEnvVar = await this.envVarService.updateProjectEnvVar(ctxUserId(), req.configurationId, { + id: req.envVarId, + censored: req.admission + ? req.admission === EnvironmentVariableAdmission.PREBUILD + ? true + : false + : undefined, + }); + + const response = new UpdateConfigurationEnvironmentVariableResponse(); + response.environmentVariable = this.apiConverter.toConfigurationEnvironmentVariable(updatedProjectEnvVar); + return response; + } + + async createConfigurationEnvironmentVariable( + req: CreateConfigurationEnvironmentVariableRequest, + _: HandlerContext, + ): Promise { + const variable: ProjectEnvVarWithValue = { + name: req.name, + value: req.value, + censored: req.admission === EnvironmentVariableAdmission.PREBUILD ? true : false, + }; + + const result = await this.envVarService.addProjectEnvVar(ctxUserId(), req.configurationId, variable); + + const response = new CreateConfigurationEnvironmentVariableResponse(); + response.environmentVariable = this.apiConverter.toConfigurationEnvironmentVariable(result); + return response; + } + + async deleteConfigurationEnvironmentVariable( + req: DeleteConfigurationEnvironmentVariableRequest, + _: HandlerContext, + ): Promise { + if (!uuidValidate(req.envVarId)) { + throw new ApplicationError(ErrorCodes.BAD_REQUEST, "envVarId is required"); + } + + await this.envVarService.deleteProjectEnvVar(ctxUserId(), req.envVarId); + + const response = new DeleteConfigurationEnvironmentVariableResponse(); + return response; + } + + async resolveWorkspaceEnvironmentVariables( + req: ResolveWorkspaceEnvironmentVariablesRequest, + _: HandlerContext, + ): Promise { + const response = new ResolveWorkspaceEnvironmentVariablesResponse(); + + const { workspace } = await this.workspaceService.getWorkspace(ctxUserId(), req.workspaceId); + const envVars = await this.envVarService.resolveEnvVariables( + workspace.ownerId, + workspace.projectId, + workspace.type, + workspace.context, + ); + + response.environmentVariables = envVars.workspace.map( + (i) => + new ResolveWorkspaceEnvironmentVariablesResponse_EnvironmentVariable({ name: i.name, value: i.value }), + ); + + return response; + } +} diff --git a/components/server/src/api/server.ts b/components/server/src/api/server.ts index eb4910b6c07fcf..62f1c7c4849924 100644 --- a/components/server/src/api/server.ts +++ b/components/server/src/api/server.ts @@ -18,6 +18,7 @@ import { OrganizationService } from "@gitpod/public-api/lib/gitpod/v1/organizati import { WorkspaceService } from "@gitpod/public-api/lib/gitpod/v1/workspace_connect"; import { ConfigurationService } from "@gitpod/public-api/lib/gitpod/v1/configuration_connect"; import { AuthProviderService } from "@gitpod/public-api/lib/gitpod/v1/authprovider_connect"; +import { EnvironmentVariableService } from "@gitpod/public-api/lib/gitpod/v1/envvar_connect"; import express from "express"; import * as http from "http"; import { decorate, inject, injectable, interfaces } from "inversify"; @@ -46,6 +47,7 @@ import { APIUserService as UserServiceAPI } from "./user"; import { WorkspaceServiceAPI } from "./workspace-service-api"; import { ConfigurationServiceAPI } from "./configuration-service-api"; import { AuthProviderServiceAPI } from "./auth-provider-service-api"; +import { EnvironmentVariableServiceAPI } from "./envvar-service-api"; import { Unauthenticated } from "./unauthenticated"; import { SubjectId } from "../auth/subject-id"; import { BearerAuth } from "../auth/bearer-authenticator"; @@ -64,6 +66,7 @@ export class API { @inject(OrganizationServiceAPI) private readonly organizationServiceApi: OrganizationServiceAPI; @inject(ConfigurationServiceAPI) private readonly configurationServiceApi: ConfigurationServiceAPI; @inject(AuthProviderServiceAPI) private readonly authProviderServiceApi: AuthProviderServiceAPI; + @inject(EnvironmentVariableServiceAPI) private readonly envvarServiceApi: EnvironmentVariableServiceAPI; @inject(StatsServiceAPI) private readonly tatsServiceApi: StatsServiceAPI; @inject(HelloServiceAPI) private readonly helloServiceApi: HelloServiceAPI; @inject(SessionHandler) private readonly sessionHandler: SessionHandler; @@ -117,6 +120,7 @@ export class API { service(OrganizationService, this.organizationServiceApi), service(ConfigurationService, this.configurationServiceApi), service(AuthProviderService, this.authProviderServiceApi), + service(EnvironmentVariableService, this.envvarServiceApi), ]) { router.service(type, new Proxy(impl, this.interceptService(type))); } @@ -367,6 +371,7 @@ export class API { bind(OrganizationServiceAPI).toSelf().inSingletonScope(); bind(ConfigurationServiceAPI).toSelf().inSingletonScope(); bind(AuthProviderServiceAPI).toSelf().inSingletonScope(); + bind(EnvironmentVariableServiceAPI).toSelf().inSingletonScope(); bind(StatsServiceAPI).toSelf().inSingletonScope(); bind(API).toSelf().inSingletonScope(); } diff --git a/components/server/src/api/teams.spec.db.ts b/components/server/src/api/teams.spec.db.ts index 07b51c8884f8cc..98469f1f2278ab 100644 --- a/components/server/src/api/teams.spec.db.ts +++ b/components/server/src/api/teams.spec.db.ts @@ -28,6 +28,7 @@ import { OrganizationService } from "../orgs/organization-service"; import { ProjectsService } from "../projects/projects-service"; import { AuthProviderService } from "../auth/auth-provider-service"; import { BearerAuth } from "../auth/bearer-authenticator"; +import { EnvVarService } from "../user/env-var-service"; const expect = chai.expect; @@ -53,6 +54,7 @@ export class APITeamsServiceSpec { this.container.bind(UserService).toConstantValue({} as UserService); this.container.bind(ProjectsService).toConstantValue({} as ProjectsService); this.container.bind(AuthProviderService).toConstantValue({} as AuthProviderService); + this.container.bind(EnvVarService).toConstantValue({} as EnvVarService); // Clean-up database const typeorm = testContainer.get(TypeORM); diff --git a/components/server/src/user/env-var-service.spec.db.ts b/components/server/src/user/env-var-service.spec.db.ts index b2e881d3777aa8..731e41d889a343 100644 --- a/components/server/src/user/env-var-service.spec.db.ts +++ b/components/server/src/user/env-var-service.spec.db.ts @@ -142,7 +142,11 @@ describe("EnvVarService", async () => { const resp1 = await es.listUserEnvVars(member.id, member.id); expect(resp1.length).to.equal(0); - await es.addUserEnvVar(member.id, member.id, { name: "var1", value: "foo", repositoryPattern: "*/*" }); + const added1 = await es.addUserEnvVar(member.id, member.id, { + name: "var1", + value: "foo", + repositoryPattern: "*/*", + }); const resp2 = await es.listUserEnvVars(member.id, member.id); expect(resp2.length).to.equal(1); @@ -152,14 +156,23 @@ describe("EnvVarService", async () => { es.addUserEnvVar(member.id, member.id, { name: "var1", value: "foo2", repositoryPattern: "*/*" }), ); - await es.updateUserEnvVar(member.id, member.id, { name: "var1", value: "foo2", repositoryPattern: "*/*" }); + await es.updateUserEnvVar(member.id, member.id, { + ...added1, + name: "var1", + value: "foo2", + repositoryPattern: "*/*", + }); const resp3 = await es.listUserEnvVars(member.id, member.id); expect(resp3.length).to.equal(1); await expectError( - ErrorCodes.BAD_REQUEST, - es.updateUserEnvVar(member.id, member.id, { name: "var2", value: "foo2", repositoryPattern: "*/*" }), + ErrorCodes.NOT_FOUND, + es.updateUserEnvVar(member.id, member.id, { + name: "var2", + value: "foo2", + repositoryPattern: "*/*", + }), ); await expectError(ErrorCodes.NOT_FOUND, es.listUserEnvVars(stranger.id, member.id)); @@ -197,15 +210,15 @@ describe("EnvVarService", async () => { }); it("should let owners create, update, delete and get project env vars", async () => { - await es.addProjectEnvVar(owner.id, project.id, { name: "FOO", value: "BAR", censored: false }); + const added1 = await es.addProjectEnvVar(owner.id, project.id, { name: "FOO", value: "BAR", censored: false }); await expectError( ErrorCodes.BAD_REQUEST, es.addProjectEnvVar(owner.id, project.id, { name: "FOO", value: "BAR2", censored: false }), ); - await es.updateProjectEnvVar(owner.id, project.id, { name: "FOO", value: "BAR2", censored: false }); + await es.updateProjectEnvVar(owner.id, project.id, { ...added1, name: "FOO", value: "BAR2", censored: false }); await expectError( - ErrorCodes.BAD_REQUEST, + ErrorCodes.NOT_FOUND, es.updateProjectEnvVar(owner.id, project.id, { name: "FOO2", value: "BAR", censored: false }), ); diff --git a/components/server/src/user/env-var-service.ts b/components/server/src/user/env-var-service.ts index 0d865617b2568e..48bf3c0ab660d8 100644 --- a/components/server/src/user/env-var-service.ts +++ b/components/server/src/user/env-var-service.ts @@ -65,7 +65,7 @@ export class EnvVarService { userId: string, variable: UserEnvVarValue, oldPermissionCheck?: (envvar: UserEnvVar) => Promise, // @deprecated - ): Promise { + ): Promise { await this.auth.checkPermissionOnUser(requestorId, "write_env_var", userId); const validationError = UserEnvVar.validate(variable); if (validationError) { @@ -93,8 +93,7 @@ export class EnvVarService { } this.analytics.track({ event: "envvar-set", userId }); - // Ensure id is not set so a new variable is created - await this.userDB.addEnvVar(userId, variable); + return await this.userDB.addEnvVar(userId, variable); } async updateUserEnvVar( @@ -102,7 +101,7 @@ export class EnvVarService { userId: string, variable: UserEnvVarValue, oldPermissionCheck?: (envvar: UserEnvVar) => Promise, // @deprecated - ): Promise { + ): Promise { await this.auth.checkPermissionOnUser(requestorId, "write_env_var", userId); const validationError = UserEnvVar.validate(variable); if (validationError) { @@ -111,18 +110,17 @@ export class EnvVarService { variable.repositoryPattern = UserEnvVar.normalizeRepoPattern(variable.repositoryPattern); - const existingVar = await this.userDB.findEnvVar(userId, variable); - if (!existingVar) { - throw new ApplicationError(ErrorCodes.BAD_REQUEST, `Env var ${variable.name} does not exists`); - } - if (oldPermissionCheck) { - await oldPermissionCheck({ ...variable, userId, id: existingVar.id }); + await oldPermissionCheck({ ...variable, userId, id: variable.id! }); } this.analytics.track({ event: "envvar-set", userId }); - // overwrite existing variable id rather than introduce a duplicate - await this.userDB.updateEnvVar(userId, { ...variable, id: existingVar.id }); + const result = await this.userDB.updateEnvVar(userId, variable); + if (!result) { + throw new ApplicationError(ErrorCodes.BAD_REQUEST, `Env var ${variable.name} does not exists`); + } + + return result; } async deleteUserEnvVar( @@ -177,7 +175,11 @@ export class EnvVarService { return result; } - async addProjectEnvVar(requestorId: string, projectId: string, envVar: ProjectEnvVarWithValue): Promise { + async addProjectEnvVar( + requestorId: string, + projectId: string, + envVar: ProjectEnvVarWithValue, + ): Promise { await this.auth.checkPermissionOnProject(requestorId, "write_env_var", projectId); if (!envVar.name) { @@ -195,10 +197,14 @@ export class EnvVarService { throw new ApplicationError(ErrorCodes.BAD_REQUEST, `Project env var ${envVar.name} already exists`); } - return this.projectDB.addProjectEnvironmentVariable(projectId, envVar); + return await this.projectDB.addProjectEnvironmentVariable(projectId, envVar); } - async updateProjectEnvVar(requestorId: string, projectId: string, envVar: ProjectEnvVarWithValue): Promise { + async updateProjectEnvVar( + requestorId: string, + projectId: string, + envVar: Partial, + ): Promise { await this.auth.checkPermissionOnProject(requestorId, "write_env_var", projectId); if (!envVar.name) { @@ -211,12 +217,12 @@ export class EnvVarService { ); } - const existingVar = await this.projectDB.findProjectEnvironmentVariable(projectId, envVar); - if (!existingVar) { + const result = await this.projectDB.updateProjectEnvironmentVariable(projectId, envVar); + if (!result) { throw new ApplicationError(ErrorCodes.BAD_REQUEST, `Project env var ${envVar.name} does not exists`); } - return this.projectDB.updateProjectEnvironmentVariable(projectId, { ...envVar, id: existingVar.id! }); + return result; } async deleteProjectEnvVar(requestorId: string, variableId: string): Promise { diff --git a/components/server/src/workspace/gitpod-server-impl.ts b/components/server/src/workspace/gitpod-server-impl.ts index 7b7effec4ab35d..69f7cd65e92c2c 100644 --- a/components/server/src/workspace/gitpod-server-impl.ts +++ b/components/server/src/workspace/gitpod-server-impl.ts @@ -1830,11 +1830,11 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { const user = await this.checkAndBlockUser("setEnvVar"); const userEnvVars = await this.envVarService.listUserEnvVars(user.id, user.id); if (userEnvVars.find((v) => v.name == variable.name && v.repositoryPattern == variable.repositoryPattern)) { - return this.envVarService.updateUserEnvVar(user.id, user.id, variable, (envvar: UserEnvVar) => { + await this.envVarService.updateUserEnvVar(user.id, user.id, variable, (envvar: UserEnvVar) => { return this.guardAccess({ kind: "envVar", subject: envvar }, "update"); }); } else { - return this.envVarService.addUserEnvVar(user.id, user.id, variable, (envvar: UserEnvVar) => { + await this.envVarService.addUserEnvVar(user.id, user.id, variable, (envvar: UserEnvVar) => { return this.guardAccess({ kind: "envVar", subject: envvar }, "create"); }); } @@ -1882,9 +1882,9 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { await this.guardProjectOperation(user, projectId, "update"); const envVars = await this.envVarService.listProjectEnvVars(user.id, projectId); if (envVars.find((v) => v.name === name)) { - return this.envVarService.updateProjectEnvVar(user.id, projectId, { name, value, censored }); + await this.envVarService.updateProjectEnvVar(user.id, projectId, { name, value, censored }); } else { - return this.envVarService.addProjectEnvVar(user.id, projectId, { name, value, censored }); + await this.envVarService.addProjectEnvVar(user.id, projectId, { name, value, censored }); } }