From b2260d021e89ccee8d0b97e430ff4dd6070a9f6c Mon Sep 17 00:00:00 2001 From: Alex Tugarev Date: Thu, 5 Oct 2023 16:37:13 +0200 Subject: [PATCH] =?UTF-8?q?Migrate=20Prebuild=20settings=20=E2=80=93=20EXP?= =?UTF-8?q?-672=20(#18842)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [protocol] Add PrebuildSettings to ProjectSettings * add migration function * add migration function and test * update protocol helper functions * update prebuild manager * use new prebuild setting in frontend * update papi * fix papi test * rename var * fix papi mapping * deprecation note * update migration function * fix prebuild-manager * fixup server * fix Enable Prebuilds on Project Settings * Use Project.getPrebuildSettings and * fix countUnabortedPrebuildsSince * improve usage of default settings * fix Build Branches select * fix papi conversion 🤯 * don't save empty pattern strings * fix papi-server conversion and align naming of prebuild settings * fix getPrebuildSettings * [prebuilds] changed prebuild semantics - no more incremental prebuilds - always incremental workspace - never wait for running prebuilds --------- Co-authored-by: svenefftinge --- .../dashboard/src/admin/ProjectDetail.tsx | 3 - .../src/projects/ProjectListItem.tsx | 2 +- .../src/projects/ProjectSettings.tsx | 268 ++++++++-------- .../dashboard/src/service/public-api.ts | 15 +- .../src/workspaces/CreateWorkspacePage.tsx | 1 - .../src/typeorm/workspace-db-impl.ts | 2 +- .../gitpod-protocol/go/gitpod-service.go | 37 +-- .../gitpod-protocol/src/gitpod-service.ts | 2 - components/gitpod-protocol/src/protocol.ts | 12 +- .../src/teams-projects-protocol.ts | 113 ++++--- .../public-api-server/pkg/apiv1/project.go | 24 +- .../pkg/apiv1/project_test.go | 10 +- .../gitpod/experimental/v1/projects.proto | 15 +- .../go/experimental/v1/projects.pb.go | 291 ++++++++---------- .../src/gitpod/experimental/v1/projects_pb.ts | 40 +-- components/server/src/config.ts | 5 - components/server/src/container-module.ts | 4 +- components/server/src/prebuilds/github-app.ts | 23 +- .../src/prebuilds/github-enterprise-app.ts | 7 +- ...ce.ts => incremental-workspace-service.ts} | 18 +- .../src/prebuilds/prebuild-manager.spec.ts | 102 +++--- .../server/src/prebuilds/prebuild-manager.ts | 111 ++----- .../src/projects/projects-service.spec.db.ts | 175 +++++++++-- .../server/src/projects/projects-service.ts | 107 +++++-- .../src/workspace/gitpod-server-impl.ts | 208 ++----------- .../server/src/workspace/workspace-factory.ts | 86 +----- .../server/src/workspace/workspace-starter.ts | 5 +- test/pkg/integration/workspace.go | 1 - test/tests/components/server/server_test.go | 1 - 29 files changed, 773 insertions(+), 915 deletions(-) rename components/server/src/prebuilds/{incremental-prebuilds-service.ts => incremental-workspace-service.ts} (92%) diff --git a/components/dashboard/src/admin/ProjectDetail.tsx b/components/dashboard/src/admin/ProjectDetail.tsx index c72d512eaaafa5..ab68d6926ba331 100644 --- a/components/dashboard/src/admin/ProjectDetail.tsx +++ b/components/dashboard/src/admin/ProjectDetail.tsx @@ -45,9 +45,6 @@ export default function ProjectDetail(props: { project: Project; owner: string |
- - {props.project.settings?.useIncrementalPrebuilds ? "Yes" : "No"} - {props.project.markedDeleted ? "Yes" : "No"}
diff --git a/components/dashboard/src/projects/ProjectListItem.tsx b/components/dashboard/src/projects/ProjectListItem.tsx index b001b3157a1941..c0f3298af25079 100644 --- a/components/dashboard/src/projects/ProjectListItem.tsx +++ b/components/dashboard/src/projects/ProjectListItem.tsx @@ -25,7 +25,7 @@ export const ProjectListItem: FunctionComponent = ({ proje const [showRemoveModal, setShowRemoveModal] = useState(false); const { data: prebuild, isLoading } = useLatestProjectPrebuildQuery({ projectId: project.id }); - const enablePrebuilds = Project.isPrebuildsEnabled(project); + const enablePrebuilds = Project.getPrebuildSettings(project).enable; return (
diff --git a/components/dashboard/src/projects/ProjectSettings.tsx b/components/dashboard/src/projects/ProjectSettings.tsx index 132874511d0db3..ff5a1798dd4bb6 100644 --- a/components/dashboard/src/projects/ProjectSettings.tsx +++ b/components/dashboard/src/projects/ProjectSettings.tsx @@ -4,12 +4,11 @@ * See License.AGPL.txt in the project root for license information. */ -import { Project, ProjectSettings } from "@gitpod/gitpod-protocol"; +import { PrebuildSettings, Project, ProjectSettings } from "@gitpod/gitpod-protocol"; import { useCallback, useContext, useState, Fragment, useMemo, useEffect } from "react"; import { useHistory } from "react-router"; import { CheckboxInputField } from "../components/forms/CheckboxInputField"; import { PageWithSubMenu } from "../components/PageWithSubMenu"; -import PillLabel from "../components/PillLabel"; import { getGitpodService } from "../service/service"; import { ProjectContext, useCurrentProject } from "./project-context"; import { getProjectSettingsMenu, getProjectTabs } from "./projects.routes"; @@ -63,7 +62,7 @@ export default function ProjectSettingsView() { if (!project) { return; } - setPrebuildBranchPattern(project?.settings?.prebuildBranchPattern || ""); + setPrebuildBranchPattern(project?.settings?.prebuilds?.branchMatchingPattern || ""); }, [project]); const setProjectName = useCallback( @@ -105,27 +104,37 @@ export default function ProjectSettingsView() { [project, setProject, toast, projectName], ); + const setPrebuildsEnabled = useCallback( + async (value: boolean) => { + if (!project) { + return; + } + + await updateProjectSettings({ + prebuilds: { + ...project.settings?.prebuilds, + enable: value, + }, + }); + }, + [project, updateProjectSettings], + ); + const setPrebuildBranchStrategy = useCallback( - async (value: ProjectSettings.PrebuildBranchStrategy) => { + async (value: PrebuildSettings.BranchStrategy) => { if (!project) { return; } - const oldValue = Project.getPrebuildBranchStrategy(project); + const oldValue = Project.getPrebuildSettings(project).branchStrategy; if (oldValue === value) { return; } - const update: ProjectSettings = {}; - if (value === "defaultBranch") { - update.prebuildDefaultBranchOnly = true; - update.prebuildBranchPattern = ""; - } - if (value === "allBranches") { - update.prebuildDefaultBranchOnly = false; - update.prebuildBranchPattern = ""; - } - if (value === "selectedBranches") { - update.prebuildDefaultBranchOnly = false; - update.prebuildBranchPattern = "**"; + const update: ProjectSettings = { ...project.settings }; + update.prebuilds = { ...update.prebuilds }; + update.prebuilds.branchStrategy = value; + + if (value === "matched-branches") { + update.prebuilds.branchMatchingPattern = update.prebuilds.branchMatchingPattern || "**"; } await updateProjectSettings(update); }, @@ -133,13 +142,23 @@ export default function ProjectSettingsView() { ); const debouncedUpdatePrebuildBranchPattern = useMemo(() => { - return debounce(async (prebuildBranchPattern) => { - await updateProjectSettings({ prebuildBranchPattern }); + return debounce(async (prebuildBranchPattern: string) => { + if (!project) { + return; + } + const update: ProjectSettings = { ...project.settings }; + update.prebuilds = { ...update.prebuilds }; + update.prebuilds.branchMatchingPattern = prebuildBranchPattern; + + await updateProjectSettings(update); }, 1500); - }, [updateProjectSettings]); + }, [updateProjectSettings, project]); const updatePrebuildBranchPattern = useCallback( async (value: string) => { + if (!value) { + return; + } setPrebuildBranchPattern(value); debouncedUpdatePrebuildBranchPattern(value); @@ -162,11 +181,28 @@ export default function ProjectSettingsView() { const setWorkspaceClassForPrebuild = useCallback( async (value: string) => { if (!project) { - return value; + return; } - const before = project.settings?.workspaceClasses?.prebuild; - updateProjectSettings({ workspaceClasses: { ...project.settings?.workspaceClasses, prebuild: value } }); - return before; + const update: ProjectSettings = { ...project.settings }; + update.prebuilds = { ...update.prebuilds }; + update.prebuilds.workspaceClass = value; + + await updateProjectSettings(update); + }, + [project, updateProjectSettings], + ); + + const setPrebuildInterval = useCallback( + async (value: string) => { + if (!project) { + return; + } + const newInterval = Math.abs(Math.min(Number.parseInt(value), 100)) || 0; + const update: ProjectSettings = { ...project.settings }; + update.prebuilds = { ...update.prebuilds }; + update.prebuilds.prebuildInterval = newInterval; + + await updateProjectSettings(update); }, [project, updateProjectSettings], ); @@ -178,9 +214,7 @@ export default function ProjectSettingsView() { // TODO: Render a generic error screen for when an entity isn't found if (!project) return null; - const enablePrebuilds = Project.isPrebuildsEnabled(project); - - const prebuildBranchStrategy = Project.getPrebuildBranchStrategy(project); + const prebuildSettings = Project.getPrebuildSettings(project); return ( @@ -203,13 +237,12 @@ export default function ProjectSettingsView() { label="Enable prebuilds" hint={ - {enablePrebuilds ? ( - - Prebuilds will run for any before or init tasks. - - ) : ( - "Requires permissions to configure repository webhooks." - )}{" "} + + Prebuilds reduce wait time for new workspaces. + {!prebuildSettings.enable + ? " Enabling requires permissions to configure repository webhooks." + : ""} + {" "} } - checked={enablePrebuilds} - onChange={(checked) => updateProjectSettings({ enablePrebuilds: checked })} + checked={!!prebuildSettings.enable} + onChange={setPrebuildsEnabled} /> - {enablePrebuilds && ( + {prebuildSettings.enable && ( <> - setPrebuildBranchStrategy(val as ProjectSettings.PrebuildBranchStrategy)} - > - - - - - {prebuildBranchStrategy === "selectedBranches" && ( +
+
+ + setPrebuildInterval(target.value)} + /> +
+ The number of commits to be skipped between prebuild runs. +
+
+
+
+ setPrebuildBranchStrategy(val as PrebuildSettings.BranchStrategy)} + > + + + + +
+ Run prebuilds on the selected branches only. +
+
+ {prebuildSettings.branchStrategy === "matched-branches" && (
- Learn more - - - } - disabled={!enablePrebuilds} - checked={project.settings?.useIncrementalPrebuilds ?? false} - onChange={(checked) => updateProjectSettings({ useIncrementalPrebuilds: checked })} - /> - updateProjectSettings({ keepOutdatedPrebuildsRunning: !checked })} - /> - - Use Last Successful Prebuild{" "} - - Alpha - - - } - hint="Skip waiting for prebuilds in progress and use the last successful prebuild from previous - commits on the same branch." - disabled={!enablePrebuilds} - checked={!!project.settings?.allowUsingPreviousPrebuilds} - onChange={(checked) => - updateProjectSettings({ - allowUsingPreviousPrebuilds: checked, - // we are disabling prebuild cancellation when incremental workspaces are enabled - keepOutdatedPrebuildsRunning: - checked || project?.settings?.keepOutdatedPrebuildsRunning, - }) - } - /> -
-
- - - updateProjectSettings({ - prebuildEveryNthCommit: - Math.abs(Math.min(Number.parseInt(target.value), 100)) || 0, - }) - } - /> -
- The number of commits that are skipped between prebuilds. -
+
+ Use a smaller machine type for cost optimization.
-
+ )}
diff --git a/components/dashboard/src/service/public-api.ts b/components/dashboard/src/service/public-api.ts index 924fc2e78343fd..e416363a475aab 100644 --- a/components/dashboard/src/service/public-api.ts +++ b/components/dashboard/src/service/public-api.ts @@ -107,17 +107,16 @@ export function projectToProtocol(project: Project): ProtocolProject { teamId: project.teamId, appInstallationId: "undefined", settings: { - enablePrebuilds: project.settings?.prebuild?.enablePrebuilds, - prebuildDefaultBranchOnly: project.settings?.prebuild?.prebuildDefaultBranchOnly, - prebuildBranchPattern: project.settings?.prebuild?.prebuildBranchPattern, - allowUsingPreviousPrebuilds: project.settings?.prebuild?.usePreviousPrebuilds, - keepOutdatedPrebuildsRunning: project.settings?.prebuild?.keepOutdatedPrebuildsRunning, - prebuildEveryNthCommit: project.settings?.prebuild?.prebuildEveryNth, - useIncrementalPrebuilds: project.settings?.prebuild?.enableIncrementalPrebuilds, workspaceClasses: { - prebuild: project.settings?.workspace?.workspaceClass?.prebuild || "", regular: project.settings?.workspace?.workspaceClass?.regular || "", }, + prebuilds: { + enable: project.settings?.prebuild?.enablePrebuilds, + branchStrategy: project.settings?.prebuild?.branchStrategy as any, + branchMatchingPattern: project.settings?.prebuild?.branchMatchingPattern, + prebuildInterval: project.settings?.prebuild?.prebuildInterval, + workspaceClass: project.settings?.prebuild?.workspaceClass, + }, }, }; } diff --git a/components/dashboard/src/workspaces/CreateWorkspacePage.tsx b/components/dashboard/src/workspaces/CreateWorkspacePage.tsx index d64dbbb52fc866..4ece679127efd8 100644 --- a/components/dashboard/src/workspaces/CreateWorkspacePage.tsx +++ b/components/dashboard/src/workspaces/CreateWorkspacePage.tsx @@ -172,7 +172,6 @@ export function CreateWorkspacePage() { // we already have shown running workspaces to the user opts.ignoreRunningWorkspaceOnSameCommit = true; - opts.ignoreRunningPrebuild = true; // if user received an INVALID_GITPOD_YML yml for their contextURL they can choose to proceed using default configuration if (workspaceContext.error?.code === ErrorCodes.INVALID_GITPOD_YML) { diff --git a/components/gitpod-db/src/typeorm/workspace-db-impl.ts b/components/gitpod-db/src/typeorm/workspace-db-impl.ts index 1485e7e7df8498..7c4ced087ac406 100644 --- a/components/gitpod-db/src/typeorm/workspace-db-impl.ts +++ b/components/gitpod-db/src/typeorm/workspace-db-impl.ts @@ -805,7 +805,7 @@ export class TypeORMWorkspaceDBImpl extends TransactionalDBImpl imp const repo = await this.getPrebuiltWorkspaceRepo(); let query = repo.createQueryBuilder("pws"); - query = query.where("pws.projectId != :projectId", { projectId }); + query = query.where("pws.projectId = :projectId", { projectId }); query = query.andWhere("pws.creationTime >= :time", { time: date.toISOString() }); query = query.andWhere("pws.state != :state", { state: abortedState }); return query.getCount(); diff --git a/components/gitpod-protocol/go/gitpod-service.go b/components/gitpod-protocol/go/gitpod-service.go index 03e39a043d7620..a471272f28ce2a 100644 --- a/components/gitpod-protocol/go/gitpod-service.go +++ b/components/gitpod-protocol/go/gitpod-service.go @@ -1678,19 +1678,9 @@ type Repository struct { // WorkspaceCreationResult is the WorkspaceCreationResult message type type WorkspaceCreationResult struct { - CreatedWorkspaceID string `json:"createdWorkspaceId,omitempty"` - ExistingWorkspaces []*WorkspaceInfo `json:"existingWorkspaces,omitempty"` - RunningPrebuildWorkspaceID string `json:"runningPrebuildWorkspaceID,omitempty"` - RunningWorkspacePrebuild *RunningWorkspacePrebuild `json:"runningWorkspacePrebuild,omitempty"` - WorkspaceURL string `json:"workspaceURL,omitempty"` -} - -// RunningWorkspacePrebuild is the RunningWorkspacePrebuild message type -type RunningWorkspacePrebuild struct { - PrebuildID string `json:"prebuildID,omitempty"` - SameCluster bool `json:"sameCluster,omitempty"` - Starting string `json:"starting,omitempty"` - WorkspaceID string `json:"workspaceID,omitempty"` + CreatedWorkspaceID string `json:"createdWorkspaceId,omitempty"` + ExistingWorkspaces []*WorkspaceInfo `json:"existingWorkspaces,omitempty"` + WorkspaceURL string `json:"workspaceURL,omitempty"` } // Workspace is the Workspace message type @@ -2027,8 +2017,6 @@ type CreateWorkspaceOptions struct { ContextURL string `json:"contextUrl,omitempty"` OrganizationId string `json:"organizationId,omitempty"` IgnoreRunningWorkspaceOnSameCommit bool `json:"ignoreRunningWorkspaceOnSameCommit,omitemopty"` - IgnoreRunningPrebuild bool `json:"ignoreRunningPrebuild,omitemopty"` - AllowUsingPreviousPrebuilds bool `json:"allowUsingPreviousPrebuilds,omitemopty"` ForceDefaultConfig bool `json:"forceDefaultConfig,omitemopty"` } @@ -2291,15 +2279,16 @@ type Project struct { } type ProjectSettings struct { - EnablePrebuilds *bool `json:"enablePrebuilds,omitempty"` - PrebuildDefaultBranchOnly *bool `json:"prebuildDefaultBranchOnly,omitempty"` - PrebuildBranchPattern *string `json:"prebuildBranchPattern,omitempty"` - UseIncrementalPrebuilds bool `json:"useIncrementalPrebuilds,omitempty"` - UsePersistentVolumeClaim bool `json:"usePersistentVolumeClaim,omitempty"` - KeepOutdatedPrebuildsRunning bool `json:"keepOutdatedPrebuildsRunning,omitempty"` - AllowUsingPreviousPrebuilds bool `json:"allowUsingPreviousPrebuilds,omitempty"` - PrebuildEveryNthCommit int `json:"prebuildEveryNthCommit,omitempty"` - WorkspaceClasses *WorkspaceClassesSettings `json:"workspaceClasses,omitempty"` + UsePersistentVolumeClaim bool `json:"usePersistentVolumeClaim,omitempty"` + WorkspaceClasses *WorkspaceClassesSettings `json:"workspaceClasses,omitempty"` + PrebuildSettings *PrebuildSettings `json:"prebuilds,omitempty"` +} +type PrebuildSettings struct { + Enable *bool `json:"enable,omitempty"` + PrebuildInterval *int32 `json:"prebuildInterval,omitempty"` + BranchStrategy *string `json:"branchStrategy,omitempty"` + BranchMatchingPattern *string `json:"branchMatchingPattern,omitempty"` + WorkspaceClass *string `json:"workspaceClass,omitempty"` } type WorkspaceClassesSettings struct { diff --git a/components/gitpod-protocol/src/gitpod-service.ts b/components/gitpod-protocol/src/gitpod-service.ts index 88633a78129b21..ebd6b624314e34 100644 --- a/components/gitpod-protocol/src/gitpod-service.ts +++ b/components/gitpod-protocol/src/gitpod-service.ts @@ -424,8 +424,6 @@ export namespace GitpodServer { // whether running workspaces on the same context should be ignored. If false (default) users will be asked. //TODO(se) remove this option and let clients do that check if they like. The new create workspace page does it already ignoreRunningWorkspaceOnSameCommit?: boolean; - ignoreRunningPrebuild?: boolean; - allowUsingPreviousPrebuilds?: boolean; forceDefaultConfig?: boolean; } diff --git a/components/gitpod-protocol/src/protocol.ts b/components/gitpod-protocol/src/protocol.ts index 2daea9c4d23c08..b21fdeb6716d10 100644 --- a/components/gitpod-protocol/src/protocol.ts +++ b/components/gitpod-protocol/src/protocol.ts @@ -340,6 +340,9 @@ export type IDESettings = { export interface WorkspaceClasses { regular?: string; + /** + * @deprecated see Project.settings.prebuilds.workspaceClass + */ prebuild?: string; } @@ -1497,16 +1500,7 @@ export interface WorkspaceCreationResult { createdWorkspaceId?: string; workspaceURL?: string; existingWorkspaces?: WorkspaceInfo[]; - runningWorkspacePrebuild?: { - prebuildID: string; - workspaceID: string; - instanceID: string; - starting: RunningWorkspacePrebuildStarting; - sameCluster: boolean; - }; - runningPrebuildWorkspaceID?: string; } -export type RunningWorkspacePrebuildStarting = "queued" | "starting" | "running"; export namespace WorkspaceCreationResult { export function is(data: any): data is WorkspaceCreationResult { diff --git a/components/gitpod-protocol/src/teams-projects-protocol.ts b/components/gitpod-protocol/src/teams-projects-protocol.ts index 99ceb17b2baf9b..897a63c7f1934e 100644 --- a/components/gitpod-protocol/src/teams-projects-protocol.ts +++ b/components/gitpod-protocol/src/teams-projects-protocol.ts @@ -14,29 +14,79 @@ export interface ProjectConfig { } export interface ProjectSettings { + /** + * Controls settings of prebuilds for this project. + */ + prebuilds?: PrebuildSettings; + + /** @deprecated see `Project.settings.prebuilds.enabled` instead. */ enablePrebuilds?: boolean; /** * Wether prebuilds (if enabled) should only be started on the default branch. * Defaults to `true` on project creation. + * + * @deprecated see `Project.settings.prebuilds.branchStrategy` instead. */ prebuildDefaultBranchOnly?: boolean; /** * Use this pattern to match branch names to run prebuilds on. * The pattern matching will only be applied if prebuilds are enabled and * they are not limited to the default branch. + * + * @deprecated see `Project.settings.prebuilds.branchMatchingPattern` instead. */ prebuildBranchPattern?: string; + /** + * how many commits in the commit history a prebuild is good (undefined and 0 means every commit is prebuilt) + * + * @deprecated see `Project.settings.prebuilds.intervall` instead. + */ + prebuildEveryNthCommit?: number; + + /** + * @deprecated always false + */ useIncrementalPrebuilds?: boolean; + + /** + * @deprecated always true (we should kill dangling prebuilds) + */ keepOutdatedPrebuildsRunning?: boolean; // whether new workspaces can start on older prebuilds and incrementally update + /** + * @deprecated always true + */ allowUsingPreviousPrebuilds?: boolean; - // how many commits in the commit history a prebuild is good (undefined and 0 means every commit is prebuilt) - prebuildEveryNthCommit?: number; + // preferred workspace classes workspaceClasses?: WorkspaceClasses; } -export namespace ProjectSettings { - export type PrebuildBranchStrategy = "defaultBranch" | "allBranches" | "selectedBranches"; +export namespace PrebuildSettings { + export type BranchStrategy = "default-branch" | "all-branches" | "matched-branches"; +} + +export interface PrebuildSettings { + enable?: boolean; + + /** + * Defines an interval of commits to run new prebuilds for. Defaults to 20 + */ + prebuildInterval?: number; + + /** + * Which branches to consider to run new prebuilds on. Default to "all-branches" + */ + branchStrategy?: PrebuildSettings.BranchStrategy; + /** + * If `branchStrategy` s set to "matched-branches", this should define a glob-pattern to be used + * to match the branch to run new prebuilds on. Defaults to "**" + */ + branchMatchingPattern?: string; + + /** + * Preferred workspace class for prebuilds. + */ + workspaceClass?: string; } export interface Project { @@ -69,46 +119,33 @@ export namespace Project { return p.name + "-" + p.id; } + export type PrebuildSettingsWithDefaults = Required> & PrebuildSettings; + + export const PREBUILD_SETTINGS_DEFAULTS: PrebuildSettingsWithDefaults = { + enable: false, + branchMatchingPattern: "**", + prebuildInterval: 20, + branchStrategy: "all-branches", + }; + /** - * If *no settings* are present on pre-existing projects, this defaults to `true` (enabled) for - * backwards compatibility. This allows to do any explicit migration of data or adjustment of - * the default behavior at a later point in time. - * - * Otherwise this returns the value of the `enablePrebuilds` settings persisted in the given - * project. + * Returns effective prebuild settings for the given project. The resulting settings + * contain default values for properties which are not set explicitly for this project. */ - export function isPrebuildsEnabled(project: Project): boolean { - // Defaulting to `true` for backwards compatibility. Ignoring non-boolean for `enablePrebuilds` - // for evaluation here allows to do any explicit migration of data or adjustment of the default - // behavior at a later point in time. - if (!hasPrebuildSettings(project)) { - return true; - } + export function getPrebuildSettings(project: Project): PrebuildSettingsWithDefaults { + // ignoring persisted properties with `undefined` values to exclude them from the override. + const overrides = Object.fromEntries( + Object.entries(project.settings?.prebuilds ?? {}).filter(([_, value]) => value !== undefined), + ); - return !!project.settings?.enablePrebuilds; + return { + ...PREBUILD_SETTINGS_DEFAULTS, + ...overrides, + }; } export function hasPrebuildSettings(project: Project) { - return !(typeof project.settings?.enablePrebuilds === "undefined"); - } - - export function getPrebuildBranchStrategy(project: Project): ProjectSettings.PrebuildBranchStrategy { - if (!hasPrebuildSettings(project)) { - // returning "all branches" to mimic the default value of projects which were added - // before introduction of persisted settings for prebuilds. - return "allBranches"; - } - if (typeof project.settings?.prebuildDefaultBranchOnly === "undefined") { - return "defaultBranch"; // default value for `settings.prebuildDefaultBranchOnly` - } - if (project.settings?.prebuildDefaultBranchOnly) { - return "defaultBranch"; - } - const prebuildBranchPattern = project.settings?.prebuildBranchPattern?.trim(); - if (typeof prebuildBranchPattern === "string" && prebuildBranchPattern.length > 1) { - return "selectedBranches"; - } - return "allBranches"; + return !(typeof project.settings?.prebuilds === "undefined"); } export interface Overview { diff --git a/components/public-api-server/pkg/apiv1/project.go b/components/public-api-server/pkg/apiv1/project.go index 1d7db268a3945a..b1380c5e99a83b 100644 --- a/components/public-api-server/pkg/apiv1/project.go +++ b/components/public-api-server/pkg/apiv1/project.go @@ -167,21 +167,21 @@ func projectSettingsToAPIResponse(s *protocol.ProjectSettings) *v1.ProjectSettin return &v1.ProjectSettings{} } - return &v1.ProjectSettings{ - Prebuild: &v1.PrebuildSettings{ - EnablePrebuilds: s.EnablePrebuilds, - PrebuildDefaultBranchOnly: s.PrebuildDefaultBranchOnly, - PrebuildBranchPattern: s.PrebuildBranchPattern, - EnableIncrementalPrebuilds: s.UseIncrementalPrebuilds, - KeepOutdatedPrebuildsRunning: s.KeepOutdatedPrebuildsRunning, - UsePreviousPrebuilds: s.AllowUsingPreviousPrebuilds, - PrebuildEveryNth: int32(s.PrebuildEveryNthCommit), - }, + settings := &v1.ProjectSettings{ + Prebuild: &v1.PrebuildSettings{}, Workspace: &v1.WorkspaceSettings{ - EnablePersistentVolumeClaim: s.UsePersistentVolumeClaim, - WorkspaceClass: workspaceClassesToAPIResponse(s.WorkspaceClasses), + WorkspaceClass: workspaceClassesToAPIResponse(s.WorkspaceClasses), }, } + if s.PrebuildSettings != nil { + settings.Prebuild.EnablePrebuilds = s.PrebuildSettings.Enable + settings.Prebuild.BranchStrategy = s.PrebuildSettings.BranchStrategy + settings.Prebuild.BranchMatchingPattern = s.PrebuildSettings.BranchMatchingPattern + settings.Prebuild.PrebuildInterval = s.PrebuildSettings.PrebuildInterval + settings.Prebuild.WorkspaceClass = s.PrebuildSettings.WorkspaceClass + } + + return settings } func workspaceClassesToAPIResponse(s *protocol.WorkspaceClassesSettings) *v1.WorkspaceClassSettings { diff --git a/components/public-api-server/pkg/apiv1/project_test.go b/components/public-api-server/pkg/apiv1/project_test.go index 4c7371177fb6ad..4c7c760db17b8f 100644 --- a/components/public-api-server/pkg/apiv1/project_test.go +++ b/components/public-api-server/pkg/apiv1/project_test.go @@ -328,6 +328,7 @@ func setupProjectsService(t *testing.T) (*protocol.MockAPIInterface, v1connect.P func newProject(p *protocol.Project) *protocol.Project { r := rand.Int() + b_false := false result := &protocol.Project{ ID: uuid.New().String(), Name: fmt.Sprintf("team-%d", r), @@ -335,15 +336,14 @@ func newProject(p *protocol.Project) *protocol.Project { CloneURL: "https://github.com/easyCZ/foobar", AppInstallationID: "1337", Settings: &protocol.ProjectSettings{ - UseIncrementalPrebuilds: true, - UsePersistentVolumeClaim: true, - KeepOutdatedPrebuildsRunning: true, - AllowUsingPreviousPrebuilds: true, - PrebuildEveryNthCommit: 5, + UsePersistentVolumeClaim: true, WorkspaceClasses: &protocol.WorkspaceClassesSettings{ Regular: "default", Prebuild: "default", }, + PrebuildSettings: &protocol.PrebuildSettings{ + Enable: &b_false, + }, }, CreationTime: "2022-09-09T09:09:09.000Z", } diff --git a/components/public-api/gitpod/experimental/v1/projects.proto b/components/public-api/gitpod/experimental/v1/projects.proto index edd4b2f3fe17fb..1fe6b7b0e8d8dd 100644 --- a/components/public-api/gitpod/experimental/v1/projects.proto +++ b/components/public-api/gitpod/experimental/v1/projects.proto @@ -43,13 +43,16 @@ message ProjectSettings { } message PrebuildSettings { - bool enable_incremental_prebuilds = 1; - bool keep_outdated_prebuilds_running = 2; - bool use_previous_prebuilds = 3; - int32 prebuild_every_nth = 4; + reserved 1; // enable_incremental_prebuilds + reserved 2; // keep_outdated_prebuilds_running + reserved 3; // use_previous_prebuilds + reserved 4; // was prebuild_every_nth optional bool enable_prebuilds = 5; - optional bool prebuild_default_branch_only = 6; - optional string prebuild_branch_pattern = 7; + reserved 6; // was prebuild_default_branch_only + optional string branch_matching_pattern = 7; + optional string branch_strategy = 8; + optional int32 prebuild_interval = 9; + optional string workspace_class = 10; } message WorkspaceSettings { diff --git a/components/public-api/go/experimental/v1/projects.pb.go b/components/public-api/go/experimental/v1/projects.pb.go index e488b7e05c64e4..fc8bb5b7214b37 100644 --- a/components/public-api/go/experimental/v1/projects.pb.go +++ b/components/public-api/go/experimental/v1/projects.pb.go @@ -183,13 +183,11 @@ type PrebuildSettings struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - EnableIncrementalPrebuilds bool `protobuf:"varint,1,opt,name=enable_incremental_prebuilds,json=enableIncrementalPrebuilds,proto3" json:"enable_incremental_prebuilds,omitempty"` - KeepOutdatedPrebuildsRunning bool `protobuf:"varint,2,opt,name=keep_outdated_prebuilds_running,json=keepOutdatedPrebuildsRunning,proto3" json:"keep_outdated_prebuilds_running,omitempty"` - UsePreviousPrebuilds bool `protobuf:"varint,3,opt,name=use_previous_prebuilds,json=usePreviousPrebuilds,proto3" json:"use_previous_prebuilds,omitempty"` - PrebuildEveryNth int32 `protobuf:"varint,4,opt,name=prebuild_every_nth,json=prebuildEveryNth,proto3" json:"prebuild_every_nth,omitempty"` - EnablePrebuilds *bool `protobuf:"varint,5,opt,name=enable_prebuilds,json=enablePrebuilds,proto3,oneof" json:"enable_prebuilds,omitempty"` - PrebuildDefaultBranchOnly *bool `protobuf:"varint,6,opt,name=prebuild_default_branch_only,json=prebuildDefaultBranchOnly,proto3,oneof" json:"prebuild_default_branch_only,omitempty"` - PrebuildBranchPattern *string `protobuf:"bytes,7,opt,name=prebuild_branch_pattern,json=prebuildBranchPattern,proto3,oneof" json:"prebuild_branch_pattern,omitempty"` + EnablePrebuilds *bool `protobuf:"varint,5,opt,name=enable_prebuilds,json=enablePrebuilds,proto3,oneof" json:"enable_prebuilds,omitempty"` + BranchMatchingPattern *string `protobuf:"bytes,7,opt,name=branch_matching_pattern,json=branchMatchingPattern,proto3,oneof" json:"branch_matching_pattern,omitempty"` + BranchStrategy *string `protobuf:"bytes,8,opt,name=branch_strategy,json=branchStrategy,proto3,oneof" json:"branch_strategy,omitempty"` + PrebuildInterval *int32 `protobuf:"varint,9,opt,name=prebuild_interval,json=prebuildInterval,proto3,oneof" json:"prebuild_interval,omitempty"` + WorkspaceClass *string `protobuf:"bytes,10,opt,name=workspace_class,json=workspaceClass,proto3,oneof" json:"workspace_class,omitempty"` } func (x *PrebuildSettings) Reset() { @@ -224,51 +222,37 @@ func (*PrebuildSettings) Descriptor() ([]byte, []int) { return file_gitpod_experimental_v1_projects_proto_rawDescGZIP(), []int{2} } -func (x *PrebuildSettings) GetEnableIncrementalPrebuilds() bool { - if x != nil { - return x.EnableIncrementalPrebuilds +func (x *PrebuildSettings) GetEnablePrebuilds() bool { + if x != nil && x.EnablePrebuilds != nil { + return *x.EnablePrebuilds } return false } -func (x *PrebuildSettings) GetKeepOutdatedPrebuildsRunning() bool { - if x != nil { - return x.KeepOutdatedPrebuildsRunning +func (x *PrebuildSettings) GetBranchMatchingPattern() string { + if x != nil && x.BranchMatchingPattern != nil { + return *x.BranchMatchingPattern } - return false + return "" } -func (x *PrebuildSettings) GetUsePreviousPrebuilds() bool { - if x != nil { - return x.UsePreviousPrebuilds +func (x *PrebuildSettings) GetBranchStrategy() string { + if x != nil && x.BranchStrategy != nil { + return *x.BranchStrategy } - return false + return "" } -func (x *PrebuildSettings) GetPrebuildEveryNth() int32 { - if x != nil { - return x.PrebuildEveryNth +func (x *PrebuildSettings) GetPrebuildInterval() int32 { + if x != nil && x.PrebuildInterval != nil { + return *x.PrebuildInterval } return 0 } -func (x *PrebuildSettings) GetEnablePrebuilds() bool { - if x != nil && x.EnablePrebuilds != nil { - return *x.EnablePrebuilds - } - return false -} - -func (x *PrebuildSettings) GetPrebuildDefaultBranchOnly() bool { - if x != nil && x.PrebuildDefaultBranchOnly != nil { - return *x.PrebuildDefaultBranchOnly - } - return false -} - -func (x *PrebuildSettings) GetPrebuildBranchPattern() string { - if x != nil && x.PrebuildBranchPattern != nil { - return *x.PrebuildBranchPattern +func (x *PrebuildSettings) GetWorkspaceClass() string { + if x != nil && x.WorkspaceClass != nil { + return *x.WorkspaceClass } return "" } @@ -805,128 +789,121 @@ var file_gitpod_experimental_v1_projects_proto_rawDesc = []byte{ 0x2e, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2e, 0x65, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x09, 0x77, 0x6f, 0x72, 0x6b, 0x73, - 0x70, 0x61, 0x63, 0x65, 0x22, 0x84, 0x04, 0x0a, 0x10, 0x50, 0x72, 0x65, 0x62, 0x75, 0x69, 0x6c, - 0x64, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x40, 0x0a, 0x1c, 0x65, 0x6e, 0x61, - 0x62, 0x6c, 0x65, 0x5f, 0x69, 0x6e, 0x63, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x6c, 0x5f, - 0x70, 0x72, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x1a, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x49, 0x6e, 0x63, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, - 0x61, 0x6c, 0x50, 0x72, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x73, 0x12, 0x45, 0x0a, 0x1f, 0x6b, - 0x65, 0x65, 0x70, 0x5f, 0x6f, 0x75, 0x74, 0x64, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x70, 0x72, 0x65, - 0x62, 0x75, 0x69, 0x6c, 0x64, 0x73, 0x5f, 0x72, 0x75, 0x6e, 0x6e, 0x69, 0x6e, 0x67, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x08, 0x52, 0x1c, 0x6b, 0x65, 0x65, 0x70, 0x4f, 0x75, 0x74, 0x64, 0x61, 0x74, - 0x65, 0x64, 0x50, 0x72, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x73, 0x52, 0x75, 0x6e, 0x6e, 0x69, - 0x6e, 0x67, 0x12, 0x34, 0x0a, 0x16, 0x75, 0x73, 0x65, 0x5f, 0x70, 0x72, 0x65, 0x76, 0x69, 0x6f, - 0x75, 0x73, 0x5f, 0x70, 0x72, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x73, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x08, 0x52, 0x14, 0x75, 0x73, 0x65, 0x50, 0x72, 0x65, 0x76, 0x69, 0x6f, 0x75, 0x73, 0x50, - 0x72, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x73, 0x12, 0x2c, 0x0a, 0x12, 0x70, 0x72, 0x65, 0x62, - 0x75, 0x69, 0x6c, 0x64, 0x5f, 0x65, 0x76, 0x65, 0x72, 0x79, 0x5f, 0x6e, 0x74, 0x68, 0x18, 0x04, - 0x20, 0x01, 0x28, 0x05, 0x52, 0x10, 0x70, 0x72, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x45, 0x76, - 0x65, 0x72, 0x79, 0x4e, 0x74, 0x68, 0x12, 0x2e, 0x0a, 0x10, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, - 0x5f, 0x70, 0x72, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, - 0x48, 0x00, 0x52, 0x0f, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x50, 0x72, 0x65, 0x62, 0x75, 0x69, - 0x6c, 0x64, 0x73, 0x88, 0x01, 0x01, 0x12, 0x44, 0x0a, 0x1c, 0x70, 0x72, 0x65, 0x62, 0x75, 0x69, - 0x6c, 0x64, 0x5f, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x62, 0x72, 0x61, 0x6e, 0x63, - 0x68, 0x5f, 0x6f, 0x6e, 0x6c, 0x79, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x48, 0x01, 0x52, 0x19, - 0x70, 0x72, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x42, - 0x72, 0x61, 0x6e, 0x63, 0x68, 0x4f, 0x6e, 0x6c, 0x79, 0x88, 0x01, 0x01, 0x12, 0x3b, 0x0a, 0x17, - 0x70, 0x72, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x5f, 0x62, 0x72, 0x61, 0x6e, 0x63, 0x68, 0x5f, - 0x70, 0x61, 0x74, 0x74, 0x65, 0x72, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x48, 0x02, 0x52, - 0x15, 0x70, 0x72, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x42, 0x72, 0x61, 0x6e, 0x63, 0x68, 0x50, - 0x61, 0x74, 0x74, 0x65, 0x72, 0x6e, 0x88, 0x01, 0x01, 0x42, 0x13, 0x0a, 0x11, 0x5f, 0x65, 0x6e, - 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x70, 0x72, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x73, 0x42, 0x1f, - 0x0a, 0x1d, 0x5f, 0x70, 0x72, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x5f, 0x64, 0x65, 0x66, 0x61, - 0x75, 0x6c, 0x74, 0x5f, 0x62, 0x72, 0x61, 0x6e, 0x63, 0x68, 0x5f, 0x6f, 0x6e, 0x6c, 0x79, 0x42, - 0x1a, 0x0a, 0x18, 0x5f, 0x70, 0x72, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x5f, 0x62, 0x72, 0x61, - 0x6e, 0x63, 0x68, 0x5f, 0x70, 0x61, 0x74, 0x74, 0x65, 0x72, 0x6e, 0x22, 0xb1, 0x01, 0x0a, 0x11, - 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, - 0x73, 0x12, 0x43, 0x0a, 0x1e, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x70, 0x65, 0x72, 0x73, - 0x69, 0x73, 0x74, 0x65, 0x6e, 0x74, 0x5f, 0x76, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x5f, 0x63, 0x6c, - 0x61, 0x69, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x1b, 0x65, 0x6e, 0x61, 0x62, 0x6c, - 0x65, 0x50, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x74, 0x56, 0x6f, 0x6c, 0x75, 0x6d, - 0x65, 0x43, 0x6c, 0x61, 0x69, 0x6d, 0x12, 0x57, 0x0a, 0x0f, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, - 0x61, 0x63, 0x65, 0x5f, 0x63, 0x6c, 0x61, 0x73, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x2e, 0x2e, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2e, 0x65, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, - 0x65, 0x6e, 0x74, 0x61, 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, - 0x63, 0x65, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x52, - 0x0e, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x22, - 0x4e, 0x0a, 0x16, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x43, 0x6c, 0x61, 0x73, - 0x73, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x72, 0x65, 0x67, - 0x75, 0x6c, 0x61, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x72, 0x65, 0x67, 0x75, - 0x6c, 0x61, 0x72, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x72, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x22, - 0x51, 0x0a, 0x14, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x39, 0x0a, 0x07, 0x70, 0x72, 0x6f, 0x6a, 0x65, - 0x63, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x67, 0x69, 0x74, 0x70, 0x6f, - 0x64, 0x2e, 0x65, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x6c, 0x2e, 0x76, - 0x31, 0x2e, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x52, 0x07, 0x70, 0x72, 0x6f, 0x6a, 0x65, - 0x63, 0x74, 0x22, 0x52, 0x0a, 0x15, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x72, 0x6f, 0x6a, - 0x65, 0x63, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x39, 0x0a, 0x07, 0x70, - 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x67, - 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2e, 0x65, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, - 0x61, 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x52, 0x07, 0x70, - 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x22, 0x32, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x50, 0x72, 0x6f, - 0x6a, 0x65, 0x63, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x70, - 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x09, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x64, 0x22, 0x4f, 0x0a, 0x12, 0x47, 0x65, - 0x74, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x12, 0x39, 0x0a, 0x07, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x1f, 0x2e, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2e, 0x65, 0x78, 0x70, 0x65, 0x72, - 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x72, 0x6f, 0x6a, 0x65, - 0x63, 0x74, 0x52, 0x07, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x22, 0x78, 0x0a, 0x13, 0x4c, - 0x69, 0x73, 0x74, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x74, 0x65, 0x61, 0x6d, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x06, 0x74, 0x65, 0x61, 0x6d, 0x49, 0x64, 0x12, 0x42, 0x0a, 0x0a, 0x70, - 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x22, 0x2e, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2e, 0x65, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, - 0x65, 0x6e, 0x74, 0x61, 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x52, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4a, - 0x04, 0x08, 0x01, 0x10, 0x02, 0x22, 0x78, 0x0a, 0x14, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x72, 0x6f, - 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3b, 0x0a, - 0x08, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, - 0x1f, 0x2e, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2e, 0x65, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, - 0x65, 0x6e, 0x74, 0x61, 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, - 0x52, 0x08, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x74, 0x6f, - 0x74, 0x61, 0x6c, 0x5f, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x05, 0x52, 0x0c, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x22, - 0x35, 0x0a, 0x14, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x72, 0x6f, 0x6a, 0x65, - 0x63, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x70, 0x72, 0x6f, - 0x6a, 0x65, 0x63, 0x74, 0x49, 0x64, 0x22, 0x17, 0x0a, 0x15, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, - 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, - 0xc5, 0x03, 0x0a, 0x0f, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x53, 0x65, 0x72, 0x76, - 0x69, 0x63, 0x65, 0x12, 0x6e, 0x0a, 0x0d, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x72, 0x6f, - 0x6a, 0x65, 0x63, 0x74, 0x12, 0x2c, 0x2e, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2e, 0x65, 0x78, - 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, - 0x65, 0x61, 0x74, 0x65, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x2d, 0x2e, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2e, 0x65, 0x78, 0x70, 0x65, - 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, + 0x70, 0x61, 0x63, 0x65, 0x22, 0x9a, 0x03, 0x0a, 0x10, 0x50, 0x72, 0x65, 0x62, 0x75, 0x69, 0x6c, + 0x64, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x2e, 0x0a, 0x10, 0x65, 0x6e, 0x61, + 0x62, 0x6c, 0x65, 0x5f, 0x70, 0x72, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x73, 0x18, 0x05, 0x20, + 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x0f, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x50, 0x72, 0x65, + 0x62, 0x75, 0x69, 0x6c, 0x64, 0x73, 0x88, 0x01, 0x01, 0x12, 0x3b, 0x0a, 0x17, 0x62, 0x72, 0x61, + 0x6e, 0x63, 0x68, 0x5f, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x69, 0x6e, 0x67, 0x5f, 0x70, 0x61, 0x74, + 0x74, 0x65, 0x72, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x48, 0x01, 0x52, 0x15, 0x62, 0x72, + 0x61, 0x6e, 0x63, 0x68, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x69, 0x6e, 0x67, 0x50, 0x61, 0x74, 0x74, + 0x65, 0x72, 0x6e, 0x88, 0x01, 0x01, 0x12, 0x2c, 0x0a, 0x0f, 0x62, 0x72, 0x61, 0x6e, 0x63, 0x68, + 0x5f, 0x73, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x48, + 0x02, 0x52, 0x0e, 0x62, 0x72, 0x61, 0x6e, 0x63, 0x68, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, + 0x79, 0x88, 0x01, 0x01, 0x12, 0x30, 0x0a, 0x11, 0x70, 0x72, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x64, + 0x5f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x18, 0x09, 0x20, 0x01, 0x28, 0x05, 0x48, + 0x03, 0x52, 0x10, 0x70, 0x72, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x49, 0x6e, 0x74, 0x65, 0x72, + 0x76, 0x61, 0x6c, 0x88, 0x01, 0x01, 0x12, 0x2c, 0x0a, 0x0f, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, + 0x61, 0x63, 0x65, 0x5f, 0x63, 0x6c, 0x61, 0x73, 0x73, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x48, + 0x04, 0x52, 0x0e, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x43, 0x6c, 0x61, 0x73, + 0x73, 0x88, 0x01, 0x01, 0x42, 0x13, 0x0a, 0x11, 0x5f, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x5f, + 0x70, 0x72, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x73, 0x42, 0x1a, 0x0a, 0x18, 0x5f, 0x62, 0x72, + 0x61, 0x6e, 0x63, 0x68, 0x5f, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x69, 0x6e, 0x67, 0x5f, 0x70, 0x61, + 0x74, 0x74, 0x65, 0x72, 0x6e, 0x42, 0x12, 0x0a, 0x10, 0x5f, 0x62, 0x72, 0x61, 0x6e, 0x63, 0x68, + 0x5f, 0x73, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x42, 0x14, 0x0a, 0x12, 0x5f, 0x70, 0x72, + 0x65, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x5f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x42, + 0x12, 0x0a, 0x10, 0x5f, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x63, 0x6c, + 0x61, 0x73, 0x73, 0x4a, 0x04, 0x08, 0x01, 0x10, 0x02, 0x4a, 0x04, 0x08, 0x02, 0x10, 0x03, 0x4a, + 0x04, 0x08, 0x03, 0x10, 0x04, 0x4a, 0x04, 0x08, 0x04, 0x10, 0x05, 0x4a, 0x04, 0x08, 0x06, 0x10, + 0x07, 0x22, 0xb1, 0x01, 0x0a, 0x11, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x53, + 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x43, 0x0a, 0x1e, 0x65, 0x6e, 0x61, 0x62, 0x6c, + 0x65, 0x5f, 0x70, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x74, 0x5f, 0x76, 0x6f, 0x6c, + 0x75, 0x6d, 0x65, 0x5f, 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x1b, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x50, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, + 0x74, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x43, 0x6c, 0x61, 0x69, 0x6d, 0x12, 0x57, 0x0a, 0x0f, + 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x63, 0x6c, 0x61, 0x73, 0x73, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2e, 0x2e, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2e, 0x65, + 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x57, + 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x53, 0x65, 0x74, + 0x74, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x0e, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, + 0x43, 0x6c, 0x61, 0x73, 0x73, 0x22, 0x4e, 0x0a, 0x16, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, + 0x63, 0x65, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x12, + 0x18, 0x0a, 0x07, 0x72, 0x65, 0x67, 0x75, 0x6c, 0x61, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x07, 0x72, 0x65, 0x67, 0x75, 0x6c, 0x61, 0x72, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x65, + 0x62, 0x75, 0x69, 0x6c, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x72, 0x65, + 0x62, 0x75, 0x69, 0x6c, 0x64, 0x22, 0x51, 0x0a, 0x14, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, + 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x39, 0x0a, + 0x07, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, + 0x2e, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2e, 0x65, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, + 0x6e, 0x74, 0x61, 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x52, + 0x07, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x22, 0x52, 0x0a, 0x15, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x22, 0x00, 0x12, 0x65, 0x0a, 0x0a, 0x47, 0x65, 0x74, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, - 0x74, 0x12, 0x29, 0x2e, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2e, 0x65, 0x78, 0x70, 0x65, 0x72, - 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x50, 0x72, - 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x67, - 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2e, 0x65, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, - 0x61, 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x6b, 0x0a, 0x0c, 0x4c, 0x69, - 0x73, 0x74, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x12, 0x2b, 0x2e, 0x67, 0x69, 0x74, - 0x70, 0x6f, 0x64, 0x2e, 0x65, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x6c, - 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x73, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2c, 0x2e, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, + 0x65, 0x12, 0x39, 0x0a, 0x07, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2e, 0x65, 0x78, 0x70, 0x65, + 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x72, 0x6f, 0x6a, + 0x65, 0x63, 0x74, 0x52, 0x07, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x22, 0x32, 0x0a, 0x11, + 0x47, 0x65, 0x74, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x69, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x64, + 0x22, 0x4f, 0x0a, 0x12, 0x47, 0x65, 0x74, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x39, 0x0a, 0x07, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, + 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2e, 0x65, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x6c, 0x2e, 0x76, 0x31, - 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x6e, 0x0a, 0x0d, 0x44, 0x65, 0x6c, 0x65, 0x74, - 0x65, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x12, 0x2c, 0x2e, 0x67, 0x69, 0x74, 0x70, 0x6f, - 0x64, 0x2e, 0x65, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x6c, 0x2e, 0x76, - 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2d, 0x2e, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2e, - 0x65, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x6c, 0x2e, 0x76, 0x31, 0x2e, + 0x2e, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x52, 0x07, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, + 0x74, 0x22, 0x78, 0x0a, 0x13, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, + 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x74, 0x65, 0x61, 0x6d, + 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x74, 0x65, 0x61, 0x6d, 0x49, + 0x64, 0x12, 0x42, 0x0a, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2e, 0x65, + 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x50, + 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4a, 0x04, 0x08, 0x01, 0x10, 0x02, 0x22, 0x78, 0x0a, 0x14, 0x4c, + 0x69, 0x73, 0x74, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x3b, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x18, + 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2e, 0x65, + 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x50, + 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x73, + 0x12, 0x23, 0x0a, 0x0d, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, + 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0c, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x52, 0x65, + 0x73, 0x75, 0x6c, 0x74, 0x73, 0x22, 0x35, 0x0a, 0x14, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x50, + 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, + 0x0a, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x09, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x64, 0x22, 0x17, 0x0a, 0x15, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x46, 0x5a, 0x44, 0x67, 0x69, 0x74, 0x68, 0x75, - 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2d, 0x69, 0x6f, 0x2f, - 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2f, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, - 0x73, 0x2f, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x2d, 0x61, 0x70, 0x69, 0x2f, 0x67, 0x6f, 0x2f, - 0x65, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x6c, 0x2f, 0x76, 0x31, 0x62, - 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0xc5, 0x03, 0x0a, 0x0f, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, + 0x74, 0x73, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x6e, 0x0a, 0x0d, 0x43, 0x72, 0x65, + 0x61, 0x74, 0x65, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x12, 0x2c, 0x2e, 0x67, 0x69, 0x74, + 0x70, 0x6f, 0x64, 0x2e, 0x65, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x6c, + 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, + 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2d, 0x2e, 0x67, 0x69, 0x74, 0x70, 0x6f, + 0x64, 0x2e, 0x65, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x6c, 0x2e, 0x76, + 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x65, 0x0a, 0x0a, 0x47, 0x65, 0x74, + 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x12, 0x29, 0x2e, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, + 0x2e, 0x65, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x6c, 0x2e, 0x76, 0x31, + 0x2e, 0x47, 0x65, 0x74, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2e, 0x65, 0x78, 0x70, 0x65, + 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x50, + 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, + 0x12, 0x6b, 0x0a, 0x0c, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x73, + 0x12, 0x2b, 0x2e, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2e, 0x65, 0x78, 0x70, 0x65, 0x72, 0x69, + 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x72, + 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2c, 0x2e, + 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2e, 0x65, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, + 0x74, 0x61, 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x72, 0x6f, 0x6a, 0x65, + 0x63, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x6e, 0x0a, + 0x0d, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x12, 0x2c, + 0x2e, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2e, 0x65, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, + 0x6e, 0x74, 0x61, 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x50, 0x72, + 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2d, 0x2e, 0x67, + 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2e, 0x65, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, + 0x61, 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x50, 0x72, 0x6f, 0x6a, + 0x65, 0x63, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x46, 0x5a, + 0x44, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x69, 0x74, 0x70, + 0x6f, 0x64, 0x2d, 0x69, 0x6f, 0x2f, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2f, 0x63, 0x6f, 0x6d, + 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x73, 0x2f, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x2d, 0x61, + 0x70, 0x69, 0x2f, 0x67, 0x6f, 0x2f, 0x65, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, + 0x61, 0x6c, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/components/public-api/typescript/src/gitpod/experimental/v1/projects_pb.ts b/components/public-api/typescript/src/gitpod/experimental/v1/projects_pb.ts index 659d41042acdd2..170281b0209058 100644 --- a/components/public-api/typescript/src/gitpod/experimental/v1/projects_pb.ts +++ b/components/public-api/typescript/src/gitpod/experimental/v1/projects_pb.ts @@ -145,39 +145,29 @@ export class ProjectSettings extends Message { */ export class PrebuildSettings extends Message { /** - * @generated from field: bool enable_incremental_prebuilds = 1; - */ - enableIncrementalPrebuilds = false; - - /** - * @generated from field: bool keep_outdated_prebuilds_running = 2; - */ - keepOutdatedPrebuildsRunning = false; - - /** - * @generated from field: bool use_previous_prebuilds = 3; + * @generated from field: optional bool enable_prebuilds = 5; */ - usePreviousPrebuilds = false; + enablePrebuilds?: boolean; /** - * @generated from field: int32 prebuild_every_nth = 4; + * @generated from field: optional string branch_matching_pattern = 7; */ - prebuildEveryNth = 0; + branchMatchingPattern?: string; /** - * @generated from field: optional bool enable_prebuilds = 5; + * @generated from field: optional string branch_strategy = 8; */ - enablePrebuilds?: boolean; + branchStrategy?: string; /** - * @generated from field: optional bool prebuild_default_branch_only = 6; + * @generated from field: optional int32 prebuild_interval = 9; */ - prebuildDefaultBranchOnly?: boolean; + prebuildInterval?: number; /** - * @generated from field: optional string prebuild_branch_pattern = 7; + * @generated from field: optional string workspace_class = 10; */ - prebuildBranchPattern?: string; + workspaceClass?: string; constructor(data?: PartialMessage) { super(); @@ -187,13 +177,11 @@ export class PrebuildSettings extends Message { static readonly runtime = proto3; static readonly typeName = "gitpod.experimental.v1.PrebuildSettings"; static readonly fields: FieldList = proto3.util.newFieldList(() => [ - { no: 1, name: "enable_incremental_prebuilds", kind: "scalar", T: 8 /* ScalarType.BOOL */ }, - { no: 2, name: "keep_outdated_prebuilds_running", kind: "scalar", T: 8 /* ScalarType.BOOL */ }, - { no: 3, name: "use_previous_prebuilds", kind: "scalar", T: 8 /* ScalarType.BOOL */ }, - { no: 4, name: "prebuild_every_nth", kind: "scalar", T: 5 /* ScalarType.INT32 */ }, { no: 5, name: "enable_prebuilds", kind: "scalar", T: 8 /* ScalarType.BOOL */, opt: true }, - { no: 6, name: "prebuild_default_branch_only", kind: "scalar", T: 8 /* ScalarType.BOOL */, opt: true }, - { no: 7, name: "prebuild_branch_pattern", kind: "scalar", T: 9 /* ScalarType.STRING */, opt: true }, + { no: 7, name: "branch_matching_pattern", kind: "scalar", T: 9 /* ScalarType.STRING */, opt: true }, + { no: 8, name: "branch_strategy", kind: "scalar", T: 9 /* ScalarType.STRING */, opt: true }, + { no: 9, name: "prebuild_interval", kind: "scalar", T: 5 /* ScalarType.INT32 */, opt: true }, + { no: 10, name: "workspace_class", kind: "scalar", T: 9 /* ScalarType.STRING */, opt: true }, ]); static fromBinary(bytes: Uint8Array, options?: Partial): PrebuildSettings { diff --git a/components/server/src/config.ts b/components/server/src/config.ts index 2bd962386943ef..ba5e114c6f53a3 100644 --- a/components/server/src/config.ts +++ b/components/server/src/config.ts @@ -149,11 +149,6 @@ export interface ConfigSerialized { /** maxConcurrentPrebuildsPerRef is the maximum number of prebuilds we allow per ref type at any given time */ maxConcurrentPrebuildsPerRef: number; - incrementalPrebuilds: { - repositoryPasslist: string[]; - commitHistory: number; - }; - blockNewUsers: { enabled: boolean; passlist: string[]; diff --git a/components/server/src/container-module.ts b/components/server/src/container-module.ts index 05f2f2c3183672..fde10a6926bbb1 100644 --- a/components/server/src/container-module.ts +++ b/components/server/src/container-module.ts @@ -85,7 +85,7 @@ import { GithubApp } from "./prebuilds/github-app"; import { GithubAppRules } from "./prebuilds/github-app-rules"; import { GitHubEnterpriseApp } from "./prebuilds/github-enterprise-app"; import { GitLabApp } from "./prebuilds/gitlab-app"; -import { IncrementalPrebuildsService } from "./prebuilds/incremental-prebuilds-service"; +import { IncrementalWorkspaceService } from "./prebuilds/incremental-workspace-service"; import { PrebuildManager } from "./prebuilds/prebuild-manager"; import { PrebuildStatusMaintainer } from "./prebuilds/prebuilt-status-maintainer"; import { ProjectsService } from "./projects/projects-service"; @@ -343,7 +343,7 @@ export const productionContainerModule = new ContainerModule( bind(BitbucketAppSupport).toSelf().inSingletonScope(); bind(GitHubEnterpriseApp).toSelf().inSingletonScope(); bind(BitbucketServerApp).toSelf().inSingletonScope(); - bind(IncrementalPrebuildsService).toSelf().inSingletonScope(); + bind(IncrementalWorkspaceService).toSelf().inSingletonScope(); // payment/billing bind(StripeService).toSelf().inSingletonScope(); diff --git a/components/server/src/prebuilds/github-app.ts b/components/server/src/prebuilds/github-app.ts index 11d7fce609f523..e8773a1e4529a6 100644 --- a/components/server/src/prebuilds/github-app.ts +++ b/components/server/src/prebuilds/github-app.ts @@ -302,11 +302,7 @@ export class GithubApp { context, }); - const shouldRun = Project.hasPrebuildSettings(project) - ? prebuildPrecondition.shouldRun - : this.appRules.shouldRunPrebuild(config, CommitContext.isDefaultBranch(context), false, false); - - if (!shouldRun) { + if (!prebuildPrecondition.shouldRun) { const reason = `GitHub push event: No prebuild.`; log.debug(logCtx, reason, { contextURL }); span.log({ "not-running": reason, config: config }); @@ -516,13 +512,18 @@ export class GithubApp { const contextURL = pr.html_url; const isFork = pr.head.repo.id !== pr.base.repo.id; - const prebuildPrecondition = this.prebuildManager.checkPrebuildPrecondition({ config, project, context }); - - const shouldRun = Project.hasPrebuildSettings(project) - ? prebuildPrecondition.shouldRun - : this.appRules.shouldRunPrebuild(config, false, true, isFork); + if (isFork) { + log.debug({ userId: user.id }, `GitHub PR event from fork.`, { + contextURL, + userId: user.id, + project, + isFork, + }); + return; + } - if (shouldRun) { + const prebuildPrecondition = this.prebuildManager.checkPrebuildPrecondition({ config, project, context }); + if (prebuildPrecondition.shouldRun) { const commitInfo = await this.getCommitInfo(user, ctx.payload.repository.html_url, pr.head.sha); const result = await this.prebuildManager.startPrebuild(tracecContext, { user, diff --git a/components/server/src/prebuilds/github-enterprise-app.ts b/components/server/src/prebuilds/github-enterprise-app.ts index 5ab7afe4b23269..412deacf839934 100644 --- a/components/server/src/prebuilds/github-enterprise-app.ts +++ b/components/server/src/prebuilds/github-enterprise-app.ts @@ -19,7 +19,6 @@ import { GitHubService } from "./github-service"; import { URL } from "url"; import { ContextParser } from "../workspace/context-parser-service"; import { RepoURL } from "../repohost"; -import { GithubAppRules } from "./github-app-rules"; import { UserService } from "../user/user-service"; import { ApplicationError, ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error"; import { ProjectsService } from "../projects/projects-service"; @@ -34,7 +33,6 @@ export class GitHubEnterpriseApp { @inject(TeamDB) private readonly teamDB: TeamDB, @inject(ContextParser) private readonly contextParser: ContextParser, @inject(WebhookEventDB) private readonly webhookEvents: WebhookEventDB, - @inject(GithubAppRules) private readonly appRules: GithubAppRules, @inject(ProjectsService) private readonly projectService: ProjectsService, ) {} @@ -174,10 +172,7 @@ export class GitHubEnterpriseApp { context, }); - const shouldRun = Project.hasPrebuildSettings(project) - ? prebuildPrecondition.shouldRun - : this.appRules.shouldRunPrebuild(config, CommitContext.isDefaultBranch(context), false, false); - if (!shouldRun) { + if (!prebuildPrecondition.shouldRun) { log.info("GitHub Enterprise push event: No prebuild.", { config, context }); await this.webhookEvents.updateEvent(event.id, { diff --git a/components/server/src/prebuilds/incremental-prebuilds-service.ts b/components/server/src/prebuilds/incremental-workspace-service.ts similarity index 92% rename from components/server/src/prebuilds/incremental-prebuilds-service.ts rename to components/server/src/prebuilds/incremental-workspace-service.ts index cdfc37023feaea..abbbf724d0f639 100644 --- a/components/server/src/prebuilds/incremental-prebuilds-service.ts +++ b/components/server/src/prebuilds/incremental-workspace-service.ts @@ -22,8 +22,10 @@ import { ConfigProvider } from "../workspace/config-provider"; import { HostContextProvider } from "../auth/host-context-provider"; import { ImageSourceProvider } from "../workspace/image-source-provider"; +const MAX_HISTORY_DEPTH = 100; + @injectable() -export class IncrementalPrebuildsService { +export class IncrementalWorkspaceService { @inject(Config) protected readonly config: Config; @inject(ConfigProvider) protected readonly configProvider: ConfigProvider; @inject(HostContextProvider) protected readonly hostContextProvider: HostContextProvider; @@ -31,7 +33,7 @@ export class IncrementalPrebuildsService { @inject(WorkspaceDB) protected readonly workspaceDB: WorkspaceDB; public async getCommitHistoryForContext(context: CommitContext, user: User): Promise { - const maxDepth = this.config.incrementalPrebuilds.commitHistory; + const maxDepth = MAX_HISTORY_DEPTH; const hostContext = this.hostContextProvider.get(context.repository.host); const repoProvider = hostContext?.services?.repositoryProvider; if (!repoProvider) { @@ -45,6 +47,7 @@ export class IncrementalPrebuildsService { context.revision, maxDepth, ); + history.commitHistory.unshift(context.revision); if (context.additionalRepositoryCheckoutInfo && context.additionalRepositoryCheckoutInfo.length > 0) { const histories = context.additionalRepositoryCheckoutInfo.map(async (info) => { const commitHistory = await repoProvider.getCommitHistory( @@ -54,6 +57,7 @@ export class IncrementalPrebuildsService { info.revision, maxDepth, ); + commitHistory.unshift(info.revision); return { cloneUrl: info.repository.cloneUrl, commitHistory, @@ -75,14 +79,15 @@ export class IncrementalPrebuildsService { return; } - const imageSource = await this.imageSourceProvider.getImageSource({}, user, context, config); + const imageSourcePromise = this.imageSourceProvider.getImageSource({}, user, context, config); // Note: This query returns only not-garbage-collected prebuilds in order to reduce cardinality // (e.g., at the time of writing, the Gitpod repository has 16K+ prebuilds, but only ~300 not-garbage-collected) const recentPrebuilds = await this.workspaceDB.findPrebuildsWithWorkspace(projectId); + const imageSource = await imageSourcePromise; for (const recentPrebuild of recentPrebuilds) { if ( - await this.isGoodBaseforIncrementalBuild( + this.isGoodBaseforIncrementalBuild( history, config, imageSource, @@ -93,15 +98,16 @@ export class IncrementalPrebuildsService { return recentPrebuild.prebuild; } } + return undefined; } - protected async isGoodBaseforIncrementalBuild( + private isGoodBaseforIncrementalBuild( history: WithCommitHistory, config: WorkspaceConfig, imageSource: WorkspaceImageSource, candidatePrebuild: PrebuiltWorkspace, candidateWorkspace: Workspace, - ): Promise { + ): boolean { if (!history.commitHistory || history.commitHistory.length === 0) { return false; } diff --git a/components/server/src/prebuilds/prebuild-manager.spec.ts b/components/server/src/prebuilds/prebuild-manager.spec.ts index e64ceb9adb7d9a..2f196817ee3173 100644 --- a/components/server/src/prebuilds/prebuild-manager.spec.ts +++ b/components/server/src/prebuilds/prebuild-manager.spec.ts @@ -14,7 +14,7 @@ import { HostContextProvider } from "../auth/host-context-provider"; import { ConfigProvider } from "../workspace/config-provider"; import { Config } from "../config"; import { ProjectsService } from "../projects/projects-service"; -import { IncrementalPrebuildsService } from "./incremental-prebuilds-service"; +import { IncrementalWorkspaceService } from "./incremental-workspace-service"; import { EntitlementService } from "../billing/entitlement-service"; import { CommitContext, Project, ProjectSettings, Repository, WorkspaceConfig } from "@gitpod/gitpod-protocol"; @@ -30,7 +30,7 @@ const containerModule = new ContainerModule((bind) => { bind(ConfigProvider).toConstantValue({} as any); bind(Config).toConstantValue({} as any); bind(ProjectsService).toConstantValue({} as any); - bind(IncrementalPrebuildsService).toConstantValue({} as any); + bind(IncrementalWorkspaceService).toConstantValue({} as any); bind(EntitlementService).toConstantValue({} as any); // #endregion }); @@ -59,7 +59,9 @@ describe("PrebuildManager", () => { ref: "main", }; const settings: ProjectSettings = { - enablePrebuilds: false, + prebuilds: { + enable: false, + }, }; const project = { settings, @@ -89,28 +91,20 @@ describe("PrebuildManager", () => { project, }, { - title: "pre-existing-project/enable-by-default(1)", - shouldRun: true, - reason: "all-branches-selected", + title: "pre-existing-project/disabled-if-not-migrated", + shouldRun: false, + reason: "prebuilds-not-enabled", config, context, project: clone(project, (p) => (p.settings = undefined)), }, - { - title: "pre-existing-project/enable-by-default(2)", - shouldRun: true, - reason: "all-branches-selected", - config, - context, - project: clone(project, (p) => delete p.settings), - }, { title: "prebuilds-not-enabled", shouldRun: false, reason: "prebuilds-not-enabled", config, context, - project: clone(project, (p) => (p.settings!.enablePrebuilds = false)), + project: clone(project, (p) => (p.settings!.prebuilds!.enable = false)), }, { title: "default-branch-only/matched(1)", @@ -122,8 +116,10 @@ describe("PrebuildManager", () => { project, (p) => (p.settings = { - enablePrebuilds: true, - prebuildDefaultBranchOnly: true, + prebuilds: { + enable: true, + branchStrategy: "default-branch", + }, }), ), }, @@ -137,8 +133,10 @@ describe("PrebuildManager", () => { project, (p) => (p.settings = { - enablePrebuilds: true, - prebuildDefaultBranchOnly: undefined, + prebuilds: { + enable: true, + branchStrategy: "default-branch", + }, }), ), }, @@ -152,8 +150,10 @@ describe("PrebuildManager", () => { project, (p) => (p.settings = { - enablePrebuilds: true, - prebuildDefaultBranchOnly: true, + prebuilds: { + enable: true, + branchStrategy: "default-branch", + }, }), ), }, @@ -167,8 +167,10 @@ describe("PrebuildManager", () => { project, (p) => (p.settings = { - enablePrebuilds: true, - prebuildDefaultBranchOnly: true, + prebuilds: { + enable: true, + branchStrategy: "default-branch", + }, }), ), }, @@ -182,8 +184,10 @@ describe("PrebuildManager", () => { project, (p) => (p.settings = { - enablePrebuilds: true, - prebuildDefaultBranchOnly: false, + prebuilds: { + enable: true, + branchStrategy: "all-branches", + }, }), ), }, @@ -197,9 +201,11 @@ describe("PrebuildManager", () => { project, (p) => (p.settings = { - enablePrebuilds: true, - prebuildDefaultBranchOnly: false, - prebuildBranchPattern: "*foo*", + prebuilds: { + enable: true, + branchStrategy: "matched-branches", + branchMatchingPattern: "*foo*", + }, }), ), }, @@ -213,9 +219,11 @@ describe("PrebuildManager", () => { project, (p) => (p.settings = { - enablePrebuilds: true, - prebuildDefaultBranchOnly: false, - prebuildBranchPattern: "*foo*", + prebuilds: { + enable: true, + branchStrategy: "matched-branches", + branchMatchingPattern: "*foo*", + }, }), ), }, @@ -229,9 +237,11 @@ describe("PrebuildManager", () => { project, (p) => (p.settings = { - enablePrebuilds: true, - prebuildDefaultBranchOnly: false, - prebuildBranchPattern: "*feature*", + prebuilds: { + enable: true, + branchStrategy: "matched-branches", + branchMatchingPattern: "*feature*", + }, }), ), }, @@ -245,9 +255,11 @@ describe("PrebuildManager", () => { project, (p) => (p.settings = { - enablePrebuilds: true, - prebuildDefaultBranchOnly: false, - prebuildBranchPattern: "main, bug-*, *feature*", + prebuilds: { + enable: true, + branchStrategy: "matched-branches", + branchMatchingPattern: "main, bug-*, *feature*", + }, }), ), }, @@ -261,9 +273,11 @@ describe("PrebuildManager", () => { project, (p) => (p.settings = { - enablePrebuilds: true, - prebuildDefaultBranchOnly: false, - prebuildBranchPattern: "bug-*, *sub/feature*", + prebuilds: { + enable: true, + branchStrategy: "matched-branches", + branchMatchingPattern: "bug-*, *sub/feature*", + }, }), ), }, @@ -277,9 +291,11 @@ describe("PrebuildManager", () => { project, (p) => (p.settings = { - enablePrebuilds: true, - prebuildDefaultBranchOnly: false, - prebuildBranchPattern: "**", + prebuilds: { + enable: true, + branchStrategy: "matched-branches", + branchMatchingPattern: "**", + }, }), ), }, diff --git a/components/server/src/prebuilds/prebuild-manager.ts b/components/server/src/prebuilds/prebuild-manager.ts index 4c9f3478e1d38f..2627717ef03e71 100644 --- a/components/server/src/prebuilds/prebuild-manager.ts +++ b/components/server/src/prebuilds/prebuild-manager.ts @@ -28,9 +28,7 @@ import { secondsBefore } from "@gitpod/gitpod-protocol/lib/util/timeutil"; import { inject, injectable } from "inversify"; import * as opentracing from "opentracing"; -import { StopWorkspacePolicy } from "@gitpod/ws-manager/lib"; -import { error } from "console"; -import { IncrementalPrebuildsService } from "./incremental-prebuilds-service"; +import { IncrementalWorkspaceService } from "./incremental-workspace-service"; import { PrebuildRateLimiterConfig } from "../workspace/prebuild-rate-limiter"; import { ErrorCodes, ApplicationError } from "@gitpod/gitpod-protocol/lib/messaging/error"; import { EntitlementService, MayStartWorkspaceResult } from "../billing/entitlement-service"; @@ -53,48 +51,16 @@ export interface StartPrebuildParams { @injectable() export class PrebuildManager { - @inject(TracedWorkspaceDB) protected readonly workspaceDB: DBWithTracing; - @inject(WorkspaceService) protected readonly workspaceService: WorkspaceService; - @inject(HostContextProvider) protected readonly hostContextProvider: HostContextProvider; - @inject(ConfigProvider) protected readonly configProvider: ConfigProvider; - @inject(Config) protected readonly config: Config; - @inject(ProjectsService) protected readonly projectService: ProjectsService; - @inject(IncrementalPrebuildsService) protected readonly incrementalPrebuildsService: IncrementalPrebuildsService; - @inject(EntitlementService) protected readonly entitlementService: EntitlementService; - - async abortPrebuildsForBranch(ctx: TraceContext, project: Project, user: User, branch: string): Promise { - const span = TraceContext.startSpan("abortPrebuildsForBranch", ctx); - try { - const prebuilds = await this.workspaceDB - .trace({ span }) - .findActivePrebuiltWorkspacesByBranch(project.id, branch); - const results: Promise[] = []; - for (const prebuild of prebuilds) { - try { - log.info( - { userId: user.id, workspaceId: prebuild.workspace.id }, - "Cancelling Prebuild workspace because a newer commit was pushed to the same branch.", - ); - results.push( - this.workspaceService.stopWorkspace( - user.id, - prebuild.workspace.id, - "prebuild cancelled because a newer commit was pushed to the same branch", - StopWorkspacePolicy.ABORT, - ), - ); - prebuild.prebuild.state = "aborted"; - prebuild.prebuild.error = "A newer commit was pushed to the same branch."; - results.push(this.workspaceDB.trace({ span }).storePrebuiltWorkspace(prebuild.prebuild)); - } catch (err) { - error("Cannot cancel prebuild", { prebuildID: prebuild.prebuild.id }, err); - } - } - await Promise.all(results); - } finally { - span.finish(); - } - } + constructor( + @inject(TracedWorkspaceDB) private readonly workspaceDB: DBWithTracing, + @inject(WorkspaceService) private readonly workspaceService: WorkspaceService, + @inject(HostContextProvider) private readonly hostContextProvider: HostContextProvider, + @inject(ConfigProvider) private readonly configProvider: ConfigProvider, + @inject(Config) private readonly config: Config, + @inject(ProjectsService) private readonly projectService: ProjectsService, + @inject(IncrementalWorkspaceService) private readonly incrementalPrebuildsService: IncrementalWorkspaceService, + @inject(EntitlementService) private readonly entitlementService: EntitlementService, + ) {} private async findNonFailedPrebuiltWorkspace(ctx: TraceContext, projectId: string, commitSHA: string) { const existingPB = await this.workspaceDB.trace(ctx).findPrebuiltWorkspaceByCommit(projectId, commitSHA); @@ -167,13 +133,6 @@ export class PrebuildManager { } } } - if (context.ref && !project.settings?.keepOutdatedPrebuildsRunning) { - try { - await this.abortPrebuildsForBranch({ span }, project, user, context.ref); - } catch (e) { - console.error("Error aborting prebuilds", e); - } - } const prebuildContext: StartPrebuildContext = { title: `Prebuild of "${context.title}"`, @@ -186,13 +145,14 @@ export class PrebuildManager { const { commitHistory, additionalRepositoryCommitHistories } = await this.incrementalPrebuildsService.getCommitHistoryForContext(context, user); - const prebuildEveryNthCommit = project?.settings?.prebuildEveryNthCommit || 0; - if (!forcePrebuild && prebuildEveryNthCommit > 0) { + const prebuildSettings = Project.getPrebuildSettings(project); + const prebuildInterval = prebuildSettings.prebuildInterval; + if (!forcePrebuild && prebuildInterval > 0) { const history = { - commitHistory: commitHistory?.slice(0, prebuildEveryNthCommit), + commitHistory: commitHistory?.slice(0, prebuildInterval), additionalRepositoryCommitHistories: additionalRepositoryCommitHistories?.map((repoHist) => ({ cloneUrl: repoHist.cloneUrl, - commitHistory: repoHist.commitHistory.slice(0, prebuildEveryNthCommit), + commitHistory: repoHist.commitHistory.slice(0, prebuildInterval), })), }; const prebuild = await this.incrementalPrebuildsService.findGoodBaseForIncrementalBuild( @@ -205,15 +165,6 @@ export class PrebuildManager { if (prebuild) { return { prebuildId: prebuild.id, wsid: prebuild.buildWorkspaceId, done: true }; } - } else if (this.shouldPrebuildIncrementally(project)) { - // We store the commit histories in the `StartPrebuildContext` in order to pass them down to - // `WorkspaceFactoryEE.createForStartPrebuild`. - if (commitHistory) { - prebuildContext.commitHistory = commitHistory; - } - if (additionalRepositoryCommitHistories) { - prebuildContext.additionalRepositoryCommitHistories = additionalRepositoryCommitHistories; - } } const workspace = await this.workspaceService.createWorkspace( @@ -352,17 +303,16 @@ export class PrebuildManager { return { shouldRun: false, reason: "no-tasks-in-gitpod-config" }; } - const isPrebuildsEnabled = Project.isPrebuildsEnabled(project); - if (!isPrebuildsEnabled) { + const prebuildSettings = Project.getPrebuildSettings(project); + if (!prebuildSettings.enable) { return { shouldRun: false, reason: "prebuilds-not-enabled" }; } - const strategy = Project.getPrebuildBranchStrategy(project); - if (strategy === "allBranches") { + if (prebuildSettings.branchStrategy === "all-branches") { return { shouldRun: true, reason: "all-branches-selected" }; } - if (strategy === "defaultBranch") { + if (prebuildSettings.branchStrategy === "default-branch") { const defaultBranch = context.repository.defaultBranch; if (!defaultBranch) { log.debug("CommitContext is missing the default branch. Ignoring request.", { context }); @@ -375,20 +325,14 @@ export class PrebuildManager { return { shouldRun: false, reason: "default-branch-unmatched" }; } - if (strategy === "selectedBranches") { + if (prebuildSettings.branchStrategy === "matched-branches" && !!prebuildSettings.branchMatchingPattern) { const branchName = context.ref; if (!branchName) { log.debug("CommitContext is missing the branch name. Ignoring request.", { context }); return { shouldRun: false, reason: "branch-name-missing-in-commit-context" }; } - const branchNamePattern = project.settings?.prebuildBranchPattern?.trim(); - if (!branchNamePattern) { - // no pattern provided is treated as run on all branches - return { shouldRun: true, reason: "all-branches-selected" }; - } - - for (let pattern of branchNamePattern.split(",")) { + for (let pattern of prebuildSettings.branchMatchingPattern.split(",")) { // prepending `**/` as branch names can be 'refs/heads/something/feature-x' // and we want to allow simple patterns like: `feature-*` pattern = "**/" + pattern.trim(); @@ -398,7 +342,7 @@ export class PrebuildManager { } } catch (error) { log.debug("Ignored error with attempt to match a branch by pattern.", { - branchNamePattern, + prebuildSettings, error: error?.message, }); } @@ -410,15 +354,6 @@ export class PrebuildManager { return { shouldRun: false, reason: "unknown-strategy" }; } - private shouldPrebuildIncrementally(project: Project): boolean { - if (project?.settings?.useIncrementalPrebuilds) { - return true; - } - const trimRepoUrl = (url: string) => url.replace(/\/$/, "").replace(/\.git$/, ""); - const repoUrl = trimRepoUrl(project.cloneUrl); - return this.config.incrementalPrebuilds.repositoryPasslist.some((url) => trimRepoUrl(url) === repoUrl); - } - async fetchConfig( ctx: TraceContext, user: User, diff --git a/components/server/src/projects/projects-service.spec.db.ts b/components/server/src/projects/projects-service.spec.db.ts index 98ea872e7ef9ca..32f7ca2aef95e9 100644 --- a/components/server/src/projects/projects-service.spec.db.ts +++ b/components/server/src/projects/projects-service.spec.db.ts @@ -4,9 +4,9 @@ * See License.AGPL.txt in the project root for license information. */ -import { TypeORM, UserDB } from "@gitpod/gitpod-db/lib"; +import { ProjectDB, TypeORM, UserDB, WorkspaceDB } from "@gitpod/gitpod-db/lib"; import { resetDB } from "@gitpod/gitpod-db/lib/test/reset-db"; -import { Organization, User } from "@gitpod/gitpod-protocol"; +import { Organization, Project, ProjectSettings, User } from "@gitpod/gitpod-protocol"; import { Experiments } from "@gitpod/gitpod-protocol/lib/experiments/configcat-server"; import { ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error"; import * as chai from "chai"; @@ -16,6 +16,7 @@ import { OrganizationService } from "../orgs/organization-service"; import { expectError } from "../test/expect-utils"; import { createTestContainer } from "../test/service-testing-container-module"; import { ProjectsService } from "./projects-service"; +import { daysBefore } from "@gitpod/gitpod-protocol/lib/util/timeutil"; const expect = chai.expect; @@ -111,26 +112,26 @@ describe("ProjectsService", async () => { await ps.updateProject(owner, { id: project.id, settings: { - prebuildEveryNthCommit: 1, + prebuilds: { prebuildInterval: 1 }, }, }); const updatedProject1 = await ps.getProject(owner.id, project.id); - expect(updatedProject1?.settings?.prebuildEveryNthCommit).to.equal(1); + expect(updatedProject1?.settings?.prebuilds?.prebuildInterval).to.equal(1); await ps.updateProject(member, { id: project.id, settings: { - prebuildEveryNthCommit: 2, + prebuilds: { prebuildInterval: 2 }, }, }); const updatedProject2 = await ps.getProject(member.id, project.id); - expect(updatedProject2?.settings?.prebuildEveryNthCommit).to.equal(2); + expect(updatedProject2?.settings?.prebuilds?.prebuildInterval).to.equal(2); await expectError(ErrorCodes.NOT_FOUND, () => ps.updateProject(stranger, { id: project.id, settings: { - prebuildEveryNthCommit: 3, + prebuilds: { prebuildInterval: 3 }, }, }), ); @@ -145,7 +146,7 @@ describe("ProjectsService", async () => { await ps.updateProject(owner, { id: project.id, settings: { - enablePrebuilds: true, + prebuilds: { enable: true }, }, }); expect(webhooks).to.contain(project.cloneUrl); @@ -156,13 +157,15 @@ describe("ProjectsService", async () => { webhooks.clear(); const cloneUrl = "https://github.com/gitpod-io/gitpod.git"; const ps = container.get(ProjectsService); - const project = await createTestProject(ps, org, owner, "test", cloneUrl, { - /* empty settings */ + const project = await createTestProject(ps, org, owner, { + name: "test-pro", + cloneUrl, + settings: {}, }); await ps.updateProject(owner, { id: project.id, settings: { - enablePrebuilds: true, + prebuilds: { enable: true }, }, }); expect(webhooks).to.contain(project.cloneUrl); @@ -172,8 +175,8 @@ describe("ProjectsService", async () => { it("should findProjects", async () => { const ps = container.get(ProjectsService); const project = await createTestProject(ps, org, owner); - await createTestProject(ps, org, owner, "my-project-2", "https://github.com/foo/bar.git"); - await createTestProject(ps, org, member, "my-project-3", "https://github.com/foo/baz.git"); + await createTestProject(ps, org, owner, { name: "my-project-2", cloneUrl: "https://host/account/repo.git" }); + await createTestProject(ps, org, member, { name: "my-project-3", cloneUrl: "https://host/account/repo.git" }); const foundProjects = await ps.findProjects(owner.id, { orderBy: "name", @@ -189,26 +192,132 @@ describe("ProjectsService", async () => { await expectError(ErrorCodes.NOT_FOUND, () => ps.getProject(stranger.id, project.id)); await expectError(ErrorCodes.NOT_FOUND, () => ps.getProjects(stranger.id, org.id)); }); -}); -async function createTestProject( - ps: ProjectsService, - org: Organization, - owner: User, - name = "my-project", - cloneUrl = "https://github.com/gitpod-io/gitpod.git", - projectSettings = ProjectsService.PROJECT_SETTINGS_DEFAULTS, -) { - const project = await ps.createProject( - { - name, - slug: name, - teamId: org.id, + it("prebuild settings migration / old and inactive project / uses defaults", async () => { + const ps = container.get(ProjectsService); + const cloneUrl = "https://github.com/gitpod-io/gitpod.git"; + const oldProject = await createTestProject(ps, org, owner, { + name: "my-project", + cloneUrl, + creationTime: daysBefore(new Date().toISOString(), 20), + settings: { + enablePrebuilds: true, + prebuildEveryNthCommit: 3, + workspaceClasses: { prebuild: "ultra" }, + prebuildDefaultBranchOnly: false, + prebuildBranchPattern: "feature-*", + }, + }); + const project = await ps.getProject(owner.id, oldProject.id); + expect(project.settings).to.deep.equal({ + prebuilds: { + ...Project.PREBUILD_SETTINGS_DEFAULTS, + enable: false, + }, + workspaceClasses: {}, + }); + }); + + it("prebuild settings migration / inactive project / uses defaults", async () => { + const ps = container.get(ProjectsService); + const cloneUrl = "https://github.com/gitpod-io/gitpod.git"; + const oldProject = await createTestProject(ps, org, owner, { + name: "my-project", + cloneUrl, + creationTime: daysBefore(new Date().toISOString(), 1), + settings: { + enablePrebuilds: true, + prebuildEveryNthCommit: 3, + workspaceClasses: { prebuild: "ultra" }, + prebuildDefaultBranchOnly: false, + prebuildBranchPattern: "feature-*", + }, + }); + const project = await ps.getProject(owner.id, oldProject.id); + expect(project.settings).to.deep.equal({ + prebuilds: { + ...Project.PREBUILD_SETTINGS_DEFAULTS, + enable: false, + }, + workspaceClasses: {}, + }); + }); + + it("prebuild settings migration / new and active project / updated settings", async () => { + const ps = container.get(ProjectsService); + const cloneUrl = "https://github.com/gitpod-io/gitpod.git"; + const oldProject = await createTestProject(ps, org, owner, { + name: "my-project", cloneUrl, - appInstallationId: "noid", + creationTime: daysBefore(new Date().toISOString(), 1), + settings: { + enablePrebuilds: true, + prebuildEveryNthCommit: 13, + workspaceClasses: { prebuild: "ultra" }, + prebuildDefaultBranchOnly: false, + prebuildBranchPattern: "feature-*", + }, + }); + await createWorkspaceForProject(oldProject.id, 1); + const project = await ps.getProject(owner.id, oldProject.id); + expect(project.settings).to.deep.equal({ + prebuilds: { + enable: true, + prebuildInterval: 20, + workspaceClass: "ultra", + branchStrategy: "matched-branches", + branchMatchingPattern: "feature-*", + }, + workspaceClasses: {}, + }); + }); + + async function createTestProject( + ps: ProjectsService, + org: Organization, + owner: User, + partial: Partial = { + name: "my-project", + cloneUrl: "https://github.com/gitpod-io/gitpod.git", + settings: { + prebuilds: Project.PREBUILD_SETTINGS_DEFAULTS, + }, }, - owner, - projectSettings, - ); - return project; -} + ) { + let project = await ps.createProject( + { + name: partial.name!, + slug: "deprecated", + teamId: org.id, + cloneUrl: partial.cloneUrl!, + appInstallationId: "noid", + }, + owner, + partial.settings, + ); + + // need to patch `creationTime`? + if (partial.creationTime) { + const projectDB = container.get(ProjectDB); + await projectDB.updateProject({ ...project, creationTime: partial.creationTime }); + project = (await projectDB.findProjectById(project.id))!; + } + + return project; + } + + async function createWorkspaceForProject(projectId: string, daysAgo: number) { + const workspaceDB = container.get(WorkspaceDB); + + await workspaceDB.storePrebuiltWorkspace({ + projectId, + id: "prebuild123", + buildWorkspaceId: "12345", + creationTime: daysBefore(new Date().toISOString(), daysAgo), + cloneURL: "", + commit: "", + state: "available", + statusVersion: 0, + }); + } +}); diff --git a/components/server/src/projects/projects-service.ts b/components/server/src/projects/projects-service.ts index 660a1c25a58cff..d632592f1314c3 100644 --- a/components/server/src/projects/projects-service.ts +++ b/components/server/src/projects/projects-service.ts @@ -18,21 +18,22 @@ import { import { HostContextProvider } from "../auth/host-context-provider"; import { RepoURL } from "../repohost"; import { log } from "@gitpod/gitpod-protocol/lib/util/logging"; -import { PartialProject, ProjectSettings, ProjectUsage } from "@gitpod/gitpod-protocol/lib/teams-projects-protocol"; +import { + PartialProject, + PrebuildSettings, + ProjectSettings, + ProjectUsage, +} from "@gitpod/gitpod-protocol/lib/teams-projects-protocol"; import { IAnalyticsWriter } from "@gitpod/gitpod-protocol/lib/analytics"; import { ErrorCodes, ApplicationError } from "@gitpod/gitpod-protocol/lib/messaging/error"; import { URL } from "url"; -import { Authorizer } from "../authorization/authorizer"; +import { Authorizer, SYSTEM_USER } from "../authorization/authorizer"; import { TransactionalContext } from "@gitpod/gitpod-db/lib/typeorm/transactional-db-impl"; import { ScmService } from "./scm-service"; +import { daysBefore, isDateSmaller } from "@gitpod/gitpod-protocol/lib/util/timeutil"; @injectable() export class ProjectsService { - public static PROJECT_SETTINGS_DEFAULTS: ProjectSettings = { - enablePrebuilds: false, - prebuildDefaultBranchOnly: true, - }; - constructor( @inject(ProjectDB) private readonly projectDB: ProjectDB, @inject(TracedWorkspaceDB) private readonly workspaceDb: DBWithTracing, @@ -49,13 +50,14 @@ export class ProjectsService { if (!project) { throw new ApplicationError(ErrorCodes.NOT_FOUND, `Project ${projectId} not found.`); } - return project; + return this.migratePrebuildSettingsOnDemand(project); } async getProjects(userId: string, orgId: string): Promise { await this.auth.checkPermissionOnOrganization(userId, "read_info", orgId); const projects = await this.projectDB.findProjects(orgId); - return await this.filterByReadAccess(userId, projects); + const filteredProjects = await this.filterByReadAccess(userId, projects); + return Promise.all(filteredProjects.map(this.migratePrebuildSettingsOnDemand)); } async findProjects( @@ -79,7 +81,7 @@ export class ProjectsService { const total = projects.total; return { total, - rows, + rows: await Promise.all(rows.map(this.migratePrebuildSettingsOnDemand)), }; } @@ -109,7 +111,7 @@ export class ProjectsService { result.push(project); } } - return result; + return Promise.all(result.map(this.migratePrebuildSettingsOnDemand)); } async markActive( @@ -195,7 +197,7 @@ export class ProjectsService { async createProject( { name, slug, cloneUrl, teamId, appInstallationId }: CreateProjectParams, installer: User, - projectSettingsDefaults: ProjectSettings = ProjectsService.PROJECT_SETTINGS_DEFAULTS, + projectSettingsDefaults: ProjectSettings = { prebuilds: Project.PREBUILD_SETTINGS_DEFAULTS }, ): Promise { await this.auth.checkPermissionOnOrganization(installer.id, "create_project", teamId); @@ -211,7 +213,7 @@ export class ProjectsService { const parsedUrl = RepoURL.parseRepoUrl(cloneUrl); if (!parsedUrl) { - throw new ApplicationError(ErrorCodes.BAD_REQUEST, "Clone URL must be a valid URL."); + throw new ApplicationError(ErrorCodes.BAD_REQUEST, "Clone URL must be a repository URL."); } const project = Project.create({ @@ -358,13 +360,13 @@ export class ProjectsService { } private async handleEnablePrebuild(user: User, partialProject: PartialProject): Promise { - const enablePrebuildsNew = partialProject?.settings?.enablePrebuilds; + const enablePrebuildsNew = partialProject?.settings?.prebuilds?.enable; if (typeof enablePrebuildsNew === "boolean") { const project = await this.projectDB.findProjectById(partialProject.id); if (!project) { return; } - const enablePrebuildsPrev = !!project.settings?.enablePrebuilds; + const enablePrebuildsPrev = !!project.settings?.prebuilds?.enable; const installWebhook = enablePrebuildsNew && !enablePrebuildsPrev; const uninstallWebhook = !enablePrebuildsNew && enablePrebuildsPrev; if (installWebhook) { @@ -378,15 +380,15 @@ export class ProjectsService { } async isProjectConsideredInactive(userId: string, projectId: string): Promise { + const isOlderThan7Days = (d1: string) => isDateSmaller(d1, daysBefore(new Date().toISOString(), 7)); + await this.auth.checkPermissionOnProject(userId, "read_info", projectId); const usage = await this.projectDB.getProjectUsage(projectId); if (!usage?.lastWorkspaceStart) { - return false; + const project = await this.projectDB.findProjectById(projectId); + return !project || isOlderThan7Days(project.creationTime); } - const now = Date.now(); - const lastUse = new Date(usage.lastWorkspaceStart).getTime(); - const inactiveProjectTime = 1000 * 60 * 60 * 24 * 7 * 1; // 1 week - return now - lastUse > inactiveProjectTime; + return isOlderThan7Days(usage.lastWorkspaceStart); } async getPrebuildEvents(userId: string, projectId: string): Promise { @@ -404,4 +406,69 @@ export class ProjectsService { message: we.message, })); } + + private async migratePrebuildSettingsOnDemand(project: Project): Promise { + if (!!project.settings?.prebuilds) { + return project; // already migrated + } + try { + const logCtx: any = { oldSettings: { ...project.settings } }; + const newPrebuildSettings: PrebuildSettings = { enable: false, ...Project.PREBUILD_SETTINGS_DEFAULTS }; + + // if workspaces were running in the past week + const isInactive = await this.isProjectConsideredInactive(SYSTEM_USER, project.id); + logCtx.isInactive = isInactive; + if (!isInactive) { + const sevenDaysAgo = new Date(daysBefore(new Date().toISOString(), 7)); + const count = await this.workspaceDb.trace({}).countUnabortedPrebuildsSince(project.id, sevenDaysAgo); + logCtx.count = count; + if (count > 0) { + const defaults = Project.PREBUILD_SETTINGS_DEFAULTS; + newPrebuildSettings.enable = true; + newPrebuildSettings.prebuildInterval = Math.max( + project.settings?.prebuildEveryNthCommit || 0, + defaults.prebuildInterval, + ); + + newPrebuildSettings.branchStrategy = !!project.settings?.prebuildBranchPattern + ? "matched-branches" + : defaults.branchStrategy; + newPrebuildSettings.branchMatchingPattern = + project.settings?.prebuildBranchPattern || defaults.branchMatchingPattern; + newPrebuildSettings.workspaceClass = project.settings?.workspaceClasses?.prebuild; + } + } + + // update new settings + project = (await this.projectDB.findProjectById(project.id))!; + if (!project) { + throw new ApplicationError(ErrorCodes.INTERNAL_SERVER_ERROR, "Not found"); + } + if (!!project.settings?.prebuilds) { + return project; // already migrated + } + const newSettings = { ...project.settings }; + project.settings = newSettings; + project.settings.prebuilds = newPrebuildSettings; + delete newSettings.enablePrebuilds; + delete newSettings.prebuildBranchPattern; + delete newSettings.prebuildDefaultBranchOnly; + delete newSettings.prebuildEveryNthCommit; + delete newSettings.allowUsingPreviousPrebuilds; + delete newSettings.keepOutdatedPrebuildsRunning; + delete newSettings.useIncrementalPrebuilds; + delete newSettings.workspaceClasses?.prebuild; + await this.projectDB.updateProject({ + id: project.id, + settings: project.settings, + }); + logCtx.newPrebuildSettings = newPrebuildSettings; + log.info("Prebuild settings migrated.", { projectId: project.id, logCtx }); + + return project; + } catch (error) { + log.error("Prebuild settings migration failed.", { projectId: project.id, error }); + return project; + } + } } diff --git a/components/server/src/workspace/gitpod-server-impl.ts b/components/server/src/workspace/gitpod-server-impl.ts index 07c5db7a32e9a6..40583e9286d6a8 100644 --- a/components/server/src/workspace/gitpod-server-impl.ts +++ b/components/server/src/workspace/gitpod-server-impl.ts @@ -83,7 +83,6 @@ import { WorkspaceAndInstance, } from "@gitpod/gitpod-protocol/lib/admin-protocol"; import { ApplicationError, ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error"; -import { Cancelable } from "@gitpod/gitpod-protocol/lib/util/cancelable"; import { log, LogContext } from "@gitpod/gitpod-protocol/lib/util/logging"; import { InterfaceWithTraceContext, @@ -148,7 +147,7 @@ import { createCookielessId, maskIp } from "../analytics"; import { getExperimentsClientForBackend } from "@gitpod/gitpod-protocol/lib/experiments/configcat-server"; import { LinkedInService } from "../linkedin-service"; import { SnapshotService, WaitForSnapshotOptions } from "./snapshot-service"; -import { IncrementalPrebuildsService } from "../prebuilds/incremental-prebuilds-service"; +import { IncrementalWorkspaceService } from "../prebuilds/incremental-workspace-service"; import { PrebuildManager } from "../prebuilds/prebuild-manager"; import { GitHubAppSupport } from "../github/github-app-support"; import { GitLabAppSupport } from "../gitlab/gitlab-app-support"; @@ -206,7 +205,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { @inject(BitbucketAppSupport) private readonly bitbucketAppSupport: BitbucketAppSupport, @inject(PrebuildManager) private readonly prebuildManager: PrebuildManager, - @inject(IncrementalPrebuildsService) private readonly incrementalPrebuildsService: IncrementalPrebuildsService, + @inject(IncrementalWorkspaceService) private readonly incrementalPrebuildsService: IncrementalWorkspaceService, @inject(ConfigProvider) private readonly configProvider: ConfigProvider, @inject(WorkspaceService) private readonly workspaceService: WorkspaceService, @inject(SnapshotService) private readonly snapshotService: SnapshotService, @@ -341,9 +340,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { projectId: string, context: WorkspaceContext, organizationId?: string, - ignoreRunningPrebuild?: boolean, - allowUsingPreviousPrebuilds?: boolean, - ): Promise { + ): Promise { const ctx = TraceContext.childContext("findPrebuiltWorkspace", parentCtx); try { if (!(CommitContext.is(context) && context.repository.cloneUrl && context.revision)) { @@ -356,162 +353,44 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { const cloneUrl = context.repository.cloneUrl; let prebuiltWorkspace: PrebuiltWorkspace | undefined; const logPayload = { - allowUsingPreviousPrebuilds, - ignoreRunningPrebuild, cloneUrl, commit: commitSHAs, prebuiltWorkspace, }; if (OpenPrebuildContext.is(context)) { prebuiltWorkspace = await this.workspaceDb.trace(ctx).findPrebuildByID(context.openPrebuildID); - if ( - prebuiltWorkspace?.cloneURL !== cloneUrl && - (ignoreRunningPrebuild || prebuiltWorkspace?.state === "available") - ) { + if (prebuiltWorkspace?.cloneURL !== cloneUrl) { // prevent users from opening arbitrary prebuilds this way - they must match the clone URL so that the resource guards are correct. - return; + return undefined; } } else { log.debug(logCtx, "Looking for prebuilt workspace: ", logPayload); - prebuiltWorkspace = await this.workspaceDb - .trace(ctx) - .findPrebuiltWorkspaceByCommit(projectId, commitSHAs); - if (!prebuiltWorkspace && allowUsingPreviousPrebuilds) { - const { config } = await this.configProvider.fetchConfig({}, user, context, organizationId); - const history = await this.incrementalPrebuildsService.getCommitHistoryForContext(context, user); - prebuiltWorkspace = await this.incrementalPrebuildsService.findGoodBaseForIncrementalBuild( - context, - config, - history, - user, - projectId, - ); - } + const configPromise = this.configProvider.fetchConfig({}, user, context, organizationId); + const history = await this.incrementalPrebuildsService.getCommitHistoryForContext(context, user); + const { config } = await configPromise; + prebuiltWorkspace = await this.incrementalPrebuildsService.findGoodBaseForIncrementalBuild( + context, + config, + history, + user, + projectId, + ); } if (!prebuiltWorkspace?.projectId) { - return; + return undefined; } // check if the user has access to the project if (!(await this.auth.hasPermissionOnProject(user.id, "read_prebuild", prebuiltWorkspace.projectId))) { return undefined; } - if (prebuiltWorkspace.state === "available") { - log.info(logCtx, `Found prebuilt workspace for ${cloneUrl}:${commitSHAs}`, logPayload); - const result: PrebuiltWorkspaceContext = { - title: context.title, - originalContext: context, - prebuiltWorkspace, - }; - return result; - } else if (prebuiltWorkspace.state === "queued") { - // waiting for a prebuild that has not even started yet, doesn't make sense. - // starting a workspace from git will be faster anyway - return; - } else if (prebuiltWorkspace.state === "building") { - if (ignoreRunningPrebuild) { - // in force mode we ignore running prebuilds as we want to start a workspace as quickly as we can. - return; - } - - const workspaceID = prebuiltWorkspace.buildWorkspaceId; - const makeResult = (instanceID: string): WorkspaceCreationResult => { - return { - runningWorkspacePrebuild: { - prebuildID: prebuiltWorkspace!.id, - workspaceID, - instanceID, - starting: "queued", - sameCluster: false, - }, - }; - }; - - const wsi = await this.workspaceDb.trace(ctx).findCurrentInstance(workspaceID); - if (!wsi || wsi.stoppedTime !== undefined) { - return; - } - - // (AT) At this point we found a running/building prebuild, which might also include - // image build in current state. - // - // The owner's client connection is automatically registered to listen on instance updates. - // For the remaining client connections which would handle `createWorkspace` and end up here, it - // also would be reasonable to listen on the instance updates of a running prebuild, or image build. - // - // We need to be forwarded the WorkspaceInstanceUpdates in the frontend, because we do not have - // any other means to reliably learn about the status about image builds, yet. - // Once we have those, we should remove this. - // - const ws = await this.workspaceDb.trace(ctx).findById(workspaceID); - const relatedPrebuildFound = !!ws && !!wsi && ws.ownerId !== this.userID; - if (relatedPrebuildFound && !this.disposables.disposed) { - const resetListenerFromRedis = this.subscriber.listenForWorkspaceInstanceUpdates( - ws.ownerId, - (ctx, instance) => { - if (instance.id === wsi.id) { - this.forwardInstanceUpdateToClient(ctx, instance); - if (instance.status.phase === "stopped") { - resetListenerFromRedis.dispose(); - } - } - }, - ); - this.disposables.push(resetListenerFromRedis); - } - - const result = makeResult(wsi.id); - - const inSameCluster = wsi.region === this.config.installationShortname; - if (!inSameCluster) { - /* We need to wait for this prebuild to finish before we return from here. - * This creation mode is meant to be used once we have gone through default mode, have confirmation from the - * message bus that the prebuild is done, and now only have to wait for dbsync to come through. Thus, - * in this mode we'll poll the database until the prebuild is ready (or we time out). - * - * Note: This polling mechanism only makes sense if the prebuild runs in cluster different from ours. - * Otherwise there's no dbsync inbetween that we might have to wait for. - * - * DB sync interval is 2 seconds at the moment, we wait ten "ticks" for the data to be synchronized. - */ - const finishedPrebuiltWorkspace = await this.pollDatabaseUntilPrebuildIsAvailable( - ctx, - prebuiltWorkspace.id, - 20000, - ); - if (!finishedPrebuiltWorkspace) { - log.warn( - logCtx, - "did not find a finished prebuild in the database despite waiting long enough after msgbus confirmed that the prebuild had finished", - logPayload, - ); - return; - } else { - return { - title: context.title, - originalContext: context, - prebuiltWorkspace: finishedPrebuiltWorkspace, - } as PrebuiltWorkspaceContext; - } - } - - /* This is the default mode behaviour: we present the running prebuild to the user so that they can see the logs - * or choose to force the creation of a workspace. - */ - if (wsi.status.phase != "running") { - result.runningWorkspacePrebuild!.starting = "starting"; - } else { - result.runningWorkspacePrebuild!.starting = "running"; - } - log.info( - logCtx, - `Found prebuilding (starting=${ - result.runningWorkspacePrebuild!.starting - }) workspace for ${cloneUrl}:${commitSHAs}`, - logPayload, - ); - return result; - } + log.info(logCtx, `Found prebuilt workspace for ${cloneUrl}:${commitSHAs}`, logPayload); + const result: PrebuiltWorkspaceContext = { + title: context.title, + originalContext: context, + prebuiltWorkspace, + }; + return result; } catch (e) { TraceContext.setError(ctx, e); throw e; @@ -1370,22 +1249,9 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { runningInstancesPromise, ); - // TODO (se) findPrebuiltWorkspace also needs the organizationId once we limit prebuild reuse to the same org - const prebuiltWorkspace = - project && - (await this.findPrebuiltWorkspace( - ctx, - user, - project.id, - context, - options.organizationId, - options.ignoreRunningPrebuild, - options.allowUsingPreviousPrebuilds || project.settings?.allowUsingPreviousPrebuilds, - )); - if (WorkspaceCreationResult.is(prebuiltWorkspace)) { - ctx.span?.log({ prebuild: "running" }); - return prebuiltWorkspace as WorkspaceCreationResult; - } + const prebuiltWorkspace = project?.settings?.prebuilds?.enable + ? await this.findPrebuiltWorkspace(ctx, user, project.id, context, options.organizationId) + : undefined; if (WorkspaceContext.is(prebuiltWorkspace)) { ctx.span?.log({ prebuild: "available" }); context = prebuiltWorkspace; @@ -1589,28 +1455,6 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { return undefined; } - private async pollDatabaseUntilPrebuildIsAvailable( - ctx: TraceContext, - prebuildID: string, - timeoutMS: number, - ): Promise { - const pollPrebuildAvailable = new Cancelable(async (cancel) => { - const prebuild = await this.workspaceDb.trace(ctx).findPrebuildByID(prebuildID); - if (prebuild && PrebuiltWorkspace.isAvailable(prebuild)) { - return prebuild; - } - return; - }); - - const result = await Promise.race([ - pollPrebuildAvailable.run(), - new Promise((resolve, reject) => setTimeout(() => resolve(undefined), timeoutMS)), - ]); - pollPrebuildAvailable.cancel(); - - return result; - } - public async getFeaturedRepositories(ctx: TraceContext): Promise { const user = await this.checkAndBlockUser("getFeaturedRepositories"); const repositories = await this.workspaceDb.trace(ctx).getFeaturedRepositories(); diff --git a/components/server/src/workspace/workspace-factory.ts b/components/server/src/workspace/workspace-factory.ts index 7dfc5c011e70db..f867134ce0c165 100644 --- a/components/server/src/workspace/workspace-factory.ts +++ b/components/server/src/workspace/workspace-factory.ts @@ -30,8 +30,6 @@ import { inject, injectable } from "inversify"; import { RepoURL } from "../repohost"; import { ConfigProvider } from "./config-provider"; import { ImageSourceProvider } from "./image-source-provider"; -import { DeepPartial } from "@gitpod/gitpod-protocol/lib/util/deep-partial"; -import { IncrementalPrebuildsService } from "../prebuilds/incremental-prebuilds-service"; import { increasePrebuildsStartedCounter } from "../prometheus-metrics"; import { Authorizer } from "../authorization/authorizer"; @@ -42,7 +40,6 @@ export class WorkspaceFactory { @inject(TeamDB) private readonly teamDB: TeamDB, @inject(ConfigProvider) private configProvider: ConfigProvider, @inject(ImageSourceProvider) private imageSourceProvider: ImageSourceProvider, - @inject(IncrementalPrebuildsService) private readonly incrementalPrebuildsService: IncrementalPrebuildsService, @inject(Authorizer) private readonly authorizer: Authorizer, ) {} @@ -111,69 +108,14 @@ export class WorkspaceFactory { await assertNoPrebuildIsRunningForSameCommit(); - const { config } = await this.configProvider.fetchConfig({ span }, user, context.actual, organizationId); - - // If an incremental prebuild was requested, see if we can find a recent prebuild to act as a base. - let ws; - const recentPrebuild = await this.incrementalPrebuildsService.findGoodBaseForIncrementalBuild( - commitContext, - config, - context, + let ws = await this.createForCommit( + { span }, user, - projectId, + organizationId, + project, + commitContext, + normalizedContextURL, ); - if (recentPrebuild) { - const loggedContext = filterForLogging(context); - log.info({ userId: user.id }, "Using incremental prebuild base", { - basePrebuildId: recentPrebuild.id, - context: loggedContext, - }); - - const incrementalPrebuildContext: PrebuiltWorkspaceContext = { - title: `Incremental prebuild of "${commitContext.title}"`, - originalContext: commitContext, - prebuiltWorkspace: recentPrebuild, - }; - - // repeated assertion on prebuilds triggered for same commit here, in order to - // reduce likelihood of duplicates if for instance handled by two different - // server pods. - await assertNoPrebuildIsRunningForSameCommit(); - - ws = await this.createForPrebuiltWorkspace( - { span }, - user, - organizationId, - project, - incrementalPrebuildContext, - normalizedContextURL, - ); - // Overwrite the config from the parent prebuild: - // `createForPrebuiltWorkspace` 1:1 copies the config from the parent prebuild. - // Above, we've made sure that the parent's prebuild tasks (before/init/prebuild) are still the same as now. - // However, other non-prebuild config items might be outdated (e.g. any command task, VS Code extension, ...) - // To fix this, we overwrite the new prebuild's config with the most-recently fetched config. - // See also: https://github.com/gitpod-io/gitpod/issues/7475 - //TODO(sven) doing side effects on objects back and forth is complicated and error-prone. We should rather make sure we pass in the config when creating the prebuiltWorkspace. - ws.config = config; - } - - // repeated assertion on prebuilds triggered for same commit here, in order to - // reduce likelihood of duplicates if for instance handled by two different - // server pods. - await assertNoPrebuildIsRunningForSameCommit(); - - if (!ws) { - // No suitable parent prebuild was found -- create a (fresh) full prebuild. - ws = await this.createForCommit( - { span }, - user, - organizationId, - project, - commitContext, - normalizedContextURL, - ); - } ws.type = "prebuild"; ws.projectId = project?.id; ws = await this.db.trace({ span }).store(ws); @@ -436,19 +378,3 @@ export class WorkspaceFactory { return await generateWorkspaceID(); } } - -function filterForLogging(context: StartPrebuildContext) { - return >{ - actual: context.actual, - branch: context.branch, - normalizedContextURL: context.normalizedContextURL, - ref: context.ref, - title: context.title, - forceCreateNewWorkspace: context.forceCreateNewWorkspace, - forceImageBuild: context.forceImageBuild, - project: context.project, - // placeholders for the actual history - commitHistoryLength: context.commitHistory?.length || 0, - additionalRepositoryCommitHistoriesLength: context.additionalRepositoryCommitHistories?.length || 0, - }; -} diff --git a/components/server/src/workspace/workspace-starter.ts b/components/server/src/workspace/workspace-starter.ts index 243cae2e2b6427..a1d678fd0808e3 100644 --- a/components/server/src/workspace/workspace-starter.ts +++ b/components/server/src/workspace/workspace-starter.ts @@ -160,7 +160,10 @@ export async function getWorkspaceClassForInstance( if (!workspaceClass) { switch (workspace.type) { case "prebuild": - workspaceClass = project?.settings?.workspaceClasses?.prebuild; + if (project) { + const prebuildSettings = Project.getPrebuildSettings(project); + workspaceClass = prebuildSettings.workspaceClass; + } break; case "regular": workspaceClass = project?.settings?.workspaceClasses?.regular; diff --git a/test/pkg/integration/workspace.go b/test/pkg/integration/workspace.go index 3d72dab00ca231..979b618589dda3 100644 --- a/test/pkg/integration/workspace.go +++ b/test/pkg/integration/workspace.go @@ -382,7 +382,6 @@ func LaunchWorkspaceWithOptions(t *testing.T, ctx context.Context, opts *LaunchW resp, err = server.CreateWorkspace(cctx, &protocol.CreateWorkspaceOptions{ ContextURL: opts.ContextURL, OrganizationId: orgId, - IgnoreRunningPrebuild: true, IgnoreRunningWorkspaceOnSameCommit: true, StartWorkspaceOptions: protocol.StartWorkspaceOptions{ IdeSettings: opts.IDESettings, diff --git a/test/tests/components/server/server_test.go b/test/tests/components/server/server_test.go index 706692072f5341..407da546fefc58 100644 --- a/test/tests/components/server/server_test.go +++ b/test/tests/components/server/server_test.go @@ -68,7 +68,6 @@ func TestStartWorkspace(t *testing.T) { resp, err := server.CreateWorkspace(ctx, &protocol.CreateWorkspaceOptions{ ContextURL: "github.com/gitpod-io/gitpod", - IgnoreRunningPrebuild: true, IgnoreRunningWorkspaceOnSameCommit: true, }) if err != nil {