import * as step from '@flow-step/step-toolkit' import * as sinon from 'sinon'; import {expect} from 'chai'; import proxyquire from 'proxyquire'; // @ts-ignore import { formatDate, getTagFromImageId, convertToVpcImage, getImageRepositoryFromImageId, getDockerBuildArgs } from '../src/toolkit'; describe('waitDockerDaemonReady', function () { let execSyncStub: sinon.SinonStub; let infoStub: sinon.SinonStub; beforeEach(function () { execSyncStub = sinon.stub(); infoStub = sinon.stub(step, 'info'); }); afterEach(function () { infoStub.restore(); }); it('should wait until Docker daemon is ready', async function () { execSyncStub.onFirstCall().throws(new Error('Command failed')); execSyncStub.onSecondCall().returns(null); const waitDockerDaemonReadyStubbed = proxyquire('../src/toolkit', { 'child_process': {execSync: execSyncStub} }).waitDockerDaemonReady; await waitDockerDaemonReadyStubbed(); expect(execSyncStub.callCount).to.equal(2); sinon.assert.calledWith(infoStub, 'Docker daemon not ready, waiting...'); sinon.assert.calledWith(infoStub, 'Docker daemon is ready'); }); it('should exit if Docker daemon is already ready', async function () { execSyncStub.returns(null); const waitDockerDaemonReadyStubbed = proxyquire('../src/toolkit', { 'child_process': {execSync: execSyncStub} }).waitDockerDaemonReady; await waitDockerDaemonReadyStubbed(); expect(execSyncStub.callCount).to.equal(1); sinon.assert.neverCalledWith(infoStub, 'Docker daemon not ready, waiting...'); sinon.assert.calledWith(infoStub, 'Docker daemon is ready'); }); }); describe('createDockerBuildxBuilder', () => { let execSyncStub: sinon.SinonStub; beforeEach(() => { execSyncStub = sinon.stub(); }); afterEach(() => { execSyncStub.reset(); }); it('should not attempt to create a new builder if one already exists', () => { execSyncStub.onFirstCall().returns(""); const createDockerBuildxBuilderStubbed = proxyquire('../src/toolkit', { 'child_process': {execSync: execSyncStub} }).createDockerBuildxBuilder; createDockerBuildxBuilderStubbed(); expect(execSyncStub.callCount).to.equal(1); sinon.assert.calledWith(execSyncStub, 'docker buildx inspect flow-build-container > /dev/null 2>&1'); }); it('should create a new builder if one does not exist', () => { execSyncStub.onFirstCall().throws(new Error('Command failed')); execSyncStub.onSecondCall().returns(""); const createDockerBuildxBuilderStubbed = proxyquire('../src/toolkit', { 'child_process': {execSync: execSyncStub} }).createDockerBuildxBuilder; createDockerBuildxBuilderStubbed(); expect(execSyncStub.callCount).to.equal(2); sinon.assert.calledWith(execSyncStub, 'docker buildx inspect flow-build-container > /dev/null 2>&1'); let arch: string = step.platform.getArch() if (arch === 'x64') { sinon.assert.calledWith(execSyncStub, 'docker buildx create --name flow-build-container --use --bootstrap --driver=docker-container --driver-opt=image=build-steps-public-registry.cn-beijing.cr.aliyuncs.com/build-steps/docker@sha256:c3cb08891c15763d426ba017fa7ce957537a779837f5bf2f7b50de3796e13918,network=host') } else if (arch === 'arm64') { sinon.assert.calledWith(execSyncStub, 'docker buildx create --name flow-build-container --use --bootstrap --driver=docker-container --driver-opt=image=build-steps-public-registry.cn-beijing.cr.aliyuncs.com/build-steps/docker@sha256:957eb319e91a8f4da122e988fc54632c929c94085ec0590ff47c77ad29a8e33f,network=host') } }); }); describe('formatContextPath', () => { let formatContextPath: (contextPath: string | undefined, dockerfilePath: string, projectDir: string) => string; let isAbsoluteStub: sinon.SinonStub; let dirnameStub: sinon.SinonStub; let joinStub: sinon.SinonStub; before(() => { isAbsoluteStub = sinon.stub(); dirnameStub = sinon.stub(); joinStub = sinon.stub(); const module = proxyquire('../src/toolkit', { 'path': { isAbsolute: isAbsoluteStub, dirname: dirnameStub, join: joinStub, }, }); formatContextPath = module.formatContextPath; }); it('should use the projectDir as context path when contextPath is undefined and dockerfilePath has no slashes', () => { const contextPath = undefined; const dockerfilePath = 'Dockerfile'; const projectDir = '/my/project/dir'; const result = formatContextPath(contextPath, dockerfilePath, projectDir); expect(result).to.equal(projectDir); }); it('should use the dirname of dockerfilePath when dockerfilePath is absolute', () => { const contextPath = undefined; const dockerfilePath = '/absolute/path/to/Dockerfile'; const projectDir = '/my/project/dir'; isAbsoluteStub.withArgs(dockerfilePath).returns(true); dirnameStub.withArgs(dockerfilePath).returns('/absolute/path/to'); const result = formatContextPath(contextPath, dockerfilePath, projectDir); sinon.assert.calledWith(isAbsoluteStub, dockerfilePath); sinon.assert.calledWith(dirnameStub, dockerfilePath); expect(result).to.equal('/absolute/path/to'); }); it('should join projectDir with contextPath when contextPath is defined', () => { const contextPath = 'my/context'; const dockerfilePath = 'Dockerfile'; const projectDir = '/my/project/dir'; joinStub.withArgs(projectDir, contextPath).returns('/my/project/dir/my/context'); const result = formatContextPath(contextPath, dockerfilePath, projectDir); sinon.assert.calledWith(joinStub, projectDir, contextPath); expect(result).to.equal('/my/project/dir/my/context'); }); it('should join projectDir with dirname of dockerfilePath when dockerfilePath contains slashes and is not absolute', () => { const contextPath = undefined; const dockerfilePath = 'path/to/Dockerfile'; const projectDir = '/my/project/dir'; isAbsoluteStub.withArgs(dockerfilePath).returns(false); dirnameStub.withArgs(dockerfilePath).returns('path/to'); joinStub.withArgs(projectDir, 'path/to').returns('/my/project/dir/path/to'); const result = formatContextPath(contextPath, dockerfilePath, projectDir); sinon.assert.calledWith(isAbsoluteStub, dockerfilePath); sinon.assert.calledWith(dirnameStub, dockerfilePath); sinon.assert.calledWith(joinStub, projectDir, 'path/to'); expect(result).to.equal('/my/project/dir/path/to'); }); it('should join projectDir contextPath if contextPath is not empty, regardless of dockerfilePath', () => { const contextPath = '/absolute/context'; const dockerfilePath = './Dockerfile'; const projectDir = '/my/project/dir'; isAbsoluteStub.withArgs(dockerfilePath).returns(false); joinStub.withArgs(projectDir, contextPath).returns('/my/project/dir/absolute/context'); const result = formatContextPath(contextPath, dockerfilePath, projectDir); sinon.assert.calledWith(joinStub, projectDir, contextPath); expect(result).to.equal('/my/project/dir/absolute/context'); }); after(() => { isAbsoluteStub.reset() dirnameStub.reset(); joinStub.reset(); }); }); describe('formatDate', () => { it('should format the date correctly', () => { const date = new Date(2022, 3, 14, 10, 30, 45); const formattedDate = formatDate(date); expect(formattedDate).to.equal('2022-04-14-10-30-45'); }); }); describe('getImageRepositoryFromImageId', () => { it('should return the repository when imageId has two parts', () => { const imageId = 'example.cr.aliyuncs.com/namespace/repo:tag'; const result = getImageRepositoryFromImageId(imageId); expect(result).to.equal('example.cr.aliyuncs.com/namespace/repo'); }); it('should return the imageId when it has only one part', () => { const imageId = 'example.cr.aliyuncs.com/namespace/repo'; const result = getImageRepositoryFromImageId(imageId); expect(result).to.equal('example.cr.aliyuncs.com/namespace/repo'); }); }); describe('getTagFromImageId', () => { it('should return the tag from imageId with format "registry-host:port/namespace/image-name:tag"', () => { const imageId = 'registry-host:port/namespace/image-name:tag'; const result = getTagFromImageId(imageId); expect(result).to.equal('tag'); }); it('should return the tag from imageId with format "registry-host/namespace/image-name:tag"', () => { const imageId = 'registry-host/namespace/image-name:tag'; const result = getTagFromImageId(imageId); expect(result).to.equal('tag'); }); it('should return empty string from imageId with format "registry-host:port/namespace/image-name"', () => { const imageId = 'registry-host:port/namespace/image-name'; const result = getTagFromImageId(imageId); expect(result).to.equal(''); }); it('should return empty string from imageId with format "registry-host/namespace/image-name"', () => { const imageId = 'registry-host/namespace/image-name'; const result = getTagFromImageId(imageId); expect(result).to.equal(''); }); it('should return empty string if imageId is empty', () => { const imageId = ''; const result = getTagFromImageId(imageId); expect(result).to.equal(''); }); }); describe('addVpcToImage', () => { it('should add -vpc to the domain when image url contains cr.aliyuncs.com', () => { const image = 'example.cr.aliyuncs.com/namespace/repo:tag'; const result = convertToVpcImage(image); expect(result).to.equal('example-vpc.cr.aliyuncs.com/namespace/repo:tag'); }); it('should add -vpc to the domain when image url contains aliyuncs.com', () => { const image = 'example.aliyuncs.com/namespace/repo:tag'; const result = convertToVpcImage(image); expect(result).to.equal('example-vpc.aliyuncs.com/namespace/repo:tag'); }); it('should not add -vpc to the domain when image url does not contain aliyuncs.com', () => { const image = 'example.com/namespace/repo:tag'; const result = convertToVpcImage(image); expect(result).to.equal(image); }); it('should not add -vpc to the domain when image url contain -vpc and aliyuncs.com', () => { const image = 'example-vpc.aliyuncs.com/namespace/repo:tag'; const result = convertToVpcImage(image); expect(result).to.equal(image); }); it('should return the original image when image is empty', () => { const image = ''; const result = convertToVpcImage(image); expect(result).to.equal(image); }); }); describe('getDockerBuildArgs', () => { it('should return the correct args when useCache is false', () => { const image = 'test/image:latest'; const targetDockerfilePath = 'Dockerfile'; const contextPath = '.'; const result = getDockerBuildArgs(image, targetDockerfilePath, contextPath, "no-cache", ""); expect(result.toString()).to.equal([ 'buildx', 'build', '--progress=plain', '-t', image, '-f', targetDockerfilePath, contextPath, '--push', '--no-cache', ].toString()); }); it('should return the correct args when cacheType is remote and cacheImageId is provided', () => { const image = 'test/image:latest'; const targetDockerfilePath = 'Dockerfile'; const contextPath = '.'; const result = getDockerBuildArgs(image, targetDockerfilePath, contextPath, "remote", "remote/cache/image:latest"); expect(result.toString()).to.equal([ 'buildx', 'build', '--progress=plain', '-t', image, '-f', targetDockerfilePath, contextPath, '--push', '--cache-from', 'remote/cache/image:latest', '--cache-to', 'remote/cache/image:latest', ].toString()); }); it('should return the correct args when cacheType is remote and cacheImageId is not provided', () => { const image = 'test/image:latest'; const targetDockerfilePath = 'Dockerfile'; const contextPath = '.'; const result = getDockerBuildArgs(image, targetDockerfilePath, contextPath, "remote", ""); const expectedCacheImageId = 'test/image:flow-docker-build-cache'; expect(result.toString()).to.equal([ 'buildx', 'build', '--progress=plain', '-t', image, '-f', targetDockerfilePath, contextPath, '--push', '--cache-from', expectedCacheImageId, '--cache-to', expectedCacheImageId, ].toString()); }); });