Skip to content

Commit

Permalink
adds tls setup, tls support in prep-charts updates
Browse files Browse the repository at this point in the history
  • Loading branch information
dghelm committed Sep 4, 2024
1 parent e66a1d4 commit c104764
Show file tree
Hide file tree
Showing 4 changed files with 295 additions and 9 deletions.
13 changes: 13 additions & 0 deletions src/commands/setup/prep-charts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
}
}
})
}
}
}
}
Expand Down
19 changes: 10 additions & 9 deletions src/commands/setup/push-secrets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> {
const content = await fs.promises.readFile(filePath, 'utf-8')
Expand All @@ -33,12 +33,12 @@ class AWSSecretService implements SecretService {
}

private async pushToAWSSecret(content: string, secretName: string): Promise<void> {
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}`))
}
}

Expand Down Expand Up @@ -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"
})
}
}
Expand Down Expand Up @@ -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)
Expand Down
258 changes: 258 additions & 0 deletions src/commands/setup/tls.ts
Original file line number Diff line number Diff line change
@@ -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<boolean> {
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<void> {
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<any> {
// TODO: Implement loading of config.yaml
}

private async updateChartIngress(chart: string, issuer: string): Promise<void> {
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<void> {
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}`))
}
}
}
14 changes: 14 additions & 0 deletions test/commands/setup/tls.test.ts
Original file line number Diff line number Diff line change
@@ -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')
})
})

0 comments on commit c104764

Please sign in to comment.