From 2bc67b2afd613f81079e0e285ef191a6fca8396e Mon Sep 17 00:00:00 2001 From: Mathieu Leplatre Date: Tue, 11 Dec 2018 16:10:21 +0000 Subject: [PATCH] Bug 1506175 - Fetch recipes from Remote Settings r=mythmon,Gijs Instead of obtaining the recipes from the Normandy server, obtain them from RemoteSettings Differential Revision: https://phabricator.services.mozilla.com/D11490 --HG-- extra : moz-landing-system : lando --- browser/app/profile/firefox.js | 1 + .../components/normandy/lib/RecipeRunner.jsm | 77 +++++++++++++------ .../test/browser/browser_RecipeRunner.js | 37 +++++++++ 3 files changed, 90 insertions(+), 25 deletions(-) diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js index 3abbb4735d3f..c3d1fbf3cf75 100644 --- a/browser/app/profile/firefox.js +++ b/browser/app/profile/firefox.js @@ -1742,6 +1742,7 @@ pref("app.normandy.first_run", true); pref("app.normandy.logging.level", 50); // Warn pref("app.normandy.run_interval_seconds", 21600); // 6 hours pref("app.normandy.shieldLearnMoreUrl", "https://support.mozilla.org/1/firefox/%VERSION%/%OS%/%LOCALE%/shield"); +pref("app.normandy.remotesettings.enabled", false); #ifdef MOZ_DATA_REPORTING pref("app.shield.optoutstudies.enabled", true); #else diff --git a/toolkit/components/normandy/lib/RecipeRunner.jsm b/toolkit/components/normandy/lib/RecipeRunner.jsm index a6aec091c00c..a09e08ef67d8 100644 --- a/toolkit/components/normandy/lib/RecipeRunner.jsm +++ b/toolkit/components/normandy/lib/RecipeRunner.jsm @@ -13,6 +13,7 @@ XPCOMUtils.defineLazyServiceGetter(this, "timerManager", "nsIUpdateTimerManager"); XPCOMUtils.defineLazyModuleGetters(this, { + RemoteSettings: "resource://services-settings/remote-settings.js", Storage: "resource://normandy/lib/Storage.jsm", FilterExpressions: "resource://gre/modules/components-utils/FilterExpressions.jsm", NormandyApi: "resource://normandy/lib/NormandyApi.jsm", @@ -26,6 +27,7 @@ var EXPORTED_SYMBOLS = ["RecipeRunner"]; const log = LogManager.getLogger("recipe-runner"); const TIMER_NAME = "recipe-client-addon-run"; +const REMOTE_SETTINGS_COLLECTION = "normandy-recipes"; const PREF_CHANGED_TOPIC = "nsPref:changed"; const TELEMETRY_ENABLED_PREF = "datareporting.healthreport.uploadEnabled"; @@ -37,6 +39,7 @@ const SHIELD_ENABLED_PREF = `${PREF_PREFIX}.enabled`; const DEV_MODE_PREF = `${PREF_PREFIX}.dev_mode`; const API_URL_PREF = `${PREF_PREFIX}.api_url`; const LAZY_CLASSIFY_PREF = `${PREF_PREFIX}.experiments.lazy_classify`; +const REMOTE_SETTINGS_ENABLED_PREF = `${PREF_PREFIX}.remotesettings.enabled`; const PREFS_TO_WATCH = [ RUN_INTERVAL_PREF, @@ -45,6 +48,12 @@ const PREFS_TO_WATCH = [ API_URL_PREF, ]; +XPCOMUtils.defineLazyGetter(this, "gRemoteSettingsClient", () => { + return RemoteSettings(REMOTE_SETTINGS_COLLECTION, { + filterFunc: async recipe => RecipeRunner.checkFilter(recipe) ? recipe : null, + }); +}); + /** * cacheProxy returns an object Proxy that will memoize properties of the target. */ @@ -202,25 +211,11 @@ var RecipeRunner = { } // Fetch recipes before execution in case we fail and exit early. - let recipes; + let recipesToRun; try { - recipes = await NormandyApi.fetchRecipes({enabled: true}); - log.debug( - `Fetched ${recipes.length} recipes from the server: ` + - recipes.map(r => r.name).join(", ") - ); - + recipesToRun = await this.loadRecipes(); } catch (e) { - const apiUrl = Services.prefs.getCharPref(API_URL_PREF); - log.error(`Could not fetch recipes from ${apiUrl}: "${e}"`); - - let status = Uptake.RUNNER_SERVER_ERROR; - if (/NetworkError/.test(e)) { - status = Uptake.RUNNER_NETWORK_ERROR; - } else if (e instanceof NormandyApi.InvalidSignatureError) { - status = Uptake.RUNNER_INVALID_SIGNATURE; - } - Uptake.reportRunner(status); + // The legacy call to `Normandy.fetchRecipes()` can throw. return; } @@ -228,14 +223,6 @@ var RecipeRunner = { await actions.fetchRemoteActions(); await actions.preExecution(); - // Evaluate recipe filters - const recipesToRun = []; - for (const recipe of recipes) { - if (await this.checkFilter(recipe)) { - recipesToRun.push(recipe); - } - } - // Execute recipes, if we have any. if (recipesToRun.length === 0) { log.debug("No recipes to execute"); @@ -250,6 +237,46 @@ var RecipeRunner = { Uptake.reportRunner(Uptake.RUNNER_SUCCESS); }, + /** + * Return the list of recipes to run, filtered for the current environment. + */ + async loadRecipes() { + // If RemoteSettings is enabled, we read the list of recipes from there. + // The JEXL filtering is done via the provided callback. + if (Services.prefs.getBoolPref(REMOTE_SETTINGS_ENABLED_PREF, false)) { + return gRemoteSettingsClient.get(); + } + // Obtain the recipes from the Normandy server (legacy). + let recipes; + try { + recipes = await NormandyApi.fetchRecipes({enabled: true}); + log.debug( + `Fetched ${recipes.length} recipes from the server: ` + + recipes.map(r => r.name).join(", ") + ); + } catch (e) { + const apiUrl = Services.prefs.getCharPref(API_URL_PREF); + log.error(`Could not fetch recipes from ${apiUrl}: "${e}"`); + + let status = Uptake.RUNNER_SERVER_ERROR; + if (/NetworkError/.test(e)) { + status = Uptake.RUNNER_NETWORK_ERROR; + } else if (e instanceof NormandyApi.InvalidSignatureError) { + status = Uptake.RUNNER_INVALID_SIGNATURE; + } + Uptake.reportRunner(status); + throw e; + } + // Evaluate recipe filters + const recipesToRun = []; + for (const recipe of recipes) { + if (await this.checkFilter(recipe)) { + recipesToRun.push(recipe); + } + } + return recipesToRun; + }, + getFilterContext(recipe) { const environment = cacheProxy(ClientEnvironment); environment.recipe = { diff --git a/toolkit/components/normandy/test/browser/browser_RecipeRunner.js b/toolkit/components/normandy/test/browser/browser_RecipeRunner.js index bed1d962dbaa..08d6f24cfbb7 100644 --- a/toolkit/components/normandy/test/browser/browser_RecipeRunner.js +++ b/toolkit/components/normandy/test/browser/browser_RecipeRunner.js @@ -10,6 +10,8 @@ ChromeUtils.import("resource://normandy/lib/ActionsManager.jsm", this); ChromeUtils.import("resource://normandy/lib/AddonStudies.jsm", this); ChromeUtils.import("resource://normandy/lib/Uptake.jsm", this); +const { RemoteSettings } = ChromeUtils.import("resource://services-settings/remote-settings.js", {}); + add_task(async function getFilterContext() { const recipe = {id: 17, arguments: {foo: "bar"}, unrelated: false}; const context = RecipeRunner.getFilterContext(recipe); @@ -171,6 +173,41 @@ decorate_task( } ); +decorate_task( + withPrefEnv({ + set: [ + ["app.normandy.remotesettings.enabled", true], + ], + }), + withStub(ActionsManager.prototype, "runRecipe"), + withStub(ActionsManager.prototype, "fetchRemoteActions"), + withStub(ActionsManager.prototype, "finalize"), + async function testReadFromRemoteSettings( + runRecipeStub, + fetchRemoteActionsStub, + finalizeStub, + ) { + const matchRecipe = { id: "match", action: "matchAction", filter_expression: "true", _status: "synced", enabled: true }; + const noMatchRecipe = { id: "noMatch", action: "noMatchAction", filter_expression: "false", _status: "synced", enabled: true }; + const missingRecipe = { id: "missing", action: "missingAction", filter_expression: "true", _status: "synced", enabled: true }; + + const rsCollection = await RemoteSettings("normandy-recipes").openCollection(); + await rsCollection.create(matchRecipe, { synced: true }); + await rsCollection.create(noMatchRecipe, { synced: true }); + await rsCollection.create(missingRecipe, { synced: true }); + await rsCollection.db.saveLastModified(42); + rsCollection.db.close(); + + await RecipeRunner.run(); + + Assert.deepEqual( + runRecipeStub.args, + [[matchRecipe], [missingRecipe]], + "recipe with matching filters should be executed", + ); + } +); + decorate_task( withMockNormandyApi, async function testRunFetchFail(mockApi) {