Add support for running routines via command line

master
garrettmills 2 years ago
parent afb35ebad8
commit f5b84b530c
No known key found for this signature in database
GPG Key ID: 6ACD58D6ADACFC6E
  1. 1
      .gitignore
  2. 1
      Units.flitter.js
  3. 17
      app/AppUnit.js
  4. 73
      app/classes/directive/Rollcall.js
  5. 100
      app/classes/directive/Routine.js
  6. 4
      app/classes/git/Repository.js
  7. 1
      app/classes/metal/Host.js
  8. 15
      app/classes/routine/Routine.js
  9. 48
      app/classes/routine/RunDirective.js
  10. 4
      app/classes/state/State.js
  11. 4
      app/classes/state/fs/DirectoryState.js
  12. 4
      app/classes/state/fs/DownloadState.js
  13. 4
      app/classes/state/fs/FileState.js
  14. 4
      app/classes/state/fs/OwnerState.js
  15. 4
      app/classes/state/fs/PackState.js
  16. 4
      app/classes/state/fs/PermissionState.js
  17. 4
      app/classes/state/fs/UnpackState.js
  18. 4
      app/classes/state/git/CheckoutState.js
  19. 4
      app/classes/state/git/CloneState.js
  20. 36
      app/classes/state/git/PullState.js
  21. 4
      app/classes/state/git/TagState.js
  22. 11
      app/classes/state/os/CommandState.js
  23. 4
      app/classes/state/os/IsAliveState.js
  24. 4
      app/classes/state/os/PackageAbsentState.js
  25. 4
      app/classes/state/os/PackageCacheClearedState.js
  26. 4
      app/classes/state/os/PackageState.js
  27. 4
      app/classes/state/os/ServiceDaemonReloadState.js
  28. 4
      app/classes/state/os/ServiceRestartState.js
  29. 4
      app/classes/state/os/ServiceState.js
  30. 4
      app/classes/state/os/ServiceStoppedState.js
  31. 4
      app/classes/state/os/UpdateState.js
  32. 3
      app/services/states.service.js
  33. 11
      config/routines/example.config.js

1
.gitignore vendored

