Repository: elastic/synthetics
Pull request #814 (2023-08-01 12:26:51)

fix: align cli options between push and locations for url/auth
fixes https://github.com/elastic/synthetics/issues/718
align cli options between push and locations for url/auth !!

<img width="866" alt="image" src="https://github.com/elastic/synthetics/assets/3505601/241393cb-8cdc-43f5-89ab-407e29750b52">



Patch:
diff --git a/src/cli.ts b/src/cli.ts
index 59b720f..3047b0a 100644
--- a/src/cli.ts
+++ b/src/cli.ts
@@ -51,11 +51,13 @@ import { error } from './helpers';
 import { LocationsMap } from './locations/public-locations';
 import { createLightweightMonitors } from './push/monitor';
 import { getVersion } from './push/kibana_api';
+import { installTransform } from './core/transform';
 
 /* eslint-disable-next-line @typescript-eslint/no-var-requires */
 const { name, version } = require('../package.json');
 
-const { params, pattern, playwrightOpts } = getCommonCommandOpts();
+const { params, pattern, playwrightOpts, auth, authMandatory } =
+  getCommonCommandOpts();
 
 program
   .name(`npx ${name}`)
@@ -78,8 +80,9 @@ program
   )
   .option('--inline', 'Run inline journeys from heartbeat')
   .option('-r, --require <modules...>', 'module(s) to preload')
-  .option('--sandbox', 'enable chromium sandboxing')
+  .option('--sandbox', 'enable chromium sand-boxing')
   .option('--rich-events', 'Mimics a heartbeat run')
+  .option('--no-headless', 'run browser in head-full mode')
   .option(
     '--capability <features...>',
     'Enable capabilities through feature flags'
@@ -157,14 +160,6 @@ program
   .description(
     'Push all journeys in the current directory to create monitors within the Kibana monitor management UI'
   )
-  .addOption(
-    new Option(
-      '--auth <auth>',
-      'API key used for Kibana authentication(https://www.elastic.co/guide/en/kibana/master/api-keys.html).'
-    )
-      .env('SYNTHETICS_API_KEY')
-      .makeOptionMandatory(true)
-  )
   .option(
     '--schedule <time-in-minutes>',
     "schedule in minutes for the pushed monitors. Setting `10`, for example, configures monitors which don't have an interval defined to run every 10 minutes.",
@@ -190,6 +185,7 @@ program
     'the target Kibana spaces for the pushed monitors — spaces help you organise pushed monitors.'
   )
   .option('-y, --yes', 'skip all questions and run non-interactively')
+  .addOption(authMandatory)
   .addOption(pattern)
   .addOption(params)
   .addOption(playwrightOpts)
@@ -245,21 +241,26 @@ program
     `List all locations to run the synthetics monitors. Pass optional '--url' and '--auth' to list private locations.`
   )
   .option('--url <url>', 'Kibana URL to fetch all public and private locations')
-  .option(
-    '--auth <auth>',
-    'API key used for Kibana authentication(https://www.elastic.co/guide/en/kibana/master/api-keys.html).'
-  )
+  .addOption(auth)
   .action(async (cmdOpts: LocationCmdOptions) => {
+    const revert = installTransform();
+    const url = cmdOpts.url ?? (await loadSettings(true))?.url;
+
     try {
-      let locations = Object.keys(LocationsMap);
-      if (cmdOpts.auth && cmdOpts.url) {
-        const allLocations = await getLocations(cmdOpts);
-        locations = formatLocations(allLocations);
+      if (url && cmdOpts.auth) {
+        const allLocations = await getLocations({
+          url,
+          auth: cmdOpts.auth,
+        });
+        renderLocations(formatLocations(allLocations));
+      } else {
+        renderLocations({ publicLocations: Object.keys(LocationsMap) });
       }
-      renderLocations(locations);
     } catch (e) {
       e && error(e);
       process.exit(1);
+    } finally {
+      revert();
     }
   });
 
diff --git a/src/common_types.ts b/src/common_types.ts
index c8c14f8..db4219f 100644
--- a/src/common_types.ts
+++ b/src/common_types.ts
@@ -223,6 +223,7 @@ export type CliArgs = BaseArgs & {
   require?: Array<string>;
   sandbox?: boolean;
   richEvents?: boolean;
+  headless?: boolean;
   capability?: Array<string>;
   ignoreHttpsErrors?: boolean;
 };
diff --git a/src/generator/index.ts b/src/generator/index.ts
index 7bd7a40..0325d78 100644
--- a/src/generator/index.ts
+++ b/src/generator/index.ts
@@ -112,7 +112,8 @@ export class Generator {
     }).run();
 
     const allLocations = await getLocations({ url, auth });
