limited-carpenter-34991
06/10/2020, 7:58 PMbetter-rainbow-14549
06/17/2020, 10:05 AMlimited-carpenter-34991
06/19/2020, 10:45 AMbetter-rainbow-14549
06/26/2020, 12:35 PMimport { all, dynamic, ID, Input, output } from "@pulumi/pulumi";
import * as azure from "@pulumi/azure";
import * as child_process from "child_process";
import * as path from "path";
import * as fs from "fs";
import * as crypto from "crypto";
export type DbSchemaProps = {
readonly schemaName?: Input<string>;
readonly serverUri: Input<string>;
readonly databaseName: Input<string>;
readonly schemaFileName: Input<string>;
};
type DbSchemaOutProps = Required<DbSchemaProps> & {
schemaFileHash: string;
};
export class DbSchemaProvider implements dynamic.ResourceProvider {
private static command = (serverUri: string, databaseName: string, command: string) => {
const result = child_process.spawnSync("sqlutil", [
"-t", <string>process.env.ARM_TENANT_ID,
"-m", "ServicePrincipal",
"-A", <string>process.env.ARM_CLIENT_ID,
"-P", <string>process.env.ARM_CLIENT_SECRET,
"-c", `Server=tcp:${serverUri};Database=${databaseName}`,
"--stdin"
], {
input: command
});
return ({
idPrefix: `${serverUri}::${databaseName}`,
success: result.status == 0,
output: result.stdout.toString("UTF-8"),
error: result.stderr.toString("UTF-8")
});
};
private static getSchemaFile = (filePath: string): { schema: string, hash: string } => {
const schema = fs.readFileSync(path.resolve(filePath)).toString("UTF8");
const hash = crypto.createHash('sha256').update(schema).digest('hex');
return ({
schema: schema,
hash: hash
});
};
check: (olds: DbSchemaOutProps, news: DbSchemaProps) => Promise<dynamic.CheckResult>;
create: (inputs: DbSchemaOutProps) => Promise<dynamic.CreateResult>;
diff: (id: ID, olds: DbSchemaOutProps, news: DbSchemaOutProps) => Promise<dynamic.DiffResult>;
update: (id: ID, olds: DbSchemaOutProps, news: DbSchemaOutProps) => Promise<dynamic.UpdateResult>;
delete: (id: ID, props: DbSchemaOutProps) => Promise<void>;
constructor() {
if (process.env.ARM_TENANT_ID == undefined) throw new Error("ARM_TENANT_ID is not configured.");
if (process.env.ARM_CLIENT_ID == undefined) throw new Error("ARM_CLIENT_ID is not configured.");
if (process.env.ARM_CLIENT_SECRET == undefined) throw new Error("ARM_CLIENT_SECRET is not configured.");
this.check = (_olds: DbSchemaOutProps, props: DbSchemaProps) => new Promise<dynamic.CheckResult>((resolve) => {
all([
props.schemaName,
props.serverUri,
props.databaseName,
props.schemaFileName
]).apply(([
schemaName,
serverUri,
databaseName,
schemaFileName
]) => {
resolve({
inputs: {
schemaName: schemaName,
serverUri: serverUri,
databaseName: databaseName,
schemaFileName: schemaFileName
},
failures: fs.existsSync(path.resolve(schemaFileName)) ? undefined : [{ property: "schemaFileName", reason: `File not found: ${path.resolve(schemaFileName)}` }]
});
})
});
this.create = (props: DbSchemaOutProps) => new Promise<dynamic.CreateResult>((resolve, reject) => {
all([
props.schemaName,
props.serverUri,
props.databaseName,
props.schemaFileName
]).apply(([
schemaName,
serverUri,
databaseName,
schemaFileName
]) => {
const script = DbSchemaProvider.getSchemaFile(schemaFileName);
let result = DbSchemaProvider.command(serverUri, databaseName, script.schema);
if (result.success) {
resolve({
id: `${result.idPrefix}::DbSchema::${schemaName}`,
outs: {
schemaName: schemaName,
serverUri: serverUri,
databaseName: databaseName,
schemaFileName: schemaFileName,
schemaFileHash: script.hash
}
});
} else {
reject({
message: `Failed: (${serverUri}/${databaseName}) Deploy ${schemaFileName}\n${result.error}`
});
}
});
});
this.diff = (_: ID, olds: DbSchemaOutProps, news: DbSchemaOutProps) => new Promise<dynamic.DiffResult>((resolve, _) => {
output(news.schemaFileName).apply(schemaFileName => {
const script = DbSchemaProvider.getSchemaFile(schemaFileName);
let props: string[] = [];
if (schemaFileName != olds.schemaFileName) props.push("schemaFileName");
if (script.hash != olds.schemaFileHash) props.push("schemaFileHash");
resolve({
replaces: undefined,
stables: [],
changes: props.length !== 0
});
})
});
this.update = (_id: string, _: DbSchemaOutProps, news: DbSchemaOutProps) => new Promise<dynamic.UpdateResult>((resolve, reject) => {
all([
news.schemaName,
news.serverUri,
news.databaseName,
news.schemaFileName
]).apply(([
schemaName,
serverUri,
databaseName,
schemaFileName
]) => {
const script = DbSchemaProvider.getSchemaFile(schemaFileName);
let result = DbSchemaProvider.command(serverUri, databaseName, script.schema);
if (result.success) {
resolve({
outs: {
schemaName: schemaName,
serverUri: serverUri,
databaseName: databaseName,
schemaFileName: schemaFileName,
schemaFileHash: script.hash
}
});
} else {
reject({
message: `Failed: (${serverUri}/${databaseName}) Deploy ${schemaFileName}\n${result.error}`
});
}
});
});
this.delete = (_: ID, props: DbSchemaProps) => new Promise<void>((_, reject) => {
output(props.schemaName).apply(schemaName => {
reject({
message: `Can't roll back a migration of schema ${schemaName}.`
});
});
});
}
}
export class DbSchema extends dynamic.Resource {
constructor(name: string, inputs: DbSchemaProps) {
super(new DbSchemaProvider(), `DbSchema-${name}`, {
serverUri: inputs.serverUri,
databaseName: inputs.databaseName,
schemaName: inputs.schemaName || name,
schemaFileName: inputs.schemaFileName
});
}
}