Skip to content

Commit 2a87679

Browse files
committed
Create build and update traffic during backends:create.
1 parent 2748f41 commit 2a87679

File tree

2 files changed

+245
-31
lines changed

2 files changed

+245
-31
lines changed

src/gcp/apphosting.ts

Lines changed: 146 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import * as proto from "../gcp/proto";
12
import { Client } from "../apiv2";
23
import { needProjectId } from "../projectUtils";
34
import { apphostingOrigin } from "../api";
@@ -46,33 +47,139 @@ export interface Build {
4647
state: State;
4748
error: Status;
4849
image: string;
50+
config?: BuildConfig;
4951
source: BuildSource;
50-
buildLogsUri: string;
51-
createTime: Date;
52-
updateTime: Date;
5352
sourceRef: string;
53+
buildLogsUri?: string;
54+
displayName?: string;
55+
labels?: Record<string, string>;
56+
annotations?: Record<string, string>;
57+
uuid: string;
58+
etag: string;
59+
reconciling: boolean;
60+
createTime: string;
61+
updateTime: string;
62+
deleteTime: string;
5463
}
5564

56-
export type BuildOutputOnlyFields = "createTime" | "updateTime" | "sourceRef";
65+
export type BuildOutputOnlyFields =
66+
| "state"
67+
| "error"
68+
| "image"
69+
| "sourceRef"
70+
| "buildLogUri"
71+
| "reconciling"
72+
| "uuid"
73+
| "etag"
74+
| "createTime"
75+
| "updateTime"
76+
| "deleteTime";
77+
78+
export interface BuildConfig {
79+
minInstances?: number;
80+
memory?: string;
81+
}
5782

5883
interface BuildSource {
59-
codeBaseSource?: CodebaseSource;
84+
codebase: CodebaseSource;
6085
}
6186

87+
interface CodebaseSource {
88+
// oneof reference
89+
branch?: string;
90+
commit?: string;
91+
tag?: string;
92+
// end oneof reference
93+
displayName: string;
94+
hash: string;
95+
commitMessage: string;
96+
uri: string;
97+
commitTime: string;
98+
}
99+
100+
export type CodebaseSourceOutputOnlyFields =
101+
| "displayName"
102+
| "hash"
103+
| "commitMessage"
104+
| "uri"
105+
| "commitTime";
106+
107+
export type BuildInput = Omit<Build, BuildOutputOnlyFields | "source"> & {
108+
source: Omit<BuildSource, "codebase"> & {
109+
codebase: Omit<CodebaseSource, CodebaseSourceOutputOnlyFields>;
110+
};
111+
};
112+
62113
interface Status {
63114
code: number;
64115
message: string;
65-
details: any[];
116+
details: unknown;
66117
}
67118

