From c10476420bde67682111f6c06a95d34d7952f030 Mon Sep 17 00:00:00 2001 From: Daniel Helm Date: Wed, 4 Sep 2024 10:19:59 -0500 Subject: [PATCH] adds tls setup, tls support in prep-charts updates --- src/commands/setup/prep-charts.ts | 13 ++ src/commands/setup/push-secrets.ts | 19 ++- src/commands/setup/tls.ts | 258 +++++++++++++++++++++++++++++ test/commands/setup/tls.test.ts | 14 ++ 4 files changed, 295 insertions(+), 9 deletions(-) create mode 100644 src/commands/setup/tls.ts create mode 100644 test/commands/setup/tls.test.ts diff --git a/src/commands/setup/prep-charts.ts b/src/commands/setup/prep-charts.ts index ce28dff..ca932e8 100644 --- a/src/commands/setup/prep-charts.ts +++ b/src/commands/setup/prep-charts.ts @@ -211,6 +211,19 @@ export default class SetupPrepCharts extends Command { productionYaml.ingress.main.hosts[0].host = configValue changes.push({ key: 'ingress.main.hosts[0].host', oldValue: oldHost, newValue: configValue }) updated = true + + // Update TLS section if it exists + if (productionYaml.ingress.main.tls && productionYaml.ingress.main.tls.length > 0) { + productionYaml.ingress.main.tls.forEach((tlsConfig: any) => { + if (tlsConfig.hosts && tlsConfig.hosts.length > 0) { + const oldTlsHost = tlsConfig.hosts[0] + if (oldTlsHost !== configValue) { + tlsConfig.hosts[0] = configValue + changes.push({ key: 'ingress.main.tls[].hosts[0]', oldValue: oldTlsHost, newValue: configValue }) + } + } + }) + } } } } diff --git a/src/commands/setup/push-secrets.ts b/src/commands/setup/push-secrets.ts index 4c343e6..663c92c 100644 --- a/src/commands/setup/push-secrets.ts +++ b/src/commands/setup/push-secrets.ts @@ -14,7 +14,7 @@ interface SecretService { } class AWSSecretService implements SecretService { - constructor(private region: string) { } + constructor(private region: string, private prefixName: string) { } private async convertToJson(filePath: string): Promise { const content = await fs.promises.readFile(filePath, 'utf-8') @@ -33,12 +33,12 @@ class AWSSecretService implements SecretService { } private async pushToAWSSecret(content: string, secretName: string): Promise { - const command = `aws secretsmanager create-secret --name "scroll/${secretName}" --secret-string '${content}' --region ${this.region}` + const command = `aws secretsmanager create-secret --name "${this.prefixName}/${secretName}" --secret-string '${content}' --region ${this.region}` try { await execAsync(command) - console.log(chalk.green(`Successfully pushed secret: scroll/${secretName}`)) + console.log(chalk.green(`Successfully pushed secret: ${this.prefixName}/${secretName}`)) } catch (error) { - console.error(chalk.red(`Failed to push secret: scroll/${secretName}`)) + console.error(chalk.red(`Failed to push secret: ${this.prefixName}/${secretName}`)) } } @@ -273,6 +273,10 @@ export default class SetupPushSecrets extends Command { secretRegion: await input({ message: chalk.cyan('Enter AWS secret region:'), default: "us-west-2" + }), + prefixName: await input({ + message: chalk.cyan('Enter secret prefix name:'), + default: "scroll" }) } } @@ -381,11 +385,8 @@ export default class SetupPushSecrets extends Command { let provider: string if (secretService === 'aws') { - const region = await input({ - message: chalk.cyan('Enter AWS region:'), - validate: (value) => value.length > 0 || chalk.red('AWS region is required'), - }) - service = new AWSSecretService(region) + const awsCredentials = await this.getAWSCredentials() + service = new AWSSecretService(awsCredentials.secretRegion, awsCredentials.prefixName) provider = 'aws' } else if (secretService === 'vault') { service = new HashicorpVaultDevService(flags.debug) diff --git a/src/commands/setup/tls.ts b/src/commands/setup/tls.ts new file mode 100644 index 0000000..82abe30 --- /dev/null +++ b/src/commands/setup/tls.ts @@ -0,0 +1,258 @@ +import { Command, Flags } from '@oclif/core' +import * as fs from 'fs' +import * as path from 'path' +import * as yaml from 'js-yaml' +import chalk from 'chalk' +import { exec } from 'child_process' +import { promisify } from 'util' +import { confirm, input, select } from '@inquirer/prompts' + +const execAsync = promisify(exec) + +export default class SetupTls extends Command { + static override description = 'Update TLS configuration in Helm charts' + + static override examples = [ + '<%= config.bin %> <%= command.id %>', + '<%= config.bin %> <%= command.id %> --debug', + ] + + static override flags = { + debug: Flags.boolean({ + char: 'd', + description: 'Show debug output and confirm before making changes', + default: false, + }), + } + + private selectedIssuer: string | null = null + private debugMode: boolean = false + + private async checkClusterIssuer(): Promise { + try { + const { stdout } = await execAsync('kubectl get clusterissuer -o jsonpath="{.items[*].metadata.name}"') + const clusterIssuers = stdout.trim().split(' ').filter(Boolean) + + if (clusterIssuers.length > 0) { + this.log(chalk.green('Found ClusterIssuer(s):')) + clusterIssuers.forEach(issuer => this.log(chalk.cyan(` - ${issuer}`))) + + if (clusterIssuers.length === 1) { + const useExisting = await confirm({ + message: chalk.yellow(`Do you want to use the existing ClusterIssuer "${clusterIssuers[0]}"?`), + }) + if (useExisting) { + this.selectedIssuer = clusterIssuers[0] + return true + } + return false + } else { + this.selectedIssuer = await select({ + message: chalk.yellow('Select which ClusterIssuer you want to use:'), + choices: clusterIssuers.map(issuer => ({ name: issuer, value: issuer })), + }) + return true + } + } else { + this.log(chalk.yellow('No ClusterIssuer found in the cluster.')) + return false + } + } catch (error) { + this.log(chalk.red('Error checking for ClusterIssuer:')) + this.log(chalk.red(error as string)) + return false + } + } + + private async createClusterIssuer(email: string): Promise { + const clusterIssuerYaml = ` +apiVersion: cert-manager.io/v1 +kind: ClusterIssuer +metadata: + name: letsencrypt-prod +spec: + acme: + server: https://acme-v02.api.letsencrypt.org/directory + email: ${email} + privateKeySecretRef: + name: letsencrypt-prod + solvers: + - http01: + ingress: + class: nginx +` + + try { + await fs.promises.writeFile('cluster-issuer.yaml', clusterIssuerYaml) + await execAsync('kubectl apply -f cluster-issuer.yaml') + await fs.promises.unlink('cluster-issuer.yaml') + this.log(chalk.green('ClusterIssuer created successfully.')) + } catch (error) { + this.error(chalk.red(`Failed to create ClusterIssuer: ${error}`)) + } + } + + private async loadConfig(): Promise { + // TODO: Implement loading of config.yaml + } + + private async updateChartIngress(chart: string, issuer: string): Promise { + const chartPath = path.join(process.cwd(), chart) + const yamlPath = path.join(chartPath, 'values', 'production.yaml') + + if (!fs.existsSync(yamlPath)) { + this.log(chalk.yellow(`production.yaml not found for ${chart}`)) + return + } + + try { + const content = fs.readFileSync(yamlPath, 'utf8') + const yamlContent: any = yaml.load(content) + + if (yamlContent.ingress?.main) { + const originalContent = yaml.dump(yamlContent.ingress.main, { lineWidth: -1, noRefs: true }) + let updated = false + + // Add or update annotation + if (!yamlContent.ingress.main.annotations) { + yamlContent.ingress.main.annotations = {} + } + if (yamlContent.ingress.main.annotations['cert-manager.io/cluster-issuer'] !== issuer) { + yamlContent.ingress.main.annotations['cert-manager.io/cluster-issuer'] = issuer + updated = true + } + + // Update or add TLS configuration + if (yamlContent.ingress.main.hosts && yamlContent.ingress.main.hosts.length > 0) { + const firstHost = yamlContent.ingress.main.hosts[0] + if (typeof firstHost === 'object' && firstHost.host) { + const hostname = firstHost.host + + if (!yamlContent.ingress.main.tls) { + yamlContent.ingress.main.tls = [{ + secretName: `${chart}-tls`, + hosts: [hostname], + }] + updated = true + } else if (yamlContent.ingress.main.tls.length === 0) { + yamlContent.ingress.main.tls.push({ + secretName: `${chart}-tls`, + hosts: [hostname], + }) + updated = true + } else { + // Update existing TLS configuration + yamlContent.ingress.main.tls.forEach((tlsConfig: any) => { + if (!tlsConfig.secretName || tlsConfig.secretName !== `${chart}-tls`) { + tlsConfig.secretName = `${chart}-tls` + updated = true + } + if (!tlsConfig.hosts || !tlsConfig.hosts.includes(hostname)) { + tlsConfig.hosts = [hostname] + updated = true + } + }) + } + } + } + + if (updated) { + const updatedContent = yaml.dump(yamlContent.ingress.main, { lineWidth: -1, noRefs: true }) + + if (this.debugMode) { + this.log(chalk.yellow(`\nProposed changes for ${chart}:`)) + this.log(chalk.red('- Original content:')) + this.log(originalContent) + this.log(chalk.green('+ Updated content:')) + this.log(updatedContent) + + const confirmUpdate = await confirm({ + message: chalk.cyan(`Do you want to apply these changes to ${chart}?`), + }) + + if (!confirmUpdate) { + this.log(chalk.yellow(`Skipped updating ${chart}`)) + return + } + } + + // Write updated YAML back to file + const updatedYamlContent = yaml.dump(yamlContent, { + lineWidth: -1, + noRefs: true, + quotingType: '"', + forceQuotes: false + }) + fs.writeFileSync(yamlPath, updatedYamlContent) + + this.log(chalk.green(`Updated TLS configuration for ${chart}`)) + } else { + this.log(chalk.green(`No changes needed for ${chart}`)) + } + } else { + this.log(chalk.yellow(`No ingress.main configuration found in ${chart}`)) + } + } catch (error) { + this.error(chalk.red(`Failed to update ${chart}: ${error}`)) + } + } + + public async run(): Promise { + const { flags } = await this.parse(SetupTls) + this.debugMode = flags.debug + + try { + this.log(chalk.blue('Starting TLS configuration update...')) + + let clusterIssuerExists = await this.checkClusterIssuer() + + while (!clusterIssuerExists) { + const createIssuer = await confirm({ + message: chalk.yellow('No suitable ClusterIssuer found. Do you want to create one?'), + }) + + if (createIssuer) { + const email = await input({ + message: chalk.cyan('Enter your email address for the ClusterIssuer:'), + validate: (value) => { + if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) { + return 'Please enter a valid email address.' + } + return true + }, + }) + + await this.createClusterIssuer(email) + clusterIssuerExists = await this.checkClusterIssuer() + } else { + this.log(chalk.yellow('ClusterIssuer is required for TLS configuration. Exiting.')) + return + } + } + + if (!this.selectedIssuer) { + this.error(chalk.red('No ClusterIssuer selected. Exiting.')) + return + } + + this.log(chalk.green(`Using ClusterIssuer: ${this.selectedIssuer}`)) + + const chartsToUpdate = [ + 'frontends', + 'blockscout', + 'coordinator-api', + 'bridge-history-api', + 'rollup-explorer-backend', + 'l2-rpc' + ] + + for (const chart of chartsToUpdate) { + await this.updateChartIngress(chart, this.selectedIssuer) + } + + this.log(chalk.green('TLS configuration update completed.')) + } catch (error) { + this.error(chalk.red(`Failed to update TLS configuration: ${error}`)) + } + } +} diff --git a/test/commands/setup/tls.test.ts b/test/commands/setup/tls.test.ts new file mode 100644 index 0000000..157bc7a --- /dev/null +++ b/test/commands/setup/tls.test.ts @@ -0,0 +1,14 @@ +import {runCommand} from '@oclif/test' +import {expect} from 'chai' + +describe('setup:tls', () => { + it('runs setup:tls cmd', async () => { + const {stdout} = await runCommand('setup:tls') + expect(stdout).to.contain('hello world') + }) + + it('runs setup:tls --name oclif', async () => { + const {stdout} = await runCommand('setup:tls --name oclif') + expect(stdout).to.contain('hello oclif') + }) +})