@ -88,3 +88,4 @@ typings/
.dynamodb/
# End of https://www.gitignore.io/api/node
demo/*

@ -20,6 +20,7 @@ const FlitterUnits = {
'Config' : require('./app/compat/ConfigUnit'),
'Services' : require('./app/compat/ServicesUnit'),
'Utility' : require('libflitter/utility/UtilityUnit'),
'Misc' : require('./app/AppUnit'),
'Cli' : require('flitter-cli/CliUnit'),
'App' : require('flitter-cli/CliAppUnit'),
}

@ -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

@ -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

@ -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`)
}
async pull({ rebase = false, target = '', ref = '' } = {}) {
await this._git_cmd(`pull${target ? ' '+target : ''}${ref ? ' '+ref : ''}${rebase ? ' --rebase' : ''}`)
}
async checkout(ref = 'master') {
await this._git_cmd(`checkout "${ref}"`)
}

@ -80,6 +80,7 @@ class Host extends Injectable {
const result = await this.execute(`echo "${unique_id}"`)
return (result.exit_code === 0 && (result.clean_out.length > 0 && result.clean_out[0] === unique_id))
} catch (e) {
this.output.debug(e)
return false
}
}

@ -22,10 +22,14 @@ class Routine extends Injectable {
this._type = type
}
async execute() {
async execute(with_logging = false) {
const result = await this._build_result()
let step_no = 1
for ( const step of result.steps ) {
this.output.info(`(${step_no}/${result.steps.length}) ${step.step.display()}`, with_logging ? 0 : 10)
step_no += 1
if ( this._type === 'checks' ) {
step.status = (await step.step.check()) ? 'success' : 'fail'
step.message = step.status === 'success' ? 'Check passed.' : step.step.check_message()
@ -41,6 +45,12 @@ class Routine extends Injectable {
} else {
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'
@ -50,7 +60,8 @@ class Routine extends Injectable {
async _build_result() {
const 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)
steps.push(result)
}

@ -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() {
throw new ImplementationError()
}
display() {
return this.constructor.name
}
}
module.exports = exports = State

@ -34,6 +34,10 @@ class DirectoryState extends State {
check_message() {
return this.failure_message()
}
display() {
return `Ensure that the directory ${this._config.path} exists...`
}
}
module.exports = exports = DirectoryState

@ -54,6 +54,10 @@ class DownloadState extends State {
check_message() {
return this.failure_message()
}
display() {
return `Ensure that the file ${this._config.path} is downloaded from ${this._config.source}...`
}
}
module.exports = exports = DownloadState

@ -39,6 +39,10 @@ class FileState extends State {
check_message() {
return this.failure_message()
}
display() {
return `Ensure that the file ${this._config.path} exists...`
}
}
module.exports = exports = FileState

@ -46,6 +46,10 @@ class OwnerState extends State {
check_message() {
return this.failure_message()
}
display() {
return `Set filesystem owner of resource ${this._config.path}...`
}
}
module.exports = exports = OwnerState

@ -63,6 +63,10 @@ class PackState extends State {
check_message() {
return this.failure_message()
}
display() {
return `Archive the contents of ${this._config.path} to ${this._config.destination}...`
}
}
module.exports = exports = PackState

@ -42,6 +42,10 @@ class PermissionState extends State {
check_message() {
return this.failure_message()
}
display() {
return `Set filesystem permissions of resource ${this._config.path}...`
}
}
module.exports = exports = PermissionState

@ -66,6 +66,10 @@ class UnpackState extends State {
check_message() {
return this.failure_message()
}
display() {
return `Un-archive the contents of ${this._config.path} to ${this._config.destination}...`
}
}
module.exports = exports = UnpackState

@ -40,6 +40,10 @@ class CheckoutState extends AbstractGitState {
check_message() {
return this.failure_message()
}
display() {
return `Check out ref ${this._config.target} in repo ${this._config.path}...`
}
}
module.exports = exports = CheckoutState

@ -30,6 +30,10 @@ class CloneState extends AbstractGitState {
check_message() {
return this.failure_message()
}
display() {
return `Clone repo at ${this._config.source} to ${this._config.path}...`
}
}
module.exports = exports = CloneState

@ -0,0 +1,36 @@
const AbstractGitState = require('./AbstractGitState')
class PullState extends AbstractGitState {
static get services() {
return [...super.services, 'output']
}
async apply() {
if ( !(await this.check()) ) {
const repo = await this._repo()
await repo.pull(this._config.target)
}
}
async check() {
return true // 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() {
return this.failure_message()
}
display() {
return `Create tag ${this._config.tag} in repo ${this._config.path}...`
}
}
module.exports = exports = TagState

@ -1,17 +1,20 @@
const State = require('../State')
class CommandState extends State {
#ran_once = false
static get services() {
return [...super.services, 'output']
}
async apply() {
const cmd = `${this._config.cmd}`
await this._host.run(cmd)
const result = await this._host.run(cmd)
this.#ran_once = true
}
async check() {
return false
return this.#ran_once
}
async reverse() {
@ -30,6 +33,10 @@ class CommandState extends State {
check_message() {
return `The command check was not successful.`
}
display() {
return `Run the command: ${this._config.cmd}`
}
}
module.exports = exports = CommandState

@ -20,6 +20,10 @@ class IsAliveState extends State {
check_message() {
return this.failure_message()
}
display() {
return `Ensure that the host is alive...`
}
}
module.exports = exports = IsAliveState

@ -27,6 +27,10 @@ class PackageAbsentState extends State {
check_message() {
return this.failure_message()
}
display() {
return `Ensure that the package ${this._config.package} is not installed...`
}
}
module.exports = exports = PackageAbsentState

@ -31,6 +31,10 @@ class PackageCacheClearedState extends State {
check_message() {
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

@ -27,6 +27,10 @@ class PackageState extends State {
check_message() {
return this.failure_message()
}
display() {
return `Ensure that the package ${this._config.package} is installed...`
}
}
module.exports = exports = PackageState

@ -31,6 +31,10 @@ class ServiceDaemonReloadState extends State {
check_message() {
return `The service daemon on host "${this._host.name}" has not been reloaded.`
}
display() {
return `Reload the service daemon...`
}
}
module.exports = exports = ServiceDaemonReloadState

@ -32,6 +32,10 @@ class ServiceRestartState extends State {
check_message() {
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

@ -33,6 +33,10 @@ class ServiceState extends State {
check_message() {
return this.failure_message()
}
display() {
return `Ensure that the ${this._config.service} service is running...`
}
}
module.exports = exports = ServiceState

@ -32,6 +32,10 @@ class ServiceStoppedState extends State {
check_message() {
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

@ -32,6 +32,10 @@ class UpdateState extends State {
check_message() {
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

@ -25,6 +25,7 @@ class StatesService extends Service {
'git.clone': require('../classes/state/git/CloneState'),
'git.checkout': require('../classes/state/git/CheckoutState'),
'git.tag': require('../classes/state/git/TagState'),
'git.pull': require('../classes/state/git/PullState'),
'os.cmd': require('../classes/state/os/CommandState'),
'os.alive': require('../classes/state/os/IsAliveState'),
@ -57,7 +58,7 @@ class StatesService extends Service {
const StepClass = this.constructor.#state_map[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)
}

@ -69,6 +69,17 @@ const example_routine = {
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',

Loading…
Cancel
Save