Skip to content

Commit

Permalink
Merge ext:* --local launch branch into master (#4257)
Browse files Browse the repository at this point in the history
* Add a --local flag to ext:install (#4204)

* mvp for install --local

* format

* resolve merge conflict

* format

* check for duplicates from the manifest

* address comments

* Add unit tests

* Add tests

* void promises

* Update ext-install.ts

* simplify

* Update manifest.ts

Co-authored-by: joehan <[email protected]>

* Extensions - Move manifest related util into manifest.ts (#4217)

* move util to manifest.ts

* Update paramHelper.ts

* Add --local flag to ext:configure (#4219)

* Allow configure --local

* format

* write new values to manifest

* Update ext-configure.ts

* Cleanup

* Add tests

* fix tests

* fix comments

* Add --local flag to ext:uninstall command. (#4223)

* uninstall --local

* Add tests

* Update manifest.spec.ts

* Fix tests with factory

* Update manifest.spec.ts

* PR fixes

* Add --local flag to ext:update (#4234)

* Update ext-update.ts

* write to manifest

* Handle missing manifest params during update

* Better logging

* Update ext-update.ts

* Fix tests

* Add deprecation warning to ext:* commands in favor of --local flag (#4243)

* add deprecation warning

* fixes

* Add preview warning when running with --local

* Revert "Add preview warning when running with --local"

This reverts commit 8b3e8c1.

* Revert "Revert "Add preview warning when running with --local""

This reverts commit 3c2e6f7.

* pr fixes

Co-authored-by: joehan <[email protected]>
  • Loading branch information
elvisun and joehan authored Mar 7, 2022
1 parent 94b3156 commit a538281
Show file tree
Hide file tree
Showing 14 changed files with 806 additions and 220 deletions.
104 changes: 97 additions & 7 deletions src/commands/ext-configure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ import * as paramHelper from "../extensions/paramHelper";
import { requirePermissions } from "../requirePermissions";
import * as utils from "../utils";
import { logger } from "../logger";
import * as refs from "../extensions/refs";
import * as manifest from "../extensions/manifest";
import { Options } from "../options";
import { partition } from "../functional";

marked.setOptions({
renderer: new TerminalRenderer(),
Expand All @@ -27,18 +31,80 @@ export default new Command("ext:configure <extensionInstanceId>")
.description("configure an existing extension instance")
.withForce()
.option("--params <paramsFile>", "path of params file with .env format.")
.option("--local", "save to firebase.json rather than directly install to a Firebase project")
.before(requirePermissions, [
"firebaseextensions.instances.update",
"firebaseextensions.instances.get",
])
.before(checkMinRequiredVersion, "extMinVersion")
.before(diagnoseAndFixProject)
.action(async (instanceId: string, options: any) => {
.action(async (instanceId: string, options: Options) => {
const projectId = needProjectId(options);

if (options.local) {
if (options.nonInteractive) {
throw new FirebaseError(
`Command not supported in non-interactive mode, edit ./extensions/${instanceId}.env directly instead`
);
}

const config = manifest.loadConfig(options);
const targetRef = manifest.getInstanceRef(instanceId, config);
const extensionVersion = await extensionsApi.getExtensionVersion(
refs.toExtensionVersionRef(targetRef)
);

const oldParamValues = manifest.readInstanceParam({
instanceId,
projectDir: config.projectDir,
});

const [immutableParams, tbdParams] = partition(
extensionVersion.spec.params,
(param) => param.immutable ?? false
);
infoImmutableParams(immutableParams, oldParamValues);

// Ask for mutable param values from user.
paramHelper.setNewDefaults(tbdParams, oldParamValues);
const mutableParamsValues = await paramHelper.getParams({
projectId,
paramSpecs: tbdParams,
nonInteractive: false,
paramsEnvPath: (options.params ?? "") as string,
instanceId,
reconfiguring: true,
});

// Merge with old immutable params.
const newParamValues = {
...oldParamValues,
...mutableParamsValues,
};

await manifest.writeToManifest(
[
{
instanceId,
ref: targetRef,
params: newParamValues,
},
],
config,
{
nonInteractive: false,
force: true, // Skip asking for permission again
}
);
manifest.showPreviewWarning();
return;
}

// TODO(b/220900194): Remove everything below and make --local the default behavior.
const spinner = ora(
`Configuring ${clc.bold(instanceId)}. This usually takes 3 to 5 minutes...`
);
try {
const projectId = needProjectId(options);
let existingInstance: extensionsApi.ExtensionInstance;
try {
existingInstance = await extensionsApi.getInstance(projectId, instanceId);
Expand All @@ -55,16 +121,13 @@ export default new Command("ext:configure <extensionInstanceId>")
}
const paramSpecWithNewDefaults =
paramHelper.getParamsWithCurrentValuesAsDefaults(existingInstance);
const immutableParams = _.remove(paramSpecWithNewDefaults, (param) => {
return param.immutable || param.param === "LOCATION";
// TODO: Stop special casing "LOCATION" once all official extensions make it immutable
});
const immutableParams = _.remove(paramSpecWithNewDefaults, (param) => param.immutable);

const params = await paramHelper.getParams({
projectId,
paramSpecs: paramSpecWithNewDefaults,
nonInteractive: options.nonInteractive,
paramsEnvPath: options.params,
paramsEnvPath: options.params as string,
instanceId,
reconfiguring: true,
});
Expand Down Expand Up @@ -97,6 +160,7 @@ export default new Command("ext:configure <extensionInstanceId>")
)}`
)
);
manifest.showDeprecationWarning();
return res;
} catch (err: any) {
if (spinner.isSpinning) {
Expand All @@ -110,3 +174,29 @@ export default new Command("ext:configure <extensionInstanceId>")
throw err;
}
});

function infoImmutableParams(
immutableParams: extensionsApi.Param[],
paramValues: { [key: string]: string }
) {
if (!immutableParams.length) {
return;
}

const plural = immutableParams.length > 1;
utils.logLabeledWarning(
logPrefix,
marked(`The following param${plural ? "s are" : " is"} immutable and won't be changed:`)
);

for (const { param } of immutableParams) {
logger.info(`param: ${param}, value: ${paramValues[param]}`);
}

logger.info(
(plural
? "To set different values for these params"
: "To set a different value for this param") +
", uninstall the extension, then install a new instance of this extension."
);
}
24 changes: 11 additions & 13 deletions src/commands/ext-export.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
import { checkMinRequiredVersion } from "../checkMinRequiredVersion";
import { Command } from "../command";
import { Config } from "../config";
import * as planner from "../deploy/extensions/planner";
import { FirebaseError } from "../error";
import {
displayExportInfo,
parameterizeProject,
setSecretParamsToLatest,
} from "../extensions/export";
import { ensureExtensionsApiEnabled } from "../extensions/extensionsHelper";
import { writeToManifest } from "../extensions/manifest";
import * as manifest from "../extensions/manifest";
import { partition } from "../functional";
import { getProjectNumber } from "../getProjectNumber";
import { logger } from "../logger";
Expand Down Expand Up @@ -66,14 +64,14 @@ module.exports = new Command("ext:export")
return;
}

const existingConfig = Config.load(options, true);
if (!existingConfig) {
throw new FirebaseError(
"Not currently in a Firebase directory. Please run `firebase init` to create a Firebase directory."
);
}
await writeToManifest(withRef, existingConfig, {
nonInteractive: options.nonInteractive,
force: options.force,
});
const existingConfig = manifest.loadConfig(options);
await manifest.writeToManifest(
withRef,
existingConfig,
{
nonInteractive: options.nonInteractive,
force: options.force,
},
true /** allowOverwrite */
);
});
95 changes: 93 additions & 2 deletions src/commands/ext-install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ import * as utils from "../utils";
import { track } from "../track";
import { logger } from "../logger";
import { previews } from "../previews";
import { Options } from "../options";
import * as manifest from "../extensions/manifest";

marked.setOptions({
renderer: new TerminalRenderer(),
Expand All @@ -56,14 +58,16 @@ export default new Command("ext:install [extensionName]")
"or run with `-i` to see all available extensions."
)
.withForce()
// TODO(b/221037520): Deprecate the params flag then remove it in the next breaking version.
.option("--params <paramsFile>", "name of params variables file with .env format.")
.option("--local", "save to firebase.json rather than directly install to a Firebase project")
.before(requirePermissions, ["firebaseextensions.instances.create"])
.before(ensureExtensionsApiEnabled)
.before(checkMinRequiredVersion, "extMinVersion")
.before(diagnoseAndFixProject)
.action(async (extensionName: string, options: any) => {
.action(async (extensionName: string, options: Options) => {
const projectId = needProjectId(options);
const paramsEnvPath = options.params;
const paramsEnvPath = (options.params ?? "") as string;
let learnMore = false;
if (!extensionName) {
if (options.interactive) {
Expand Down Expand Up @@ -93,6 +97,11 @@ export default new Command("ext:install [extensionName]")
// Otherwise, treat the input as an extension reference and proceed with reference-based installation.
if (isLocalOrURLPath(extensionName)) {
void track("Extension Install", "Install by Source", options.interactive ? 1 : 0);
if (options.local) {
throw new FirebaseError(
"Installing a local source locally is not supported yet, please use ext:dev:emulator commands"
);
}
source = await infoInstallBySource(projectId, extensionName);
} else {
void track("Extension Install", "Install by Extension Ref", options.interactive ? 1 : 0);
Expand Down Expand Up @@ -128,6 +137,32 @@ export default new Command("ext:install [extensionName]")
`View details: https://firebase.google.com/products/extensions/${spec.name}\n`
);
}

if (options.local) {
try {
return installToManifest({
paramsEnvPath,
projectId,
extensionName,
source,
extVersion,
nonInteractive: options.nonInteractive,
force: options.force,
});
} catch (err: any) {
if (!(err instanceof FirebaseError)) {
throw new FirebaseError(
`Error occurred saving the extension to manifest: ${err.message}`,
{
original: err,
}
);
}
throw err;
}
}

// TODO(b/220900194): Remove this and make --local the default behavior.
try {
return installExtension({
paramsEnvPath,
Expand Down Expand Up @@ -200,6 +235,61 @@ interface InstallExtensionOptions {
force?: boolean;
}

/**
* Saves the extension instance config values to the manifest.
*
* Requires running `firebase deploy` to install it to the Firebase project.
* @param options
*/
async function installToManifest(options: InstallExtensionOptions): Promise<void> {
const { projectId, extensionName, extVersion, paramsEnvPath, nonInteractive, force } = options;
const spec = extVersion?.spec;
if (!spec) {
throw new FirebaseError(
`Could not find the extension.yaml for ${extensionName}. Please make sure this is a valid extension and try again.`
);
}

const config = manifest.loadConfig(options);

let instanceId = spec.name;
while (manifest.instanceExists(instanceId, config)) {
instanceId = await promptForValidInstanceId(`${spec.name}-${getRandomString(4)}`);
}

const params = await paramHelper.getParams({
projectId,
paramSpecs: spec.params,
nonInteractive,
paramsEnvPath,
instanceId,
});

const ref = refs.parse(extVersion.ref);
await manifest.writeToManifest(
[
{
instanceId,
ref,
params,
},
],
config,
{ nonInteractive, force: force ?? false }
);
manifest.showPreviewWarning();
}

/**
* Installs the extension in user's project.
*
* 1. Checks products are provisioned.
* 2. Checks billings are enabled if needed.
* 3. Asks for permission to grant sa roles.
* 4. Asks for extension params
* 5. Install
* @param options
*/
async function installExtension(options: InstallExtensionOptions): Promise<void> {
const { projectId, extensionName, source, extVersion, paramsEnvPath, nonInteractive, force } =
options;
Expand Down Expand Up @@ -350,6 +440,7 @@ async function installExtension(options: InstallExtensionOptions): Promise<void>
"including those to update, reconfigure, or delete your installed extension."
)
);
manifest.showDeprecationWarning();
} catch (err: any) {
if (spinner.isSpinning) {
spinner.fail();
Expand Down
18 changes: 16 additions & 2 deletions src/commands/ext-uninstall.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import { promptOnce } from "../prompt";
import { requirePermissions } from "../requirePermissions";
import * as utils from "../utils";
import { logger } from "../logger";
import * as manifest from "../extensions/manifest";
import { Options } from "../options";

marked.setOptions({
renderer: new TerminalRenderer(),
Expand All @@ -29,14 +31,25 @@ marked.setOptions({
export default new Command("ext:uninstall <extensionInstanceId>")
.description("uninstall an extension that is installed in your Firebase project by instance ID")
.withForce()
.option(
"--local",
"remove from firebase.json rather than directly uninstall from a Firebase project"
)
.before(requirePermissions, ["firebaseextensions.instances.delete"])
.before(ensureExtensionsApiEnabled)
.before(checkMinRequiredVersion, "extMinVersion")
.before(diagnoseAndFixProject)
.action(async (instanceId: string, options: any) => {
.action(async (instanceId: string, options: Options) => {
if (options.local) {
const config = manifest.loadConfig(options);
manifest.removeFromManifest(instanceId, config);
manifest.showPreviewWarning();
return;
}

// TODO(b/220900194): Remove everything below and make --local the default behavior.
const projectId = needProjectId(options);
let instance;

try {
instance = await extensionsApi.getInstance(projectId, instanceId);
} catch (err: any) {
Expand Down Expand Up @@ -121,6 +134,7 @@ export default new Command("ext:uninstall <extensionInstanceId>")
return utils.reject(`Error occurred uninstalling extension ${instanceId}`, { original: err });
}
utils.logLabeledSuccess(logPrefix, `uninstalled ${clc.bold(instanceId)}`);
manifest.showDeprecationWarning();
});

/**
Expand Down
Loading

0 comments on commit a538281

Please sign in to comment.