зеркало из https://github.com/Azure/mysql.git
Azure MySQL Action (#1)
* Azure MySQL Action * Addressed review comments * code fixes
This commit is contained in:
Родитель
5f9b34cdfb
Коммит
0871478cfe
|
@ -1,330 +1,93 @@
|
|||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
##
|
||||
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
|
||||
__tests__/runner/*
|
||||
|
||||
# User-specific files
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
|
||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||
*.userprefs
|
||||
|
||||
# Build results
|
||||
[Dd]ebug/
|
||||
[Dd]ebugPublic/
|
||||
[Rr]elease/
|
||||
[Rr]eleases/
|
||||
x64/
|
||||
x86/
|
||||
bld/
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
[Ll]og/
|
||||
|
||||
# Visual Studio 2015/2017 cache/options directory
|
||||
.vs/
|
||||
# Uncomment if you have tasks that create the project's static files in wwwroot
|
||||
#wwwroot/
|
||||
|
||||
# Visual Studio 2017 auto generated files
|
||||
Generated\ Files/
|
||||
|
||||
# MSTest test Results
|
||||
[Tt]est[Rr]esult*/
|
||||
[Bb]uild[Ll]og.*
|
||||
|
||||
# NUNIT
|
||||
*.VisualState.xml
|
||||
TestResult.xml
|
||||
|
||||
# Build Results of an ATL Project
|
||||
[Dd]ebugPS/
|
||||
[Rr]eleasePS/
|
||||
dlldata.c
|
||||
|
||||
# Benchmark Results
|
||||
BenchmarkDotNet.Artifacts/
|
||||
|
||||
# .NET Core
|
||||
project.lock.json
|
||||
project.fragment.lock.json
|
||||
artifacts/
|
||||
**/Properties/launchSettings.json
|
||||
|
||||
# StyleCop
|
||||
StyleCopReport.xml
|
||||
|
||||
# Files built by Visual Studio
|
||||
*_i.c
|
||||
*_p.c
|
||||
*_i.h
|
||||
*.ilk
|
||||
*.meta
|
||||
*.obj
|
||||
*.iobj
|
||||
*.pch
|
||||
*.pdb
|
||||
*.ipdb
|
||||
*.pgc
|
||||
*.pgd
|
||||
*.rsp
|
||||
*.sbr
|
||||
*.tlb
|
||||
*.tli
|
||||
*.tlh
|
||||
*.tmp
|
||||
*.tmp_proj
|
||||
*.log
|
||||
*.vspscc
|
||||
*.vssscc
|
||||
.builds
|
||||
*.pidb
|
||||
*.svclog
|
||||
*.scc
|
||||
|
||||
# Chutzpah Test files
|
||||
_Chutzpah*
|
||||
|
||||
# Visual C++ cache files
|
||||
ipch/
|
||||
*.aps
|
||||
*.ncb
|
||||
*.opendb
|
||||
*.opensdf
|
||||
*.sdf
|
||||
*.cachefile
|
||||
*.VC.db
|
||||
*.VC.VC.opendb
|
||||
|
||||
# Visual Studio profiler
|
||||
*.psess
|
||||
*.vsp
|
||||
*.vspx
|
||||
*.sap
|
||||
|
||||
# Visual Studio Trace Files
|
||||
*.e2e
|
||||
|
||||
# TFS 2012 Local Workspace
|
||||
$tf/
|
||||
|
||||
# Guidance Automation Toolkit
|
||||
*.gpState
|
||||
|
||||
# ReSharper is a .NET coding add-in
|
||||
_ReSharper*/
|
||||
*.[Rr]e[Ss]harper
|
||||
*.DotSettings.user
|
||||
|
||||
# JustCode is a .NET coding add-in
|
||||
.JustCode
|
||||
|
||||
# TeamCity is a build add-in
|
||||
_TeamCity*
|
||||
|
||||
# DotCover is a Code Coverage Tool
|
||||
*.dotCover
|
||||
|
||||
# AxoCover is a Code Coverage Tool
|
||||
.axoCover/*
|
||||
!.axoCover/settings.json
|
||||
|
||||
# Visual Studio code coverage results
|
||||
*.coverage
|
||||
*.coveragexml
|
||||
|
||||
# NCrunch
|
||||
_NCrunch_*
|
||||
.*crunch*.local.xml
|
||||
nCrunchTemp_*
|
||||
|
||||
# MightyMoose
|
||||
*.mm.*
|
||||
AutoTest.Net/
|
||||
|
||||
# Web workbench (sass)
|
||||
.sass-cache/
|
||||
|
||||
# Installshield output folder
|
||||
[Ee]xpress/
|
||||
|
||||
# DocProject is a documentation generator add-in
|
||||
DocProject/buildhelp/
|
||||
DocProject/Help/*.HxT
|
||||
DocProject/Help/*.HxC
|
||||
DocProject/Help/*.hhc
|
||||
DocProject/Help/*.hhk
|
||||
DocProject/Help/*.hhp
|
||||
DocProject/Help/Html2
|
||||
DocProject/Help/html
|
||||
|
||||
# Click-Once directory
|
||||
publish/
|
||||
|
||||
# Publish Web Output
|
||||
*.[Pp]ublish.xml
|
||||
*.azurePubxml
|
||||
# Note: Comment the next line if you want to checkin your web deploy settings,
|
||||
# but database connection strings (with potential passwords) will be unencrypted
|
||||
*.pubxml
|
||||
*.publishproj
|
||||
|
||||
# Microsoft Azure Web App publish settings. Comment the next line if you want to
|
||||
# checkin your Azure Web App publish settings, but sensitive information contained
|
||||
# in these scripts will be unencrypted
|
||||
PublishScripts/
|
||||
|
||||
# NuGet Packages
|
||||
*.nupkg
|
||||
# The packages folder can be ignored because of Package Restore
|
||||
**/[Pp]ackages/*
|
||||
# except build/, which is used as an MSBuild target.
|
||||
!**/[Pp]ackages/build/
|
||||
# Uncomment if necessary however generally it will be regenerated when needed
|
||||
#!**/[Pp]ackages/repositories.config
|
||||
# NuGet v3's project.json files produces more ignorable files
|
||||
*.nuget.props
|
||||
*.nuget.targets
|
||||
|
||||
# Microsoft Azure Build Output
|
||||
csx/
|
||||
*.build.csdef
|
||||
|
||||
# Microsoft Azure Emulator
|
||||
ecf/
|
||||
rcf/
|
||||
|
||||
# Windows Store app package directories and files
|
||||
AppPackages/
|
||||
BundleArtifacts/
|
||||
Package.StoreAssociation.xml
|
||||
_pkginfo.txt
|
||||
*.appx
|
||||
|
||||
# Visual Studio cache files
|
||||
# files ending in .cache can be ignored
|
||||
*.[Cc]ache
|
||||
# but keep track of directories ending in .cache
|
||||
!*.[Cc]ache/
|
||||
|
||||
# Others
|
||||
ClientBin/
|
||||
~$*
|
||||
*~
|
||||
*.dbmdl
|
||||
*.dbproj.schemaview
|
||||
*.jfm
|
||||
*.pfx
|
||||
*.publishsettings
|
||||
orleans.codegen.cs
|
||||
|
||||
# Including strong name files can present a security risk
|
||||
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
|
||||
#*.snk
|
||||
|
||||
# Since there are multiple workflows, uncomment next line to ignore bower_components
|
||||
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
|
||||
#bower_components/
|
||||
|
||||
# RIA/Silverlight projects
|
||||
Generated_Code/
|
||||
|
||||
# Backup & report files from converting an old project file
|
||||
# to a newer Visual Studio version. Backup files are not needed,
|
||||
# because we have git ;-)
|
||||
_UpgradeReport_Files/
|
||||
Backup*/
|
||||
UpgradeLog*.XML
|
||||
UpgradeLog*.htm
|
||||
ServiceFabricBackup/
|
||||
*.rptproj.bak
|
||||
|
||||
# SQL Server files
|
||||
*.mdf
|
||||
*.ldf
|
||||
*.ndf
|
||||
|
||||
# Business Intelligence projects
|
||||
*.rdl.data
|
||||
*.bim.layout
|
||||
*.bim_*.settings
|
||||
*.rptproj.rsuser
|
||||
|
||||
# Microsoft Fakes
|
||||
FakesAssemblies/
|
||||
|
||||
# GhostDoc plugin setting file
|
||||
*.GhostDoc.xml
|
||||
|
||||
# Node.js Tools for Visual Studio
|
||||
.ntvs_analysis.dat
|
||||
# comment out in distribution branches
|
||||
node_modules/
|
||||
|
||||
# Visual Studio 6 build log
|
||||
*.plg
|
||||
# Rest pulled from https://github.com/github/gitignore/blob/master/Node.gitignore
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Visual Studio 6 workspace options file
|
||||
*.opt
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
|
||||
*.vbw
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Visual Studio LightSwitch build output
|
||||
**/*.HTMLClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/ModelManifest.xml
|
||||
**/*.Server/GeneratedArtifacts
|
||||
**/*.Server/ModelManifest.xml
|
||||
_Pvt_Extensions
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Paket dependency manager
|
||||
.paket/paket.exe
|
||||
paket-files/
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# FAKE - F# Make
|
||||
.fake/
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# JetBrains Rider
|
||||
.idea/
|
||||
*.sln.iml
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# CodeRush
|
||||
.cr/
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# Python Tools for Visual Studio (PTVS)
|
||||
__pycache__/
|
||||
*.pyc
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Cake - Uncomment if you are using it
|
||||
# tools/**
|
||||
# !tools/packages.config
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Tabs Studio
|
||||
*.tss
|
||||
# Dependency directories
|
||||
jspm_packages/
|
||||
|
||||
# Telerik's JustMock configuration file
|
||||
*.jmconfig
|
||||
# TypeScript v1 declaration files
|
||||
typings/
|
||||
|
||||
# BizTalk build output
|
||||
*.btp.cs
|
||||
*.btm.cs
|
||||
*.odx.cs
|
||||
*.xsd.cs
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# OpenCover UI analysis results
|
||||
OpenCover/
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Azure Stream Analytics local run output
|
||||
ASALocalRun/
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# MSBuild Binary and Structured Log
|
||||
*.binlog
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# NVidia Nsight GPU debugger configuration file
|
||||
*.nvuser
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# MFractors (Xamarin productivity tool) working folder
|
||||
.mfractor/
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
.env.test
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
|
||||
# next.js build output
|
||||
.next
|
||||
|
||||
# nuxt.js build output
|
||||
.nuxt
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
name: 'Azure MYSQL Deploy'
|
||||
description: 'Deploy to Azure MySQL database using SQL script files'
|
||||
inputs:
|
||||
server-name:
|
||||
description: 'Server name of Azure DB for Mysql. Example: fabrikam.mysql.database.azure.com. When you connect using Mysql Workbench, this is the same value that is used for Hostname in Parameters'
|
||||
required: true
|
||||
connection-string:
|
||||
description: 'The connection string, including authentication information, for the Azure MySQL Server.'
|
||||
required: true
|
||||
sql-file:
|
||||
description: 'Path to SQL script file. *.sql or a folder to deploy'
|
||||
required: true
|
||||
arguments:
|
||||
description: 'Additional options supported by mysql simple SQL shell. These options will be applied when executing the given file on the Azure DB for Mysql.'
|
||||
runs:
|
||||
using: 'node12'
|
||||
main: 'lib/main.js'
|
|
@ -0,0 +1,11 @@
|
|||
module.exports = {
|
||||
clearMocks: true,
|
||||
moduleFileExtensions: ['js', 'ts'],
|
||||
testEnvironment: 'node',
|
||||
testMatch: ['**/*.test.ts'],
|
||||
testRunner: 'jest-circus/runner',
|
||||
transform: {
|
||||
'^.+\\.ts$': 'ts-jest'
|
||||
},
|
||||
verbose: true
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
"use strict";
|
||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
var __importStar = (this && this.__importStar) || function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
|
||||
result["default"] = mod;
|
||||
return result;
|
||||
};
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const core = __importStar(require("@actions/core"));
|
||||
const exec = __importStar(require("@actions/exec"));
|
||||
const AzureMySqlActionHelper_1 = __importDefault(require("./AzureMySqlActionHelper"));
|
||||
class AzureMySqlAction {
|
||||
constructor(inputs) {
|
||||
this._inputs = inputs;
|
||||
}
|
||||
execute() {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
core.debug('Begin executing action...');
|
||||
let mySqlClientPath = yield AzureMySqlActionHelper_1.default.getMySqlClientPath();
|
||||
yield exec.exec(`"${mySqlClientPath}" -h ${this._inputs.serverName} -D ${this._inputs.connectionString.database} -u ${this._inputs.connectionString.userId} --password="${this._inputs.connectionString.password}" ${this._inputs.additionalArguments} -e "source ${this._inputs.sqlFile}"`);
|
||||
console.log('Successfully executed sql file on target database');
|
||||
});
|
||||
}
|
||||
}
|
||||
exports.default = AzureMySqlAction;
|
|
@ -0,0 +1,142 @@
|
|||
"use strict";
|
||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
var __importStar = (this && this.__importStar) || function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
|
||||
result["default"] = mod;
|
||||
return result;
|
||||
};
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const core = __importStar(require("@actions/core"));
|
||||
const io = __importStar(require("@actions/io"));
|
||||
const os = __importStar(require("os"));
|
||||
const path = __importStar(require("path"));
|
||||
const fs = __importStar(require("fs"));
|
||||
const glob = __importStar(require("glob"));
|
||||
const winreg_1 = __importDefault(require("winreg"));
|
||||
const IS_WINDOWS = process.platform === 'win32';
|
||||
class AzureMySqlActionHelper {
|
||||
static getMySqlClientPath() {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
core.debug(`Getting location of MySql client on ${os.hostname()}`);
|
||||
if (IS_WINDOWS) {
|
||||
return this._getMySqlClientOnWindows();
|
||||
}
|
||||
else {
|
||||
return this._getMySqlClientOnLinux();
|
||||
}
|
||||
});
|
||||
}
|
||||
static resolveFilePath(filePathPattern) {
|
||||
let filePath = filePathPattern;
|
||||
if (glob.hasMagic(filePathPattern)) {
|
||||
let matchedFiles = glob.sync(filePathPattern);
|
||||
if (matchedFiles.length === 0) {
|
||||
throw new Error(`No files found matching pattern ${filePathPattern}`);
|
||||
}
|
||||
if (matchedFiles.length > 1) {
|
||||
throw new Error(`Muliple files found matching pattern ${filePathPattern}`);
|
||||
}
|
||||
filePath = matchedFiles[0];
|
||||
}
|
||||
if (!fs.existsSync(filePath)) {
|
||||
throw new Error(`Unable to find file at location: ${filePath}`);
|
||||
}
|
||||
return filePath;
|
||||
}
|
||||
static getRegistrySubKeys(path) {
|
||||
return new Promise((resolve) => {
|
||||
core.debug(`Getting sub-keys at registry path: HKLM:${path}`);
|
||||
let regKey = new winreg_1.default({
|
||||
hive: winreg_1.default.HKLM,
|
||||
key: path
|
||||
});
|
||||
regKey.keys((error, result) => {
|
||||
return !!error ? '' : resolve(result);
|
||||
});
|
||||
});
|
||||
}
|
||||
static getRegistryValue(registryKey, name) {
|
||||
return new Promise((resolve) => {
|
||||
core.debug(`Getting registry value ${name} at path: HKLM:${registryKey.key}`);
|
||||
registryKey.get(name, (error, result) => {
|
||||
resolve(!!error ? '' : result.value);
|
||||
});
|
||||
});
|
||||
}
|
||||
static registryKeyExists(path) {
|
||||
core.debug(`Checking if registry key 'HKLM:${path}' exists.`);
|
||||
return new Promise((resolve) => {
|
||||
let regKey = new winreg_1.default({
|
||||
hive: winreg_1.default.HKLM,
|
||||
key: path
|
||||
});
|
||||
regKey.keyExists((error, result) => {
|
||||
resolve(!!error ? false : result);
|
||||
});
|
||||
});
|
||||
}
|
||||
static _getMySqlClientOnWindows() {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
let mySqlClientRegistryKey = path.join('\\', 'Software', 'MySQL AB');
|
||||
let mySqlClientRegistryKeyWow6432 = path.join('\\', 'Software', 'Wow6432Node', 'MySQL AB');
|
||||
let mySqlClientPath = '';
|
||||
if (yield AzureMySqlActionHelper.registryKeyExists(mySqlClientRegistryKey)) {
|
||||
mySqlClientPath = yield this._getMySqlClientPathFromRegistry(mySqlClientRegistryKey);
|
||||
}
|
||||
if (!mySqlClientPath && (yield AzureMySqlActionHelper.registryKeyExists(mySqlClientRegistryKeyWow6432))) {
|
||||
mySqlClientPath = yield this._getMySqlClientPathFromRegistry(mySqlClientRegistryKeyWow6432);
|
||||
}
|
||||
if (!mySqlClientPath) {
|
||||
core.debug(`Unable to find mysql client executable on ${os.hostname()} from registry.`);
|
||||
core.debug(`Getting location of mysql.exe from PATH environment variable.`);
|
||||
mySqlClientPath = yield io.which('mysql', false);
|
||||
}
|
||||
if (mySqlClientPath) {
|
||||
core.debug(`MySql client found at path ${mySqlClientPath}`);
|
||||
return mySqlClientPath;
|
||||
}
|
||||
else {
|
||||
throw new Error(`Unable to find mysql client executable on ${os.hostname()}.`);
|
||||
}
|
||||
});
|
||||
}
|
||||
static _getMySqlClientPathFromRegistry(registryPath) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
core.debug(`Getting location of mysql.exe from registryPath HKLM:${registryPath}`);
|
||||
let registrySubKeys = yield AzureMySqlActionHelper.getRegistrySubKeys(registryPath);
|
||||
for (let registryKey of registrySubKeys) {
|
||||
if (registryKey.key.match('MySQL Server')) {
|
||||
let mySqlServerPath = yield AzureMySqlActionHelper.getRegistryValue(registryKey, 'Location');
|
||||
if (!!mySqlServerPath) {
|
||||
let mySqlClientExecutablePath = path.join(mySqlServerPath, 'bin', 'mysql.exe');
|
||||
if (fs.existsSync(mySqlClientExecutablePath)) {
|
||||
core.debug(`MySQL client executable found at path ${mySqlClientExecutablePath}`);
|
||||
return mySqlClientExecutablePath;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return '';
|
||||
});
|
||||
}
|
||||
static _getMySqlClientOnLinux() {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
let mySqlClientPath = yield io.which('mysql', true);
|
||||
core.debug(`MySQL client found at path ${mySqlClientPath}`);
|
||||
return mySqlClientPath;
|
||||
});
|
||||
}
|
||||
}
|
||||
exports.default = AzureMySqlActionHelper;
|
|
@ -0,0 +1,191 @@
|
|||
"use strict";
|
||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
var __importStar = (this && this.__importStar) || function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
|
||||
result["default"] = mod;
|
||||
return result;
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const core = __importStar(require("@actions/core"));
|
||||
const AzureRestClient_1 = require("azure-actions-webclient/AzureRestClient");
|
||||
class AzureMySqlResourceManager {
|
||||
constructor(resourceAuthorizer) {
|
||||
// making the constructor private, so that object initialization can only be done by the class factory GetResourceManager
|
||||
this._restClient = new AzureRestClient_1.ServiceClient(resourceAuthorizer);
|
||||
}
|
||||
static getResourceManager(serverName, resourceAuthorizer) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
// a factory method to return asynchronously created object
|
||||
let resourceManager = new AzureMySqlResourceManager(resourceAuthorizer);
|
||||
yield resourceManager._populateMySqlServerData(serverName);
|
||||
return resourceManager;
|
||||
});
|
||||
}
|
||||
getMySqlServer() {
|
||||
return this._resource;
|
||||
}
|
||||
addFirewallRule(startIpAddress, endIpAddress) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
let firewallRuleName = `ClientIPAddress_${Date.now()}`;
|
||||
let httpRequest = {
|
||||
method: 'PUT',
|
||||
uri: this._restClient.getRequestUri(`/${this._resource.id}/firewallRules/${firewallRuleName}`, {}, [], '2017-12-01'),
|
||||
body: JSON.stringify({
|
||||
'properties': {
|
||||
'startIpAddress': startIpAddress,
|
||||
'endIpAddress': endIpAddress
|
||||
}
|
||||
})
|
||||
};
|
||||
try {
|
||||
let httpResponse = yield this._restClient.beginRequest(httpRequest);
|
||||
if (httpResponse.statusCode === 202) {
|
||||
let asyncOperationResponse = yield this._getLongRunningOperationResult(httpResponse);
|
||||
if (asyncOperationResponse.statusCode === 200 && asyncOperationResponse.body['status'] && asyncOperationResponse.body['status'].toLowerCase() === 'succeeded') {
|
||||
core.debug(JSON.stringify(asyncOperationResponse.body));
|
||||
return this.getFirewallRule(firewallRuleName);
|
||||
}
|
||||
else {
|
||||
throw AzureRestClient_1.ToError(asyncOperationResponse);
|
||||
}
|
||||
}
|
||||
else if (httpResponse.statusCode === 200 || httpResponse.statusCode === 201) {
|
||||
return httpResponse.body;
|
||||
}
|
||||
throw AzureRestClient_1.ToError(httpResponse);
|
||||
}
|
||||
catch (error) {
|
||||
if (error instanceof AzureRestClient_1.AzureError) {
|
||||
throw new Error(JSON.stringify(error));
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
}
|
||||
removeFirewallRule(firewallRule) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
let httpRequest = {
|
||||
method: 'DELETE',
|
||||
uri: this._restClient.getRequestUri(`/${this._resource.id}/firewallRules/${firewallRule.name}`, {}, [], '2017-12-01')
|
||||
};
|
||||
try {
|
||||
let httpResponse = yield this._restClient.beginRequest(httpRequest);
|
||||
if (httpResponse.statusCode === 202) {
|
||||
let asyncOperationResponse = yield this._getLongRunningOperationResult(httpResponse);
|
||||
if (asyncOperationResponse.statusCode === 200 && asyncOperationResponse.body['status'] && asyncOperationResponse.body['status'].toLowerCase() === 'succeeded') {
|
||||
core.debug(JSON.stringify(asyncOperationResponse.body));
|
||||
}
|
||||
else {
|
||||
throw AzureRestClient_1.ToError(asyncOperationResponse);
|
||||
}
|
||||
}
|
||||
else if (httpResponse.statusCode !== 200 && httpResponse.statusCode !== 204) {
|
||||
throw AzureRestClient_1.ToError(httpResponse);
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
if (error instanceof AzureRestClient_1.AzureError) {
|
||||
throw new Error(JSON.stringify(error));
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
}
|
||||
getFirewallRule(ruleName) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
let httpRequest = {
|
||||
method: 'GET',
|
||||
uri: this._restClient.getRequestUri(`/${this._resource.id}/firewallRules/${ruleName}`, {}, [], '2017-12-01')
|
||||
};
|
||||
try {
|
||||
let httpResponse = yield this._restClient.beginRequest(httpRequest);
|
||||
if (httpResponse.statusCode !== 200) {
|
||||
throw AzureRestClient_1.ToError(httpResponse);
|
||||
}
|
||||
return httpResponse.body;
|
||||
}
|
||||
catch (error) {
|
||||
if (error instanceof AzureRestClient_1.AzureError) {
|
||||
throw new Error(JSON.stringify(error));
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
}
|
||||
_getLongRunningOperationResult(response) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
let timeoutInMinutes = 2;
|
||||
let timeout = new Date().getTime() + timeoutInMinutes * 60 * 1000;
|
||||
let request = {
|
||||
method: 'GET',
|
||||
uri: response.headers['azure-asyncoperation'] || response.headers['location']
|
||||
};
|
||||
if (!request.uri) {
|
||||
throw new Error('Unable to find the Azure-Async operation polling URI.');
|
||||
}
|
||||
while (true) {
|
||||
response = yield this._restClient.beginRequest(request);
|
||||
if (response.statusCode === 202 || (response.body && (response.body.status == 'Accepted' || response.body.status == 'Running' || response.body.status == 'InProgress'))) {
|
||||
if (timeout < new Date().getTime()) {
|
||||
throw new Error(`Async polling request timed out. URI: ${request.uri}`);
|
||||
}
|
||||
let retryAfterInterval = response.headers['retry-after'] && parseInt(response.headers['retry-after']) || 15;
|
||||
yield this._sleep(retryAfterInterval);
|
||||
}
|
||||
else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return response;
|
||||
});
|
||||
}
|
||||
_populateMySqlServerData(serverName) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
// trim the cloud hostname suffix from servername
|
||||
serverName = serverName.split('.')[0];
|
||||
let httpRequest = {
|
||||
method: 'GET',
|
||||
uri: this._restClient.getRequestUri('//subscriptions/{subscriptionId}/providers/Microsoft.DBforMySQL/servers', {}, [], '2017-12-01')
|
||||
};
|
||||
core.debug(`Get MySQL server '${serverName}' details`);
|
||||
try {
|
||||
let httpResponse = yield this._restClient.beginRequest(httpRequest);
|
||||
if (httpResponse.statusCode !== 200) {
|
||||
throw AzureRestClient_1.ToError(httpResponse);
|
||||
}
|
||||
let sqlServers = httpResponse.body && httpResponse.body.value;
|
||||
if (sqlServers && sqlServers.length > 0) {
|
||||
this._resource = sqlServers.filter((sqlResource) => sqlResource.name === serverName)[0];
|
||||
if (!this._resource) {
|
||||
throw new Error(`Unable to get details of MySQL server ${serverName}. MySql server '${serverName}' was not found in the subscription.`);
|
||||
}
|
||||
core.debug(JSON.stringify(this._resource));
|
||||
}
|
||||
else {
|
||||
throw new Error(`Unable to get details of MySQL server ${serverName}. No MySQL servers were found in the subscription.`);
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
if (error instanceof AzureRestClient_1.AzureError) {
|
||||
throw new Error(JSON.stringify(error));
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
}
|
||||
_sleep(sleepDurationInSeconds) {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, sleepDurationInSeconds * 1000);
|
||||
});
|
||||
}
|
||||
}
|
||||
exports.default = AzureMySqlResourceManager;
|
|
@ -0,0 +1,84 @@
|
|||
"use strict";
|
||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
var __importStar = (this && this.__importStar) || function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
|
||||
result["default"] = mod;
|
||||
return result;
|
||||
};
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const core = __importStar(require("@actions/core"));
|
||||
const exec = __importStar(require("@actions/exec"));
|
||||
const AzureMySqlActionHelper_1 = __importDefault(require("./AzureMySqlActionHelper"));
|
||||
const ipv4MatchPattern = /\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/;
|
||||
class FirewallManager {
|
||||
constructor(azureMySqlResourceManager) {
|
||||
this._resourceManager = azureMySqlResourceManager;
|
||||
}
|
||||
addFirewallRule(serverName, connectionString) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
let ipAddress = yield this._detectIPAddress(serverName, connectionString);
|
||||
if (!ipAddress) {
|
||||
core.debug(`Client has access to MySql server. Skip adding firewall exception.`);
|
||||
return;
|
||||
}
|
||||
console.log(`Client does not have access to MySql server. Adding firewall exception for client's IP address.`);
|
||||
this._firewallRule = yield this._resourceManager.addFirewallRule(ipAddress, ipAddress);
|
||||
core.debug(JSON.stringify(this._firewallRule));
|
||||
console.log(`Successfully added firewall rule ${this._firewallRule.name}.`);
|
||||
});
|
||||
}
|
||||
removeFirewallRule() {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
if (this._firewallRule) {
|
||||
console.log(`Removing firewall rule '${this._firewallRule.name}'.`);
|
||||
yield this._resourceManager.removeFirewallRule(this._firewallRule);
|
||||
console.log('Successfully removed firewall rule.');
|
||||
}
|
||||
else {
|
||||
core.debug('No firewall exception was added.');
|
||||
}
|
||||
});
|
||||
}
|
||||
_detectIPAddress(serverName, connectionString) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
let mySqlClientPath = yield AzureMySqlActionHelper_1.default.getMySqlClientPath();
|
||||
let ipAddress = '';
|
||||
let mySqlError = '';
|
||||
try {
|
||||
core.debug(`Validating if client has access to MySql Server '${serverName}'.`);
|
||||
core.debug(`"${mySqlClientPath}" -h ${serverName} -u "${connectionString.userId}" -e "show databases"`);
|
||||
yield exec.exec(`"${mySqlClientPath}" -h ${serverName} -u "${connectionString.userId}" --password="${connectionString.password}" -e "show databases"`, [], {
|
||||
silent: true,
|
||||
listeners: {
|
||||
stderr: (data) => mySqlError += data.toString()
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
core.debug(mySqlError);
|
||||
let ipAddresses = mySqlError.match(ipv4MatchPattern);
|
||||
if (!!ipAddresses) {
|
||||
ipAddress = ipAddresses[0];
|
||||
}
|
||||
else {
|
||||
throw new Error(`Failed to add firewall rule. Unable to detect client IP Address. ${mySqlError} ${error}`);
|
||||
}
|
||||
}
|
||||
//ipAddress will be an empty string if client has access to SQL server
|
||||
return ipAddress;
|
||||
});
|
||||
}
|
||||
}
|
||||
exports.default = FirewallManager;
|
|
@ -0,0 +1,107 @@
|
|||
"use strict";
|
||||
var __importStar = (this && this.__importStar) || function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
|
||||
result["default"] = mod;
|
||||
return result;
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
/**
|
||||
* The basic format of a connection string includes a series of keyword/value pairs separated by semicolons.
|
||||
* The equal sign (=) connects each keyword and its value. (Ex: Key1=Val1;Key2=Val2)
|
||||
*
|
||||
* Following rules are to be followed while passing special characters in values:
|
||||
1. To include values that contain a semicolon, single-quote character, or double-quote character, the value must be enclosed in double quotation marks.
|
||||
2. If the value contains both a semicolon and a double-quote character, the value can be enclosed in single quotation marks.
|
||||
3. The single quotation mark is also useful if the value starts with a double-quote character. Conversely, the double quotation mark can be used if the value starts with a single quotation mark.
|
||||
4. If the value contains both single-quote and double-quote characters, the quotation mark character used to enclose the value must be doubled every time it occurs within the value.
|
||||
|
||||
Regex used by the parser(connectionStringParserRegex) to parse the VALUE:
|
||||
|
||||
('[^']*(''[^']*)*') -> value enclosed with single quotes and has consecutive single quotes
|
||||
|("[^"]*(""[^"]*)*") -> value enclosed with double quotes and has consecutive double quotes
|
||||
|((?!['"])[^;]*)) -> value does not start with quotes does not contain any special character. Here we do a positive lookahead to ensure that the value doesn't start with quotes which should have been handled in previous cases
|
||||
Regex used to validate the entire connection string:
|
||||
|
||||
A connection string is considered valid if it is a series of key/value pairs separated by semicolons. Each key/value pair must satisy the connectionStringParserRegex to ensure it is a valid key/value pair.
|
||||
^[;\s]*{KeyValueRegex}(;[;\s]*{KeyValueRegex})*[;\s]*$
|
||||
where KeyValueRegex = ([\w\s]+=(?:('[^']*(''[^']*)*')|("[^"]*(""[^"]*)*")|((?!['"])[^;]*))))
|
||||
*/
|
||||
const core = __importStar(require("@actions/core"));
|
||||
const connectionStringParserRegex = /(?<key>[\w\s]+)=(?<val>('[^']*(''[^']*)*')|("[^"]*(""[^"]*)*")|((?!['"])[^;]*))/g;
|
||||
const connectionStringTester = /^[;\s]*([\w\s]+=(?:('[^']*(''[^']*)*')|("[^"]*(""[^"]*)*")|((?!['"])[^;]*)))(;[;\s]*([\w\s]+=(?:('[^']*(''[^']*)*')|("[^"]*(""[^"]*)*")|((?!['"])[^;]*))))*[;\s]*$/;
|
||||
class MySqlConnectionStringBuilder {
|
||||
constructor(connectionString) {
|
||||
this._connectionString = '';
|
||||
this._connectionString = connectionString;
|
||||
this._validateConnectionString();
|
||||
this._parsedConnectionString = this._parseConnectionString();
|
||||
}
|
||||
get connectionString() {
|
||||
return this._connectionString;
|
||||
}
|
||||
get userId() {
|
||||
return this._parsedConnectionString.userId;
|
||||
}
|
||||
get password() {
|
||||
return this._parsedConnectionString.password;
|
||||
}
|
||||
get database() {
|
||||
return this._parsedConnectionString.database;
|
||||
}
|
||||
_validateConnectionString() {
|
||||
if (!connectionStringTester.test(this._connectionString)) {
|
||||
throw new Error('Invalid connection string. A valid connection string is a series of keyword/value pairs separated by semi-colons. If there are any special characters like quotes, semi-colons in the keyword value, enclose the value within quotes. Refer this link for more info on conneciton string https://aka.ms/sqlconnectionstring');
|
||||
}
|
||||
}
|
||||
_parseConnectionString() {
|
||||
let result = this._connectionString.matchAll(connectionStringParserRegex);
|
||||
let parsedConnectionString = {};
|
||||
for (let match of result) {
|
||||
if (match.groups) {
|
||||
let key = match.groups.key.trim();
|
||||
let val = match.groups.val.trim();
|
||||
/**
|
||||
* If the first character of val is a single/double quote and there are two consecutive single/double quotes in between,
|
||||
* convert the consecutive single/double quote characters into one single/double quote character respectively (Point no. 4 above)
|
||||
*/
|
||||
if (val[0] === "'") {
|
||||
val = val.slice(1, -1);
|
||||
val = val.replace(/''/g, "'");
|
||||
}
|
||||
else if (val[0] === '"') {
|
||||
val = val.slice(1, -1);
|
||||
val = val.replace(/""/g, '"');
|
||||
}
|
||||
switch (key.toLowerCase()) {
|
||||
case 'user id':
|
||||
case 'uid': {
|
||||
parsedConnectionString.userId = val;
|
||||
break;
|
||||
}
|
||||
case 'password':
|
||||
case 'pwd': {
|
||||
parsedConnectionString.password = val;
|
||||
// masking the connection string password to prevent logging to console
|
||||
core.setSecret(val);
|
||||
break;
|
||||
}
|
||||
case 'database': {
|
||||
parsedConnectionString.database = val;
|
||||
break;
|
||||
}
|
||||
case 'server': {
|
||||
parsedConnectionString.server = val;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!parsedConnectionString.userId || !parsedConnectionString.password || !parsedConnectionString.database) {
|
||||
throw new Error(`Missing required keys in connection string. Please ensure that the keys 'User Id', 'Password', 'Database' are provided in the connection string.`);
|
||||
}
|
||||
return parsedConnectionString;
|
||||
}
|
||||
}
|
||||
exports.default = MySqlConnectionStringBuilder;
|
|
@ -0,0 +1,77 @@
|
|||
"use strict";
|
||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
var __importStar = (this && this.__importStar) || function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
|
||||
result["default"] = mod;
|
||||
return result;
|
||||
};
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const core = __importStar(require("@actions/core"));
|
||||
const crypto = __importStar(require("crypto"));
|
||||
const path = __importStar(require("path"));
|
||||
const AuthorizerFactory_1 = require("azure-actions-webclient/AuthorizerFactory");
|
||||
const AzureMySqlActionHelper_1 = __importDefault(require("./AzureMySqlActionHelper"));
|
||||
const AzureMySqlAction_1 = __importDefault(require("./AzureMySqlAction"));
|
||||
const FirewallManager_1 = __importDefault(require("./FirewallManager"));
|
||||
const AzureMySqlResourceManager_1 = __importDefault(require("./AzureMySqlResourceManager"));
|
||||
const MySqlConnectionStringBuilder_1 = __importDefault(require("./MySqlConnectionStringBuilder"));
|
||||
let userAgentPrefix = !!process.env.AZURE_HTTP_USER_AGENT ? `${process.env.AZURE_HTTP_USER_AGENT}` : "";
|
||||
function run() {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
let firewallManager;
|
||||
try {
|
||||
// Set user agent variable
|
||||
let usrAgentRepo = crypto.createHash('sha256').update(`${process.env.GITHUB_REPOSITORY}`).digest('hex');
|
||||
let actionName = 'AzureMySqlAction';
|
||||
let userAgentString = (!!userAgentPrefix ? `${userAgentPrefix}+` : '') + `GITHUBACTIONS_${actionName}_${usrAgentRepo}`;
|
||||
core.exportVariable('AZURE_HTTP_USER_AGENT', userAgentString);
|
||||
let inputs = getInputs();
|
||||
let azureMySqlAction = new AzureMySqlAction_1.default(inputs);
|
||||
let azureResourceAuthorizer = yield AuthorizerFactory_1.AuthorizerFactory.getAuthorizer();
|
||||
let azureMySqlResourceManager = yield AzureMySqlResourceManager_1.default.getResourceManager(inputs.serverName, azureResourceAuthorizer);
|
||||
firewallManager = new FirewallManager_1.default(azureMySqlResourceManager);
|
||||
yield firewallManager.addFirewallRule(inputs.serverName, inputs.connectionString);
|
||||
yield azureMySqlAction.execute();
|
||||
}
|
||||
catch (error) {
|
||||
core.setFailed(error.message);
|
||||
}
|
||||
finally {
|
||||
if (firewallManager) {
|
||||
yield firewallManager.removeFirewallRule();
|
||||
}
|
||||
// Reset AZURE_HTTP_USER_AGENT
|
||||
core.exportVariable('AZURE_HTTP_USER_AGENT', userAgentPrefix);
|
||||
}
|
||||
});
|
||||
}
|
||||
exports.run = run;
|
||||
function getInputs() {
|
||||
let serverName = core.getInput('server-name', { required: true });
|
||||
let connectionString = core.getInput('connection-string', { required: true });
|
||||
let connectionStringBuilder = new MySqlConnectionStringBuilder_1.default(connectionString);
|
||||
let sqlFile = AzureMySqlActionHelper_1.default.resolveFilePath(core.getInput('sql-file', { required: true }));
|
||||
if (path.extname(sqlFile).toLowerCase() !== '.sql') {
|
||||
throw new Error(`Invalid sql file path provided as input ${sqlFile}`);
|
||||
}
|
||||
let additionalArguments = core.getInput('arguments');
|
||||
return {
|
||||
serverName: serverName,
|
||||
connectionString: connectionStringBuilder,
|
||||
sqlFile: sqlFile,
|
||||
additionalArguments: additionalArguments
|
||||
};
|
||||
}
|
||||
run();
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -0,0 +1,40 @@
|
|||
{
|
||||
"name": "azure-mysql-action",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"description": "Azure MySql Action",
|
||||
"main": "lib/main.js",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"test": "jest"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/Azure/mysql-action.git"
|
||||
},
|
||||
"keywords": [
|
||||
"actions",
|
||||
"node",
|
||||
"setup"
|
||||
],
|
||||
"author": "Microsoft",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.2.0",
|
||||
"@actions/exec": "^1.0.1",
|
||||
"@actions/io": "^1.0.1",
|
||||
"azure-actions-webclient": "^1.0.4",
|
||||
"glob": "^7.1.5",
|
||||
"winreg": "^1.2.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/glob": "^7.1.1",
|
||||
"@types/jest": "^24.0.13",
|
||||
"@types/node": "^12.0.4",
|
||||
"@types/winreg": "^1.2.30",
|
||||
"jest": "^24.8.0",
|
||||
"jest-circus": "^24.7.1",
|
||||
"ts-jest": "^24.0.2",
|
||||
"typescript": "^3.5.1"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
import * as core from '@actions/core';
|
||||
import * as exec from '@actions/exec';
|
||||
import AzureMySqlActionHelper from './AzureMySqlActionHelper';
|
||||
import MySqlConnectionStringBuilder from './MySqlConnectionStringBuilder';
|
||||
|
||||
export interface IActionInputs {
|
||||
serverName: string;
|
||||
connectionString: MySqlConnectionStringBuilder;
|
||||
sqlFile: string;
|
||||
additionalArguments: string;
|
||||
}
|
||||
|
||||
export default class AzureMySqlAction {
|
||||
constructor(inputs: IActionInputs) {
|
||||
this._inputs = inputs;
|
||||
}
|
||||
|
||||
public async execute() {
|
||||
core.debug('Begin executing action...');
|
||||
|
||||
let mySqlClientPath = await AzureMySqlActionHelper.getMySqlClientPath();
|
||||
await exec.exec(`"${mySqlClientPath}" -h ${this._inputs.serverName} -D ${this._inputs.connectionString.database} -u ${this._inputs.connectionString.userId} --password='${this._inputs.connectionString.password}' ${this._inputs.additionalArguments} -e "source ${this._inputs.sqlFile}"`)
|
||||
|
||||
console.log('Successfully executed sql file on target database');
|
||||
}
|
||||
|
||||
private _inputs: IActionInputs;
|
||||
}
|
|
@ -0,0 +1,134 @@
|
|||
import * as core from '@actions/core';
|
||||
import * as io from '@actions/io';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import * as glob from 'glob';
|
||||
import winreg from 'winreg';
|
||||
|
||||
const IS_WINDOWS = process.platform === 'win32';
|
||||
|
||||
export default class AzureMySqlActionHelper {
|
||||
|
||||
public static async getMySqlClientPath(): Promise<string> {
|
||||
core.debug(`Getting location of MySql client on ${os.hostname()}`);
|
||||
if (IS_WINDOWS) {
|
||||
return this._getMySqlClientOnWindows();
|
||||
}
|
||||
else {
|
||||
return this._getMySqlClientOnLinux();
|
||||
}
|
||||
}
|
||||
|
||||
public static resolveFilePath(filePathPattern: string): string {
|
||||
let filePath = filePathPattern;
|
||||
if (glob.hasMagic(filePathPattern)) {
|
||||
let matchedFiles: string[] = glob.sync(filePathPattern);
|
||||
if (matchedFiles.length === 0) {
|
||||
throw new Error(`No files found matching pattern ${filePathPattern}`);
|
||||
}
|
||||
|
||||
if (matchedFiles.length > 1) {
|
||||
throw new Error(`Muliple files found matching pattern ${filePathPattern}`);
|
||||
}
|
||||
|
||||
filePath = matchedFiles[0];
|
||||
}
|
||||
|
||||
if (!fs.existsSync(filePath)) {
|
||||
throw new Error(`Unable to find file at location: ${filePath}`);
|
||||
}
|
||||
|
||||
return filePath;
|
||||
}
|
||||
|
||||
public static getRegistrySubKeys(path: string): Promise<winreg.Registry[]> {
|
||||
return new Promise((resolve) => {
|
||||
core.debug(`Getting sub-keys at registry path: HKLM:${path}`);
|
||||
let regKey = new winreg({
|
||||
hive: winreg.HKLM,
|
||||
key: path
|
||||
});
|
||||
|
||||
regKey.keys((error, result) => {
|
||||
return !!error ? '' : resolve(result);
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
public static getRegistryValue(registryKey: winreg.Registry, name: string): Promise<string> {
|
||||
return new Promise((resolve) => {
|
||||
core.debug(`Getting registry value ${name} at path: HKLM:${registryKey.key}`);
|
||||
registryKey.get(name, (error, result: winreg.RegistryItem) => {
|
||||
resolve(!!error ? '' : result.value);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public static registryKeyExists(path: string): Promise<boolean> {
|
||||
core.debug(`Checking if registry key 'HKLM:${path}' exists.`);
|
||||
return new Promise((resolve) => {
|
||||
let regKey = new winreg({
|
||||
hive: winreg.HKLM,
|
||||
key: path
|
||||
});
|
||||
|
||||
regKey.keyExists((error, result: boolean) => {
|
||||
resolve(!!error ? false : result);
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
private static async _getMySqlClientOnWindows(): Promise<string> {
|
||||
let mySqlClientRegistryKey = path.join('\\', 'Software', 'MySQL AB');
|
||||
let mySqlClientRegistryKeyWow6432 = path.join('\\', 'Software', 'Wow6432Node', 'MySQL AB');
|
||||
let mySqlClientPath = '';
|
||||
|
||||
if (await AzureMySqlActionHelper.registryKeyExists(mySqlClientRegistryKey)) {
|
||||
mySqlClientPath = await this._getMySqlClientPathFromRegistry(mySqlClientRegistryKey);
|
||||
}
|
||||
|
||||
if (!mySqlClientPath && await AzureMySqlActionHelper.registryKeyExists(mySqlClientRegistryKeyWow6432)) {
|
||||
mySqlClientPath = await this._getMySqlClientPathFromRegistry(mySqlClientRegistryKeyWow6432);
|
||||
}
|
||||
|
||||
if (!mySqlClientPath) {
|
||||
core.debug(`Unable to find mysql client executable on ${os.hostname()} from registry.`);
|
||||
core.debug(`Getting location of mysql.exe from PATH environment variable.`);
|
||||
mySqlClientPath = await io.which('mysql', false);
|
||||
}
|
||||
|
||||
if (mySqlClientPath) {
|
||||
core.debug(`MySql client found at path ${mySqlClientPath}`);
|
||||
return mySqlClientPath;
|
||||
}
|
||||
else {
|
||||
throw new Error(`Unable to find mysql client executable on ${os.hostname()}.`);
|
||||
}
|
||||
}
|
||||
|
||||
private static async _getMySqlClientPathFromRegistry(registryPath: string): Promise<string> {
|
||||
core.debug(`Getting location of mysql.exe from registryPath HKLM:${registryPath}`);
|
||||
let registrySubKeys = await AzureMySqlActionHelper.getRegistrySubKeys(registryPath);
|
||||
for (let registryKey of registrySubKeys) {
|
||||
if (registryKey.key.match('MySQL Server')) {
|
||||
let mySqlServerPath = await AzureMySqlActionHelper.getRegistryValue(registryKey, 'Location');
|
||||
if (!!mySqlServerPath) {
|
||||
let mySqlClientExecutablePath = path.join(mySqlServerPath, 'bin', 'mysql.exe');
|
||||
if (fs.existsSync(mySqlClientExecutablePath)) {
|
||||
core.debug(`MySQL client executable found at path ${mySqlClientExecutablePath}`);
|
||||
return mySqlClientExecutablePath;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
private static async _getMySqlClientOnLinux(): Promise<string> {
|
||||
let mySqlClientPath = await io.which('mysql', true);
|
||||
core.debug(`MySQL client found at path ${mySqlClientPath}`);
|
||||
return mySqlClientPath;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,233 @@
|
|||
import * as core from '@actions/core';
|
||||
import { IAuthorizer } from 'azure-actions-webclient/Authorizer/IAuthorizer';
|
||||
import { WebRequest, WebResponse } from 'azure-actions-webclient/WebClient';
|
||||
import { ServiceClient as AzureRestClient, ToError, AzureError } from 'azure-actions-webclient/AzureRestClient';
|
||||
|
||||
export interface AzureMySqlServer {
|
||||
id: string;
|
||||
location: string;
|
||||
name: string;
|
||||
properties: {
|
||||
administratorLogin: string;
|
||||
earliestRestoreDate: string;
|
||||
fullyQualifiedDomainName: string;
|
||||
masterServerId: string;
|
||||
replicaCapacity: number;
|
||||
replicationRole: string;
|
||||
storageProfile: {
|
||||
backupRetentionDays: string;
|
||||
geoRedundantBackup: string;
|
||||
storageAutogrow: string;
|
||||
storageMB: number;
|
||||
};
|
||||
userVisibleState: string;
|
||||
sslEnforcement: string;
|
||||
version: string;
|
||||
};
|
||||
type: string;
|
||||
sku: {
|
||||
capacity: number;
|
||||
family: string;
|
||||
name: string;
|
||||
size: string;
|
||||
tier: string;
|
||||
}
|
||||
}
|
||||
|
||||
export interface FirewallRule {
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
properties: {
|
||||
startIpAddress: string;
|
||||
endIpAddress: string;
|
||||
}
|
||||
}
|
||||
|
||||
export default class AzureMySqlResourceManager {
|
||||
private constructor(resourceAuthorizer: IAuthorizer) {
|
||||
// making the constructor private, so that object initialization can only be done by the class factory GetResourceManager
|
||||
this._restClient = new AzureRestClient(resourceAuthorizer);
|
||||
}
|
||||
|
||||
public static async getResourceManager(serverName: string, resourceAuthorizer: IAuthorizer): Promise<AzureMySqlResourceManager> {
|
||||
// a factory method to return asynchronously created object
|
||||
let resourceManager = new AzureMySqlResourceManager(resourceAuthorizer);
|
||||
await resourceManager._populateMySqlServerData(serverName);
|
||||
return resourceManager;
|
||||
}
|
||||
|
||||
public getMySqlServer() {
|
||||
return this._resource;
|
||||
}
|
||||
|
||||
public async addFirewallRule(startIpAddress: string, endIpAddress: string): Promise<FirewallRule> {
|
||||
let firewallRuleName = `ClientIPAddress_${Date.now()}`;
|
||||
let httpRequest: WebRequest = {
|
||||
method: 'PUT',
|
||||
uri: this._restClient.getRequestUri(`/${this._resource!.id}/firewallRules/${firewallRuleName}`, {}, [], '2017-12-01'),
|
||||
body: JSON.stringify({
|
||||
'properties': {
|
||||
'startIpAddress': startIpAddress,
|
||||
'endIpAddress': endIpAddress
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
try {
|
||||
let httpResponse = await this._restClient.beginRequest(httpRequest);
|
||||
if (httpResponse.statusCode === 202) {
|
||||
let asyncOperationResponse = await this._getLongRunningOperationResult(httpResponse);
|
||||
if (asyncOperationResponse.statusCode === 200 && asyncOperationResponse.body['status'] && asyncOperationResponse.body['status'].toLowerCase() === 'succeeded') {
|
||||
core.debug(JSON.stringify(asyncOperationResponse.body));
|
||||
return this.getFirewallRule(firewallRuleName);
|
||||
}
|
||||
else {
|
||||
throw ToError(asyncOperationResponse);
|
||||
}
|
||||
}
|
||||
else if (httpResponse.statusCode === 200 || httpResponse.statusCode === 201) {
|
||||
return httpResponse.body as FirewallRule;
|
||||
}
|
||||
|
||||
throw ToError(httpResponse);
|
||||
}
|
||||
catch(error) {
|
||||
if (error instanceof AzureError) {
|
||||
throw new Error(JSON.stringify(error));
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async removeFirewallRule(firewallRule: FirewallRule): Promise<void> {
|
||||
let httpRequest: WebRequest = {
|
||||
method: 'DELETE',
|
||||
uri: this._restClient.getRequestUri(`/${this._resource!.id}/firewallRules/${firewallRule.name}`, {}, [], '2017-12-01')
|
||||
};
|
||||
|
||||
try {
|
||||
let httpResponse = await this._restClient.beginRequest(httpRequest);
|
||||
if (httpResponse.statusCode === 202) {
|
||||
let asyncOperationResponse = await this._getLongRunningOperationResult(httpResponse);
|
||||
if (asyncOperationResponse.statusCode === 200 && asyncOperationResponse.body['status'] && asyncOperationResponse.body['status'].toLowerCase() === 'succeeded') {
|
||||
core.debug(JSON.stringify(asyncOperationResponse.body));
|
||||
}
|
||||
else {
|
||||
throw ToError(asyncOperationResponse);
|
||||
}
|
||||
}
|
||||
else if (httpResponse.statusCode !== 200 && httpResponse.statusCode !== 204) {
|
||||
throw ToError(httpResponse);
|
||||
}
|
||||
}
|
||||
catch(error) {
|
||||
if (error instanceof AzureError) {
|
||||
throw new Error(JSON.stringify(error));
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async getFirewallRule(ruleName: string): Promise<FirewallRule> {
|
||||
let httpRequest: WebRequest = {
|
||||
method: 'GET',
|
||||
uri: this._restClient.getRequestUri(`/${this._resource!.id}/firewallRules/${ruleName}`, {}, [], '2017-12-01')
|
||||
};
|
||||
|
||||
try {
|
||||
let httpResponse = await this._restClient.beginRequest(httpRequest);
|
||||
if (httpResponse.statusCode !== 200) {
|
||||
throw ToError(httpResponse);
|
||||
}
|
||||
|
||||
return httpResponse.body as FirewallRule;
|
||||
}
|
||||
catch(error) {
|
||||
if (error instanceof AzureError) {
|
||||
throw new Error(JSON.stringify(error));
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private async _getLongRunningOperationResult(response: WebResponse): Promise<WebResponse> {
|
||||
let timeoutInMinutes = 2;
|
||||
let timeout = new Date().getTime() + timeoutInMinutes * 60 * 1000;
|
||||
|
||||
let request = {
|
||||
method: 'GET',
|
||||
uri: response.headers['azure-asyncoperation'] || response.headers['location']
|
||||
} as WebRequest;
|
||||
|
||||
if (!request.uri) {
|
||||
throw new Error('Unable to find the Azure-Async operation polling URI.');
|
||||
}
|
||||
|
||||
while (true) {
|
||||
response = await this._restClient.beginRequest(request);
|
||||
if (response.statusCode === 202 || (response.body && (response.body.status == 'Accepted' || response.body.status == 'Running' || response.body.status == 'InProgress'))) {
|
||||
if (timeout < new Date().getTime()) {
|
||||
throw new Error(`Async polling request timed out. URI: ${request.uri}`);
|
||||
}
|
||||
|
||||
let retryAfterInterval = response.headers['retry-after'] && parseInt(response.headers['retry-after']) || 15;
|
||||
await this._sleep(retryAfterInterval);
|
||||
}
|
||||
else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
private async _populateMySqlServerData(serverName: string) {
|
||||
// trim the cloud hostname suffix from servername
|
||||
serverName = serverName.split('.')[0];
|
||||
let httpRequest: WebRequest = {
|
||||
method: 'GET',
|
||||
uri: this._restClient.getRequestUri('//subscriptions/{subscriptionId}/providers/Microsoft.DBforMySQL/servers', {}, [], '2017-12-01')
|
||||
}
|
||||
|
||||
core.debug(`Get MySQL server '${serverName}' details`);
|
||||
try {
|
||||
let httpResponse = await this._restClient.beginRequest(httpRequest);
|
||||
if (httpResponse.statusCode !== 200) {
|
||||
throw ToError(httpResponse);
|
||||
}
|
||||
|
||||
let sqlServers = httpResponse.body && httpResponse.body.value as AzureMySqlServer[];
|
||||
if (sqlServers && sqlServers.length > 0) {
|
||||
this._resource = sqlServers.filter((sqlResource) => sqlResource.name === serverName)[0];
|
||||
if (!this._resource) {
|
||||
throw new Error(`Unable to get details of MySQL server ${serverName}. MySql server '${serverName}' was not found in the subscription.`);
|
||||
}
|
||||
|
||||
core.debug(JSON.stringify(this._resource));
|
||||
}
|
||||
else {
|
||||
throw new Error(`Unable to get details of MySQL server ${serverName}. No MySQL servers were found in the subscription.`);
|
||||
}
|
||||
}
|
||||
catch(error) {
|
||||
if (error instanceof AzureError) {
|
||||
throw new Error(JSON.stringify(error));
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private _sleep(sleepDurationInSeconds: number): Promise<any> {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, sleepDurationInSeconds * 1000);
|
||||
});
|
||||
}
|
||||
|
||||
private _resource?: AzureMySqlServer;
|
||||
private _restClient: AzureRestClient;
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
import * as core from '@actions/core';
|
||||
import * as exec from '@actions/exec';
|
||||
import AzureMySqlActionHelper from "./AzureMySqlActionHelper";
|
||||
import AzureMySqlResourceManager from './AzureMySqlResourceManager';
|
||||
|
||||
const ipv4MatchPattern = /\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/;
|
||||
|
||||
export default class FirewallManager {
|
||||
constructor(azureMySqlResourceManager: AzureMySqlResourceManager) {
|
||||
this._resourceManager = azureMySqlResourceManager;
|
||||
}
|
||||
|
||||
public async addFirewallRule(serverName: string, connectionString: any): Promise<void> {
|
||||
let ipAddress = await this._detectIPAddress(serverName, connectionString);
|
||||
if (!ipAddress) {
|
||||
core.debug(`Client has access to MySql server. Skip adding firewall exception.`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Client does not have access to MySql server. Adding firewall exception for client's IP address.`)
|
||||
|
||||
this._firewallRule = await this._resourceManager.addFirewallRule(ipAddress, ipAddress);
|
||||
core.debug(JSON.stringify(this._firewallRule));
|
||||
|
||||
console.log(`Successfully added firewall rule ${this._firewallRule.name}.`);
|
||||
}
|
||||
|
||||
public async removeFirewallRule(): Promise<void> {
|
||||
if (this._firewallRule) {
|
||||
console.log(`Removing firewall rule '${this._firewallRule.name}'.`);
|
||||
await this._resourceManager.removeFirewallRule(this._firewallRule);
|
||||
console.log('Successfully removed firewall rule.');
|
||||
}
|
||||
else {
|
||||
core.debug('No firewall exception was added.')
|
||||
}
|
||||
}
|
||||
|
||||
private async _detectIPAddress(serverName: string, connectionString: any): Promise<string> {
|
||||
let mySqlClientPath = await AzureMySqlActionHelper.getMySqlClientPath();
|
||||
|
||||
let ipAddress = '';
|
||||
let mySqlError = '';
|
||||
|
||||
try {
|
||||
core.debug(`Validating if client has access to MySql Server '${serverName}'.`);
|
||||
core.debug(`"${mySqlClientPath}" -h ${serverName} -u "${connectionString.userId}" -e "show databases"`);
|
||||
await exec.exec(`"${mySqlClientPath}" -h ${serverName} -u "${connectionString.userId}" --password="${connectionString.password}" -e "show databases"`, [], {
|
||||
silent: true,
|
||||
listeners: {
|
||||
stderr: (data: Buffer) => mySqlError += data.toString()
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
core.debug(mySqlError);
|
||||
|
||||
let ipAddresses = mySqlError.match(ipv4MatchPattern);
|
||||
if (!!ipAddresses) {
|
||||
ipAddress = ipAddresses[0];
|
||||
}
|
||||
else {
|
||||
throw new Error(`Failed to add firewall rule. Unable to detect client IP Address. ${mySqlError} ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
//ipAddress will be an empty string if client has access to SQL server
|
||||
return ipAddress;
|
||||
}
|
||||
|
||||
private _firewallRule: any; // assign proper type
|
||||
private _resourceManager: AzureMySqlResourceManager;
|
||||
}
|
|
@ -0,0 +1,121 @@
|
|||
/**
|
||||
* The basic format of a connection string includes a series of keyword/value pairs separated by semicolons.
|
||||
* The equal sign (=) connects each keyword and its value. (Ex: Key1=Val1;Key2=Val2)
|
||||
*
|
||||
* Following rules are to be followed while passing special characters in values:
|
||||
1. To include values that contain a semicolon, single-quote character, or double-quote character, the value must be enclosed in double quotation marks.
|
||||
2. If the value contains both a semicolon and a double-quote character, the value can be enclosed in single quotation marks.
|
||||
3. The single quotation mark is also useful if the value starts with a double-quote character. Conversely, the double quotation mark can be used if the value starts with a single quotation mark.
|
||||
4. If the value contains both single-quote and double-quote characters, the quotation mark character used to enclose the value must be doubled every time it occurs within the value.
|
||||
|
||||
Regex used by the parser(connectionStringParserRegex) to parse the VALUE:
|
||||
|
||||
('[^']*(''[^']*)*') -> value enclosed with single quotes and has consecutive single quotes
|
||||
|("[^"]*(""[^"]*)*") -> value enclosed with double quotes and has consecutive double quotes
|
||||
|((?!['"])[^;]*)) -> value does not start with quotes does not contain any special character. Here we do a positive lookahead to ensure that the value doesn't start with quotes which should have been handled in previous cases
|
||||
Regex used to validate the entire connection string:
|
||||
|
||||
A connection string is considered valid if it is a series of key/value pairs separated by semicolons. Each key/value pair must satisy the connectionStringParserRegex to ensure it is a valid key/value pair.
|
||||
^[;\s]*{KeyValueRegex}(;[;\s]*{KeyValueRegex})*[;\s]*$
|
||||
where KeyValueRegex = ([\w\s]+=(?:('[^']*(''[^']*)*')|("[^"]*(""[^"]*)*")|((?!['"])[^;]*))))
|
||||
*/
|
||||
import * as core from '@actions/core';
|
||||
|
||||
const connectionStringParserRegex = /(?<key>[\w\s]+)=(?<val>('[^']*(''[^']*)*')|("[^"]*(""[^"]*)*")|((?!['"])[^;]*))/g
|
||||
const connectionStringTester = /^[;\s]*([\w\s]+=(?:('[^']*(''[^']*)*')|("[^"]*(""[^"]*)*")|((?!['"])[^;]*)))(;[;\s]*([\w\s]+=(?:('[^']*(''[^']*)*')|("[^"]*(""[^"]*)*")|((?!['"])[^;]*))))*[;\s]*$/
|
||||
|
||||
export interface MySqlConnectionString {
|
||||
server: string;
|
||||
userId: string;
|
||||
password: string;
|
||||
database: string;
|
||||
}
|
||||
|
||||
export default class MySqlConnectionStringBuilder {
|
||||
constructor(connectionString: string) {
|
||||
this._connectionString = connectionString;
|
||||
this._validateConnectionString();
|
||||
this._parsedConnectionString = this._parseConnectionString();
|
||||
}
|
||||
|
||||
public get connectionString(): string {
|
||||
return this._connectionString;
|
||||
}
|
||||
|
||||
public get userId(): string {
|
||||
return this._parsedConnectionString.userId;
|
||||
}
|
||||
|
||||
public get password(): string {
|
||||
return this._parsedConnectionString.password;
|
||||
}
|
||||
|
||||
public get database(): string {
|
||||
return this._parsedConnectionString.database;
|
||||
}
|
||||
|
||||
private _validateConnectionString() {
|
||||
if (!connectionStringTester.test(this._connectionString)) {
|
||||
throw new Error('Invalid connection string. A valid connection string is a series of keyword/value pairs separated by semi-colons. If there are any special characters like quotes, semi-colons in the keyword value, enclose the value within quotes. Refer this link for more info on conneciton string https://aka.ms/sqlconnectionstring');
|
||||
}
|
||||
}
|
||||
|
||||
private _parseConnectionString(): MySqlConnectionString {
|
||||
let result = this._connectionString.matchAll(connectionStringParserRegex);
|
||||
let parsedConnectionString: MySqlConnectionString = {} as any;
|
||||
|
||||
for(let match of result) {
|
||||
if (match.groups) {
|
||||
let key = match.groups.key.trim();
|
||||
let val = match.groups.val.trim();
|
||||
|
||||
/**
|
||||
* If the first character of val is a single/double quote and there are two consecutive single/double quotes in between,
|
||||
* convert the consecutive single/double quote characters into one single/double quote character respectively (Point no. 4 above)
|
||||
*/
|
||||
|
||||
if (val[0] === "'") {
|
||||
val = val.slice(1, -1);
|
||||
val = val.replace(/''/g, "'");
|
||||
|
||||
}
|
||||
else if (val[0] === '"') {
|
||||
val = val.slice(1, -1);
|
||||
val = val.replace(/""/g, '"');
|
||||
}
|
||||
|
||||
switch(key.toLowerCase()) {
|
||||
case 'user id':
|
||||
case 'uid': {
|
||||
parsedConnectionString.userId = val;
|
||||
break;
|
||||
}
|
||||
case 'password':
|
||||
case 'pwd': {
|
||||
parsedConnectionString.password = val;
|
||||
// masking the connection string password to prevent logging to console
|
||||
core.setSecret(val);
|
||||
break;
|
||||
}
|
||||
case 'database': {
|
||||
parsedConnectionString.database = val;
|
||||
break;
|
||||
}
|
||||
case 'server': {
|
||||
parsedConnectionString.server = val;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!parsedConnectionString.userId || !parsedConnectionString.password || !parsedConnectionString.database) {
|
||||
throw new Error(`Missing required keys in connection string. Please ensure that the keys 'User Id', 'Password', 'Database' are provided in the connection string.`);
|
||||
}
|
||||
|
||||
return parsedConnectionString;
|
||||
}
|
||||
|
||||
private _connectionString: string = '';
|
||||
private _parsedConnectionString: MySqlConnectionString;
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
import * as core from '@actions/core';
|
||||
import * as crypto from 'crypto';
|
||||
import * as path from 'path';
|
||||
import { AuthorizerFactory } from 'azure-actions-webclient/AuthorizerFactory';
|
||||
|
||||
import AzureMySqlActionHelper from './AzureMySqlActionHelper';
|
||||
import AzureMySqlAction, { IActionInputs } from "./AzureMySqlAction";
|
||||
import FirewallManager from './FirewallManager';
|
||||
import AzureMySqlResourceManager from './AzureMySqlResourceManager';
|
||||
import MySqlConnectionStringBuilder from './MySqlConnectionStringBuilder';
|
||||
|
||||
let userAgentPrefix = !!process.env.AZURE_HTTP_USER_AGENT ? `${process.env.AZURE_HTTP_USER_AGENT}` : "";
|
||||
|
||||
export async function run() {
|
||||
let firewallManager;
|
||||
try {
|
||||
// Set user agent variable
|
||||
let usrAgentRepo = crypto.createHash('sha256').update(`${process.env.GITHUB_REPOSITORY}`).digest('hex');
|
||||
let actionName = 'AzureMySqlAction';
|
||||
let userAgentString = (!!userAgentPrefix ? `${userAgentPrefix}+` : '') + `GITHUBACTIONS_${actionName}_${usrAgentRepo}`;
|
||||
core.exportVariable('AZURE_HTTP_USER_AGENT', userAgentString);
|
||||
|
||||
let inputs = getInputs();
|
||||
let azureMySqlAction = new AzureMySqlAction(inputs);
|
||||
let azureResourceAuthorizer = await AuthorizerFactory.getAuthorizer();
|
||||
let azureMySqlResourceManager = await AzureMySqlResourceManager.getResourceManager(inputs.serverName, azureResourceAuthorizer);
|
||||
firewallManager = new FirewallManager(azureMySqlResourceManager);
|
||||
|
||||
await firewallManager.addFirewallRule(inputs.serverName, inputs.connectionString);
|
||||
await azureMySqlAction.execute();
|
||||
}
|
||||
catch(error) {
|
||||
core.setFailed(error.message);
|
||||
}
|
||||
finally {
|
||||
if (firewallManager) {
|
||||
await firewallManager.removeFirewallRule();
|
||||
}
|
||||
|
||||
// Reset AZURE_HTTP_USER_AGENT
|
||||
core.exportVariable('AZURE_HTTP_USER_AGENT', userAgentPrefix);
|
||||
}
|
||||
}
|
||||
|
||||
function getInputs(): IActionInputs {
|
||||
let serverName = core.getInput('server-name', { required: true });
|
||||
|
||||
let connectionString = core.getInput('connection-string', { required: true });
|
||||
let connectionStringBuilder = new MySqlConnectionStringBuilder(connectionString);
|
||||
|
||||
let sqlFile = AzureMySqlActionHelper.resolveFilePath(core.getInput('sql-file', { required: true }));
|
||||
if (path.extname(sqlFile).toLowerCase() !== '.sql') {
|
||||
throw new Error(`Invalid sql file path provided as input ${sqlFile}`);
|
||||
}
|
||||
|
||||
let additionalArguments = core.getInput('arguments');
|
||||
|
||||
return {
|
||||
serverName: serverName,
|
||||
connectionString: connectionStringBuilder,
|
||||
sqlFile: sqlFile,
|
||||
additionalArguments: additionalArguments
|
||||
};
|
||||
}
|
||||
|
||||
run();
|
|
@ -0,0 +1,64 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
/* Basic Options */
|
||||
// "incremental": true, /* Enable incremental compilation */
|
||||
"target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
|
||||
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
|
||||
"lib": ["es2020.string"], /* Specify library files to be included in the compilation. */
|
||||
// "allowJs": true, /* Allow javascript files to be compiled. */
|
||||
// "checkJs": true, /* Report errors in .js files. */
|
||||
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
|
||||
// "declaration": true, /* Generates corresponding '.d.ts' file. */
|
||||
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
|
||||
// "sourceMap": true, /* Generates corresponding '.map' file. */
|
||||
// "outFile": "./", /* Concatenate and emit output to single file. */
|
||||
"outDir": "./lib", /* Redirect output structure to the directory. */
|
||||
"rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
|
||||
// "composite": true, /* Enable project compilation */
|
||||
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
|
||||
// "removeComments": true, /* Do not emit comments to output. */
|
||||
// "noEmit": true, /* Do not emit outputs. */
|
||||
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
|
||||
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
|
||||
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
|
||||
|
||||
/* Strict Type-Checking Options */
|
||||
"strict": true, /* Enable all strict type-checking options. */
|
||||
"noImplicitAny": false, /* Raise error on expressions and declarations with an implied 'any' type. */
|
||||
// "strictNullChecks": true, /* Enable strict null checks. */
|
||||
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
|
||||
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
|
||||
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
|
||||
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
|
||||
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
|
||||
|
||||
/* Additional Checks */
|
||||
// "noUnusedLocals": true, /* Report errors on unused locals. */
|
||||
// "noUnusedParameters": true, /* Report errors on unused parameters. */
|
||||
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
|
||||
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
|
||||
|
||||
/* Module Resolution Options */
|
||||
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
|
||||
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
|
||||
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
|
||||
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
|
||||
// "typeRoots": [], /* List of folders to include type definitions from. */
|
||||
// "types": [], /* Type declaration files to be included in compilation. */
|
||||
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
|
||||
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
|
||||
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
|
||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||
|
||||
/* Source Map Options */
|
||||
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
|
||||
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
|
||||
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
|
||||
|
||||
/* Experimental Options */
|
||||
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
|
||||
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
|
||||
},
|
||||
"exclude": ["node_modules", "**/*.test.ts"]
|
||||
}
|
Загрузка…
Ссылка в новой задаче