Skip to content
Closed
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
10 changes: 10 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,16 @@ inputs:
tls-key:
description: 'Base64 encoded TLS private key (PEM format)'
required: false
use-invoke-command:
description: 'Set to true to use az aks command invoke for private clusters (recommended for GitHub-hosted runners with private AKS)'
required: false
default: 'false'
cluster-resource-group:
description: 'AKS cluster resource group (required when use-invoke-command is true)'
required: false
cluster-name:
description: 'AKS cluster name (required when use-invoke-command is true)'
required: false
outputs:
secret-name:
description: 'Secret name'
Expand Down
66 changes: 65 additions & 1 deletion src/run.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import * as core from '@actions/core'
import * as exec from '@actions/exec'
import * as fs from 'fs'
import * as os from 'os'
import * as path from 'path'
import {
CoreV1ApiCreateNamespacedSecretRequest,
CoreV1ApiDeleteNamespacedSecretRequest,
Expand Down Expand Up @@ -51,7 +55,60 @@ export function buildContainerRegistryDockerConfigJSON(
}
return dockerConfigJson //Buffer.from(JSON.stringify(dockerConfigJson)).toString('base64');
}

export async function runKubectlViaAz(
secret: V1Secret,
namespace: string,
secretName: string
) {
const resourceGroup = core.getInput('cluster-resource-group')
const clusterName = core.getInput('cluster-name')
if (!resourceGroup || !clusterName) {
throw new Error(
'cluster-resource-group and cluster-name are required for private cluster support'
)
}
// Write secret to temp file
const tempFile = path.join(os.tmpdir(), `secret-${secretName}.json`)

Copilot AI Jul 29, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The temporary file name is predictable and could lead to race conditions or security issues. Consider using a more secure temporary file creation method with random suffixes or proper file permissions.

Suggested change
const tempFile = path.join(os.tmpdir(), `secret-${secretName}.json`)
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'secret-'))
const tempFile = path.join(tempDir, `${secretName}.json`)

Copilot uses AI. Check for mistakes.
Comment thread
ReinierCC marked this conversation as resolved.

Copilot AI Aug 12, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line creates a tempFile variable that is immediately overwritten on line 74. This dead code should be removed as it serves no purpose and could cause confusion.

Suggested change
const tempFile = path.join(os.tmpdir(), `secret-${secretName}.json`)

Copilot uses AI. Check for mistakes.

Copilot AI Aug 12, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line redeclares the tempFile variable that was already declared on line 71, which will cause a compilation error in TypeScript due to duplicate variable declaration in the same scope.

Suggested change
const tempFile = path.join(os.tmpdir(), `secret-${secretName}.json`)

Copilot uses AI. Check for mistakes.
Comment on lines +70 to +71

Copilot AI Dec 8, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assignment to variable tempFile, which is declared constant.

Suggested change
// Write secret to temp file
const tempFile = path.join(os.tmpdir(), `secret-${secretName}.json`)

Copilot uses AI. Check for mistakes.
Comment on lines +70 to +71

Copilot AI Dec 8, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assignment to variable tempFile, which is declared constant.

Suggested change
// Write secret to temp file
const tempFile = path.join(os.tmpdir(), `secret-${secretName}.json`)

Copilot uses AI. Check for mistakes.
// Write secret to temp file securely
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'secret-'));
Comment on lines +71 to +73

Copilot AI Dec 8, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variable tempDir is declared on line 73 but never has a corresponding declaration statement. This will cause a reference error since tempDir is used in the finally block (lines 107-109) but is only accessible within the scope after line 73. The tempDir variable needs to be declared before line 73 (e.g., let tempDir: string;) to be accessible in the finally block.

Suggested change
const tempFile = path.join(os.tmpdir(), `secret-${secretName}.json`)
// Write secret to temp file securely
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'secret-'));
let tempDir: string;
// Write secret to temp file securely
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'secret-'));