-    const locChoices = formatLocations(allLocations);
+    const { allLocations: locChoices } = formatLocations(allLocations);
+
     if (locChoices.length === 0) {
       throw 'Follow the docs to set up your first private locations - https://www.elastic.co/guide/en/observability/current/uptime-set-up-choose-agent.html#private-locations';
     }
diff --git a/src/locations/index.ts b/src/locations/index.ts
index 1972e25..d5acf09 100644
--- a/src/locations/index.ts
+++ b/src/locations/index.ts
@@ -42,7 +42,7 @@ export type LocationAPIResponse = {
   locations: Array<LocationMetadata>;
 };
 
-const PRIVATE_KEYWORD = '(private)';
+export const PRIVATE_KEYWORD = '(private)';
 
 export async function getLocations(options: LocationCmdOptions) {
   const resp = await sendReqAndHandleError<LocationAPIResponse>({
@@ -54,25 +54,33 @@ export async function getLocations(options: LocationCmdOptions) {
 }
 
 export function formatLocations(locations: Array<LocationMetadata>) {
-  const formatted: Array<string> = [];
+  const publicLocations: Array<string> = [];
+  const privateLocations: Array<string> = [];
   for (const location of locations) {
     let name = location.label;
     if (location.isServiceManaged) {
       [, name] = name.includes('-') ? name.split('-') : [, name];
       name = name.toLowerCase().trim().split(' ').join('_');
+      publicLocations.push(name);
     } else {
-      name = `${name}${PRIVATE_KEYWORD}`;
+      privateLocations.push(name);
     }
-    formatted.push(name);
   }
-  return formatted;
+
+  return {
+    publicLocations,
+    privateLocations,
+    allLocations: publicLocations.concat(
+      privateLocations.map(loc => `${loc} ${PRIVATE_KEYWORD}`)
+    ),
+  };
 }
 
 export function groupLocations(locations: Array<string>) {
   const grouped = { locations: [], privateLocations: [] };
   for (const loc of locations) {
     if (loc.includes(PRIVATE_KEYWORD)) {
-      grouped.privateLocations.push(loc.replace(PRIVATE_KEYWORD, ''));
+      grouped.privateLocations.push(loc.replace(PRIVATE_KEYWORD, '').trim());
     } else {
       grouped.locations.push(loc);
     }
@@ -80,13 +88,30 @@ export function groupLocations(locations: Array<string>) {
   return grouped;
 }
 
-export function renderLocations(locations: Array<string>) {
-  let outer = 'Available locations: \n';
-  let inner = '';
-  for (const location of locations) {
-    inner += `* ${location}\n`;
+export function renderLocations({
+  publicLocations,
+  privateLocations = [],
+}: {
+  privateLocations?: Array<string>;
+  publicLocations: Array<string>;
+}) {
+  let outer = '';
+  if (publicLocations.length) {
+    outer = 'Public locations: \n';
+    let inner = '';
+    for (const location of publicLocations) {
+      inner += `* ${location}\n`;
+    }
+    outer += indent(inner);
+  }
+  if (privateLocations.length > 0) {
+    outer += '\nPrivate locations: \n';
+    let privateInner = '';
+    for (const location of privateLocations) {
+      privateInner += `* ${location}\n`;
+    }
+    outer += indent(privateInner);
   }
-  outer += indent(inner);
   outer += `\nSet default location for monitors via
   - Synthetics config file 'monitors.locations' | 'monitors.privateLocations' field
   - Monitor API 'monitor.use({ locations: ["japan"], privateLocations: ["custom-location"] }')`;
diff --git a/src/options.ts b/src/options.ts
index 58a3994..4734d77 100644
--- a/src/options.ts
+++ b/src/options.ts
@@ -106,6 +106,7 @@ export function normalizeOptions(
   );
   options.playwrightOptions = {
     ...playwrightOpts,
+    headless: cliArgs.headless ?? playwrightOpts?.headless,
     chromiumSandbox: cliArgs.sandbox ?? playwrightOpts?.chromiumSandbox,
     ignoreHTTPSErrors:
       cliArgs.ignoreHttpsErrors ?? playwrightOpts?.ignoreHTTPSErrors,
@@ -185,7 +186,19 @@ export function getCommonCommandOpts() {
     'RegExp pattern to match journey/monitor files that are different from the default (ex: /*.journey.(ts|js)$/)'
   );
 
+  const apiDocsLink =
+    'API key used for Kibana authentication(https://www.elastic.co/guide/en/kibana/master/api-keys.html).';
+  const auth = createOption('--auth <auth>', apiDocsLink).env(
+    'SYNTHETICS_API_KEY'
+  );
+
+  const authMandatory = createOption('--auth <auth>', apiDocsLink)
+    .env('SYNTHETICS_API_KEY')
+    .makeOptionMandatory(true);
+
   return {
+    auth,
+    authMandatory,
     params,
     playwrightOpts,
     pattern,
diff --git a/src/push/index.ts b/src/push/index.ts
index f4eeab5..0341205 100644
--- a/src/push/index.ts
+++ b/src/push/index.ts
@@ -160,7 +160,7 @@ export function formatDuplicateError(monitors: Set<Monitor>) {
 
 const INSTALLATION_HELP = `Run 'npx @elastic/synthetics init' to create project with default settings.`;
 
-export async function loadSettings() {
+export async function loadSettings(ignoreMissing = false) {
   try {
     const config = await readConfig(process.env['NODE_ENV'] || 'development');
     // Missing config file, fake throw to capture as missing file
@@ -169,9 +169,11 @@ export async function loadSettings() {
     }
     return config.project || ({} as ProjectSettings);
   } catch (e) {
-    throw error(`Aborted (missing synthetics config file), Project not set up correctly.
+    if (!ignoreMissing) {
+      throw error(`Aborted (missing synthetics config file), Project not set up correctly.
 
 ${INSTALLATION_HELP}`);
+    }
   }
 }
 


Test patch:
diff --git a/__tests__/locations/index.test.ts b/__tests__/locations/index.test.ts
index d944e62..23b88cd 100644
--- a/__tests__/locations/index.test.ts
+++ b/__tests__/locations/index.test.ts
@@ -23,6 +23,7 @@
  *
  */
 
+import { join } from 'path';
 import {
   formatLocations,
   getLocations,
@@ -31,6 +32,7 @@ import {
 import { LOCATIONS } from '../fixtures/locationinfo';
 import { Server } from '../utils/server';
 import { CLIMock } from '../utils/test-config';
+import { mkdir, rm, writeFile } from 'fs/promises';
 
 describe('Locations', () => {
   const apiKey = 'foo';
@@ -60,28 +62,48 @@ describe('Locations', () => {
       auth: apiKey,
     });
     const formatted = formatLocations(locations);
-    expect(formatted).toEqual([
-      'japan',
-      'new_location',
-      'custom location 1(private)',
-      'custom location 2(private)',
-      'us_west',
-    ]);
+    expect(formatted).toEqual({
+      privateLocations: ['custom location 1', 'custom location 2'],
+      publicLocations: ['japan', 'new_location', 'us_west'],
+      allLocations: [
+        'japan',
+        'new_location',
+        'us_west',
+        'custom location 1 (private)',
+        'custom location 2 (private)',
+      ],
+    });
 
-    expect(groupLocations(formatted)).toEqual({
-      locations: ['japan', 'new_location', 'us_west'],
+    expect(groupLocations(formatted.allLocations)).toEqual({
       privateLocations: ['custom location 1', 'custom location 2'],
+      locations: ['japan', 'new_location', 'us_west'],
     });
   });
 
   describe('CLI command', () => {
+    const PROJECT_DIR = join(__dirname, 'new-project');
+    async function fakeProjectSetup(settings) {
+      await writeFile(
+        join(PROJECT_DIR, 'synthetics.config.ts'),
+        `export default { project: ${JSON.stringify(settings)} }`
+      );
+    }
+
+    beforeAll(async () => {
+      await mkdir(PROJECT_DIR, { recursive: true });
+    });
+    afterAll(async () => {
+      await rm(PROJECT_DIR, { recursive: true, force: true });
+    });
+
     const runLocations = async (args: Array<string> = []) => {
       const cli = new CLIMock()
         .args(['locations', ...args])
-        .run({ cwd: __dirname });
+        .run({ cwd: PROJECT_DIR, env: process.env });
       expect(await cli.exitCode).toBe(0);
       return cli.stderr();
     };
+
     it('render public locations by default', async () => {
       const output = await runLocations();
       expect(output).not.toContain(`custom location`);
@@ -94,7 +116,14 @@ describe('Locations', () => {
         '--auth',
         apiKey,
       ]);
-      expect(output).toContain(`custom location 1(private)`);
+      expect(output).toContain(`custom location 1`);
+    });
+
+    it('use project url settings for private locations', async () => {
+      await fakeProjectSetup({ url: server.PREFIX });
+      const output = await runLocations(['--auth', apiKey]);
+      expect(output).toContain(`custom location 1`);
+      expect(output).toContain(`custom location 2`);
     });
   });
 });
diff --git a/__tests__/options.test.ts b/__tests__/options.test.ts
index 8a251ef..0d998ee 100644
--- a/__tests__/options.test.ts
+++ b/__tests__/options.test.ts
@@ -119,4 +119,18 @@ describe('options', () => {
       },
     });
   });
+
+  it('cli arg headless override playwright headless arg', () => {
+    const cliArgs: CliArgs = {
+      playwrightOptions: {
+        headless: false,
+      },
+      headless: true,
+    };
+    expect(normalizeOptions(cliArgs)).toMatchObject({
+      playwrightOptions: {
+        headless: true,
+      },
+    });
+  });
 });