/// <summary> /// Constructs a configuration step that executes a command under <b>sudo</b> /// on a specific Docker node. /// </summary> /// <param name="nodeName">The Docker node name.</param> /// <param name="command">The Linux command.</param> /// <param name="args">The command arguments.</param> /// <remarks> /// <note> /// You can add <see cref="CommandBundle.ArgBreak"/> as one of the arguments. This is /// a meta argument that indicates that the following non-command line option /// is not to be considered to be the value for the previous command line option. /// This is a formatting hint for <see cref="ToBash(string)"/> and will /// not be included in the command itself. /// </note> /// </remarks> private CommandStep(string nodeName, string command, params object[] args) { Covenant.Requires <ArgumentNullException>(!string.IsNullOrEmpty(nodeName)); Covenant.Requires <ArgumentNullException>(!string.IsNullOrEmpty(command)); Covenant.Requires <ArgumentNullException>(args != null); this.nodeName = nodeName; this.commandBundle = new CommandBundle(command, args); }
/// <summary> /// Ensures that the Docker <b>config.json</b> file for the node's root /// user matches that for the sysadmin user. /// </summary> private void SyncDockerConf(SshProxy <NodeDefinition> node) { // We also need to manage the login for the [root] account due // to issue // // https://github.com/jefflill/NeonForge/issues/265 // $hack(jeff.lill): // // We're simply going ensure that the [/root/.docker/config.json] // file matches the equivalent file for the node sysadmin account, // removing the root file if this was deleted for sysadmin. // // This is a bit of a hack because it assumes that the Docker config // for the root and sysadmin account never diverge, which is probably // a reasonable assumption given that these are managed hosts. // // We're also going to ensure that these directories and files have the // correct owners and permissions. var bundle = new CommandBundle("./sync.sh"); bundle.AddFile("sync.sh", $@"#!/bin/bash if [ ! -d /root/.docker ] ; then mkdir -p /root/.docker fi if [ -f /home/{node.Username}/.docker/config.json ] ; then cp /home/{node.Username}/.docker/config.json /root/.docker/config.json else if [ -f /root/.docker/config.json ] ; then rm /root/.docker/config.json fi fi if [ -d /root/.docker ] ; then chown -R root:root /root/.docker chmod 660 /root/.docker/* fi if [ -d /home/{node.Username}/.docker ] ; then chown -R {node.Username}:{node.Username} /home/{node.Username}/.docker chmod 660 /home/{node.Username}/.docker/* fi ", isExecutable: true); var response = node.SudoCommand(bundle); if (response.ExitCode != 0) { throw new HiveException(response.ErrorSummary); } }
/// <summary> /// Creates or updates a hive Docker binary config. /// </summary> /// <param name="configName">The config name.</param> /// <param name="value">The config value.</param> /// <param name="options">Optional command run options.</param> /// <exception cref="HiveException">Thrown if the operation failed.</exception> public void Set(string configName, byte[] value, RunOptions options = RunOptions.None) { Covenant.Requires <ArgumentException>(HiveDefinition.IsValidName(configName)); Covenant.Requires <ArgumentNullException>(value != null); var bundle = new CommandBundle("./create-config.sh"); bundle.AddFile("config.dat", value); bundle.AddFile("create-config.sh", $@"#!/bin/bash if docker config inspect {configName} ; then echo ""Config already exists; not setting it again."" else cat config.dat | docker config create {configName} - # It appears that Docker configs may not be available # immediately after they are created. So, we're going # poll for a while until we can inspect the new config. count=0 while [ $count -le 30 ] do if docker config inspect {configName} ; then exit 0 fi sleep 1 count=$(( count + 1 )) done echo ""Created config [{configName}] is not ready after 30 seconds."" >&2 exit 1 fi ", isExecutable: true); var response = hive.GetReachableManager().SudoCommand(bundle, options); if (response.ExitCode != 0) { throw new HiveException(response.ErrorSummary); } }
/// <inheritdoc/> public override string ToString() { var sb = new StringBuilder(); sb.Append($"{commandBundle.Command}"); foreach (var arg in CommandBundle.NormalizeArgs(commandBundle.Args)) { sb.AppendWithSeparator(arg); } if (commandBundle.Count > 0) { sb.Append($" [files={commandBundle.Count}]"); } return(sb.ToString()); }
/// <summary> /// Removes then local Docker registry from the hive. /// </summary> /// <param name="progress">Optional action that will be called with a progress message.</param> /// <exception cref="HiveException">Thrown if no registry is deployed or there was an error removing it.</exception> public void PruneLocalRegistry(Action <string> progress = null) { // We're going to upload a script to one of the managers that handles // putting the [neon-registry] service into READ-ONLY mode, running // the garbage collection container and then restoring [neon-registry] // to READ/WRITE mode. // // The nice thing about this is that the operation will continue to // completion on the manager node even if we lose the SSH connection. var manager = hive.GetReachableManager(); var updateScript = @"#!/bin/bash # Update [neon-registry] to READ-ONLY mode: docker service update --env-rm READ_ONLY --env-add READ_ONLY=true neon-registry # Prune the registry: docker run \ --name neon-registry-prune \ --restart-condition=none \ --mount type=volume,src=neon-registry,volume-driver=neon,dst=/var/lib/neon-registry \ nhive/neon-registry garbage-collect # Restore [neon-registry] to READ/WRITE mode: docker service update --env-rm READ_ONLY --env-add READ_ONLY=false neon-registry "; var bundle = new CommandBundle("./collect.sh"); bundle.AddFile("collect.sh", updateScript, isExecutable: true); progress?.Invoke("Registry prune started."); var pruneResponse = manager.SudoCommand(bundle, RunOptions.None); if (pruneResponse.ExitCode != 0) { throw new HiveException($"The prune operation failed. The registry may be running in READ-ONLY mode: {pruneResponse.ErrorText}"); } progress?.Invoke("Registry prune completed."); }
/// <summary> /// Sets a Vault access control policy. /// </summary> /// <param name="policy">The policy.</param> /// <returns>The command response.</returns> public CommandResponse SetPolicy(VaultPolicy policy) { Covenant.Requires <ArgumentNullException>(policy != null); VerifyToken(); var bundle = new CommandBundle("./create-vault-policy.sh"); bundle.AddFile("create-vault-policy.sh", $@"#!/bin/bash export VAULT_TOKEN={hive.HiveLogin.VaultCredentials.RootToken} vault policy-write {policy.Name} policy.hcl ", isExecutable: true); bundle.AddFile("policy.hcl", policy); var response = hive.GetReachableManager().SudoCommand(bundle, hive.SecureRunOptions | RunOptions.FaultOnError); response.BashCommand = bundle.ToBash(); return(response); }
/// <summary> /// Executes a command on a specific hive manager node using the root Vault token. /// </summary> /// <param name="manager">The target manager.</param> /// <param name="command">The command (including the <b>vault</b>).</param> /// <param name="args">The optional arguments.</param> /// <returns>The command response.</returns> /// <remarks> /// <note> /// This method does not fault or throw an exception if the command returns /// a non-zero exit code. /// </note> /// </remarks> public CommandResponse CommandNoFault(SshProxy <NodeDefinition> manager, string command, params object[] args) { Covenant.Requires <ArgumentNullException>(manager != null); Covenant.Requires <ArgumentNullException>(command != null); VerifyToken(); var scriptBundle = new CommandBundle(command, args); var bundle = new CommandBundle("./vault-command.sh"); bundle.AddFile("vault-command.sh", $@"#!/bin/bash export VAULT_TOKEN={hive.HiveLogin.VaultCredentials.RootToken} {scriptBundle} ", isExecutable: true); var response = manager.SudoCommand(bundle, hive.SecureRunOptions); response.BashCommand = bundle.ToBash(); return(response); }
/// <summary> /// Deletes a hive Docker config. /// </summary> /// <param name="configName">The config name.</param> /// <param name="options">Optional command run options.</param> /// <exception cref="HiveException">Thrown if the operation failed.</exception> public void Remove(string configName, RunOptions options = RunOptions.None) { Covenant.Requires <ArgumentException>(HiveDefinition.IsValidName(configName)); var bundle = new CommandBundle("./delete-config.sh"); bundle.AddFile("delete-config.sh", $@"#!/bin/bash docker config inspect {configName} if [ ""$?"" != ""0"" ] ; then echo ""Config doesn't exist."" else docker config rm {configName} fi ", isExecutable: true); var response = hive.GetReachableManager().SudoCommand(bundle, RunOptions.None); if (response.ExitCode != 0) { throw new HiveException(response.ErrorSummary); } }