Copilot uses AI. Check for mistakes.
const tempFile = path.join(tempDir, `${secretName}.json`);
fs.writeFileSync(tempFile, JSON.stringify(secret, null, 2), { mode: 0o600 });
try {
await exec.exec('az', [
'aks',
'command',
'invoke',
'--resource-group',
resourceGroup,
'--name',
clusterName,
'--command',
`kubectl delete secret ${secretName} -n ${namespace} --ignore-not-found`

Copilot AI Jul 29, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The secretName and namespace parameters are directly interpolated into the shell command without sanitization, which could lead to command injection vulnerabilities if these values contain malicious characters.

Suggested change
`kubectl delete secret ${secretName} -n ${namespace} --ignore-not-found`
'kubectl',
'delete',
'secret',
secretName,
'-n',
namespace,
'--ignore-not-found'

Copilot uses AI. Check for mistakes.

Copilot AI Aug 12, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Direct string interpolation in shell commands can lead to command injection vulnerabilities if secretName or namespace contain malicious characters. Consider validating these inputs or using a safer command construction method.

Copilot uses AI. Check for mistakes.
])
await exec.exec('az', [
'aks',
'command',
'invoke',
'--resource-group',
resourceGroup,
'--name',
clusterName,
'--command',
`kubectl apply -f - -n ${namespace}`,

Copilot AI Jul 29, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The namespace parameter is directly interpolated into the shell command without sanitization, which could lead to command injection vulnerabilities if the namespace value contains malicious characters.

Suggested change
`kubectl apply -f - -n ${namespace}`,
'kubectl apply -f -',
'--namespace',
namespace,

Copilot uses AI. Check for mistakes.

Copilot AI Aug 12, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Direct string interpolation in shell commands can lead to command injection vulnerabilities if namespace contains malicious characters. Consider validating the namespace input or using a safer command construction method.

Copilot uses AI. Check for mistakes.

Copilot AI Aug 12, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The kubectl command uses -f - (stdin) but then specifies --file parameter with the temp file path. This is inconsistent - either use -f - with stdin or -f ${tempFile} with the file parameter, but not both.

Suggested change
`kubectl apply -f - -n ${namespace}`,
`kubectl apply -f ${secretName}.json -n ${namespace}`,

Copilot uses AI. Check for mistakes.
'--file',
tempFile
])
} finally {
// Remove temp file if it exists
if (fs.existsSync(tempFile)) {
fs.unlinkSync(tempFile)
}
Comment on lines +102 to +105

Copilot AI Dec 8, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cleanup logic is redundant. Lines 103-105 attempt to delete tempFile individually, but lines 107-109 already delete the entire tempDir recursively with { recursive: true, force: true }, which would remove tempFile as well. The individual file deletion (lines 103-105) is unnecessary and can be removed.

Suggested change
// Remove temp file if it exists
if (fs.existsSync(tempFile)) {
fs.unlinkSync(tempFile)
}

Copilot uses AI. Check for mistakes.
// Remove temp directory and its contents
if (fs.existsSync(tempDir)) {
fs.rmSync(tempDir, { recursive: true, force: true })
}
}
}
export async function buildSecret(
secretName: string,
namespace: string,
Expand Down Expand Up @@ -219,6 +276,13 @@ export async function run() {
// The namespace in which to place the secret
const namespace: string = core.getInput('namespace') || 'default'

const sec = await buildSecret(secretName, namespace, secretType)

if (core.getInput('use-invoke-command') === 'true') {
await runKubectlViaAz(sec, namespace, secretName)
return
}
Comment on lines +279 to +284

Copilot AI Dec 8, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The checkClusterContext() function is still called unconditionally at the start of run(), which checks for the KUBECONFIG environment variable. However, when use-invoke-command is true, the KUBECONFIG is not needed since the action uses az aks command invoke instead. This will cause the action to fail for private clusters even when the invoke command path is selected. Consider making this check conditional or moving it to only execute when the Kubernetes API path is used.

Copilot uses AI. Check for mistakes.

// Delete if exists
let deleteSecretResponse
try {
Expand Down
56 changes: 55 additions & 1 deletion test/run.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {kStringMaxLength} from 'buffer'
import * as fs from 'fs'
import * as path from 'path'

import * as exec from '@actions/exec'
import * as core from '@actions/core'

const k8s = require('@kubernetes/client-node')
Expand Down Expand Up @@ -176,3 +176,57 @@ describe('buildContainerRegistryDockerConfigJSON', () => {
expect(dockerConfigJson).toEqual(exptectedDockerConfigJsonNoEmail)
})
})

describe('runKubectlViaAz', () => {
const mockSecret = {
apiVersion: 'v1',
kind: 'Secret',
metadata: {name: 'test-secret', namespace: 'test-ns'},
type: 'Opaque',
data: {foo: 'YmFy'}
}

beforeEach(() => {
jest.spyOn(fs, 'writeFileSync').mockImplementation(() => {})
jest.spyOn(fs, 'unlinkSync').mockImplementation(() => {})
jest.spyOn(exec, 'exec').mockResolvedValue(0)
})
Comment on lines +189 to +193

Copilot AI Dec 8, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test is missing mocks for fs.mkdtempSync, fs.existsSync, and fs.rmSync which are called in the runKubectlViaAz function (lines 73, 103, 107-108 in src/run.ts). Without these mocks, the tests may attempt to create/delete actual temporary directories on the filesystem during test execution. Add mocks for these methods in the beforeEach block.

Copilot uses AI. Check for mistakes.

afterEach(() => jest.restoreAllMocks())

it('calls az aks command invoke for delete and apply', async () => {
jest
.spyOn(core, 'getInput')
.mockImplementation((name: string) =>
name === 'cluster-resource-group'
? 'rg'
: name === 'cluster-name'
? 'aks'
: ''
)
const {runKubectlViaAz} = require('../src/run')

Copilot AI Aug 12, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Using require() inside test functions can lead to module caching issues and makes it harder to mock dependencies consistently. Consider importing the function at the top level or using dynamic imports with proper cleanup.

Suggested change
const {runKubectlViaAz} = require('../src/run')

Copilot uses AI. Check for mistakes.
await runKubectlViaAz(mockSecret, 'test-ns', 'test-secret')
expect(exec.exec).toHaveBeenCalledWith(
'az',
expect.arrayContaining([
'aks',
'command',
'invoke',
'--resource-group',
'rg',
'--name',
'aks'
])
)
})
Comment on lines +197 to +221

Copilot AI Dec 8, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test only verifies that exec.exec was called with certain arguments but doesn't verify:

  1. That it was called exactly twice (once for delete, once for apply)
  2. The complete command arguments including the kubectl commands
  3. That the temp file operations occurred in the correct order

Consider using toHaveBeenCalledTimes(2) and more specific matchers to verify both the delete and apply commands were executed with the correct full argument lists.

Copilot uses AI. Check for mistakes.

it('throws if required inputs are missing', async () => {
jest.spyOn(core, 'getInput').mockReturnValue('')
const {runKubectlViaAz} = require('../src/run')

Copilot AI Aug 12, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Using require() inside test functions can lead to module caching issues and makes it harder to mock dependencies consistently. Consider importing the function at the top level or using dynamic imports with proper cleanup.

Suggested change
const {runKubectlViaAz} = require('../src/run')

Copilot uses AI. Check for mistakes.
await expect(
runKubectlViaAz(mockSecret, 'test-ns', 'test-secret')
).rejects.toThrow(
'cluster-resource-group and cluster-name are required for private cluster support'
)
})
})