68-
interface CodebaseSource {
69-
// oneof reference
70-
branch: string;
71-
commit: string;
72-
tag: string;
73-
// end oneof reference
119+
export interface Traffic {
120+
name: string;
121+
// oneof traffic_management
122+
target?: TrafficSet;
123+
rolloutPolicy?: RolloutPolicy;
124+
// end oneof traffic_management
125+
current: TrafficSet;
126+
reconciling: boolean;
127+
createTime: string;
128+
updateTime: string;
129+
annotations?: Record<string, string>;
130+
etag: string;
131+
uid: string;
74132
}
75133

134+
export type TrafficOutputOnlyFields =
135+
| "current"
136+
| "reconciling"
137+
| "createTime"
138+
| "updateTime"
139+
| "etag"
140+
| "uid";
141+
142+
export interface TrafficSet {
143+
splits: TrafficSplit[];
144+
}
145+
146+
export interface TrafficSplit {
147+
build: string;
148+
percent: number;
149+
}
150+
151+
export interface RolloutPolicy {
152+
// oneof trigger
153+
codebaseBranch?: string;
154+
codebaseTagPatter?: string;
155+
// end oneof trigger
156+
stages?: RolloutStage[];
157+
disabled?: boolean;
158+
disabledTime: string;
159+
}
160+
161+
export type RolloutPolicyOutputOnlyFields = "disabledtime";
162+
163+
export type RolloutProgression =
164+
| "PROGRESSION_UNSPECIFIED"
165+
| "IMMEDIATE"
166+
| "LINEAR"
167+
| "EXPONENTIAL"
168+
| "PAUSE";
169+
170+
export interface RolloutStage {
171+
progression: RolloutProgression;
172+
duration?: {
173+
seconds: number;
174+
nanos: number;
175+
};
176+
targetPercent?: number;
177+
startTime: string;
178+
endTime: string;
179+
}
180+
181+
export type RolloutStageOutputOnlyFields = "startTime" | "endTime";
182+
76183
interface OperationMetadata {
77184
createTime: string;
78185
endTime: string;
@@ -162,15 +269,14 @@ export async function createBuild(
162269
projectId: string,
163270
location: string,
164271
backendId: string,
165-
buildInput: Omit<Build, BuildOutputOnlyFields>
272+
buildId: string,
273+
buildInput: Omit<BuildInput, "name">
166274
): Promise<Operation> {
167-
const buildId = buildInput.name;
168-
const res = await client.post<Omit<Build, BuildOutputOnlyFields>, Operation>(
275+
const res = await client.post<Omit<BuildInput, "name">, Operation>(
169276
`projects/${projectId}/locations/${location}/backends/${backendId}/builds`,
170277
buildInput,
171278
{ queryParams: { buildId } }
172279
);
173-
174280
return res.body;
175281
}
176282

@@ -184,6 +290,30 @@ interface ListLocationsResponse {
184290
nextPageToken?: string;
185291
}
186292

293+
/**
294+
* Update traffic of a backend
295+
*/
296+
export async function updateTraffic(
297+
projectId: string,
298+
location: string,
299+
backendId: string,
300+
traffic: Omit<Traffic, TrafficOutputOnlyFields | "name">
301+
): Promise<Operation> {
302+
const fieldMasks = proto.fieldMasks(traffic);
303+
const queryParams = {
304+
updateMask: fieldMasks.join(","),
305+
};
306+
const name = `projects/${projectId}/locations/${location}/backends/${backendId}/traffic`;
307+
const res = await client.patch<Omit<Traffic, TrafficOutputOnlyFields>, Operation>(
308+
name,
309+
{ ...traffic, name },
310+
{
311+
queryParams,
312+
}
313+
);
314+
return res.body;
315+
}
316+
187317
/**
188318
* Lists information about the supported locations.
189319
*/

src/init/features/apphosting/index.ts

Lines changed: 99 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,13 @@ import * as poller from "../../../operation-poller";
44
import * as apphosting from "../../../gcp/apphosting";
55
import { logBullet, logSuccess, logWarning } from "../../../utils";
66
import { apphostingOrigin } from "../../../api";
7-
import { Backend, BackendOutputOnlyFields, API_VERSION } from "../../../gcp/apphosting";
7+
import {
8+
Backend,
9+
BackendOutputOnlyFields,
10+
API_VERSION,
11+
Build,
12+
BuildInput,
13+
} from "../../../gcp/apphosting";
814
import { Repository } from "../../../gcp/cloudbuild";
915
import { FirebaseError } from "../../../error";
1016
import { promptOnce } from "../../../prompt";
@@ -66,27 +72,50 @@ export async function doSetup(setup: any, projectId: string): Promise<void> {
6672
}
6773
logWarning(`Backend with id ${backendId} already exists in ${location}`);
6874
}
69-
const backend: Backend = await onboardBackend(projectId, location, backendId);
75+
const backend = await onboardBackend(projectId, location, backendId);
76+
77+
const branch = await promptOnce({
78+
name: "branch",
79+
type: "input",
80+
default: "main",
81+
message: "Which branch do you want to deploy?",
82+
});
83+
const build = await createBuild(projectId, location, backendId, {
84+
source: {
85+
codebase: {
86+
branch,
87+
},
88+
},
89+
});
90+
91+
if (build.state !== "READY") {
92+
throw new FirebaseError(
93+
`Failed to build your app. Please inspect the build logs at ${build.buildLogsUri}.`,
94+
{ children: [build.error] }
95+
);
96+
}
97+
98+
await setDefaultTraffic(projectId, location, backendId, build.name.split("/").pop()!);
7099

71100
if (backend) {
72101
logSuccess(`Successfully created backend:\n\t${backend.name}`);
73-
logSuccess(`Your site is being deployed at:\n\thttps://${backend.uri}`);
102+
logSuccess(`Your site is now deployed at:\n\thttps://${backend.uri}`);
74103
logSuccess(
75104
`View the rollout status by running:\n\tfirebase apphosting:backends:get ${backendId} --project ${projectId}`
76105
);
77106
}
78107
}
79108

80109
async function promptLocation(projectId: string, locations: string[]): Promise<string> {
81-
return await promptOnce({
110+
return (await promptOnce({
82111
name: "region",
83112
type: "list",
84113
default: DEFAULT_REGION,
85114
message:
86115
"Please select a region " +
87116
`(${clc.yellow("info")}: Your region determines where your backend is located):\n`,
88117
choices: locations.map((loc) => ({ value: loc })),
89-
});
118+
})) as string;
90119
}
91120

92121
function toBackend(cloudBuildConnRepo: Repository): Omit<Backend, BackendOutputOnlyFields> {
@@ -109,20 +138,12 @@ export async function onboardBackend(
109138
backendId: string
110139
): Promise<Backend> {
111140
const cloudBuildConnRepo = await repo.linkGitHubRepository(projectId, location);
112-
const barnchName = await promptOnce({
113-
name: "branchName",
114-
type: "input",
115-
default: "main",
116-
message: "Which branch do you want to deploy?",
117-
});
118-
// branchName unused for now.
119-
void barnchName;
120141
const backendDetails = toBackend(cloudBuildConnRepo);
121142
return await createBackend(projectId, location, backendDetails, backendId);
122143
}
123144

124145
/**
125-
* Creates backend object from long running operations.
146+
* Creates (and waits for) a new backend.
126147
*/
127148
export async function createBackend(
128149
projectId: string,
@@ -136,6 +157,69 @@ export async function createBackend(
136157
pollerName: `create-${projectId}-${location}-${backendId}`,
137158
operationResourceName: op.name,
138159
});
139-
140160
return backend;
141161
}
162+
163+
/**
164+
* Only lowercase, digits, and hyphens; must begin with letter, and cannot end with hyphen
165+
*/
166+
function generateBuildId(n = 6) {
167+
const letters = "abcdefghijklmnopqrstuvwxyz";
168+
const allChars = "01234567890-abcdefghijklmnopqrstuvwxyz";
169+
let id = letters[Math.floor(Math.random() * letters.length)];
170+
for (let i = 1; i < n; i++) {
171+
const idx = Math.floor(Math.random() * allChars.length);
172+
id += allChars[idx];
173+
}
174+
return id;
175+
}
176+
177+
/**
178+
* Create (and waits for) a new build.
179+
*/
180+
export async function createBuild(
181+
projectId: string,
182+
location: string,
183+
backendId: string,
184+
buildInput: Omit<BuildInput, "name">
185+
): Promise<Build> {
186+
logBullet("Creating a new build... this may take a few minutes.");
187+
const buildId = generateBuildId();
188+
const op = await apphosting.createBuild(projectId, location, backendId, buildId, buildInput);
189+
const build = await poller.pollOperation<Build>({
190+
...apphostingPollerOptions,
191+
pollerName: `create-${projectId}-${location}-backend-${backendId}-build-${buildId}`,
192+
operationResourceName: op.name,
193+
});
194+
logSuccess("Build finished.");
195+
return build;
196+
}
197+
198+
/**
199+
* Set (and waits for) a default traffic that directs all traffic to the buildId.
200+
*/
201+
export async function setDefaultTraffic(
202+
projectId: string,
203+
location: string,
204+
backendId: string,
205+
buildId: string
206+
): Promise<Build> {
207+
logBullet("Directing traffic to the new build...");
208+
const op = await apphosting.updateTraffic(projectId, location, backendId, {
209+
target: {
210+
splits: [
211+
{
212+
build: buildId,
213+
percent: 100,
214+
},
215+
],
216+
},
217+
});
218+
const traffic = await poller.pollOperation<Build>({
219+
...apphostingPollerOptions,
220+
pollerName: `create-${projectId}-${location}-backend-${backendId}-traffic`,
221+
operationResourceName: op.name,
222+
});
223+
logSuccess("Traffic update finished.");
224+
return traffic;
225+
}

0 commit comments

Comments
 (0)