Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

configurable schedule #28

Closed
github-actions bot opened this issue Jun 8, 2023 · 1 comment
Closed

configurable schedule #28

github-actions bot opened this issue Jun 8, 2023 · 1 comment
Assignees
Labels

Comments

@github-actions
Copy link

github-actions bot commented Jun 8, 2023

https://api.github.com/groton-school/blackbaud-to-google-group-sync/blob/d87e266cf36df1864a097c23f7cfa7ac256f2f22/scripts/setup.js#L291

(async () => {
  const process = require('process');
  const fs = require('fs');
  const child = require('child_process');
  const { rword } = require('rword');
  const path = require('path');
  const readline = require('readline/promises').createInterface({
    input: process.stdin,
    output: process.stdout
  });

  rword.load('big');
  function createProjectId() {
    const word1 = rword.generate(1, {
      length: 4 + Math.floor(Math.random() * 7)
    });
    const word2 = rword.generate(1, {
      length: 4 + Math.floor(Math.random() * (30 - 8 - word1.length - 4))
    });
    return `${word1}-${word2}-${Math.floor(99999 + Math.random() * 900001)}`;
  }

  const exec = (command) => child.execSync(command, { stdio: 'inherit' });

  function versionTest({
    name,
    download = undefined,
    command = undefined,
    fail = true
  }) {
    command = command || `${name} --version`;
    if (!/\d+\.\d/.test(child.execSync(command))) {
      if (fail) {
        console.error(
          `${name} is required${download ? `, install from ${download}` : ''}`
        );
        process.exit(1);
      } else {
        return false;
      }
    }
    return true;
  }

  // TODO configure project name
  const appName = 'Blackbaud-to-Google Group Sync';
  // TODO configure project ID
  const projectId = createProjectId();

  const flags = '--quiet --format=json';
  const flagsWithProject = `${flags} --project=${projectId}`;
  function gcloud(command, withProjectId = true) {
    let actualFlags = flagsWithProject;
    if (withProjectId === null) {
      actualFlags = '';
    } else if (actualFlags === false) {
      actualFlags = flags;
    }
    const result = child.execSync(`gcloud ${command} ${actualFlags} `);
    try {
      return JSON.parse(result);
    } catch (e) {
      return result;
    }
  }

  async function choiceFrom({ prompt, list, display, defaultChoice = 1 }) {
    switch (list.length) {
      case 0:
        throw new Error('empty list');
      case 1:
        defaultChoice = 1;
        break;
      default:
        if (defaultChoice > list.length) {
          defaultChoice = undefined;
        }
    }
    console.log(prompt);
    list.forEach((item, i) => console.log(`  ${i}. ${item[display]}`));

    let choice;
    let question = `(${defaultChoice > 1 ? 1 : '[1]'}-${defaultChoice < list.length ? `[${defaultChoice}]` : ''
      }-${defaultChoice === list.length ? `[${defaultChoice}]` : list.length})`;
    do {
      choice = await readline.question(question);
      if (choice.length) {
        choice = (parseInt(choice) || 0) - 1;
      } else {
        choice = defaultChoice - 1;
      }
    } while (choice < 0 || choice >= list.length);
    return list[choice];
  }

  async function nonEmpty({ prompt }) {
    let response;
    do {
      response = await readline.question(prompt);
    } while (response.length === 0);
    return response;
  }

  async function untilBlank({ prompt }) {
    const responses = [];
    let response;
    do {
      response = await readline.question(`${prompt} [<Enter> to end]`);
      if (response.length > 0) {
        responses.push(response);
      }
    } while (response.length > 0);
    return responses;
  }

  // set project root as cwd
  process.chdir(path.join(__dirname, '..'));

  // test for CLI dependencies
  versionTest({
    name: 'npm',
    download: 'https://nodejs.org/'
  });
  versionTest({
    name: 'composer',
    download: 'https://getcomposer.org/'
  });
  versionTest({
    name: 'gcloud',
    download: 'https://cloud.google.com/sdk/docs/install'
  });
  const pnpm = versionTest({
    name: 'pnpm',
    dowload: 'https://pnpm.io/',
    fail: false
  });

  // install dependencies
  exec(`${pnpm ? 'pnpm' : 'npm'} install`);
  exec('composer install');

  // create a new project
  let response = gcloud(
    `projects create --name="${appName}" ${projectId}`,
    false
  );
  if (/error/i.test(response)) {
    console.error(response);
    process.exit(1);
  }

  // enable billing
  gcloud(`components install beta`, null);
  const accountId = path.basename(
    (
      await choiceFrom({
        prompt: 'Select a billing account for this project',
        list: gcloud(`beta billing accounts list --filter=open=true`),
        display: 'displayName'
      })
    ).name
  );
  gcloud(
    `beta billing projects link ${projectId} --billing-account="${accountId}`,
    false
  );

  // enable APIs
  gcloud(`services enable admin.googleapis.com`);
  gcloud(`services enable iap.googleapis.com`);
  gcloud(`services enable secretmanager.googleapis.com`);
  gcloud(`services enable cloudscheduler.googleapis.com`);
  gcloud(`services enable appengine.googleapis.com`);

  // configure workspace admin as owner
  // TODO output directions/links
  const googleDelegatedAdmin = await readline.question(
    'Enter the Google ID for a Workspace Admin who will delegate authority for this app'
  );
  gcloud(
    `projects add-iam-policy-binding ${projectId} --member="user:${googleDelegatedAdmin}" --role="roles/owner"`,
    false
  );

  // create App Engine instance
  // TODO set default region us-east4
  const region = await choiceFrom({
    prompt: 'Select a region for the app engine instance',
    list: gcloud(`app regions list`),
    display: 'region'
  }).region;
  gcloud(`app create --region=${region}`);
  const url = `https://${gcloud(`app describe`).defaultHostname}`;
  fs.writeFileSync(
    '.env',
    `PROJECT=${projectId}
URL=${url}`
  );

  // create default instance so IAP can be configured
  exec(`npm run build`);
  exec(`npm run deploy`);

  // configure IAP (and OAuth consent screen)
  const supportEmail = await nonEmpty({
    prompt: 'Enter a support email address for the app'
  });
  const brand = gcloud(
    `iap oauth-brands create --application_title${appName} --support_email=${supportEmail}`
  ).name;
  const oauth = gcloud(
    `iap oauth-clients create ${brand} --display_name=IAP-App-Engine-app`
  );
  gcloud(
    `iap web enable --resource-type=app-engine --oauth2-client-id=${path.basename(
      oauth.name
    )} --oauth2-client-secret=${oauth.secret}`
  );
  (
    await untilBlank({
      prompt: 'Email address of user who can access the app interface'
    })
  ).forEach((userEmail) =>
    gcloud(
      `projects add-iam-policy-binding ${projectId} --member="user:${userEmail}" --role="roles/iap.httpsResourceAccessor"`,
      false
    )
  );

  // configure Blackbaud SKY app
  const blackbaudAccessKey = await nonEmpty({
    prompt:
      'Enter a subscription access key from https://developer.blackbaud.com/subscriptions'
  });
  console.log('Create a new app at https://developer.blackbaud.com/apps');
  const blackbaudClientId = await nonEmpty({
    prompt: "Enter the app's OAuth client ID"
  });
  const blackbaudClientSecret = await nonEmpty({
    prompt: "Enter one of the app's OAuth secrets"
  });
  const blackbaudRedirectUrl = `${url}/redirect`;
  console.log(`Configure ${blackbaudRedirectUrl} as the app's redirect URL`);
  // TODO pause here?
  // TODO directions for limiting scope of app

  // configure delegated admin service account
  const serviceAccount = gcloud(
    `iam service-accounts create ${appName
      .toLowerCase()
      .replace(/[^a-z]/g, '-')
      .replace(/--/g, '-')} --display-name="Google Delegated Admin"`
  );
  console.log(
    `${googleDelegatedAdmin} needs to follow the directions at https://github.com/groton-school/blackbaud-to-google-group-sync/blob/main/docs/google-workspace-admin.md`
  );
  const credentials = `${serviceAccount.uniqueId}.json`;
  gcloud(
    `iam service-accounts keys create ${credentials} --iam-account=${serviceAccount.email}`
  );
  console.log(`The Service Account Unique ID is ${serviceAccount.uniqueId}`);
  // TODO pause here?

  // store secrets
  exec(
    `echo "${blackbaudAccessKey}" | gcloud secrets create BLACKBAUD_ACCESS_KEY --data-file=- ${flagsWithProject}`
  );
  exec(
    `echo "null" | gcloud secrets create BLACKBAUD_API_TOKEN --data-file=- ${flagsWithProject}`
  );
  exec(
    `echo "${blackbaudClientId}" | gcloud secrets create BLACKBAUD_CLIENT_ID --data-file=- ${flagsWithProject}`
  );
  exec(
    `echo "${blackbaudClientSecret}" | gcloud secrets create BLACKBAUD_CLIENT_SECRET --data-file=- ${flagsWithProject}`
  );
  exec(
    `echo "${blackbaudRedirectUrl}" | gcloud secrets create BLACKBAUD_REDIRECT_URL --data-file=- ${flagsWithProject}`
  );
  gcloud(`secrets create GOOGLE_CREDENTIALS --data-file=${credentials}`);
  fs.unlinkSync(credentials);

  exec(
    `echo "${googleDelegatedAdmin}" | gcloud secrets create GOOGLE_DELEGATED_ADMIN --data-file=- ${flagsWithProject}`
  );

  console.log(`Authorize the app at ${url}`);
  // TODO pause here?

  // schedule daily sync
  // TODO configurable schedule
  // TODO configurable job name
  gcloud(
    `scheduler jobs create app-engine daily-blackbaud-to-google-sync --schedule="0 1 * * *" --relative-url="/sync"`
  );
})();
@github-actions
Copy link
Author

Closed in a5798ad

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant