import * as fs from 'fs' import { containerPorts } from '../src/k8s' import { generateContainerName, writeRunScript, mergePodSpecWithOptions, mergeContainerWithOptions, readExtensionFromFile, ENV_HOOK_TEMPLATE_PATH } from '../src/k8s/utils' import * as k8s from '@kubernetes/client-node' import { TestHelper } from './test-setup' let testHelper: TestHelper describe('k8s utils', () => { describe('write entrypoint', () => { beforeEach(async () => { testHelper = new TestHelper() await testHelper.initialize() }) afterEach(async () => { await testHelper.cleanup() }) it('should not throw', () => { expect(() => writeRunScript('/test', 'sh', ['-e', 'script.sh'], ['/prepend/path'], { SOME_ENV: 'SOME_VALUE' }) ).not.toThrow() }) it('should throw if RUNNER_TEMP is not set', () => { delete process.env.RUNNER_TEMP expect(() => writeRunScript('/test', 'sh', ['-e', 'script.sh'], ['/prepend/path'], { SOME_ENV: 'SOME_VALUE' }) ).toThrow() }) it('should throw if environment variable name contains double quote', () => { expect(() => writeRunScript('/test', 'sh', ['-e', 'script.sh'], ['/prepend/path'], { 'SOME"_ENV': 'SOME_VALUE' }) ).toThrow() }) it('should throw if environment variable name contains =', () => { expect(() => writeRunScript('/test', 'sh', ['-e', 'script.sh'], ['/prepend/path'], { 'SOME=ENV': 'SOME_VALUE' }) ).toThrow() }) it('should throw if environment variable name contains single quote', () => { expect(() => writeRunScript('/test', 'sh', ['-e', 'script.sh'], ['/prepend/path'], { "SOME'_ENV": 'SOME_VALUE' }) ).toThrow() }) it('should throw if environment variable name contains dollar', () => { expect(() => writeRunScript('/test', 'sh', ['-e', 'script.sh'], ['/prepend/path'], { SOME_$_ENV: 'SOME_VALUE' }) ).toThrow() }) it('should escape double quote, dollar and backslash in environment variable values', () => { const { runnerPath } = writeRunScript( '/test', 'sh', ['-e', 'script.sh'], ['/prepend/path'], { DQUOTE: '"', BACK_SLASH: '\\', DOLLAR: '$' } ) expect(fs.existsSync(runnerPath)).toBe(true) const script = fs.readFileSync(runnerPath, 'utf8') expect(script).toContain('"DQUOTE=\\"') expect(script).toContain('"BACK_SLASH=\\\\"') expect(script).toContain('"DOLLAR=\\$"') }) it('should return object with containerPath and runnerPath', () => { const { containerPath, runnerPath } = writeRunScript( '/test', 'sh', ['-e', 'script.sh'], ['/prepend/path'], { SOME_ENV: 'SOME_VALUE' } ) expect(containerPath).toMatch(/\/__w\/_temp\/.*\.sh/) const re = new RegExp(`${process.env.RUNNER_TEMP}/.*\\.sh`) expect(runnerPath).toMatch(re) }) it('should write entrypoint path and the file should exist', () => { const { runnerPath } = writeRunScript( '/test', 'sh', ['-e', 'script.sh'], ['/prepend/path'], { SOME_ENV: 'SOME_VALUE' } ) expect(fs.existsSync(runnerPath)).toBe(true) }) }) describe('container volumes', () => { beforeEach(async () => { testHelper = new TestHelper() await testHelper.initialize() }) afterEach(async () => { await testHelper.cleanup() }) it('should parse container ports', () => { const tt = [ { spec: '8080:80', want: { containerPort: 80, hostPort: 8080, protocol: 'TCP' } }, { spec: '8080:80/udp', want: { containerPort: 80, hostPort: 8080, protocol: 'UDP' } }, { spec: '8080/udp', want: { containerPort: 8080, hostPort: undefined, protocol: 'UDP' } }, { spec: '8080', want: { containerPort: 8080, hostPort: undefined, protocol: 'TCP' } } ] for (const tc of tt) { const got = containerPorts({ portMappings: [tc.spec] }) for (const [key, value] of Object.entries(tc.want)) { expect(got[0][key]).toBe(value) } } }) it('should throw when ports are out of range (0, 65536)', () => { expect(() => containerPorts({ portMappings: ['65536'] })).toThrow() expect(() => containerPorts({ portMappings: ['0'] })).toThrow() expect(() => containerPorts({ portMappings: ['65536/udp'] })).toThrow() expect(() => containerPorts({ portMappings: ['0/udp'] })).toThrow() expect(() => containerPorts({ portMappings: ['1:65536'] })).toThrow() expect(() => containerPorts({ portMappings: ['65536:1'] })).toThrow() expect(() => containerPorts({ portMappings: ['1:65536/tcp'] })).toThrow() expect(() => containerPorts({ portMappings: ['65536:1/tcp'] })).toThrow() expect(() => containerPorts({ portMappings: ['1:'] })).toThrow() expect(() => containerPorts({ portMappings: [':1'] })).toThrow() expect(() => containerPorts({ portMappings: ['1:/tcp'] })).toThrow() expect(() => containerPorts({ portMappings: [':1/tcp'] })).toThrow() }) it('should throw on multi ":" splits', () => { expect(() => containerPorts({ portMappings: ['1:1:1'] })).toThrow() }) it('should throw on multi "/" splits', () => { expect(() => containerPorts({ portMappings: ['1:1/tcp/udp'] })).toThrow() expect(() => containerPorts({ portMappings: ['1/tcp/udp'] })).toThrow() }) }) describe('generate container name', () => { it('should return the container name from image string', () => { expect( generateContainerName('public.ecr.aws/localstack/localstack') ).toEqual('localstack') expect( generateContainerName( 'public.ecr.aws/url/with/multiple/slashes/postgres:latest' ) ).toEqual('postgres') expect(generateContainerName('postgres')).toEqual('postgres') expect(generateContainerName('postgres:latest')).toEqual('postgres') expect(generateContainerName('localstack/localstack')).toEqual( 'localstack' ) expect(generateContainerName('localstack/localstack:latest')).toEqual( 'localstack' ) }) it('should throw on invalid image string', () => { expect(() => generateContainerName('localstack/localstack/:latest') ).toThrow() expect(() => generateContainerName(':latest')).toThrow() }) }) describe('read extension', () => { beforeEach(async () => { testHelper = new TestHelper() await testHelper.initialize() }) afterEach(async () => { await testHelper.cleanup() }) it('should throw if env variable is set but file does not exist', () => { process.env[ENV_HOOK_TEMPLATE_PATH] = '/path/that/does/not/exist/data.yaml' expect(() => readExtensionFromFile()).toThrow() }) it('should return undefined if env variable is not set', () => { delete process.env[ENV_HOOK_TEMPLATE_PATH] expect(readExtensionFromFile()).toBeUndefined() }) it('should throw if file is empty', () => { let filePath = testHelper.createFile('data.yaml') process.env[ENV_HOOK_TEMPLATE_PATH] = filePath expect(() => readExtensionFromFile()).toThrow() }) it('should throw if file is not valid yaml', () => { let filePath = testHelper.createFile('data.yaml') fs.writeFileSync(filePath, 'invalid yaml') process.env[ENV_HOOK_TEMPLATE_PATH] = filePath expect(() => readExtensionFromFile()).toThrow() }) it('should return object if file is valid', () => { let filePath = testHelper.createFile('data.yaml') fs.writeFileSync( filePath, ` metadata: labels: label-name: label-value annotations: annotation-name: annotation-value spec: containers: - name: test image: node:22 - name: job image: ubuntu:latest` ) process.env[ENV_HOOK_TEMPLATE_PATH] = filePath const extension = readExtensionFromFile() expect(extension).toBeDefined() }) }) it('should merge container spec', () => { const base = { image: 'node:22', name: 'test', env: [ { name: 'TEST', value: 'TEST' } ], ports: [ { containerPort: 8080, hostPort: 8080, protocol: 'TCP' } ] } as k8s.V1Container const from = { ports: [ { containerPort: 9090, hostPort: 9090, protocol: 'TCP' } ], env: [ { name: 'TEST_TWO', value: 'TEST_TWO' } ], image: 'ubuntu:latest', name: 'overwrite' } as k8s.V1Container const expectContainer = { name: base.name, image: base.image, ports: [ ...(base.ports as k8s.V1ContainerPort[]), ...(from.ports as k8s.V1ContainerPort[]) ], env: [...(base.env as k8s.V1EnvVar[]), ...(from.env as k8s.V1EnvVar[])] } const expectJobContainer = JSON.parse(JSON.stringify(expectContainer)) expectJobContainer.name = base.name mergeContainerWithOptions(base, from) expect(base).toStrictEqual(expectContainer) }) it('should merge pod spec', () => { const base = { containers: [ { image: 'node:22', name: 'test', env: [ { name: 'TEST', value: 'TEST' } ], ports: [ { containerPort: 8080, hostPort: 8080, protocol: 'TCP' } ] } ], restartPolicy: 'Never' } as k8s.V1PodSpec const from = { securityContext: { runAsUser: 1000, fsGroup: 2000 }, restartPolicy: 'Always', volumes: [ { name: 'work', emptyDir: {} } ], containers: [ { image: 'ubuntu:latest', name: 'side-car', env: [ { name: 'TEST', value: 'TEST' } ], ports: [ { containerPort: 8080, hostPort: 8080, protocol: 'TCP' } ] } ] } as k8s.V1PodSpec const expected = JSON.parse(JSON.stringify(base)) expected.securityContext = from.securityContext expected.restartPolicy = from.restartPolicy expected.volumes = from.volumes expected.containers.push(from.containers[0]) mergePodSpecWithOptions(base, from) expect(base).toStrictEqual(expected) }) })