UNPKG

sfdx-hardis

Version:

Swiss-army-knife Toolbox for Salesforce. Allows you to define a complete CD/CD Pipeline. Orchestrate base commands and assist users with interactive wizards

234 lines (225 loc) • 11.9 kB
/* jscpd:ignore-start */ import { SfCommand, Flags } from '@salesforce/sf-plugins-core'; import fs from 'fs-extra'; import c from "chalk"; import * as path from "path"; import { Messages, SfError } from '@salesforce/core'; import Cloudflare from 'cloudflare'; import { execCommand, getCurrentGitBranch, uxLog } from '../../../common/utils/index.js'; import { CONSTANTS, getEnvVar } from '../../../config/index.js'; import which from 'which'; import { generateMkDocsHTML } from '../../../common/docBuilder/docUtils.js'; Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); const messages = Messages.loadMessages('sfdx-hardis', 'org'); export default class MkDocsToCloudflare extends SfCommand { static title = 'MkDocs to Cloudflare'; static description = `Generates MkDocs HTML pages and upload them to Cloudflare as a static pages This command performs the following operations: - Generates MkDocs HTML pages (using locally installed mkdocs-material, or using mkdocs docker image) - Creates a Cloudflare pages app - Assigns a policy restricting access to the application - Opens the new WebSite in the default browser (only if not in CI context) Note: the documentation must have been previously generated using "sf hardis:doc:project2markdown --with-history" You can: - Override default styles by customizing mkdocs.yml More info on [Documentation section](${CONSTANTS.DOC_URL_ROOT}/salesforce-project-documentation/) | Variable | Description | Default | | :----------------------------------------- | :---------- | :-----: | | \`CLOUDFLARE_EMAIL\` | Cloudflare account email | <!--- Required --> | | \`CLOUDFLARE_API_TOKEN\` | Cloudflare API token | <!--- Required --> | | \`CLOUDFLARE_ACCOUNT_ID\` | Cloudflare account | <!--- Required --> | | \`CLOUDFLARE_PROJECT_NAME\` | Project name, that will also be used for site URL | Built from git branch name | | \`CLOUDFLARE_DEFAULT_LOGIN_METHOD_TYPE\` | Cloudflare default login method type | \`onetimepin\` | | \`CLOUDFLARE_DEFAULT_ACCESS_EMAIL_DOMAIN\` | Cloudflare default access email domain | \`@cloudity.com\` | | \`CLOUDFLARE_EXTRA_ACCESS_POLICY_ID_LIST\` | Policies to assign to every application access | <!--- Optional --> | `; static examples = [ '$ sf hardis:doc:mkdocs-to-cf', ]; static flags = { debug: Flags.boolean({ char: 'd', default: false, description: messages.getMessage('debugMode'), }), websocket: Flags.string({ description: messages.getMessage('websocket'), }), skipauth: Flags.boolean({ description: 'Skip authentication check when a default username is required', }) }; // Set this to true if your command requires a project workspace; 'requiresProject' is false by default static requiresProject = true; debugMode = false; apiEmail; apiToken; accountId; client; projectName; currentGitBranch; defaultLoginMethodType = process.env.CLOUDFLARE_DEFAULT_LOGIN_METHOD_TYPE || "onetimepin"; defaultAccessEmailDomain = process.env.CLOUDFLARE_DEFAULT_ACCESS_EMAIL_DOMAIN || "@cloudity.com"; pagesProjectName; pagesProject; accessPolicyName; accessPolicy; extraPolicyIds = (process.env.CLOUDFLARE_EXTRA_ACCESS_POLICY_ID_LIST || "").split(",").filter(p => p); accessApp; /* jscpd:ignore-end */ async run() { const { flags } = await this.parse(MkDocsToCloudflare); this.debugMode = flags.debug || false; // Check if the project is a MkDocs project const mkdocsYmlFile = path.join(process.cwd(), "mkdocs.yml"); if (!fs.existsSync(mkdocsYmlFile)) { throw new SfError('This command needs a mkdocs.yml config file. Generate one using "sf hardis:doc:project2markdown --with-history"'); } this.currentGitBranch = await getCurrentGitBranch() || "main"; this.projectName = await this.getProjectName(); this.pagesProjectName = `sfdoc-${this.projectName}`; this.accessPolicyName = `access-policy-${this.projectName}`; // Create connection to Cloudflare this.setupCloudflareClient(); // Generate HTML pages if ((process.env?.SKIP_BUILD_HTML || "false") !== "true") { await generateMkDocsHTML(); } // Get or Create Cloudflare Pages project await this.ensureCloudflarePagesProject(); // Ensure there is a policy restricting access to the application await this.ensureCloudflareAccessPolicy(); // Ensure there is an access application await this.ensureCloudflareAccessApplication(); // Ensure the access application has the right policy await this.ensureCloudflareAccessApplicationPolicy(); // Upload pages await this.uploadHtmlPages(); return { success: true }; } // Search first for CLOUDFLARE_PROJECT_NAME_<lang> env var, then CLOUDFLARE_PROJECT_NAME, then git branch name // If none of them is found, use the default project name async getProjectName() { const defaultProjectName = (getEnvVar('CLOUDFLARE_PROJECT_NAME') || this.currentGitBranch).replace(/\//g, "-").toLowerCase(); const promptsLanguage = getEnvVar('PROMPTS_LANGUAGE') || 'en'; const languageScopedProjectVariableName = `CLOUDFLARE_PROJECT_NAME_${promptsLanguage?.toUpperCase()}`; if (getEnvVar(languageScopedProjectVariableName)) { return getEnvVar(languageScopedProjectVariableName) || defaultProjectName; } return defaultProjectName; } setupCloudflareClient() { this.apiEmail = process.env.CLOUDFLARE_EMAIL; this.apiToken = process.env.CLOUDFLARE_API_TOKEN; this.accountId = process.env.CLOUDFLARE_ACCOUNT_ID; if (!this.apiEmail || !this.accountId || !this.apiToken) { throw new Error('Missing CLOUDFLARE_EMAIL or CLOUDFLARE_API_TOKEN or CLOUDFLARE_ACCOUNT_ID'); } this.client = new Cloudflare({ apiEmail: this.apiEmail, apiToken: this.apiToken, }); uxLog(this, c.grey("Cloudflare client info found")); } async ensureCloudflarePagesProject() { uxLog(this, c.cyan("Checking Cloudflare Pages project...")); try { this.pagesProject = await this.client.pages.projects.get(this.pagesProjectName, { account_id: this.accountId || "" }); uxLog(this, c.cyan("Cloudflare Pages project found: " + this.pagesProjectName)); } catch (e) { uxLog(this, c.grey(e.message)); this.pagesProject = await this.client.pages.projects.create({ name: this.pagesProjectName, account_id: this.accountId || "", production_branch: this.currentGitBranch || "main", }); uxLog(this, c.green("Cloudflare Pages project created: " + this.pagesProjectName)); } uxLog(this, c.grey(JSON.stringify(this.pagesProject, null, 2))); } async ensureCloudflareAccessPolicy() { uxLog(this, c.cyan("Checking Cloudflare Access policy...")); const accessPolicies = await this.client.zeroTrust.access.policies.list({ account_id: this.accountId || "" }); this.accessPolicy = accessPolicies.result.find((p) => p.name === this.accessPolicyName) || null; if (this.accessPolicy) { uxLog(this, c.cyan("Cloudflare policy found: " + this.accessPolicyName)); } else { const loginMethods = await this.client.zeroTrust.identityProviders.list({ account_id: this.accountId || "" }); const defaultLoginMethod = loginMethods.result.find((m) => m.type === this.defaultLoginMethodType); if (!defaultLoginMethod) { throw new SfError(`No login method of type ${this.defaultLoginMethodType} found in Cloudflare account. Please create one in Zero Trust/Settings before running this command`); } this.accessPolicy = await this.client.zeroTrust.access.policies.create({ name: this.accessPolicyName, account_id: this.accountId || "", decision: "allow", include: [ { email_domain: { domain: this.defaultAccessEmailDomain } }, ], require: [ { login_method: { id: defaultLoginMethod.id } } ], }); uxLog(this, c.green("Cloudflare policy created: " + this.accessPolicyName)); } uxLog(this, c.grey(JSON.stringify(this.accessPolicy, null, 2))); } async ensureCloudflareAccessApplication() { uxLog(this, c.cyan("Checking Cloudflare access application...")); const accessApplications = await this.client.zeroTrust.access.applications.list({ account_id: this.accountId || "" }); this.accessApp = (accessApplications.result.find((a) => a.name === this.pagesProject?.domains?.[0]) || null); if (this.accessApp) { uxLog(this, c.cyan("Cloudflare access application found: " + this.pagesProject?.domains?.[0])); } else { this.accessApp = await this.client.zeroTrust.access.applications.create({ name: this.pagesProject?.domains?.[0], account_id: this.accountId || "", type: "self_hosted", domain: this.pagesProject?.domains?.[0], destinations: [ { "type": "public", "uri": `${this.pagesProject?.domains?.[0]}` }, { "type": "public", "uri": `*.${this.pagesProject?.domains?.[0]}` } ] }); uxLog(this, c.green("Cloudflare access application created: " + this.pagesProject?.domains?.[0])); } uxLog(this, c.grey(JSON.stringify(this.accessApp, null, 2))); } async ensureCloudflareAccessApplicationPolicy() { uxLog(this, c.cyan("Checking Cloudflare access application policy...")); if (this.accessApp?.policies?.length && this.accessApp.policies.find(p => p.id === this.accessPolicy?.id)) { uxLog(this, c.cyan(`Access Application ${this.accessApp.name} already has the policy ${this.accessPolicy?.name}`)); } else { const policiesWithExtra = this.extraPolicyIds.concat([this.accessPolicy?.id || ""]).filter(p => p); this.accessApp = (await this.client.zeroTrust.access.applications.update(this.accessApp?.id || "", { account_id: this.accountId, domain: this.accessApp?.domain, destinations: this.accessApp?.destinations, type: this.accessApp?.type, policies: policiesWithExtra, })); uxLog(this, c.green(`Access Application ${this.accessApp?.name} updated with the policy ${this.accessPolicy?.name}`)); } uxLog(this, c.grey(JSON.stringify(this.accessApp, null, 2))); } async uploadHtmlPages() { uxLog(this, c.cyan("Uploading HTML pages to Cloudflare Pages...")); let wranglerCommand = `wrangler pages deploy ./site --project-name="${this.pagesProjectName}" --branch=${this.currentGitBranch}`; const isWranglerAvailable = await which("wrangler", { nothrow: true }); if (!isWranglerAvailable) { wranglerCommand = "npx --yes " + wranglerCommand; } await execCommand(wranglerCommand, this, { fail: true, output: true, debug: this.debugMode }); } } //# sourceMappingURL=mkdocs-to-cf.js.map