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

Refactor resolve-aws-secret-version #1774

Merged
merged 1 commit into from
Jan 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions resolve-aws-secret-version/README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
# resolve-aws-secret-version

This is an action to resolve version IDs of `AWSSecret` in a manifest.
This is an action to resolve the secret versions of manifests.
It is designed for https://github.com/mumoshu/aws-secret-operator.

## Inputs

| Name | Type | Description |
| ----------- | ---------------- | ------------------------------ |
| `manifests` | multiline string | Glob pattern(s) to manifest(s) |
| `manifests` | Multiline string | Glob pattern(s) to manifest(s) |

## Example

Expand Down
2 changes: 1 addition & 1 deletion resolve-aws-secret-version/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as core from '@actions/core'
import { run } from './run.js'

async function main(): Promise<void> {
const main = async () => {
const inputs = {
manifests: core.getInput('manifests'),
}
Expand Down
70 changes: 39 additions & 31 deletions resolve-aws-secret-version/src/resolve.ts
Original file line number Diff line number Diff line change
@@ -1,64 +1,72 @@
import { promises as fs } from 'fs'
import * as fs from 'fs/promises'
import * as core from '@actions/core'
import * as yaml from 'js-yaml'
import { assertKubernetesAWSSecret, isKubernetesObject } from './kubernetes.js'
import assert from 'assert'

interface AWSSecretsManager {
type AWSSecretsManager = {
getCurrentVersionId(secretId: string): Promise<string>
}

// resolve placeholders of AWSSecret to the current version IDs and write in-place
export const resolveInplace = async (inputManifestPath: string, manager: AWSSecretsManager): Promise<void> => {
core.info(`reading ${inputManifestPath}`)
const inputManifest = (await fs.readFile(inputManifestPath)).toString()
const outputManifest = await resolve(inputManifest, manager)
export const updateManifest = async (manifestPath: string, manager: AWSSecretsManager): Promise<void> => {
core.info(`Reading the manifest: ${manifestPath}`)
const inputManifest = await fs.readFile(manifestPath, 'utf-8')
const outputManifest = await replaceSecretVersionIds(inputManifest, manager)

core.info(`writing to ${inputManifestPath}`)
await fs.writeFile(inputManifestPath, outputManifest, { encoding: 'utf-8' })
core.info(`Writing the manifest: ${manifestPath}`)
await fs.writeFile(manifestPath, outputManifest, { encoding: 'utf-8' })
}

// resolve placeholders of AWSSecret to the current version IDs
export const resolve = async (inputManifest: string, manager: AWSSecretsManager): Promise<string> => {
const awsSecrets = findAWSSecrets(inputManifest)
let resolved = inputManifest
export const replaceSecretVersionIds = async (manifest: string, manager: AWSSecretsManager): Promise<string> => {
const awsSecrets = findAWSSecretsFromManifest(manifest)
let resolved = manifest
for (const awsSecret of awsSecrets) {
core.info(`processing AWSSecret resource name=${awsSecret.name}, secretId=${awsSecret.secretId}`)
core.info(
`Finding the current versionId of ${awsSecret.kind}: name=${awsSecret.name}, secretId=${awsSecret.secretId}`,
)
const currentVersionId = await manager.getCurrentVersionId(awsSecret.secretId)
core.info(`found the current version ${currentVersionId} for secret ${awsSecret.secretId}`)
resolved = replaceAll(resolved, awsSecret.versionId, currentVersionId)
core.info(`Replacing ${awsSecret.versionId} with the current versionId ${currentVersionId}`)
resolved = resolved.replaceAll(awsSecret.versionId, currentVersionId)
}
return resolved
}

const replaceAll = (s: string, oldString: string, newString: string): string => s.split(oldString).join(newString)

interface AWSSecret {
type AWSSecret = {
kind: string
name: string
secretId: string
versionId: string
}

// find all AWSSecret resources from a manifest string
const findAWSSecrets = (manifest: string): AWSSecret[] => {
const findAWSSecretsFromManifest = (manifest: string): AWSSecret[] => {
const secrets: AWSSecret[] = []
const documents = yaml.loadAll(manifest)
for (const d of documents) {
if (!isKubernetesObject(d)) {
for (const doc of documents) {
if (!isKubernetesObject(doc)) {
continue
}
if (d.kind !== 'AWSSecret') {
if (doc.kind !== 'AWSSecret') {
continue
}
core.info(`Parsing the AWSSecret: ${JSON.stringify(d)}`)
assertKubernetesAWSSecret(d)

const name = d.metadata.name
const secretId = d.spec.stringDataFrom.secretsManagerSecretRef.secretId
const versionId = d.spec.stringDataFrom.secretsManagerSecretRef.versionId
try {
assertKubernetesAWSSecret(doc)
} catch (error) {
if (error instanceof assert.AssertionError) {
core.error(`Invalid AWSSecret object: ${JSON.stringify(doc)}`)
}
throw error
}
const versionId = doc.spec.stringDataFrom.secretsManagerSecretRef.versionId
if (!versionId.startsWith('${') || !versionId.endsWith('}')) {
continue
}
secrets.push({ name, secretId, versionId })

secrets.push({
kind: doc.kind,
name: doc.metadata.name,
secretId: doc.spec.stringDataFrom.secretsManagerSecretRef.secretId,
versionId,
})
}
return secrets
}
4 changes: 2 additions & 2 deletions resolve-aws-secret-version/src/run.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as glob from '@actions/glob'
import * as awsSecretsManager from './awsSecretsManager'
import { resolveInplace } from './resolve.js'
import { updateManifest } from './resolve.js'

type Inputs = {
manifests: string
Expand All @@ -9,6 +9,6 @@ type Inputs = {
export const run = async (inputs: Inputs): Promise<void> => {
const manifests = await glob.create(inputs.manifests, { matchDirectories: false })
for await (const manifest of manifests.globGenerator()) {
await resolveInplace(manifest, awsSecretsManager)
await updateManifest(manifest, awsSecretsManager)
}
}
2 changes: 1 addition & 1 deletion resolve-aws-secret-version/tests/awsSecretsManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import * as awsSecretsManager from '../src/awsSecretsManager.js'

const secretsManagerMock = mockClient(SecretsManagerClient)

test('getCurrentVersionId returns the current version id', async () => {
it('returns the current version id', async () => {
secretsManagerMock.on(ListSecretVersionIdsCommand, { SecretId: 'microservice/develop' }).resolves(
// this is an actual payload of the command:
// $ aws secretsmanager list-secret-version-ids --secret-id microservice/develop
Expand Down
18 changes: 9 additions & 9 deletions resolve-aws-secret-version/tests/resolve.test.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,28 @@
import { promises as fs } from 'fs'
import * as os from 'os'
import { resolve, resolveInplace } from '../src/resolve.js'
import { replaceSecretVersionIds, updateManifest } from '../src/resolve.js'

test('the placeholder is replaced with the current version id by in-place', async () => {
it('replaces the placeholder with the current version id', async () => {
const manager = { getCurrentVersionId: jest.fn() }
manager.getCurrentVersionId.mockResolvedValue('c7ea50c5-b2be-4970-bf90-2237bef3b4cf')

const tempdir = await fs.mkdtemp(`${os.tmpdir()}/resolve-aws-secret-version-action-`)
const fixtureFile = `${tempdir}/fixture.yaml`
await fs.copyFile(`${__dirname}/fixtures/input-with-awssecret-placeholder.yaml`, fixtureFile)

await resolveInplace(fixtureFile, manager)
await updateManifest(fixtureFile, manager)
const output = (await fs.readFile(fixtureFile)).toString()
const expected = (await fs.readFile(`${__dirname}/fixtures/expected-with-awssecret-placeholder.yaml`)).toString()
expect(output).toBe(expected)
})

test('no effect to empty string', async () => {
it('does nothing for an empty string', async () => {
const manager = { getCurrentVersionId: jest.fn() }
const output = await resolve('', manager)
const output = await replaceSecretVersionIds('', manager)
expect(output).toBe('')
})

test('no effect to an AWSSecret without a placeholder', async () => {
it('does nothing for an AWSSecret without a placeholder', async () => {
const manager = { getCurrentVersionId: jest.fn() }
const manifest = `---
apiVersion: mumoshu.github.io/v1alpha1
Expand All @@ -37,11 +37,11 @@ spec:
versionId: 2eb0efcf-14ee-4526-b8ce-971ec82b3aca
type: kubernetes.io/dockerconfigjson
`
const output = await resolve(manifest, manager)
const output = await replaceSecretVersionIds(manifest, manager)
expect(output).toBe(manifest)
})

test('throw an error if invalid AWSSecret', async () => {
it('throws an error if invalid AWSSecret', async () => {
const manager = { getCurrentVersionId: jest.fn() }
const manifest = `---
apiVersion: mumoshu.github.io/v1alpha1
Expand All @@ -53,5 +53,5 @@ spec:
secretsManagerSecretRef:
secretId: this-has-no-versionId-field
`
await expect(resolve(manifest, manager)).rejects.toThrow('AWSSecret must have versionId field')
await expect(replaceSecretVersionIds(manifest, manager)).rejects.toThrow('AWSSecret must have versionId field')
})
Loading