mirror of
https://github.com/Azure/setup-kubectl.git
synced 2026-05-22 13:35:53 +00:00
feat: add downloadBaseURL and checksum inputs (closes #206)
This commit is contained in:
parent
8756b858da
commit
3a4e8803ed
5 changed files with 591 additions and 124 deletions
29
.github/workflows/integration-tests.yml
vendored
29
.github/workflows/integration-tests.yml
vendored
|
|
@ -54,3 +54,32 @@ jobs:
|
|||
|
||||
- name: Validate kubectl setup old version
|
||||
run: python test/validate-kubectl.py 'v1.15.1'
|
||||
|
||||
- name: Fetch known SHA256 for v1.30.0 linux/amd64
|
||||
id: sha
|
||||
run: |
|
||||
value=$(curl -fsSL https://dl.k8s.io/release/v1.30.0/bin/linux/amd64/kubectl.sha256)
|
||||
echo "value=$value" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Setup kubectl with valid checksum
|
||||
uses: ./
|
||||
with:
|
||||
version: 'v1.30.0'
|
||||
checksum: ${{ steps.sha.outputs.value }}
|
||||
|
||||
- name: Validate kubectl setup with checksum
|
||||
run: python test/validate-kubectl.py 'v1.30.0'
|
||||
|
||||
- name: Setup kubectl with bad checksum (expect failure)
|
||||
id: badsum
|
||||
continue-on-error: true
|
||||
uses: ./
|
||||
with:
|
||||
version: 'v1.29.0'
|
||||
checksum: 'deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef'
|
||||
|
||||
- name: Assert bad-checksum step failed
|
||||
if: steps.badsum.outcome != 'failure'
|
||||
run: |
|
||||
echo "Expected bad-checksum step to fail, but outcome was: ${{ steps.badsum.outcome }}"
|
||||
exit 1
|
||||
|
|
@ -5,6 +5,14 @@ inputs:
|
|||
description: 'Version of kubectl'
|
||||
required: true
|
||||
default: 'latest'
|
||||
downloadBaseURL:
|
||||
description: 'Base URL to download kubectl from (https only). Use for private mirrors.'
|
||||
required: false
|
||||
default: 'https://dl.k8s.io'
|
||||
checksum:
|
||||
description: 'Expected SHA256 of the kubectl binary. Recommended when overriding downloadBaseURL.'
|
||||
required: false
|
||||
default: ''
|
||||
outputs:
|
||||
kubectl-path:
|
||||
description: 'Path to the cached kubectl binary'
|
||||
|
|
|
|||
|
|
@ -1,8 +1,55 @@
|
|||
import * as os from 'os'
|
||||
import * as util from 'util'
|
||||
import * as fs from 'fs'
|
||||
import * as core from '@actions/core'
|
||||
import * as toolCache from '@actions/tool-cache'
|
||||
|
||||
export const DEFAULT_KUBECTL_BASE_URL = 'https://dl.k8s.io'
|
||||
|
||||
export function validateBaseURL(input: string): URL {
|
||||
let url: URL
|
||||
try {
|
||||
url = new URL(input)
|
||||
} catch {
|
||||
throw new Error(`Invalid downloadBaseURL: "${input}" is not a valid URL.`)
|
||||
}
|
||||
|
||||
if (url.protocol !== 'https:') {
|
||||
throw new Error(
|
||||
`downloadBaseURL must use https://, got "${url.protocol}" in "${input}".`
|
||||
)
|
||||
}
|
||||
|
||||
if (url.username || url.password) {
|
||||
throw new Error(
|
||||
'downloadBaseURL must not contain userinfo (user:pass@host).'
|
||||
)
|
||||
}
|
||||
|
||||
const host = url.hostname.toLowerCase().replace(/^\[|\]$/g, '')
|
||||
const isLoopback =
|
||||
host === 'localhost' || host === '127.0.0.1' || host === '::1'
|
||||
const isLinkLocal = host.startsWith('169.254.')
|
||||
const isPrivateV4 =
|
||||
/^10\./.test(host) ||
|
||||
/^192\.168\./.test(host) ||
|
||||
/^172\.(1[6-9]|2\d|3[0-1])\./.test(host)
|
||||
if (isLoopback || isLinkLocal || isPrivateV4) {
|
||||
throw new Error(
|
||||
`downloadBaseURL host "${host}" is loopback/link-local/private and is blocked by default.`
|
||||
)
|
||||
}
|
||||
|
||||
return url
|
||||
}
|
||||
|
||||
export function validateVersion(version: string): void {
|
||||
if (!/^v?\d+\.\d+\.\d+$/.test(version)) {
|
||||
throw new Error(
|
||||
`Invalid kubectl version: "${version}". Expected a value like "v1.30.0".`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export function getKubectlArch(): string {
|
||||
const arch = os.arch()
|
||||
if (arch === 'x64') {
|
||||
|
|
@ -11,26 +58,47 @@ export function getKubectlArch(): string {
|
|||
return arch
|
||||
}
|
||||
|
||||
export function getkubectlDownloadURL(version: string, arch: string): string {
|
||||
export function getkubectlDownloadURL(
|
||||
version: string,
|
||||
arch: string,
|
||||
baseURL: string = DEFAULT_KUBECTL_BASE_URL
|
||||
): string {
|
||||
validateVersion(version)
|
||||
const url = validateBaseURL(baseURL)
|
||||
|
||||
let osDir: string
|
||||
let file: string
|
||||
switch (os.type()) {
|
||||
case 'Linux':
|
||||
return `https://dl.k8s.io/release/${version}/bin/linux/${arch}/kubectl`
|
||||
|
||||
osDir = 'linux'
|
||||
file = 'kubectl'
|
||||
break
|
||||
case 'Darwin':
|
||||
return `https://dl.k8s.io/release/${version}/bin/darwin/${arch}/kubectl`
|
||||
|
||||
osDir = 'darwin'
|
||||
file = 'kubectl'
|
||||
break
|
||||
case 'Windows_NT':
|
||||
default:
|
||||
return `https://dl.k8s.io/release/${version}/bin/windows/${arch}/kubectl.exe`
|
||||
osDir = 'windows'
|
||||
file = 'kubectl.exe'
|
||||
break
|
||||
}
|
||||
|
||||
const basePath = url.pathname.replace(/\/+$/, '')
|
||||
url.pathname = `${basePath}/release/${version}/bin/${osDir}/${arch}/${file}`
|
||||
return url.toString()
|
||||
}
|
||||
|
||||
export async function getLatestPatchVersion(
|
||||
major: string,
|
||||
minor: string
|
||||
minor: string,
|
||||
baseURL: string = DEFAULT_KUBECTL_BASE_URL
|
||||
): Promise<string> {
|
||||
const version = `${major}.${minor}`
|
||||
const sourceURL = `https://dl.k8s.io/release/stable-${version}.txt`
|
||||
const url = validateBaseURL(baseURL)
|
||||
const basePath = url.pathname.replace(/\/+$/, '')
|
||||
url.pathname = `${basePath}/release/stable-${version}.txt`
|
||||
const sourceURL = url.toString()
|
||||
try {
|
||||
const downloadPath = await toolCache.downloadTool(sourceURL)
|
||||
const latestPatch = fs
|
||||
|
|
|
|||
461
src/run.test.ts
461
src/run.test.ts
|
|
@ -1,6 +1,7 @@
|
|||
import {vi, describe, test, expect, beforeEach} from 'vitest'
|
||||
import * as path from 'path'
|
||||
import * as util from 'util'
|
||||
import {Readable} from 'stream'
|
||||
|
||||
vi.mock('os')
|
||||
vi.mock('fs')
|
||||
|
|
@ -14,32 +15,67 @@ vi.mock('@actions/tool-cache', async (importOriginal) => {
|
|||
}
|
||||
})
|
||||
vi.mock('@actions/core')
|
||||
vi.mock('@actions/http-client', () => {
|
||||
const get = vi.fn()
|
||||
return {
|
||||
HttpClient: vi.fn().mockImplementation(function () {
|
||||
return {get}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const os = await import('os')
|
||||
const fs = await import('fs')
|
||||
const toolCache = await import('@actions/tool-cache')
|
||||
const core = await import('@actions/core')
|
||||
const httpClient = await import('@actions/http-client')
|
||||
const run = await import('./run.js')
|
||||
const {
|
||||
DEFAULT_KUBECTL_BASE_URL,
|
||||
getkubectlDownloadURL,
|
||||
getKubectlArch,
|
||||
getExecutableExtension,
|
||||
getLatestPatchVersion
|
||||
getLatestPatchVersion,
|
||||
validateBaseURL,
|
||||
validateVersion
|
||||
} = await import('./helpers.js')
|
||||
|
||||
function mockInputs(inputs: Record<string, string>) {
|
||||
vi.mocked(core.getInput).mockImplementation(
|
||||
(name: string) => inputs[name] ?? ''
|
||||
)
|
||||
}
|
||||
|
||||
function fakeHttpResponse(opts: {
|
||||
status: number
|
||||
body?: string
|
||||
location?: string
|
||||
}) {
|
||||
const body = Readable.from([Buffer.from(opts.body ?? '')])
|
||||
const message: any = body
|
||||
message.statusCode = opts.status
|
||||
message.headers = opts.location ? {location: opts.location} : {}
|
||||
return {message, readBody: async () => opts.body ?? ''}
|
||||
}
|
||||
|
||||
function mockHttpGet(response: ReturnType<typeof fakeHttpResponse>) {
|
||||
;(httpClient.HttpClient as any).mockImplementation(function () {
|
||||
return {get: vi.fn().mockResolvedValue(response)}
|
||||
})
|
||||
}
|
||||
|
||||
describe('Testing all functions in run file.', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
test('getExecutableExtension() - return .exe when os is Windows', () => {
|
||||
vi.mocked(os.type).mockReturnValue('Windows_NT')
|
||||
expect(getExecutableExtension()).toBe('.exe')
|
||||
expect(os.type).toHaveBeenCalled()
|
||||
})
|
||||
test('getExecutableExtension() - return empty string for non-windows OS', () => {
|
||||
vi.mocked(os.type).mockReturnValue('Darwin')
|
||||
expect(getExecutableExtension()).toBe('')
|
||||
expect(os.type).toHaveBeenCalled()
|
||||
})
|
||||
test.each([
|
||||
['arm', 'arm'],
|
||||
|
|
@ -50,65 +86,153 @@ describe('Testing all functions in run file.', () => {
|
|||
(osArch, kubectlArch) => {
|
||||
vi.mocked(os.arch).mockReturnValue(osArch as NodeJS.Architecture)
|
||||
expect(getKubectlArch()).toBe(kubectlArch)
|
||||
expect(os.arch).toHaveBeenCalled()
|
||||
}
|
||||
)
|
||||
|
||||
test.each([['arm'], ['arm64'], ['amd64']])(
|
||||
'getkubectlDownloadURL() - return the URL to download %s kubectl for Linux',
|
||||
'getkubectlDownloadURL() - default base URL, Linux %s',
|
||||
(arch) => {
|
||||
vi.mocked(os.type).mockReturnValue('Linux')
|
||||
const kubectlLinuxUrl = util.format(
|
||||
const expected = util.format(
|
||||
'https://dl.k8s.io/release/v1.15.0/bin/linux/%s/kubectl',
|
||||
arch
|
||||
)
|
||||
expect(getkubectlDownloadURL('v1.15.0', arch)).toBe(kubectlLinuxUrl)
|
||||
expect(os.type).toHaveBeenCalled()
|
||||
expect(getkubectlDownloadURL('v1.15.0', arch)).toBe(expected)
|
||||
}
|
||||
)
|
||||
test.each([['arm'], ['arm64'], ['amd64']])(
|
||||
'getkubectlDownloadURL() - return the URL to download %s kubectl for Darwin',
|
||||
'getkubectlDownloadURL() - default base URL, Darwin %s',
|
||||
(arch) => {
|
||||
vi.mocked(os.type).mockReturnValue('Darwin')
|
||||
const kubectlDarwinUrl = util.format(
|
||||
const expected = util.format(
|
||||
'https://dl.k8s.io/release/v1.15.0/bin/darwin/%s/kubectl',
|
||||
arch
|
||||
)
|
||||
expect(getkubectlDownloadURL('v1.15.0', arch)).toBe(kubectlDarwinUrl)
|
||||
expect(os.type).toHaveBeenCalled()
|
||||
expect(getkubectlDownloadURL('v1.15.0', arch)).toBe(expected)
|
||||
}
|
||||
)
|
||||
test.each([['arm'], ['arm64'], ['amd64']])(
|
||||
'getkubectlDownloadURL() - return the URL to download %s kubectl for Windows',
|
||||
'getkubectlDownloadURL() - default base URL, Windows %s',
|
||||
(arch) => {
|
||||
vi.mocked(os.type).mockReturnValue('Windows_NT')
|
||||
const kubectlWindowsUrl = util.format(
|
||||
const expected = util.format(
|
||||
'https://dl.k8s.io/release/v1.15.0/bin/windows/%s/kubectl.exe',
|
||||
arch
|
||||
)
|
||||
expect(getkubectlDownloadURL('v1.15.0', arch)).toBe(kubectlWindowsUrl)
|
||||
expect(os.type).toHaveBeenCalled()
|
||||
expect(getkubectlDownloadURL('v1.15.0', arch)).toBe(expected)
|
||||
}
|
||||
)
|
||||
test('getStableKubectlVersion() - download stable version file, read version and return it', async () => {
|
||||
|
||||
test('getkubectlDownloadURL() - custom base URL', () => {
|
||||
vi.mocked(os.type).mockReturnValue('Linux')
|
||||
expect(
|
||||
getkubectlDownloadURL('v1.15.0', 'amd64', 'https://mirror.example.com')
|
||||
).toBe(
|
||||
'https://mirror.example.com/release/v1.15.0/bin/linux/amd64/kubectl'
|
||||
)
|
||||
})
|
||||
test('getkubectlDownloadURL() - strips trailing slash from base URL', () => {
|
||||
vi.mocked(os.type).mockReturnValue('Darwin')
|
||||
expect(
|
||||
getkubectlDownloadURL(
|
||||
'v1.15.0',
|
||||
'arm64',
|
||||
'https://mirror.example.com/'
|
||||
)
|
||||
).toBe(
|
||||
'https://mirror.example.com/release/v1.15.0/bin/darwin/arm64/kubectl'
|
||||
)
|
||||
})
|
||||
test('getkubectlDownloadURL() - base URL with path prefix', () => {
|
||||
vi.mocked(os.type).mockReturnValue('Linux')
|
||||
expect(
|
||||
getkubectlDownloadURL(
|
||||
'v1.30.0',
|
||||
'amd64',
|
||||
'https://mirror.example.com/k8s'
|
||||
)
|
||||
).toBe(
|
||||
'https://mirror.example.com/k8s/release/v1.30.0/bin/linux/amd64/kubectl'
|
||||
)
|
||||
})
|
||||
|
||||
test.each([
|
||||
['https://mirror.example.com'],
|
||||
['https://mirror.example.com:8443'], // non-standard port
|
||||
['https://172.32.0.1'] // outside RFC1918 172.16/12 range
|
||||
])('validateBaseURL() - accepts %s', (input) => {
|
||||
expect(() => validateBaseURL(input)).not.toThrow()
|
||||
})
|
||||
|
||||
test.each([
|
||||
['not a url', /not a valid URL/],
|
||||
['http://mirror.example.com', /must use https/],
|
||||
['https://user:pass@mirror.example.com', /userinfo/],
|
||||
['https://localhost', /loopback\/link-local\/private/],
|
||||
['https://127.0.0.1', /loopback\/link-local\/private/],
|
||||
['https://[::1]', /loopback\/link-local\/private/], // caught a real bug
|
||||
['https://169.254.169.254', /loopback\/link-local\/private/], // IMDS
|
||||
['https://10.0.0.5', /loopback\/link-local\/private/], // RFC1918
|
||||
['https://192.168.1.1', /loopback\/link-local\/private/], // RFC1918
|
||||
['https://172.16.0.1', /loopback\/link-local\/private/], // RFC1918 low edge
|
||||
['https://172.31.255.254', /loopback\/link-local\/private/] // RFC1918 high edge
|
||||
])('validateBaseURL() - rejects %s', (input, expected) => {
|
||||
expect(() => validateBaseURL(input)).toThrow(expected)
|
||||
})
|
||||
|
||||
test.each([['v1.30.0'], ['1.30.0']])(
|
||||
'validateVersion() - accepts %s',
|
||||
(input) => {
|
||||
expect(() => validateVersion(input)).not.toThrow()
|
||||
}
|
||||
)
|
||||
|
||||
test.each([
|
||||
[''],
|
||||
['v1.30.0\n'], // trailing newline
|
||||
['v1.2.3.4'], // extra segment
|
||||
[' v1.30.0'] // leading whitespace
|
||||
])('validateVersion() - rejects %s', (input) => {
|
||||
expect(() => validateVersion(input)).toThrow(/Invalid kubectl version/)
|
||||
})
|
||||
|
||||
test('getStableKubectlVersion() - default base URL', async () => {
|
||||
vi.mocked(toolCache.downloadTool).mockResolvedValue('pathToTool')
|
||||
vi.mocked(fs.readFileSync).mockReturnValue('v1.20.4')
|
||||
expect(await run.getStableKubectlVersion()).toBe('v1.20.4')
|
||||
expect(toolCache.downloadTool).toHaveBeenCalled()
|
||||
expect(fs.readFileSync).toHaveBeenCalledWith('pathToTool', 'utf8')
|
||||
expect(toolCache.downloadTool).toHaveBeenCalledWith(
|
||||
'https://dl.k8s.io/release/stable.txt'
|
||||
)
|
||||
})
|
||||
test('getStableKubectlVersion() - return default v1.15.0 if version read is empty', async () => {
|
||||
test('getStableKubectlVersion() - honours custom base URL (air-gap fix)', async () => {
|
||||
vi.mocked(toolCache.downloadTool).mockResolvedValue('pathToTool')
|
||||
vi.mocked(fs.readFileSync).mockReturnValue('v1.20.4')
|
||||
expect(
|
||||
await run.getStableKubectlVersion('https://mirror.example.com')
|
||||
).toBe('v1.20.4')
|
||||
expect(toolCache.downloadTool).toHaveBeenCalledWith(
|
||||
'https://mirror.example.com/release/stable.txt'
|
||||
)
|
||||
})
|
||||
test('getStableKubectlVersion() - path-prefixed mirror composes correctly', async () => {
|
||||
vi.mocked(toolCache.downloadTool).mockResolvedValue('pathToTool')
|
||||
vi.mocked(fs.readFileSync).mockReturnValue('v1.20.4')
|
||||
await run.getStableKubectlVersion('https://mirror.example.com/k8s/')
|
||||
expect(toolCache.downloadTool).toHaveBeenCalledWith(
|
||||
'https://mirror.example.com/k8s/release/stable.txt'
|
||||
)
|
||||
})
|
||||
test('getStableKubectlVersion() - falls back to v1.15.0 on empty file', async () => {
|
||||
vi.mocked(toolCache.downloadTool).mockResolvedValue('pathToTool')
|
||||
vi.mocked(fs.readFileSync).mockReturnValue('')
|
||||
expect(await run.getStableKubectlVersion()).toBe('v1.15.0')
|
||||
expect(toolCache.downloadTool).toHaveBeenCalled()
|
||||
expect(fs.readFileSync).toHaveBeenCalledWith('pathToTool', 'utf8')
|
||||
})
|
||||
test('getStableKubectlVersion() - return default v1.15.0 if unable to download file', async () => {
|
||||
test('getStableKubectlVersion() - falls back to v1.15.0 on download failure', async () => {
|
||||
vi.mocked(toolCache.downloadTool).mockRejectedValue('Unable to download.')
|
||||
expect(await run.getStableKubectlVersion()).toBe('v1.15.0')
|
||||
expect(toolCache.downloadTool).toHaveBeenCalled()
|
||||
})
|
||||
test('downloadKubectl() - download kubectl, add it to toolCache and return path to it', async () => {
|
||||
|
||||
test('downloadKubectl() - download and cache, default base URL', async () => {
|
||||
vi.mocked(toolCache.find).mockReturnValue('')
|
||||
vi.mocked(toolCache.downloadTool).mockResolvedValue('pathToTool')
|
||||
vi.mocked(toolCache.cacheFile).mockResolvedValue('pathToCachedTool')
|
||||
|
|
@ -117,32 +241,28 @@ describe('Testing all functions in run file.', () => {
|
|||
expect(await run.downloadKubectl('v1.15.0')).toBe(
|
||||
path.join('pathToCachedTool', 'kubectl.exe')
|
||||
)
|
||||
expect(toolCache.find).toHaveBeenCalledWith('kubectl', 'v1.15.0')
|
||||
expect(toolCache.downloadTool).toHaveBeenCalled()
|
||||
expect(toolCache.cacheFile).toHaveBeenCalled()
|
||||
expect(os.type).toHaveBeenCalled()
|
||||
expect(fs.chmodSync).toHaveBeenCalledWith(
|
||||
path.join('pathToCachedTool', 'kubectl.exe'),
|
||||
'775'
|
||||
expect(toolCache.downloadTool).toHaveBeenCalledWith(
|
||||
'https://dl.k8s.io/release/v1.15.0/bin/windows/amd64/kubectl.exe'
|
||||
)
|
||||
})
|
||||
test('downloadKubectl() - throw DownloadKubectlFailed error when unable to download kubectl', async () => {
|
||||
vi.mocked(toolCache.find).mockReturnValue('')
|
||||
vi.mocked(toolCache.downloadTool).mockRejectedValue(
|
||||
'Unable to download kubectl.'
|
||||
test('downloadKubectl() - rejects invalid version (path traversal)', async () => {
|
||||
await expect(run.downloadKubectl('../etc')).rejects.toThrow(
|
||||
/Invalid kubectl version/
|
||||
)
|
||||
})
|
||||
test('downloadKubectl() - throws DownloadKubectlFailed on generic error', async () => {
|
||||
vi.mocked(toolCache.find).mockReturnValue('')
|
||||
vi.mocked(toolCache.downloadTool).mockRejectedValue('boom')
|
||||
await expect(run.downloadKubectl('v1.15.0')).rejects.toThrow(
|
||||
'DownloadKubectlFailed'
|
||||
)
|
||||
expect(toolCache.find).toHaveBeenCalledWith('kubectl', 'v1.15.0')
|
||||
expect(toolCache.downloadTool).toHaveBeenCalled()
|
||||
})
|
||||
test('downloadKubectl() - throw kubectl not found error when receive 404 response', async () => {
|
||||
test('downloadKubectl() - 404 maps to "not found" message', async () => {
|
||||
const kubectlVersion = 'v1.15.0'
|
||||
const arch = 'arm128'
|
||||
const arch = 'arm64'
|
||||
vi.mocked(os.arch).mockReturnValue(arch as NodeJS.Architecture)
|
||||
vi.mocked(toolCache.find).mockReturnValue('')
|
||||
vi.mocked(toolCache.downloadTool).mockImplementation((_) => {
|
||||
vi.mocked(toolCache.downloadTool).mockImplementation(() => {
|
||||
throw new toolCache.HTTPError(404)
|
||||
})
|
||||
await expect(run.downloadKubectl(kubectlVersion)).rejects.toThrow(
|
||||
|
|
@ -152,114 +272,255 @@ describe('Testing all functions in run file.', () => {
|
|||
arch
|
||||
)
|
||||
)
|
||||
expect(os.arch).toHaveBeenCalled()
|
||||
expect(toolCache.find).toHaveBeenCalledWith('kubectl', kubectlVersion)
|
||||
expect(toolCache.downloadTool).toHaveBeenCalled()
|
||||
})
|
||||
test('downloadKubectl() - return path to existing cache of kubectl', async () => {
|
||||
vi.mocked(core.getInput).mockImplementation(() => 'v1.15.5')
|
||||
test('downloadKubectl() - returns existing cache without redownloading', async () => {
|
||||
vi.mocked(toolCache.find).mockReturnValue('pathToCachedTool')
|
||||
vi.mocked(os.type).mockReturnValue('Windows_NT')
|
||||
vi.mocked(fs.chmodSync).mockImplementation(() => {})
|
||||
expect(await run.downloadKubectl('v1.15.0')).toBe(
|
||||
path.join('pathToCachedTool', 'kubectl.exe')
|
||||
)
|
||||
expect(toolCache.find).toHaveBeenCalledWith('kubectl', 'v1.15.0')
|
||||
expect(os.type).toHaveBeenCalled()
|
||||
expect(fs.chmodSync).toHaveBeenCalledWith(
|
||||
path.join('pathToCachedTool', 'kubectl.exe'),
|
||||
'775'
|
||||
expect(toolCache.downloadTool).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test('downloadKubectl() - rejects malformed checksum input', async () => {
|
||||
vi.mocked(toolCache.find).mockReturnValue('')
|
||||
vi.mocked(toolCache.downloadTool).mockResolvedValue('pathToTool')
|
||||
vi.mocked(os.type).mockReturnValue('Linux')
|
||||
vi.mocked(os.arch).mockReturnValue('x64')
|
||||
vi.mocked(fs.readFileSync).mockReturnValue(Buffer.from('hi') as never)
|
||||
await expect(
|
||||
run.downloadKubectl('v1.15.0', DEFAULT_KUBECTL_BASE_URL, 'not-a-hash')
|
||||
).rejects.toThrow(/Invalid checksum input/)
|
||||
})
|
||||
test('downloadKubectl() - rejects checksum mismatch', async () => {
|
||||
vi.mocked(toolCache.find).mockReturnValue('')
|
||||
vi.mocked(toolCache.downloadTool).mockResolvedValue('pathToTool')
|
||||
vi.mocked(os.type).mockReturnValue('Linux')
|
||||
vi.mocked(os.arch).mockReturnValue('x64')
|
||||
vi.mocked(fs.readFileSync).mockReturnValue(Buffer.from('hi') as never)
|
||||
const wrong = 'a'.repeat(64)
|
||||
await expect(
|
||||
run.downloadKubectl('v1.15.0', DEFAULT_KUBECTL_BASE_URL, wrong)
|
||||
).rejects.toThrow(/Checksum mismatch/)
|
||||
})
|
||||
test('downloadKubectl() - accepts matching checksum', async () => {
|
||||
vi.mocked(toolCache.find).mockReturnValue('')
|
||||
vi.mocked(toolCache.downloadTool).mockResolvedValue('pathToTool')
|
||||
vi.mocked(toolCache.cacheFile).mockResolvedValue('pathToCachedTool')
|
||||
vi.mocked(os.type).mockReturnValue('Linux')
|
||||
vi.mocked(os.arch).mockReturnValue('x64')
|
||||
vi.mocked(fs.chmodSync).mockImplementation(() => {})
|
||||
// sha256('hi') = 8f434346648f6b96df89dda901c5176b10a6d83961dd3c1ac88b59b2dc327aa4
|
||||
vi.mocked(fs.readFileSync).mockReturnValue(Buffer.from('hi') as never)
|
||||
const correct =
|
||||
'8f434346648f6b96df89dda901c5176b10a6d83961dd3c1ac88b59b2dc327aa4'
|
||||
await expect(
|
||||
run.downloadKubectl('v1.15.0', DEFAULT_KUBECTL_BASE_URL, correct)
|
||||
).resolves.toBe(path.join('pathToCachedTool', 'kubectl'))
|
||||
})
|
||||
|
||||
test.each([[301], [302], [307], [308]])(
|
||||
'downloadKubectl() - custom mirror: rejects %i redirect (SSRF guard)',
|
||||
async (status) => {
|
||||
vi.mocked(toolCache.find).mockReturnValue('')
|
||||
vi.mocked(os.type).mockReturnValue('Linux')
|
||||
vi.mocked(os.arch).mockReturnValue('x64')
|
||||
mockHttpGet(
|
||||
fakeHttpResponse({
|
||||
status,
|
||||
location: 'http://169.254.169.254/latest/meta-data/'
|
||||
})
|
||||
)
|
||||
await expect(
|
||||
run.downloadKubectl('v1.15.0', 'https://mirror.example.com')
|
||||
).rejects.toThrow('DownloadKubectlFailed')
|
||||
expect(toolCache.downloadTool).not.toHaveBeenCalled()
|
||||
}
|
||||
)
|
||||
|
||||
test('downloadKubectl() - custom mirror: non-200/non-404 (500) fails', async () => {
|
||||
vi.mocked(toolCache.find).mockReturnValue('')
|
||||
vi.mocked(os.type).mockReturnValue('Linux')
|
||||
vi.mocked(os.arch).mockReturnValue('x64')
|
||||
mockHttpGet(fakeHttpResponse({status: 500}))
|
||||
await expect(
|
||||
run.downloadKubectl('v1.15.0', 'https://mirror.example.com')
|
||||
).rejects.toThrow('DownloadKubectlFailed')
|
||||
expect(toolCache.downloadTool).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test('downloadKubectl() - custom mirror: 404 maps to "not found"', async () => {
|
||||
vi.mocked(toolCache.find).mockReturnValue('')
|
||||
vi.mocked(os.type).mockReturnValue('Linux')
|
||||
const arch = 'amd64'
|
||||
vi.mocked(os.arch).mockReturnValue('x64')
|
||||
mockHttpGet(fakeHttpResponse({status: 404}))
|
||||
await expect(
|
||||
run.downloadKubectl('v1.15.0', 'https://mirror.example.com')
|
||||
).rejects.toThrow(
|
||||
util.format("Kubectl '%s' for '%s' arch not found.", 'v1.15.0', arch)
|
||||
)
|
||||
expect(toolCache.downloadTool).not.toHaveBeenCalled()
|
||||
})
|
||||
test('getLatestPatchVersion() - download and return latest patch version', async () => {
|
||||
|
||||
test('downloadKubectl() - custom mirror: 200 streams body to temp file and caches', async () => {
|
||||
vi.mocked(toolCache.find).mockReturnValue('')
|
||||
vi.mocked(toolCache.cacheFile).mockResolvedValue('pathToCachedTool')
|
||||
vi.mocked(os.type).mockReturnValue('Linux')
|
||||
vi.mocked(os.arch).mockReturnValue('x64')
|
||||
vi.mocked(os.tmpdir).mockReturnValue('/tmp')
|
||||
vi.mocked(fs.chmodSync).mockImplementation(() => {})
|
||||
vi.mocked(fs.writeFileSync).mockImplementation(() => {})
|
||||
mockHttpGet(fakeHttpResponse({status: 200, body: 'kubectl-bytes'}))
|
||||
|
||||
await expect(
|
||||
run.downloadKubectl('v1.15.0', 'https://mirror.example.com')
|
||||
).resolves.toBe(path.join('pathToCachedTool', 'kubectl'))
|
||||
expect(toolCache.downloadTool).not.toHaveBeenCalled()
|
||||
expect(toolCache.cacheFile).toHaveBeenCalled()
|
||||
expect(fs.writeFileSync).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test('getLatestPatchVersion() - default base URL', async () => {
|
||||
vi.mocked(toolCache.downloadTool).mockResolvedValue('pathToTool')
|
||||
vi.mocked(fs.readFileSync).mockReturnValue('v1.27.15')
|
||||
|
||||
const result = await getLatestPatchVersion('1', '27')
|
||||
|
||||
expect(result).toBe('v1.27.15')
|
||||
expect(await getLatestPatchVersion('1', '27')).toBe('v1.27.15')
|
||||
expect(toolCache.downloadTool).toHaveBeenCalledWith(
|
||||
'https://dl.k8s.io/release/stable-1.27.txt'
|
||||
)
|
||||
})
|
||||
|
||||
test('getLatestPatchVersion() - throw error when patch version is empty', async () => {
|
||||
test('getLatestPatchVersion() - honours custom base URL', async () => {
|
||||
vi.mocked(toolCache.downloadTool).mockResolvedValue('pathToTool')
|
||||
vi.mocked(fs.readFileSync).mockReturnValue('v1.27.15')
|
||||
expect(
|
||||
await getLatestPatchVersion('1', '27', 'https://mirror.example.com')
|
||||
).toBe('v1.27.15')
|
||||
expect(toolCache.downloadTool).toHaveBeenCalledWith(
|
||||
'https://mirror.example.com/release/stable-1.27.txt'
|
||||
)
|
||||
})
|
||||
test('getLatestPatchVersion() - path-prefixed mirror composes correctly', async () => {
|
||||
vi.mocked(toolCache.downloadTool).mockResolvedValue('pathToTool')
|
||||
vi.mocked(fs.readFileSync).mockReturnValue('v1.27.15')
|
||||
await getLatestPatchVersion('1', '27', 'https://mirror.example.com/k8s')
|
||||
expect(toolCache.downloadTool).toHaveBeenCalledWith(
|
||||
'https://mirror.example.com/k8s/release/stable-1.27.txt'
|
||||
)
|
||||
})
|
||||
test('getLatestPatchVersion() - throws on empty file', async () => {
|
||||
vi.mocked(toolCache.downloadTool).mockResolvedValue('pathToTool')
|
||||
vi.mocked(fs.readFileSync).mockReturnValue('')
|
||||
|
||||
await expect(getLatestPatchVersion('1', '27')).rejects.toThrow(
|
||||
'Failed to get latest patch version for 1.27'
|
||||
)
|
||||
})
|
||||
test('getLatestPatchVersion() - throws on download failure', async () => {
|
||||
vi.mocked(toolCache.downloadTool).mockRejectedValue(new Error('Network'))
|
||||
await expect(getLatestPatchVersion('1', '27')).rejects.toThrow(
|
||||
'Failed to get latest patch version for 1.27'
|
||||
)
|
||||
})
|
||||
|
||||
test('getLatestPatchVersion() - throw error when download fails', async () => {
|
||||
vi.mocked(toolCache.downloadTool).mockRejectedValue(
|
||||
new Error('Network error')
|
||||
)
|
||||
|
||||
await expect(getLatestPatchVersion('1', '27')).rejects.toThrow(
|
||||
'Failed to get latest patch version for 1.27'
|
||||
)
|
||||
})
|
||||
test('resolveKubectlVersion() - expands major.minor to latest patch', async () => {
|
||||
test('resolveKubectlVersion() - expands major.minor', async () => {
|
||||
vi.mocked(toolCache.downloadTool).mockResolvedValue('pathToTool')
|
||||
vi.mocked(fs.readFileSync).mockReturnValue('v1.27.15')
|
||||
|
||||
const result = await run.resolveKubectlVersion('1.27')
|
||||
expect(result).toBe('v1.27.15')
|
||||
expect(await run.resolveKubectlVersion('1.27')).toBe('v1.27.15')
|
||||
})
|
||||
|
||||
test('resolveKubectlVersion() - returns full version unchanged', async () => {
|
||||
const result = await run.resolveKubectlVersion('v1.27.15')
|
||||
expect(result).toBe('v1.27.15')
|
||||
expect(await run.resolveKubectlVersion('v1.27.15')).toBe('v1.27.15')
|
||||
})
|
||||
test('resolveKubectlVersion() - adds v prefix to full version', async () => {
|
||||
const result = await run.resolveKubectlVersion('1.27.15')
|
||||
expect(result).toBe('v1.27.15')
|
||||
test('resolveKubectlVersion() - adds v prefix', async () => {
|
||||
expect(await run.resolveKubectlVersion('1.27.15')).toBe('v1.27.15')
|
||||
})
|
||||
test('resolveKubectlVersion() - expands v-prefixed major.minor to latest patch', async () => {
|
||||
vi.mocked(toolCache.downloadTool).mockResolvedValue('pathToTool')
|
||||
vi.mocked(fs.readFileSync).mockReturnValue('v1.27.15')
|
||||
|
||||
const result = await run.resolveKubectlVersion('v1.27')
|
||||
expect(result).toBe('v1.27.15')
|
||||
})
|
||||
test('run() - download specified version and set output', async () => {
|
||||
vi.mocked(core.getInput).mockReturnValue('v1.15.5')
|
||||
test('run() - reads version, downloadBaseURL, checksum inputs; defaults applied', async () => {
|
||||
mockInputs({version: 'v1.15.5'})
|
||||
vi.mocked(toolCache.find).mockReturnValue('pathToCachedTool')
|
||||
vi.mocked(os.type).mockReturnValue('Windows_NT')
|
||||
vi.mocked(fs.chmodSync).mockImplementation()
|
||||
vi.mocked(core.addPath).mockImplementation()
|
||||
vi.spyOn(console, 'log').mockImplementation()
|
||||
vi.mocked(core.setOutput).mockImplementation()
|
||||
expect(await run.run()).toBeUndefined()
|
||||
|
||||
await expect(run.run()).resolves.toBeUndefined()
|
||||
expect(core.getInput).toHaveBeenCalledWith('version', {required: true})
|
||||
expect(core.addPath).toHaveBeenCalledWith('pathToCachedTool')
|
||||
expect(core.getInput).toHaveBeenCalledWith('downloadBaseURL', {
|
||||
required: false
|
||||
})
|
||||
expect(core.getInput).toHaveBeenCalledWith('checksum', {required: false})
|
||||
// Default base URL: no audit notice should fire.
|
||||
expect(core.notice).not.toHaveBeenCalled()
|
||||
expect(core.setOutput).toHaveBeenCalledWith(
|
||||
'kubectl-path',
|
||||
path.join('pathToCachedTool', 'kubectl.exe')
|
||||
)
|
||||
})
|
||||
test('run() - get latest version, download it and set output', async () => {
|
||||
vi.mocked(core.getInput).mockReturnValue('latest')
|
||||
|
||||
test('run() - latest + custom mirror routes stable.txt through the mirror', async () => {
|
||||
mockInputs({
|
||||
version: 'latest',
|
||||
downloadBaseURL: 'https://mirror.example.com'
|
||||
})
|
||||
vi.mocked(toolCache.downloadTool).mockResolvedValue('pathToTool')
|
||||
vi.mocked(fs.readFileSync).mockReturnValue('v1.20.4')
|
||||
vi.mocked(toolCache.find).mockReturnValue('pathToCachedTool')
|
||||
vi.mocked(os.type).mockReturnValue('Windows_NT')
|
||||
vi.mocked(fs.chmodSync).mockImplementation()
|
||||
vi.mocked(core.addPath).mockImplementation()
|
||||
vi.spyOn(console, 'log').mockImplementation()
|
||||
vi.mocked(core.setOutput).mockImplementation()
|
||||
expect(await run.run()).toBeUndefined()
|
||||
|
||||
await expect(run.run()).resolves.toBeUndefined()
|
||||
// The fix: stable.txt comes from the custom mirror, not dl.k8s.io.
|
||||
expect(toolCache.downloadTool).toHaveBeenCalledWith(
|
||||
'https://dl.k8s.io/release/stable.txt'
|
||||
'https://mirror.example.com/release/stable.txt'
|
||||
)
|
||||
expect(core.getInput).toHaveBeenCalledWith('version', {required: true})
|
||||
expect(core.addPath).toHaveBeenCalledWith('pathToCachedTool')
|
||||
expect(core.setOutput).toHaveBeenCalledWith(
|
||||
'kubectl-path',
|
||||
path.join('pathToCachedTool', 'kubectl.exe')
|
||||
// Audit notice fires for any non-default base URL.
|
||||
expect(core.notice).toHaveBeenCalledWith(
|
||||
expect.stringContaining('https://mirror.example.com')
|
||||
)
|
||||
// No checksum + custom mirror => loud warning.
|
||||
expect(core.warning).toHaveBeenCalledWith(
|
||||
expect.stringContaining('checksum')
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
test('run() - empty/whitespace downloadBaseURL falls back to default', async () => {
|
||||
mockInputs({version: 'v1.15.5', downloadBaseURL: ' '})
|
||||
vi.mocked(toolCache.find).mockReturnValue('pathToCachedTool')
|
||||
vi.mocked(os.type).mockReturnValue('Windows_NT')
|
||||
vi.mocked(fs.chmodSync).mockImplementation()
|
||||
vi.mocked(core.addPath).mockImplementation()
|
||||
vi.mocked(core.setOutput).mockImplementation()
|
||||
|
||||
await expect(run.run()).resolves.toBeUndefined()
|
||||
// Default fallback => no audit notice.
|
||||
expect(core.notice).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test('run() - invalid downloadBaseURL (http) fails fast', async () => {
|
||||
mockInputs({version: 'v1.15.5', downloadBaseURL: 'http://insecure'})
|
||||
await expect(run.run()).rejects.toThrow(/must use https/)
|
||||
})
|
||||
|
||||
test('run() - uppercase checksum input is normalized and accepted', async () => {
|
||||
// sha256('hi') = 8f43...aa4 — supplied uppercase to prove run() lowercases it
|
||||
// before validateChecksum's strict /^[a-f0-9]{64}$/ regex sees it.
|
||||
const correctLower =
|
||||
'8f434346648f6b96df89dda901c5176b10a6d83961dd3c1ac88b59b2dc327aa4'
|
||||
mockInputs({
|
||||
version: 'v1.15.5',
|
||||
checksum: correctLower.toUpperCase()
|
||||
})
|
||||
vi.mocked(toolCache.find).mockReturnValue('')
|
||||
vi.mocked(toolCache.downloadTool).mockResolvedValue('pathToTool')
|
||||
vi.mocked(toolCache.cacheFile).mockResolvedValue('pathToCachedTool')
|
||||
vi.mocked(os.type).mockReturnValue('Linux')
|
||||
vi.mocked(os.arch).mockReturnValue('x64')
|
||||
vi.mocked(fs.readFileSync).mockReturnValue(Buffer.from('hi') as never)
|
||||
vi.mocked(fs.chmodSync).mockImplementation()
|
||||
vi.mocked(core.addPath).mockImplementation()
|
||||
vi.mocked(core.setOutput).mockImplementation()
|
||||
|
||||
await expect(run.run()).resolves.toBeUndefined()
|
||||
})
|
||||
})
|
||||
131
src/run.ts
131
src/run.ts
|
|
@ -1,27 +1,58 @@
|
|||
import * as path from 'path'
|
||||
import * as util from 'util'
|
||||
import * as os from 'os'
|
||||
import * as fs from 'fs'
|
||||
import * as crypto from 'crypto'
|
||||
import {buffer as readStreamToBuffer} from 'stream/consumers'
|
||||
import * as toolCache from '@actions/tool-cache'
|
||||
import * as core from '@actions/core'
|
||||
import {HttpClient} from '@actions/http-client'
|
||||
import {
|
||||
DEFAULT_KUBECTL_BASE_URL,
|
||||
getkubectlDownloadURL,
|
||||
getKubectlArch,
|
||||
getExecutableExtension,
|
||||
getLatestPatchVersion
|
||||
getLatestPatchVersion,
|
||||
validateBaseURL,
|
||||
validateVersion
|
||||
} from './helpers.js'
|
||||
|
||||
const kubectlToolName = 'kubectl'
|
||||
const stableKubectlVersion = 'v1.15.0'
|
||||
const stableVersionUrl = 'https://dl.k8s.io/release/stable.txt'
|
||||
|
||||
export async function run() {
|
||||
let version = core.getInput('version', {required: true})
|
||||
if (version.toLocaleLowerCase() === 'latest') {
|
||||
version = await getStableKubectlVersion()
|
||||
} else {
|
||||
version = await resolveKubectlVersion(version)
|
||||
|
||||
const rawBaseURL = core.getInput('downloadBaseURL', {required: false}).trim()
|
||||
const downloadBaseURL = rawBaseURL || DEFAULT_KUBECTL_BASE_URL
|
||||
validateBaseURL(downloadBaseURL)
|
||||
|
||||
const expectedChecksum = core
|
||||
.getInput('checksum', {required: false})
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
|
||||
if (downloadBaseURL !== DEFAULT_KUBECTL_BASE_URL) {
|
||||
core.notice(
|
||||
`kubectl will be downloaded from a custom mirror: ${downloadBaseURL}`
|
||||
)
|
||||
if (!expectedChecksum) {
|
||||
core.warning(
|
||||
'Custom downloadBaseURL set without a `checksum` input; downloaded binary will not be integrity-verified.'
|
||||
)
|
||||
}
|
||||
}
|
||||
const cachedPath = await downloadKubectl(version)
|
||||
|
||||
if (version.toLocaleLowerCase() === 'latest') {
|
||||
version = await getStableKubectlVersion(downloadBaseURL)
|
||||
} else {
|
||||
version = await resolveKubectlVersion(version, downloadBaseURL)
|
||||
}
|
||||
const cachedPath = await downloadKubectl(
|
||||
version,
|
||||
downloadBaseURL,
|
||||
expectedChecksum
|
||||
)
|
||||
|
||||
core.addPath(path.dirname(cachedPath))
|
||||
|
||||
|
|
@ -31,7 +62,14 @@ export async function run() {
|
|||
core.setOutput('kubectl-path', cachedPath)
|
||||
}
|
||||
|
||||
export async function getStableKubectlVersion(): Promise<string> {
|
||||
export async function getStableKubectlVersion(
|
||||
baseURL: string = DEFAULT_KUBECTL_BASE_URL
|
||||
): Promise<string> {
|
||||
const url = validateBaseURL(baseURL)
|
||||
const basePath = url.pathname.replace(/\/+$/, '')
|
||||
url.pathname = `${basePath}/release/stable.txt`
|
||||
const stableVersionUrl = url.toString()
|
||||
|
||||
return toolCache.downloadTool(stableVersionUrl).then(
|
||||
(downloadPath) => {
|
||||
let version = fs.readFileSync(downloadPath, 'utf8').toString().trim()
|
||||
|
|
@ -48,15 +86,24 @@ export async function getStableKubectlVersion(): Promise<string> {
|
|||
)
|
||||
}
|
||||
|
||||
export async function downloadKubectl(version: string): Promise<string> {
|
||||
export async function downloadKubectl(
|
||||
version: string,
|
||||
baseURL: string = DEFAULT_KUBECTL_BASE_URL,
|
||||
expectedChecksum: string = ''
|
||||
): Promise<string> {
|
||||
validateVersion(version)
|
||||
|
||||
let cachedToolpath = toolCache.find(kubectlToolName, version)
|
||||
let kubectlDownloadPath = ''
|
||||
const arch = getKubectlArch()
|
||||
if (!cachedToolpath) {
|
||||
const downloadURL = getkubectlDownloadURL(version, arch, baseURL)
|
||||
try {
|
||||
kubectlDownloadPath = await toolCache.downloadTool(
|
||||
getkubectlDownloadURL(version, arch)
|
||||
)
|
||||
if (baseURL === DEFAULT_KUBECTL_BASE_URL) {
|
||||
kubectlDownloadPath = await toolCache.downloadTool(downloadURL)
|
||||
} else {
|
||||
kubectlDownloadPath = await secureDownload(downloadURL)
|
||||
}
|
||||
} catch (exception) {
|
||||
if (
|
||||
exception instanceof toolCache.HTTPError &&
|
||||
|
|
@ -74,6 +121,10 @@ export async function downloadKubectl(version: string): Promise<string> {
|
|||
}
|
||||
}
|
||||
|
||||
if (expectedChecksum) {
|
||||
verifyChecksum(kubectlDownloadPath, expectedChecksum)
|
||||
}
|
||||
|
||||
cachedToolpath = await toolCache.cacheFile(
|
||||
kubectlDownloadPath,
|
||||
kubectlToolName + getExecutableExtension(),
|
||||
|
|
@ -90,7 +141,57 @@ export async function downloadKubectl(version: string): Promise<string> {
|
|||
return kubectlPath
|
||||
}
|
||||
|
||||
export async function resolveKubectlVersion(version: string): Promise<string> {
|
||||
async function secureDownload(downloadURL: string): Promise<string> {
|
||||
const client = new HttpClient('setup-kubectl', [], {
|
||||
allowRedirects: false
|
||||
})
|
||||
const response = await client.get(downloadURL)
|
||||
const status = response.message.statusCode
|
||||
|
||||
if (status && status >= 300 && status < 400) {
|
||||
const location = response.message.headers['location']
|
||||
response.message.resume()
|
||||
throw new Error(
|
||||
`Refusing redirect from custom downloadBaseURL (status ${status} -> ${location}).`
|
||||
)
|
||||
}
|
||||
if (status === 404) {
|
||||
response.message.resume()
|
||||
throw new toolCache.HTTPError(404)
|
||||
}
|
||||
if (status !== 200) {
|
||||
response.message.resume()
|
||||
throw new Error(`Download failed with status ${status}`)
|
||||
}
|
||||
|
||||
const tmpDir = process.env['RUNNER_TEMP'] || os.tmpdir()
|
||||
const tmpFile = path.join(tmpDir, `kubectl-${crypto.randomUUID()}`)
|
||||
const body = await readStreamToBuffer(response.message)
|
||||
fs.writeFileSync(tmpFile, body)
|
||||
return tmpFile
|
||||
}
|
||||
|
||||
function verifyChecksum(filePath: string, expected: string): void {
|
||||
if (!/^[a-f0-9]{64}$/.test(expected)) {
|
||||
throw new Error(
|
||||
`Invalid checksum input: expected a 64-character hex SHA256 string.`
|
||||
)
|
||||
}
|
||||
const actual = crypto
|
||||
.createHash('sha256')
|
||||
.update(fs.readFileSync(filePath))
|
||||
.digest('hex')
|
||||
if (actual !== expected) {
|
||||
throw new Error(
|
||||
`Checksum mismatch for downloaded kubectl. Expected ${expected}, got ${actual}.`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function resolveKubectlVersion(
|
||||
version: string,
|
||||
baseURL: string = DEFAULT_KUBECTL_BASE_URL
|
||||
): Promise<string> {
|
||||
const cleanedVersion = version.trim()
|
||||
const versionMatch = cleanedVersion.match(
|
||||
/^v?(?<major>\d+)\.(?<minor>\d+)(?:\.(?<patch>\d+))?$/
|
||||
|
|
@ -111,6 +212,6 @@ export async function resolveKubectlVersion(version: string): Promise<string> {
|
|||
: `v${cleanedVersion}`
|
||||
}
|
||||
|
||||
// Patch version is missing, fetch the latest
|
||||
return await getLatestPatchVersion(major, minor)
|
||||
// Patch version is missing, fetch the latest from the (possibly custom) mirror.
|
||||
return await getLatestPatchVersion(major, minor, baseURL)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue