Skip to content

Commit

Permalink
feat: add ODBC installer
Browse files Browse the repository at this point in the history
  • Loading branch information
dhensby committed Sep 12, 2023
1 parent 27e800f commit e9706fb
Show file tree
Hide file tree
Showing 10 changed files with 170 additions and 1 deletion.
1 change: 1 addition & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ jobs:
with:
sqlserver-version: ${{ matrix.sqlserver }}
native-client-version: 11
odbc-version: 17
release:
name: Release
concurrency: release
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ See [action.yml](./action.yml):
# Version of native client to install. Only 11 is supported.
native-client-version: ''

# Version of ODBC to install. Supported versions: 17, 18.
odbc-version: ''

# The SA user password to use.
# Default: yourStrong(!)Password
sa-password: ''
Expand Down
2 changes: 2 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ inputs:
default: 'latest'
native-client-version:
description: 'Version of native client to install. Only 11 is supported.'
odbc-version:
description: 'Version of ODBC to install. Supported versions: 17, 18.'
sa-password:
description: 'The SA user password to use.'
default: 'yourStrong(!)Password'
Expand Down
2 changes: 1 addition & 1 deletion lib/main/index.js

Large diffs are not rendered by default.

28 changes: 28 additions & 0 deletions src/install-odbc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { MsiInstaller, Urls } from './installers';

// https://learn.microsoft.com/en-us/sql/connect/odbc/download-odbc-driver-for-sql-server?view=sql-server-ver16
const VERSIONS = new Map<string, Urls>([
['18', {
x64: 'https://go.microsoft.com/fwlink/?linkid=2242886',
x86: 'https://go.microsoft.com/fwlink/?linkid=2242980',
}],
['17', {
x64: 'https://go.microsoft.com/fwlink/?linkid=2239168',
x86: 'https://go.microsoft.com/fwlink/?linkid=2238791',
}],
]);

export default async function installOdbc(version: string) {
if (!VERSIONS.has(version)) {
throw new TypeError(`Invalid ODBC version supplied ${version}. Must be one of ${Array.from(VERSIONS.keys()).join(', ')}.`);
}
const installer = new MsiInstaller({
name: 'msodbcsql',
urls: VERSIONS.get(version)!,
version,
extraArgs: [
'IACCEPTMSODBCSQLLICENSETERMS=YES',
],
});
return installer.install();
}
5 changes: 5 additions & 0 deletions src/install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
waitForDatabase,
} from './utils';
import installNativeClient from './install-native-client';
import installOdbc from './install-odbc';

/**
* Attempt to load the installer from the tool-cache, otherwise, fetch it.
Expand Down Expand Up @@ -42,6 +43,7 @@ export default async function install() {
wait,
skipOsCheck,
nativeClientVersion,
odbcVersion,
} = gatherInputs();
// we only support windows for now. But allow crazy people to skip this check if they like...
if (!skipOsCheck && os.platform() !== 'win32') {
Expand Down Expand Up @@ -79,6 +81,9 @@ export default async function install() {
if (nativeClientVersion) {
await core.group('Installing SQL Native Client', () => installNativeClient(nativeClientVersion));
}
if (odbcVersion) {
await core.group('Installing ODBC', () => installOdbc(odbcVersion));
}
// Initial checks complete - fetch the installer
const toolPath = await core.group(`Fetching install media for ${version}`, () => findOrDownloadTool(config));
const instanceName = 'MSSQLSERVER';
Expand Down
2 changes: 2 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export interface Inputs {
wait: boolean;
skipOsCheck: boolean;
nativeClientVersion: string;
odbcVersion: string;
}

/**
Expand All @@ -66,6 +67,7 @@ export function gatherInputs(): Inputs {
wait: core.getBooleanInput('wait-for-ready'),
skipOsCheck: core.getBooleanInput('skip-os-check'),
nativeClientVersion: core.getInput('native-client-version'),
odbcVersion: core.getInput('odbc-version'),
};
}

Expand Down
91 changes: 91 additions & 0 deletions test/install-odbc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { restore, SinonStubbedInstance, stub } from 'sinon';
import * as core from '@actions/core';
import * as tc from '@actions/tool-cache';
import * as exec from '@actions/exec';
import * as io from '@actions/io';
import installOdbc from '../src/install-odbc';
import { expect, use } from 'chai';
import sinonChai from 'sinon-chai';
use(sinonChai);

describe('install-odbc', () => {
// let coreStub: SinonStubbedInstance<typeof core>;
let tcStub: SinonStubbedInstance<typeof tc>;
let execStub: SinonStubbedInstance<typeof exec>;
let ioStub: SinonStubbedInstance<typeof io>;
let arch: PropertyDescriptor;
beforeEach('stub deps', () => {
stub(core);
arch = Object.getOwnPropertyDescriptor(process, 'arch')!;
tcStub = stub(tc);
tcStub.find.returns('');
execStub = stub(exec);
execStub.exec.resolves();
ioStub = stub(io);
ioStub.mv.resolves();
});
afterEach('restore stubs', () => {
Object.defineProperty(process, 'arch', arch);
restore();
});
describe('.installOdbc()', () => {
it('throws for bad version', async () => {
try {
await installOdbc('10');
} catch (e) {
expect(e).to.have.property('message', 'Invalid ODBC version supplied 10. Must be one of 18, 17.');
return;
}
expect.fail('expected to throw');
});
it('installs from cache', async () => {
tcStub.find.returns('C:/tmp/');
await installOdbc('18');
expect(tcStub.downloadTool).to.have.callCount(0);
expect(execStub.exec).to.have.been.calledOnceWith('msiexec', [
'/passive',
'/i',
'C:/tmp/msodbcsql.msi',
'IACCEPTMSODBCSQLLICENSETERMS=YES',
], {
windowsVerbatimArguments: true,
});
});
it('installs from web (x64)', async () => {
Object.defineProperty(process, 'arch', {
value: 'x64',
});
tcStub.cacheFile.resolves('C:/tmp/cache/');
tcStub.downloadTool.resolves('C:/tmp/downloads');
await installOdbc('17');
expect(tcStub.downloadTool).to.have.been.calledOnceWith('https://go.microsoft.com/fwlink/?linkid=2239168');
expect(tcStub.cacheFile).to.have.callCount(1);
expect(execStub.exec).to.have.been.calledOnceWith('msiexec', [
'/passive',
'/i',
'C:/tmp/cache/msodbcsql.msi',
'IACCEPTMSODBCSQLLICENSETERMS=YES',
], {
windowsVerbatimArguments: true,
});
});
it('installs from web (x32)', async () => {
Object.defineProperty(process, 'arch', {
value: 'x32',
});
tcStub.cacheFile.resolves('C:/tmp/cache/');
tcStub.downloadTool.resolves('C:/tmp/downloads');
await installOdbc('17');
expect(tcStub.downloadTool).to.have.been.calledOnceWith('https://go.microsoft.com/fwlink/?linkid=2238791');
expect(tcStub.cacheFile).to.have.callCount(1);
expect(execStub.exec).to.have.been.calledOnceWith('msiexec', [
'/passive',
'/i',
'C:/tmp/cache/msodbcsql.msi',
'IACCEPTMSODBCSQLLICENSETERMS=YES',
], {
windowsVerbatimArguments: true,
});
});
});
});
29 changes: 29 additions & 0 deletions test/install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import * as tc from '@actions/tool-cache';
import * as exec from '@actions/exec';
import * as versions from '../src/versions';
import * as nativeClient from '../src/install-native-client';
import * as odbcDriver from '../src/install-odbc';
import { match, restore, SinonStubbedInstance, stub, useFakeTimers } from 'sinon';
import * as utils from '../src/utils';
import install from '../src/install';
Expand All @@ -22,8 +23,10 @@ describe('install', () => {
let tcStub: SinonStubbedInstance<typeof tc>;
let execStub: SinonStubbedInstance<typeof exec>;
let stubNc: SinonStubbedInstance<typeof nativeClient>;
let stubOdbc: SinonStubbedInstance<typeof odbcDriver>;
beforeEach('stub deps', () => {
stubNc = stub(nativeClient);
stubOdbc = stub(odbcDriver);
versionStub = stub(versions.VERSIONS);
versionStub.keys.returns(['box', 'exe', 'maxOs', 'minOs', 'minMaxOs'][Symbol.iterator]());
versionStub.has.callsFake((name) => {
Expand Down Expand Up @@ -69,6 +72,7 @@ describe('install', () => {
wait: true,
skipOsCheck: false,
nativeClientVersion: '',
odbcVersion: '',
});
utilsStub.getOsVersion.resolves(2022);
utilsStub.gatherSummaryFiles.resolves([]);
Expand Down Expand Up @@ -110,6 +114,7 @@ describe('install', () => {
wait: true,
skipOsCheck: false,
nativeClientVersion: '',
odbcVersion: '',
});
try {
await install();
Expand All @@ -132,6 +137,7 @@ describe('install', () => {
wait: true,
skipOsCheck: false,
nativeClientVersion: '',
odbcVersion: '',
});
await install();
expect(execStub.exec).to.have.been.calledWith('"C:/tmp/exe/setup.exe"', match.array, { windowsVerbatimArguments: true });
Expand All @@ -150,6 +156,7 @@ describe('install', () => {
wait: true,
skipOsCheck: false,
nativeClientVersion: '',
odbcVersion: '',
});
try {
await install();
Expand All @@ -169,6 +176,7 @@ describe('install', () => {
wait: true,
skipOsCheck: false,
nativeClientVersion: '',
odbcVersion: '',
});
try {
await install();
Expand All @@ -188,6 +196,7 @@ describe('install', () => {
wait: true,
skipOsCheck: false,
nativeClientVersion: '',
odbcVersion: '',
});
try {
await install();
Expand All @@ -207,6 +216,7 @@ describe('install', () => {
wait: true,
skipOsCheck: false,
nativeClientVersion: '',
odbcVersion: '',
});
await install();
expect(execStub.exec).to.have.been.calledWith('"C:/tmp/exe/setup.exe"', match.array, { windowsVerbatimArguments: true });
Expand All @@ -221,6 +231,7 @@ describe('install', () => {
wait: true,
skipOsCheck: false,
nativeClientVersion: '',
odbcVersion: '',
});
await install();
expect(execStub.exec).to.have.been.calledWith('"C:/tmp/exe/setup.exe"', match.array, { windowsVerbatimArguments: true });
Expand All @@ -234,6 +245,7 @@ describe('install', () => {
wait: true,
skipOsCheck: true,
nativeClientVersion: '',
odbcVersion: '',
});
await install();
expect(execStub.exec).to.have.been.calledWith('"C:/tmp/exe/setup.exe"', match.array, { windowsVerbatimArguments: true });
Expand Down Expand Up @@ -269,6 +281,7 @@ describe('install', () => {
wait: false,
skipOsCheck: false,
nativeClientVersion: '',
odbcVersion: '',
});
const stubReadfile = stub(fs, 'readFile');
stubReadfile.resolves(Buffer.from('test data'));
Expand Down Expand Up @@ -313,8 +326,24 @@ describe('install', () => {
wait: false,
skipOsCheck: false,
nativeClientVersion: '11',
odbcVersion: '',
});
await install();
expect(stubNc.default).to.have.been.calledOnceWith('11');
});
it('installs odbc driver if needed', async () => {
utilsStub.gatherInputs.returns({
version: 'box',
password: 'secret password',
collation: 'SQL_Latin1_General_CP1_CI_AS',
installArgs: [],
wait: false,
skipOsCheck: false,
nativeClientVersion: '11',
odbcVersion: '18',
});
await install();
expect(stubNc.default).to.have.been.calledOnceWith('11');
expect(stubOdbc.default).to.have.been.calledOnceWith('18');
});
});
8 changes: 8 additions & 0 deletions test/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ describe('utils', () => {
coreStub.getInput.withArgs('sa-password').returns('secret password');
coreStub.getInput.withArgs('db-collation').returns('SQL_Latin1_General_CP1_CI_AS');
coreStub.getInput.withArgs('native-client-version').returns('');
coreStub.getInput.withArgs('odbc-version').returns('');
coreStub.getMultilineInput.withArgs('install-arguments').returns([]);
coreStub.getBooleanInput.withArgs('wait-for-ready').returns(true);
coreStub.getBooleanInput.withArgs('skip-os-check').returns(false);
Expand All @@ -200,13 +201,15 @@ describe('utils', () => {
wait: true,
skipOsCheck: false,
nativeClientVersion: '',
odbcVersion: '',
});
});
it('constructs input object with no sql- prefix', () => {
coreStub.getInput.withArgs('sqlserver-version').returns('2022');
coreStub.getInput.withArgs('sa-password').returns('secret password');
coreStub.getInput.withArgs('db-collation').returns('SQL_Latin1_General_CP1_CI_AS');
coreStub.getInput.withArgs('native-client-version').returns('');
coreStub.getInput.withArgs('odbc-version').returns('');
coreStub.getMultilineInput.withArgs('install-arguments').returns([]);
coreStub.getBooleanInput.withArgs('wait-for-ready').returns(true);
coreStub.getBooleanInput.withArgs('skip-os-check').returns(false);
Expand All @@ -219,13 +222,15 @@ describe('utils', () => {
wait: true,
skipOsCheck: false,
nativeClientVersion: '',
odbcVersion: '',
});
});
it('constructs input object with "latest" version', () => {
coreStub.getInput.withArgs('sqlserver-version').returns('latest');
coreStub.getInput.withArgs('sa-password').returns('secret password');
coreStub.getInput.withArgs('db-collation').returns('SQL_Latin1_General_CP1_CI_AS');
coreStub.getInput.withArgs('native-client-version').returns('');
coreStub.getInput.withArgs('odbc-version').returns('');
coreStub.getMultilineInput.withArgs('install-arguments').returns([]);
coreStub.getBooleanInput.withArgs('wait-for-ready').returns(true);
coreStub.getBooleanInput.withArgs('skip-os-check').returns(false);
Expand All @@ -238,13 +243,15 @@ describe('utils', () => {
wait: true,
skipOsCheck: false,
nativeClientVersion: '',
odbcVersion: '',
});
});
it('constructs input object with default version', () => {
coreStub.getInput.withArgs('sqlserver-version').returns('');
coreStub.getInput.withArgs('sa-password').returns('secret password');
coreStub.getInput.withArgs('db-collation').returns('SQL_Latin1_General_CP1_CI_AS');
coreStub.getInput.withArgs('native-client-version').returns('');
coreStub.getInput.withArgs('odbc-version').returns('');
coreStub.getMultilineInput.withArgs('install-arguments').returns([]);
coreStub.getBooleanInput.withArgs('wait-for-ready').returns(true);
coreStub.getBooleanInput.withArgs('skip-os-check').returns(false);
Expand All @@ -257,6 +264,7 @@ describe('utils', () => {
wait: true,
skipOsCheck: false,
nativeClientVersion: '',
odbcVersion: '',
});
});
});
Expand Down

0 comments on commit e9706fb

Please sign in to comment.