Compare commits
4 Commits
Author | SHA1 | Date | |
---|---|---|---|
1511ec76fb | |||
|
ba8119903e | ||
|
f5b84b530c | ||
|
afb35ebad8 |
1
.gitignore
vendored
1
.gitignore
vendored
@ -88,3 +88,4 @@ typings/
|
|||||||
.dynamodb/
|
.dynamodb/
|
||||||
|
|
||||||
# End of https://www.gitignore.io/api/node
|
# End of https://www.gitignore.io/api/node
|
||||||
|
demo/*
|
||||||
|
@ -20,6 +20,7 @@ const FlitterUnits = {
|
|||||||
'Config' : require('./app/compat/ConfigUnit'),
|
'Config' : require('./app/compat/ConfigUnit'),
|
||||||
'Services' : require('./app/compat/ServicesUnit'),
|
'Services' : require('./app/compat/ServicesUnit'),
|
||||||
'Utility' : require('libflitter/utility/UtilityUnit'),
|
'Utility' : require('libflitter/utility/UtilityUnit'),
|
||||||
|
'Misc' : require('./app/AppUnit'),
|
||||||
'Cli' : require('flitter-cli/CliUnit'),
|
'Cli' : require('flitter-cli/CliUnit'),
|
||||||
'App' : require('flitter-cli/CliAppUnit'),
|
'App' : require('flitter-cli/CliAppUnit'),
|
||||||
}
|
}
|
||||||
|
17
app/AppUnit.js
Normal file
17
app/AppUnit.js
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
const { Unit } = require('libflitter')
|
||||||
|
const RunDirective = require('./classes/routine/RunDirective')
|
||||||
|
const Rollcall = require('./classes/directive/Rollcall')
|
||||||
|
const Routine = require('./classes/directive/Routine')
|
||||||
|
|
||||||
|
class AppUnit extends Unit {
|
||||||
|
static get name() { return 'misc' }
|
||||||
|
|
||||||
|
directives() {
|
||||||
|
return {
|
||||||
|
Rollcall,
|
||||||
|
Routine,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = exports = AppUnit
|
73
app/classes/directive/Rollcall.js
Normal file
73
app/classes/directive/Rollcall.js
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
const Directive = require('flitter-cli/Directive')
|
||||||
|
const path = require('path')
|
||||||
|
|
||||||
|
class Rollcall extends Directive {
|
||||||
|
static get services() {
|
||||||
|
return [...super.services, 'configs', 'hosts']
|
||||||
|
}
|
||||||
|
|
||||||
|
static options() {
|
||||||
|
return [
|
||||||
|
'--hosts -h {host file} | path to the host definition file'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
static name() {
|
||||||
|
return 'rollcall'
|
||||||
|
}
|
||||||
|
|
||||||
|
static help() {
|
||||||
|
return 'Ping the configured hosts to determine if we can access them'
|
||||||
|
}
|
||||||
|
|
||||||
|
async handle(app, argv) {
|
||||||
|
const host_path = this.option('hosts')
|
||||||
|
|
||||||
|
if ( host_path ) {
|
||||||
|
try {
|
||||||
|
// Override the host config externally
|
||||||
|
const host_conf = require(path.resolve(host_path))
|
||||||
|
if (typeof host_conf === 'object') {
|
||||||
|
this.configs.canonical_items.hosts = host_conf
|
||||||
|
this.info('Loaded host definition file.')
|
||||||
|
} else {
|
||||||
|
this.error('Invalid host definition file!')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.error('Invalid host definition file!')
|
||||||
|
this.output.debug(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const successes = []
|
||||||
|
const failures = []
|
||||||
|
|
||||||
|
const host_config = this.configs.get('hosts')
|
||||||
|
const hostnames = Object.keys(host_config)
|
||||||
|
for ( const name of hostnames ) {
|
||||||
|
this.info(`Pinging ${name}...`)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const host = this.hosts.get(name)
|
||||||
|
const is_alive = await host.is_alive()
|
||||||
|
if ( !is_alive ) {
|
||||||
|
failures.push(name)
|
||||||
|
this.error('Unable to ping host and verify execution.')
|
||||||
|
} else {
|
||||||
|
successes.push(name)
|
||||||
|
this.success('Connected to host and verified execution.')
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
failures.push(name)
|
||||||
|
this.error('Unable to ping host and verify execution.')
|
||||||
|
this.output.debug(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.hosts.close()
|
||||||
|
this.info(`Successfully pinged ${successes.length} host(s).`)
|
||||||
|
if ( failures.length > 0 ) this.info(`Unable to ping ${failures.length} host(s): ${failures.join(', ')}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = exports = Rollcall
|
100
app/classes/directive/Routine.js
Normal file
100
app/classes/directive/Routine.js
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
const Directive = require('flitter-cli/Directive')
|
||||||
|
const path = require('path')
|
||||||
|
|
||||||
|
class Routine extends Directive {
|
||||||
|
static get services() {
|
||||||
|
return [...super.services, 'configs', 'hosts', 'routines']
|
||||||
|
}
|
||||||
|
|
||||||
|
static options() {
|
||||||
|
return [
|
||||||
|
'--hosts -h {host file} | path to the host definition file',
|
||||||
|
'--target -t {host name} | the host to run the routine on',
|
||||||
|
'--routine -r {routine file} | path to the routine definition file',
|
||||||
|
'--type -t {run type} | how to execute the routine: checks | apply'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
static name() {
|
||||||
|
return 'routine'
|
||||||
|
}
|
||||||
|
|
||||||
|
static help() {
|
||||||
|
return 'Run the specified routine file'
|
||||||
|
}
|
||||||
|
|
||||||
|
async handle(app, argv) {
|
||||||
|
const host_path = this.option('hosts')
|
||||||
|
const routine_path = this.option('routine')
|
||||||
|
const target_host = this.option('target')
|
||||||
|
const run_type = this.option('type')
|
||||||
|
|
||||||
|
if ( host_path ) {
|
||||||
|
try {
|
||||||
|
// Override the host config externally
|
||||||
|
const host_conf = require(path.resolve(host_path))
|
||||||
|
if (typeof host_conf === 'object') {
|
||||||
|
this.configs.canonical_items.hosts = host_conf
|
||||||
|
this.info('Loaded host definition file.')
|
||||||
|
} else {
|
||||||
|
this.error('Invalid host definition file!')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.error('Invalid host definition file!')
|
||||||
|
this.output.debug(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// loaded_from_cli
|
||||||
|
|
||||||
|
const host = this.hosts.get(target_host)
|
||||||
|
if ( !host ) {
|
||||||
|
this.error('Invalid host name. Unable to find configuration for host with that name.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( !['checks', 'apply'].includes(run_type) ) {
|
||||||
|
this.error('Invalid run type. Must be one of: checks, apply')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( routine_path ) {
|
||||||
|
try {
|
||||||
|
// Override the routine config externally
|
||||||
|
const routine_conf = require(path.resolve(routine_path))
|
||||||
|
if (typeof routine_conf === 'object') {
|
||||||
|
routine_conf.hosts = [target_host]
|
||||||
|
routine_conf.type = run_type
|
||||||
|
if ( Array.isArray(routine_conf.steps) ) {
|
||||||
|
routine_conf.steps = routine_conf.steps.map(step => {
|
||||||
|
step.host = target_host
|
||||||
|
return step
|
||||||
|
})
|
||||||
|
}
|
||||||
|
this.configs.canonical_items['routines:loaded_from_cli'] = routine_conf
|
||||||
|
this.info('Loaded routine definition file.')
|
||||||
|
} else {
|
||||||
|
this.error('Invalid routine definition file!')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.error('Invalid routine definition file!')
|
||||||
|
this.output.debug(e)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.error('Missing required parameter: --routine')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const routine = await this.routines.get('loaded_from_cli')
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await routine.execute(true)
|
||||||
|
} catch (e) {
|
||||||
|
this.output.error(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.hosts.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = exports = Routine
|
@ -68,6 +68,10 @@ class Repository extends Injectable {
|
|||||||
await this._git_cmd(`push${target ? ' '+target : ''}${ref ? ' '+ref : ''} --tags`)
|
await this._git_cmd(`push${target ? ' '+target : ''}${ref ? ' '+ref : ''} --tags`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async pull({ rebase = false, target = '', ref = '' } = {}) {
|
||||||
|
await this._git_cmd(`pull${target ? ' '+target : ''}${ref ? ' '+ref : ''}${rebase ? ' --rebase' : ''}`)
|
||||||
|
}
|
||||||
|
|
||||||
async checkout(ref = 'master') {
|
async checkout(ref = 'master') {
|
||||||
await this._git_cmd(`checkout "${ref}"`)
|
await this._git_cmd(`checkout "${ref}"`)
|
||||||
}
|
}
|
||||||
|
@ -80,6 +80,7 @@ class Host extends Injectable {
|
|||||||
const result = await this.execute(`echo "${unique_id}"`)
|
const result = await this.execute(`echo "${unique_id}"`)
|
||||||
return (result.exit_code === 0 && (result.clean_out.length > 0 && result.clean_out[0] === unique_id))
|
return (result.exit_code === 0 && (result.clean_out.length > 0 && result.clean_out[0] === unique_id))
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
this.output.debug(e)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -22,10 +22,15 @@ class Routine extends Injectable {
|
|||||||
this._type = type
|
this._type = type
|
||||||
}
|
}
|
||||||
|
|
||||||
async execute() {
|
async execute(with_logging = false) {
|
||||||
const result = await this._build_result()
|
const result = await this._build_result()
|
||||||
|
|
||||||
|
let step_no = 1
|
||||||
for ( const step of result.steps ) {
|
for ( const step of result.steps ) {
|
||||||
|
if ( with_logging ) this.output.info('', 0)
|
||||||
|
this.output.info(`(${step_no}/${result.steps.length}) ${step.step.display()}`, with_logging ? 0 : 10)
|
||||||
|
step_no += 1
|
||||||
|
|
||||||
if ( this._type === 'checks' ) {
|
if ( this._type === 'checks' ) {
|
||||||
step.status = (await step.step.check()) ? 'success' : 'fail'
|
step.status = (await step.step.check()) ? 'success' : 'fail'
|
||||||
step.message = step.status === 'success' ? 'Check passed.' : step.step.check_message()
|
step.message = step.status === 'success' ? 'Check passed.' : step.step.check_message()
|
||||||
@ -41,6 +46,12 @@ class Routine extends Injectable {
|
|||||||
} else {
|
} else {
|
||||||
throw new InvalidRoutineTypeError(this._type)
|
throw new InvalidRoutineTypeError(this._type)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ( step.status === 'success' ) {
|
||||||
|
this.output.success(` ${step.message}`, with_logging ? 0 : 10)
|
||||||
|
} else {
|
||||||
|
this.output.error(` ${step.message}`, with_logging ? 0 : 10)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
result.overall_state = result.steps.every(x => x.status === 'success') ? 'success' : 'fail'
|
result.overall_state = result.steps.every(x => x.status === 'success') ? 'success' : 'fail'
|
||||||
@ -50,7 +61,8 @@ class Routine extends Injectable {
|
|||||||
async _build_result() {
|
async _build_result() {
|
||||||
const steps = []
|
const steps = []
|
||||||
for ( const step_config of this._config.steps ) {
|
for ( const step_config of this._config.steps ) {
|
||||||
const step = this.states.from_config(this._hosts[step_config.host], step_config)
|
const host = this._hosts[step_config.host]
|
||||||
|
const step = this.states.from_config(host, step_config)
|
||||||
const result = new StepResult(this, step)
|
const result = new StepResult(this, step)
|
||||||
steps.push(result)
|
steps.push(result)
|
||||||
}
|
}
|
||||||
|
48
app/classes/routine/RunDirective.js
Normal file
48
app/classes/routine/RunDirective.js
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
const Directive = require('flitter-cli/Directive')
|
||||||
|
const path = require('path')
|
||||||
|
|
||||||
|
class RunDirective extends Directive {
|
||||||
|
static options() {
|
||||||
|
return [
|
||||||
|
'--hosts -h {host file} | path to the host definition file',
|
||||||
|
'{routine file} | path to the routine definition file',
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
static name() {
|
||||||
|
return 'run'
|
||||||
|
}
|
||||||
|
|
||||||
|
static help() {
|
||||||
|
return 'Run a specified routine'
|
||||||
|
}
|
||||||
|
|
||||||
|
async handle(app, argv) {
|
||||||
|
const routine_path = this.option('routine file')
|
||||||
|
const host_path = this.option('hosts')
|
||||||
|
|
||||||
|
if ( !host_path )
|
||||||
|
return this.error('Missing required parameter: --hosts')
|
||||||
|
|
||||||
|
let routine_conf
|
||||||
|
let host_conf
|
||||||
|
|
||||||
|
try {
|
||||||
|
routine_conf = require(path.resolve(routine_path))
|
||||||
|
} catch (e) {
|
||||||
|
this.error('Routine file is invalid.')
|
||||||
|
return this.output.debug(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
host_conf = require(path.resolve(host_path))
|
||||||
|
} catch (e) {
|
||||||
|
this.error('Host definition file is invalid.')
|
||||||
|
return this.output.debug(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log({ routine_conf, host_conf })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = exports = RunDirective
|
@ -27,6 +27,10 @@ class State extends Injectable {
|
|||||||
check_message() {
|
check_message() {
|
||||||
throw new ImplementationError()
|
throw new ImplementationError()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
display() {
|
||||||
|
return this.constructor.name
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = exports = State
|
module.exports = exports = State
|
||||||
|
@ -34,6 +34,10 @@ class DirectoryState extends State {
|
|||||||
check_message() {
|
check_message() {
|
||||||
return this.failure_message()
|
return this.failure_message()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
display() {
|
||||||
|
return `Ensure that the directory ${this._config.path} exists...`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = exports = DirectoryState
|
module.exports = exports = DirectoryState
|
||||||
|
@ -54,6 +54,10 @@ class DownloadState extends State {
|
|||||||
check_message() {
|
check_message() {
|
||||||
return this.failure_message()
|
return this.failure_message()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
display() {
|
||||||
|
return `Ensure that the file ${this._config.path} is downloaded from ${this._config.source}...`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = exports = DownloadState
|
module.exports = exports = DownloadState
|
||||||
|
@ -39,6 +39,10 @@ class FileState extends State {
|
|||||||
check_message() {
|
check_message() {
|
||||||
return this.failure_message()
|
return this.failure_message()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
display() {
|
||||||
|
return `Ensure that the file ${this._config.path} exists...`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = exports = FileState
|
module.exports = exports = FileState
|
||||||
|
@ -46,6 +46,10 @@ class OwnerState extends State {
|
|||||||
check_message() {
|
check_message() {
|
||||||
return this.failure_message()
|
return this.failure_message()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
display() {
|
||||||
|
return `Set filesystem owner of resource ${this._config.path}...`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = exports = OwnerState
|
module.exports = exports = OwnerState
|
||||||
|
@ -63,6 +63,10 @@ class PackState extends State {
|
|||||||
check_message() {
|
check_message() {
|
||||||
return this.failure_message()
|
return this.failure_message()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
display() {
|
||||||
|
return `Archive the contents of ${this._config.path} to ${this._config.destination}...`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = exports = PackState
|
module.exports = exports = PackState
|
||||||
|
@ -13,10 +13,13 @@ class PermissionState extends State {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async check() {
|
async check() {
|
||||||
const path = await this._path()
|
try {
|
||||||
const permissions = `${await path.permissions()}`.trim()
|
const path = await this._path()
|
||||||
const target = `${this._config.level}`.trim()
|
const permissions = `${await path.permissions()}`.trim()
|
||||||
return permissions === target
|
const target = `${this._config.level}`.trim()
|
||||||
|
return permissions === target
|
||||||
|
} catch (e) {}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
async reverse() {
|
async reverse() {
|
||||||
@ -42,6 +45,10 @@ class PermissionState extends State {
|
|||||||
check_message() {
|
check_message() {
|
||||||
return this.failure_message()
|
return this.failure_message()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
display() {
|
||||||
|
return `Set filesystem permissions of resource ${this._config.path}...`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = exports = PermissionState
|
module.exports = exports = PermissionState
|
||||||
|
@ -66,6 +66,10 @@ class UnpackState extends State {
|
|||||||
check_message() {
|
check_message() {
|
||||||
return this.failure_message()
|
return this.failure_message()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
display() {
|
||||||
|
return `Un-archive the contents of ${this._config.path} to ${this._config.destination}...`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = exports = UnpackState
|
module.exports = exports = UnpackState
|
||||||
|
@ -40,6 +40,10 @@ class CheckoutState extends AbstractGitState {
|
|||||||
check_message() {
|
check_message() {
|
||||||
return this.failure_message()
|
return this.failure_message()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
display() {
|
||||||
|
return `Check out ref ${this._config.target} in repo ${this._config.path}...`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = exports = CheckoutState
|
module.exports = exports = CheckoutState
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
const AbstractGitState = require('./AbstractGitState')
|
const AbstractGitState = require('./AbstractGitState')
|
||||||
|
|
||||||
|
// TODO add 'bare' option
|
||||||
|
// TODO add 'new_repo' option (rm -rf .git && git init)
|
||||||
class CloneState extends AbstractGitState {
|
class CloneState extends AbstractGitState {
|
||||||
|
|
||||||
async apply() {
|
async apply() {
|
||||||
@ -28,6 +30,10 @@ class CloneState extends AbstractGitState {
|
|||||||
check_message() {
|
check_message() {
|
||||||
return this.failure_message()
|
return this.failure_message()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
display() {
|
||||||
|
return `Clone repo at ${this._config.source} to ${this._config.path}...`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = exports = CloneState
|
module.exports = exports = CloneState
|
||||||
|
39
app/classes/state/git/PullState.js
Normal file
39
app/classes/state/git/PullState.js
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
const AbstractGitState = require('./AbstractGitState')
|
||||||
|
|
||||||
|
class PullState extends AbstractGitState {
|
||||||
|
#ran_once = false
|
||||||
|
|
||||||
|
static get services() {
|
||||||
|
return [...super.services, 'output']
|
||||||
|
}
|
||||||
|
|
||||||
|
async apply() {
|
||||||
|
if ( !(await this.check()) ) {
|
||||||
|
const repo = await this._repo()
|
||||||
|
await repo.pull(this._config.target)
|
||||||
|
this.#ran_once = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async check() {
|
||||||
|
return this.#ran_once // TODO support a better check here
|
||||||
|
}
|
||||||
|
|
||||||
|
async reverse() {
|
||||||
|
this.output.warn('Pull state does not currently support automatic reversal.')
|
||||||
|
}
|
||||||
|
|
||||||
|
failure_message() {
|
||||||
|
return `The Git repo at "${this._config.path}" on host "${this._host.name}" will be pulled.`
|
||||||
|
}
|
||||||
|
|
||||||
|
check_message() {
|
||||||
|
return this.failure_message()
|
||||||
|
}
|
||||||
|
|
||||||
|
display() {
|
||||||
|
return `Pull refs in repo ${this._config.path}...`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = exports = PullState
|
@ -33,6 +33,10 @@ class TagState extends AbstractGitState {
|
|||||||
check_message() {
|
check_message() {
|
||||||
return this.failure_message()
|
return this.failure_message()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
display() {
|
||||||
|
return `Create tag ${this._config.tag} in repo ${this._config.path}...`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = exports = TagState
|
module.exports = exports = TagState
|
||||||
|
43
app/classes/state/node/YarnInstallState.js
Normal file
43
app/classes/state/node/YarnInstallState.js
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
const State = require('../State')
|
||||||
|
|
||||||
|
class YarnInstallState extends State {
|
||||||
|
#ran_once = false
|
||||||
|
|
||||||
|
static get services() {
|
||||||
|
return [...super.services, 'output']
|
||||||
|
}
|
||||||
|
|
||||||
|
async apply() {
|
||||||
|
const cmd = `cd "${this._config.path}" && yarn install`
|
||||||
|
await this._host.run(cmd)
|
||||||
|
this.#ran_once = true
|
||||||
|
}
|
||||||
|
|
||||||
|
async check() {
|
||||||
|
if ( this._config.force ) return this.#ran_once
|
||||||
|
else {
|
||||||
|
const node_modules = this._host.get_path(`${this._config.path}/node_modules`)
|
||||||
|
await node_modules.classify()
|
||||||
|
return node_modules.is_directory()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async reverse() {
|
||||||
|
const node_modules = this._host.get_path(`${this._config.path}/node_modules`)
|
||||||
|
await node_modules.unlink()
|
||||||
|
}
|
||||||
|
|
||||||
|
failure_message() {
|
||||||
|
return `The Yarn packages at ${this._config.path} need to be installed.`
|
||||||
|
}
|
||||||
|
|
||||||
|
check_message() {
|
||||||
|
return this.failure_message()
|
||||||
|
}
|
||||||
|
|
||||||
|
display() {
|
||||||
|
return `Ensure that the Yarn packages at ${this._config.path} are installed...`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = exports = YarnInstallState
|
@ -1,17 +1,20 @@
|
|||||||
const State = require('../State')
|
const State = require('../State')
|
||||||
|
|
||||||
class CommandState extends State {
|
class CommandState extends State {
|
||||||
|
#ran_once = false
|
||||||
|
|
||||||
static get services() {
|
static get services() {
|
||||||
return [...super.services, 'output']
|
return [...super.services, 'output']
|
||||||
}
|
}
|
||||||
|
|
||||||
async apply() {
|
async apply() {
|
||||||
const cmd = `${this._config.cmd}`
|
const cmd = `${this._config.cmd}`
|
||||||
await this._host.run(cmd)
|
const result = await this._host.run(cmd)
|
||||||
|
this.#ran_once = true
|
||||||
}
|
}
|
||||||
|
|
||||||
async check() {
|
async check() {
|
||||||
return false
|
return this.#ran_once
|
||||||
}
|
}
|
||||||
|
|
||||||
async reverse() {
|
async reverse() {
|
||||||
@ -30,6 +33,10 @@ class CommandState extends State {
|
|||||||
check_message() {
|
check_message() {
|
||||||
return `The command check was not successful.`
|
return `The command check was not successful.`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
display() {
|
||||||
|
return `Run the command: ${this._config.cmd}`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = exports = CommandState
|
module.exports = exports = CommandState
|
||||||
|
@ -20,6 +20,10 @@ class IsAliveState extends State {
|
|||||||
check_message() {
|
check_message() {
|
||||||
return this.failure_message()
|
return this.failure_message()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
display() {
|
||||||
|
return `Ensure that the host is alive...`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = exports = IsAliveState
|
module.exports = exports = IsAliveState
|
||||||
|
@ -27,6 +27,10 @@ class PackageAbsentState extends State {
|
|||||||
check_message() {
|
check_message() {
|
||||||
return this.failure_message()
|
return this.failure_message()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
display() {
|
||||||
|
return `Ensure that the package ${this._config.package} is not installed...`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = exports = PackageAbsentState
|
module.exports = exports = PackageAbsentState
|
||||||
|
@ -31,6 +31,10 @@ class PackageCacheClearedState extends State {
|
|||||||
check_message() {
|
check_message() {
|
||||||
return `The package cache on host "${this._host.name}" has not been cleared.`
|
return `The package cache on host "${this._host.name}" has not been cleared.`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
display() {
|
||||||
|
return `Ensure that the package cache is cleared...`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = exports = PackageCacheClearedState
|
module.exports = exports = PackageCacheClearedState
|
||||||
|
@ -27,6 +27,10 @@ class PackageState extends State {
|
|||||||
check_message() {
|
check_message() {
|
||||||
return this.failure_message()
|
return this.failure_message()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
display() {
|
||||||
|
return `Ensure that the package ${this._config.package} is installed...`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = exports = PackageState
|
module.exports = exports = PackageState
|
||||||
|
@ -31,6 +31,10 @@ class ServiceDaemonReloadState extends State {
|
|||||||
check_message() {
|
check_message() {
|
||||||
return `The service daemon on host "${this._host.name}" has not been reloaded.`
|
return `The service daemon on host "${this._host.name}" has not been reloaded.`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
display() {
|
||||||
|
return `Reload the service daemon...`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = exports = ServiceDaemonReloadState
|
module.exports = exports = ServiceDaemonReloadState
|
||||||
|
@ -32,6 +32,10 @@ class ServiceRestartState extends State {
|
|||||||
check_message() {
|
check_message() {
|
||||||
return `The service "${this._config.service}" on host "${this._host.name}" has not been restarted.`
|
return `The service "${this._config.service}" on host "${this._host.name}" has not been restarted.`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
display() {
|
||||||
|
return `Restart the ${this._config.service} service...`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = exports = ServiceRestartState
|
module.exports = exports = ServiceRestartState
|
||||||
|
@ -33,6 +33,10 @@ class ServiceState extends State {
|
|||||||
check_message() {
|
check_message() {
|
||||||
return this.failure_message()
|
return this.failure_message()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
display() {
|
||||||
|
return `Ensure that the ${this._config.service} service is running...`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = exports = ServiceState
|
module.exports = exports = ServiceState
|
||||||
|
@ -32,6 +32,10 @@ class ServiceStoppedState extends State {
|
|||||||
check_message() {
|
check_message() {
|
||||||
return `The service "${this._config.service}" on host "${this._host.name}" has not been stopped.`
|
return `The service "${this._config.service}" on host "${this._host.name}" has not been stopped.`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
display() {
|
||||||
|
return `Ensure that the ${this._config.service} service is not running...`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = exports = ServiceStoppedState
|
module.exports = exports = ServiceStoppedState
|
||||||
|
@ -32,6 +32,10 @@ class UpdateState extends State {
|
|||||||
check_message() {
|
check_message() {
|
||||||
return `There are package updates pending on the host "${this._host.name}."`
|
return `There are package updates pending on the host "${this._host.name}."`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
display() {
|
||||||
|
return `Ensure that all packages are up to date...`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = exports = UpdateState
|
module.exports = exports = UpdateState
|
||||||
|
@ -15,11 +15,12 @@ class hosts extends Service {
|
|||||||
|
|
||||||
get(name) {
|
get(name) {
|
||||||
const config = this.config[name]
|
const config = this.config[name]
|
||||||
config.name = name
|
|
||||||
if ( !config ) {
|
if ( !config ) {
|
||||||
throw new Error(`Could not get host ${name}: No such host configured.`)
|
throw new Error(`Could not get host ${name}: No such host configured.`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
config.name = name
|
||||||
|
|
||||||
if ( config.type === 'localhost' ) {
|
if ( config.type === 'localhost' ) {
|
||||||
const host = this.app.di().make(LocalHost, config)
|
const host = this.app.di().make(LocalHost, config)
|
||||||
this._running_hosts.push(host)
|
this._running_hosts.push(host)
|
||||||
|
@ -12,18 +12,21 @@ const Routine = require('../classes/routine/Routine')
|
|||||||
*/
|
*/
|
||||||
class RoutinesService extends Service {
|
class RoutinesService extends Service {
|
||||||
static get services() {
|
static get services() {
|
||||||
return [...super.services, 'app', 'configs', 'hosts']
|
return [...super.services, 'injector', 'configs', 'hosts']
|
||||||
}
|
}
|
||||||
|
|
||||||
async get(name) {
|
async get(name) {
|
||||||
this.app.make(Routine)
|
|
||||||
const config = this.configs.get(`routines:${name}`)
|
const config = this.configs.get(`routines:${name}`)
|
||||||
|
if ( !config ) {
|
||||||
|
throw new Error(`Unable to find a routine with the name: ${name}`)
|
||||||
|
}
|
||||||
|
|
||||||
const hosts = {}
|
const hosts = {}
|
||||||
for ( const host_name of config.hosts ) {
|
for ( const host_name of config.hosts ) {
|
||||||
hosts[host_name] = await this.hosts.get(host_name)
|
hosts[host_name] = await this.hosts.get(host_name)
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Routine(hosts, config, config.type)
|
return this.injector.make(Routine, hosts, config, config.type)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,6 +25,7 @@ class StatesService extends Service {
|
|||||||
'git.clone': require('../classes/state/git/CloneState'),
|
'git.clone': require('../classes/state/git/CloneState'),
|
||||||
'git.checkout': require('../classes/state/git/CheckoutState'),
|
'git.checkout': require('../classes/state/git/CheckoutState'),
|
||||||
'git.tag': require('../classes/state/git/TagState'),
|
'git.tag': require('../classes/state/git/TagState'),
|
||||||
|
'git.pull': require('../classes/state/git/PullState'),
|
||||||
|
|
||||||
'os.cmd': require('../classes/state/os/CommandState'),
|
'os.cmd': require('../classes/state/os/CommandState'),
|
||||||
'os.alive': require('../classes/state/os/IsAliveState'),
|
'os.alive': require('../classes/state/os/IsAliveState'),
|
||||||
@ -40,6 +41,8 @@ class StatesService extends Service {
|
|||||||
'service.daemon.reloaded': require('../classes/state/os/ServiceDaemonReloadState'),
|
'service.daemon.reloaded': require('../classes/state/os/ServiceDaemonReloadState'),
|
||||||
|
|
||||||
'web.download': require('../classes/state/fs/DownloadState'),
|
'web.download': require('../classes/state/fs/DownloadState'),
|
||||||
|
|
||||||
|
'node.yarn.installed': require('../classes/state/node/YarnInstallState'),
|
||||||
}
|
}
|
||||||
|
|
||||||
static get services() {
|
static get services() {
|
||||||
@ -57,7 +60,7 @@ class StatesService extends Service {
|
|||||||
|
|
||||||
const StepClass = this.constructor.#state_map[type]
|
const StepClass = this.constructor.#state_map[type]
|
||||||
if ( !StepClass ) throw new Error(`Invalid or unknown step type: ${type}`)
|
if ( !StepClass ) throw new Error(`Invalid or unknown step type: ${type}`)
|
||||||
this.app.make(StepClass)
|
this.app.di().inject(StepClass)
|
||||||
|
|
||||||
return new StepClass(host, state_config)
|
return new StepClass(host, state_config)
|
||||||
}
|
}
|
||||||
|
165
config/routines/example.config.js
Normal file
165
config/routines/example.config.js
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
const example_routine = {
|
||||||
|
type: 'checks',
|
||||||
|
hosts: ['localhost', 'other_server'],
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
// State: the file exists on the host
|
||||||
|
type: 'fs.file',
|
||||||
|
host: 'localhost', // localhost | other_server
|
||||||
|
path: '/tmp/some.file',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// State: the directory exists on the host
|
||||||
|
type: 'fs.directory',
|
||||||
|
host: 'localhost',
|
||||||
|
path: '/tmp/some.directory',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// State: the archive is unpacked into the destination
|
||||||
|
type: 'fs.unpack',
|
||||||
|
host: 'localhost',
|
||||||
|
path: '/tmp/some.zip', // or /tmp/some.tar.gz
|
||||||
|
destination: '/tmp/destination.dir',
|
||||||
|
format: 'zip', // Optional: tar | zip (by default, from file extension)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// State: the path is packed into a destination archive
|
||||||
|
type: 'fs.pack',
|
||||||
|
host: 'localhost',
|
||||||
|
path: '/tmp/some.directory/or.file',
|
||||||
|
destination: '/tmp/packed.tar.gz',
|
||||||
|
format: 'tar', // Optional: tar | zip (by default, from file extension)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// State: the fs permissions are set to the specified level
|
||||||
|
type: 'fs.permission',
|
||||||
|
host: 'localhost',
|
||||||
|
path: '/tmp/some.directory/or.file',
|
||||||
|
level: '644', // Some chmod permission level
|
||||||
|
recursive: true, // default: false,
|
||||||
|
revert_to: '555', // Permissions to revert to on undo, optional
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// State: the fs owners are set to the specified user/group
|
||||||
|
type: 'fs.ownership',
|
||||||
|
host: 'localhost',
|
||||||
|
owners: { // Specify one or both of the following:
|
||||||
|
user: 'some_user',
|
||||||
|
group: 'some_group',
|
||||||
|
},
|
||||||
|
path: '/tmp/some.file/or.dir',
|
||||||
|
recursive: true, // default: false
|
||||||
|
revert_to: {
|
||||||
|
user: 'root',
|
||||||
|
group: 'root',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// State: the source repo is cloned into the path
|
||||||
|
type: 'git.clone',
|
||||||
|
host: 'localhost',
|
||||||
|
path: '/tmp/some.dir.on.host',
|
||||||
|
source: 'https://git.host/some/repo',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// State: the specified ref is checked out
|
||||||
|
type: 'git.checkout',
|
||||||
|
host: 'localhost',
|
||||||
|
path: '/tmp/some.dir.on.host',
|
||||||
|
target: 'develop', // ref to check out
|
||||||
|
revert_to: 'master', // ref to revert to on undo
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// State: the specified repo is pulled
|
||||||
|
type: 'git.pull',
|
||||||
|
host: 'localhost',
|
||||||
|
path: '/tmp/some.dir.on.host',
|
||||||
|
target: {
|
||||||
|
target: 'origin',
|
||||||
|
ref: 'master',
|
||||||
|
rebase: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// State: the specified tag exists
|
||||||
|
type: 'git.tag',
|
||||||
|
host: 'localhost',
|
||||||
|
path: '/tmp/some.dir.on.host',
|
||||||
|
tag: 'rc-1', // tag to be created
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// State: the command has been run
|
||||||
|
type: 'os.cmd',
|
||||||
|
host: 'localhost',
|
||||||
|
cmd: 'useradd some_user', // Command to run on execute
|
||||||
|
reverse: 'userdel some_user', // Command to run on undo
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// State: the host is alive and responding
|
||||||
|
type: 'os.alive',
|
||||||
|
host: 'localhost',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// State: the package is installed on the host
|
||||||
|
type: 'package.present',
|
||||||
|
host: 'localhost',
|
||||||
|
package: 'gcc',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// State: the package is not installed on the host
|
||||||
|
type: 'package.absent',
|
||||||
|
host: 'localhost',
|
||||||
|
package: 'gcc',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// State: the packages are all up to date
|
||||||
|
type: 'package.updates',
|
||||||
|
host: 'localhost',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// State: the package cache has been cleared
|
||||||
|
type: 'package.cache.clear',
|
||||||
|
host: 'localhost',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// State: the specified service(s) is (are) running
|
||||||
|
type: 'service.running',
|
||||||
|
host: 'localhost',
|
||||||
|
service: 'apache2', // or array of services: ['apache2', 'redis']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// State: the specified service(s) is (are) stopped
|
||||||
|
type: 'service.stopped',
|
||||||
|
host: 'localhost',
|
||||||
|
service: 'apache2', // or array of services: ['apache2', 'redis']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// State: the specified service(s) has (have) been restarted
|
||||||
|
type: 'service.restarted',
|
||||||
|
host: 'localhost',
|
||||||
|
service: 'apache2', // or array of services: ['apache2', 'redis']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// State: the service daemon has been reloaded
|
||||||
|
type: 'service.daemon.reloaded',
|
||||||
|
host: 'localhost',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// State: the source file has been downloaded to the destination path
|
||||||
|
type: 'web.download',
|
||||||
|
host: 'localhost',
|
||||||
|
method: 'get', // HTTP verb
|
||||||
|
source: 'https://static.garrettmills.dev/assets/flitter/flitter_f.png',
|
||||||
|
path: '/tmp/flitter_f.png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// State: the packages for the given path are installed via Yarn
|
||||||
|
type: 'node.yarn.installed',
|
||||||
|
host: 'localhost',
|
||||||
|
path: '/home/user/path/to/project',
|
||||||
|
force: true, // default: false
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = exports = example_routine
|
@ -1,20 +0,0 @@
|
|||||||
const login_config = {
|
|
||||||
type: 'checks',
|
|
||||||
hosts: ['core', 'localhost', 'edge'],
|
|
||||||
steps: [
|
|
||||||
{
|
|
||||||
type: 'os.alive',
|
|
||||||
host: 'core',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'os.alive',
|
|
||||||
host: 'edge',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'os.alive',
|
|
||||||
host: 'localhost',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = exports = login_config
|
|
@ -1,18 +0,0 @@
|
|||||||
const tmpdir_config = {
|
|
||||||
type: 'checks',
|
|
||||||
hosts: ['core', 'localhost'],
|
|
||||||
steps: [
|
|
||||||
{
|
|
||||||
type: 'fs.directory',
|
|
||||||
host: 'core',
|
|
||||||
path: '/tmp/glmdev',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'fs.directory',
|
|
||||||
host: 'localhost',
|
|
||||||
path: '/tmp/glmdev',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = exports = tmpdir_config
|
|
@ -1,16 +0,0 @@
|
|||||||
const updates_config = {
|
|
||||||
type: 'checks',
|
|
||||||
hosts: ['core', 'localhost'],
|
|
||||||
steps: [
|
|
||||||
{
|
|
||||||
type: 'package.updates',
|
|
||||||
host: 'core',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'package.updates',
|
|
||||||
host: 'localhost',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = exports = updates_config
|
|
@ -21,7 +21,7 @@
|
|||||||
"flitter-cli": "^0.16.0",
|
"flitter-cli": "^0.16.0",
|
||||||
"flitter-di": "^0.5.0",
|
"flitter-di": "^0.5.0",
|
||||||
"flitter-flap": "^0.5.2",
|
"flitter-flap": "^0.5.2",
|
||||||
"libflitter": "^0.53.1",
|
"libflitter": "^0.54.0",
|
||||||
"moment": "^2.24.0",
|
"moment": "^2.24.0",
|
||||||
"scp": "^0.0.3",
|
"scp": "^0.0.3",
|
||||||
"ssh2": "^0.8.7",
|
"ssh2": "^0.8.7",
|
||||||
|
@ -2211,10 +2211,10 @@ leven@^1.0.2:
|
|||||||
resolved "https://registry.yarnpkg.com/leven/-/leven-1.0.2.tgz#9144b6eebca5f1d0680169f1a6770dcea60b75c3"
|
resolved "https://registry.yarnpkg.com/leven/-/leven-1.0.2.tgz#9144b6eebca5f1d0680169f1a6770dcea60b75c3"
|
||||||
integrity sha1-kUS27ryl8dBoAWnxpncNzqYLdcM=
|
integrity sha1-kUS27ryl8dBoAWnxpncNzqYLdcM=
|
||||||
|
|
||||||
libflitter@^0.53.1:
|
libflitter@^0.54.0:
|
||||||
version "0.53.1"
|
version "0.54.0"
|
||||||
resolved "https://registry.yarnpkg.com/libflitter/-/libflitter-0.53.1.tgz#30b1838763a228fba8b9c820d2cad501c3aa0117"
|
resolved "https://registry.yarnpkg.com/libflitter/-/libflitter-0.54.0.tgz#5758fd0069de0ed75a80385c66809078b8210c36"
|
||||||
integrity sha512-EK3okZyt0pmnpsZNx2lYOIcwgtmSOEPh4a5xE3pXM9RVc3dtXXscgJ5h9OvLTIN9WfRc7T5VTdpOjeAK6Xmysg==
|
integrity sha512-/pRgUD5dXdpEPxR6B3Do5BZQTGLrTYj3u0ZxYr7GViB4NysLePkR8Vg3gBzuNYUkEznzWxDR8VcmtuIiEowhFw==
|
||||||
dependencies:
|
dependencies:
|
||||||
colors "^1.3.3"
|
colors "^1.3.3"
|
||||||
connect-mongodb-session "^2.2.0"
|
connect-mongodb-session "^2.2.0"
|
||||||
|
Loading…
Reference in New Issue
Block a user