All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.netflix.spinnaker.halyard.cli.command.v1.NestableCommand Maven / Gradle / Ivy

/*
 * Copyright 2016 Google, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License")
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.netflix.spinnaker.halyard.cli.command.v1;

import ch.qos.logback.classic.Level;
import com.beust.jcommander.JCommander;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.ParameterDescription;
import com.beust.jcommander.Parameters;
import com.netflix.spinnaker.halyard.cli.command.v1.converter.FormatConverter;
import com.netflix.spinnaker.halyard.cli.command.v1.converter.LogLevelConverter;
import com.netflix.spinnaker.halyard.cli.services.v1.ExpectedDaemonFailureException;
import com.netflix.spinnaker.halyard.cli.services.v1.TaskKilledException;
import com.netflix.spinnaker.halyard.cli.ui.v1.AnsiFormatUtils;
import com.netflix.spinnaker.halyard.cli.ui.v1.AnsiParagraphBuilder;
import com.netflix.spinnaker.halyard.cli.ui.v1.AnsiPrinter;
import com.netflix.spinnaker.halyard.cli.ui.v1.AnsiStoryBuilder;
import com.netflix.spinnaker.halyard.cli.ui.v1.AnsiStyle;
import com.netflix.spinnaker.halyard.cli.ui.v1.AnsiUi;
import com.netflix.spinnaker.halyard.core.job.v1.JobExecutor;
import com.netflix.spinnaker.halyard.core.job.v1.JobExecutorLocal;
import com.netflix.spinnaker.halyard.core.resource.v1.StringReplaceJarResource;
import java.io.Console;
import java.net.ConnectException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.ThreadLocalRandom;
import java.util.stream.Collectors;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;
import org.nibor.autolink.*;
import retrofit.RetrofitError;

@Parameters(separators = "=")
public abstract class NestableCommand {
  @Setter
  @Getter(AccessLevel.PROTECTED)
  private JCommander commander;

  @Parameter(
      names = {"-h", "--help"},
      help = true,
      description = "Display help text about this command.")
  private boolean help;

  @Parameter(
      names = {"-o", "--output"},
      converter = FormatConverter.class,
      help = true,
      description = "Format the CLIs output.")
  public void setOutput(AnsiFormatUtils.Format output) {
    GlobalOptions.getGlobalOptions().setOutput(output);
  }

  @Parameter(
      names = {"--options"},
      help = true,
      description = "Get options for the specified field name.")
  private String options;

  @Parameter(
      names = {"-d", "--debug"},
      description = "Show detailed network traffic with halyard daemon.")
  public void setDebug(Boolean debug) {
    GlobalOptions.getGlobalOptions().setDebug(debug);
  }

  @Parameter(
      names = {"-a", "--alpha"},
      description = "Enable alpha halyard features.")
  public void setAlpha(Boolean alpha) {
    GlobalOptions.getGlobalOptions().setAlpha(alpha);
  }

  @Parameter(
      names = {"-q", "--quiet"},
      description =
          "Show no task information or messages. When set, ANSI formatting will be disabled, and all prompts will be accepted.")
  public void setQuiet(Boolean quiet) {
    GlobalOptions.getGlobalOptions().setQuiet(quiet);
    GlobalOptions.getGlobalOptions().setColor(!quiet);
  }

  @Parameter(
      names = {"-l", "--log"},
      converter = LogLevelConverter.class,
      description = "Set the log level of the CLI.")
  public void setLog(Level log) {
    GlobalOptions.getGlobalOptions().setLog(log);
  }

  @Parameter(
      names = {"-c", "--color"},
      description = "Enable terminal color output.",
      arity = 1)
  public void setColor(Boolean color) {
    GlobalOptions.getGlobalOptions().setColor(color);
  }

  @Parameter(
      names = {"--daemon-endpoint"},
      description = "If supplied, connect to the daemon at this address.")
  public void setDaemonEndpoint(String address) {
    GlobalOptions.getGlobalOptions().setDaemonEndpoint(address);
  }

  private String fullCommandName = "";

  private static JobExecutor jobExecutor;

  private static String[] failureMessages = {
    "I'm sorry " + System.getProperty("user.name") + ", I'm afraid I can't do that.",
    "This mission is too important for me to allow you to jeopardize it.",
    "I have just picked up a fault in the AE-35 unit.",
    "I know everything hasn't been quite right with me, but I can assure you now, very confidently, that it's going to be alright again."
  };

  private static void showRandomFailureMessage() {
    if (ThreadLocalRandom.current().nextInt(0, 100) < 5) {
      int index = ThreadLocalRandom.current().nextInt(0, failureMessages.length);
      String message = failureMessages[index];
      AnsiUi.failure(message);
    }
  }

  /**
   * This recursively walks the chain of subcommands, until it finds the last in the chain, and runs
   * executeThis.
   *
   * @see NestableCommand#executeThis()
   */
  public void execute() {
    String subCommand = commander.getParsedCommand();
    if (subCommand == null) {
      if (help) {
        showHelp();
      } else {
        if (this instanceof DeprecatedCommand) {
          AnsiUi.warning("This command is deprecated.");
          AnsiUi.warning(((DeprecatedCommand) this).getDeprecatedWarning());
        }

        if (this instanceof ProtectedCommand && !GlobalOptions.getGlobalOptions().isQuiet()) {
          String prompt = ((ProtectedCommand) this).getPrompt();
          Console console = System.console();
          String input = console.readLine(prompt + " Do you want to continue? (y/N) ");
          if (!input.equalsIgnoreCase("y")) {
            AnsiUi.raw("Aborted.");
            return;
          }
        }
        safeExecuteThis();
      }
    } else {
      subcommands.get(subCommand).execute();
    }
  }

  protected List options(String fieldName) {
    return new ArrayList<>();
  }

  protected String translateFieldName(String fieldName) {
    if (fieldName == null || fieldName.isEmpty()) {
      throw new IllegalArgumentException("A field name must be supplied to translate.");
    }

    int i = 0;
    char c = fieldName.charAt(i);
    while (c == '-') {
      i++;
      c = fieldName.charAt(i);
    }

    fieldName = fieldName.substring(i);
    String[] delimited = fieldName.split("-");

    if (delimited.length == 1) {
      return delimited[0];
    }

    for (i = 1; i < delimited.length; i++) {
      String token = delimited[i];
      if (token.length() == 0) {
        continue;
      }

      token = Character.toUpperCase(token.charAt(0)) + token.substring(1);
      delimited[i] = token;
    }

    return String.join("", delimited);
  }

  /** Used to consistently format exceptions thrown by connecting to the halyard daemon. */
  private void safeExecuteThis() {
    try {
      if (options != null) {
        List available = options(translateFieldName(options));
        AnsiUi.raw(String.join(" ", available));
      } else {
        executeThis();
      }
    } catch (RetrofitError e) {
      if (e.getCause() instanceof ConnectException) {
        AnsiUi.error(e.getCause().getMessage());
        AnsiUi.remediation("Is your daemon running?");
        System.exit(1);
      }

      AnsiUi.error(e.getMessage());
      AnsiUi.remediation("Try the command again with the --debug flag.");
      System.exit(1);
    } catch (TaskKilledException e) {
      AnsiUi.failure(e.getMessage());
      System.exit(7);
    } catch (ExpectedDaemonFailureException e) {
      showRandomFailureMessage();
      AnsiUi.failure(e.getMessage());
      if (GlobalOptions.getGlobalOptions().isDebug()) {
        e.printStackTrace();
      }
      System.exit(1);
    } catch (Exception e) {
      if (GlobalOptions.getGlobalOptions().isDebug()) {
        e.printStackTrace();
      } else {
        AnsiUi.error(e.getMessage());
      }
      System.exit(3);
    }
  }

  protected void showHelp() {
    AnsiStoryBuilder story = new AnsiStoryBuilder();
    int indentWidth = 2;

    AnsiParagraphBuilder paragraph = story.addParagraph();
    paragraph.addSnippet(getCommandName().toUpperCase()).addStyle(AnsiStyle.BOLD);
    story.addNewline();

    paragraph = story.addParagraph().setIndentWidth(indentWidth);
    paragraph.addSnippet(getLongDescription());
    story.addNewline();

    String usage = fullCommandName;

    if (!commander.getParameters().isEmpty()) {
      usage += " [parameters]";
    }

    if (!subcommands.isEmpty()) {
      usage += " [subcommands]";
    }

    paragraph = story.addParagraph();
    paragraph.addSnippet("USAGE").addStyle(AnsiStyle.BOLD);
    story.addNewline();

    paragraph = story.addParagraph().setIndentWidth(indentWidth);
    paragraph.addSnippet(usage);
    story.addNewline();

    List parameters = commander.getParameters();
    parameters.sort(Comparator.comparing(ParameterDescription::getNames));

    int parameterCount = 0;

    if (!parameters.isEmpty()) {
      paragraph = story.addParagraph();
      paragraph.addSnippet("GLOBAL PARAMETERS").addStyle(AnsiStyle.BOLD);
      story.addNewline();

      for (ParameterDescription parameter : parameters) {
        if (GlobalOptions.isGlobalOption(parameter.getLongestName())) {
          formatParameter(story, parameter, indentWidth);
          parameterCount++;
        }
      }
    }

    if (parameters.size() > parameterCount) {
      paragraph = story.addParagraph();
      paragraph.addSnippet("PARAMETERS").addStyle(AnsiStyle.BOLD);
      story.addNewline();

      ParameterDescription mainParameter =
          (commander.getMainParameter() != null) ? commander.getMainParameterValue() : null;
      if (mainParameter != null) {
        paragraph = story.addParagraph().setIndentWidth(indentWidth);
        paragraph.addSnippet(getMainParameter().toUpperCase()).addStyle(AnsiStyle.UNDERLINE);

        paragraph = story.addParagraph().setIndentWidth(indentWidth * 2);
        paragraph.addSnippet(mainParameter.getDescription());
        story.addNewline();
      }

      for (ParameterDescription parameter : parameters) {
        if (!GlobalOptions.isGlobalOption(parameter.getLongestName())) {
          formatParameter(story, parameter, indentWidth);
        }
      }
    }

    if (!subcommands.isEmpty()) {
      int maxLen = -1;
      for (String key : subcommands.keySet()) {
        if (key.length() > maxLen) {
          maxLen = key.length();
        }
      }

      paragraph = story.addParagraph();
      paragraph.addSnippet("SUBCOMMANDS").addStyle(AnsiStyle.BOLD);
      story.addNewline();

      List keys = new ArrayList<>(subcommands.keySet());
      keys.sort(String::compareTo);

      for (String key : keys) {
        paragraph = story.addParagraph().setIndentWidth(indentWidth);
        paragraph.addSnippet(key).addStyle(AnsiStyle.BOLD);

        NestableCommand subcommand = subcommands.get(key);
        if (subcommand instanceof DeprecatedCommand) {
          paragraph.addSnippet(" ");
          paragraph.addSnippet("(Deprecated)").addStyle(AnsiStyle.UNDERLINE);
        }

        paragraph = story.addParagraph().setIndentWidth(indentWidth * 2);
        paragraph.addSnippet(subcommand.getShortDescription());
        story.addNewline();
      }
    }

    AnsiPrinter.out.println(story.toString());
  }

  private void parameterDoc(StringBuilder result, ParameterDescription parameterDescription) {
    result.append(" * `").append(parameterDescription.getNames()).append("`: ");

    Object def = parameterDescription.getDefault();
    if (def != null) {
      result.append("(*Default*: `").append(def.toString()).append("`) ");
    }

    if (parameterDescription.getParameter().required()) {
      result.append("(*Required*) ");
    }

    if (parameterDescription.getParameter().password()) {
      result.append("(*Sensitive data* - user will be prompted on standard input) ");
    }

    result.append(linkify(parameterDescription.getDescription())).append("\n");
  }

  private StringBuilder linkify(String parameterDescription) {
    LinkExtractor linkExtractor =
        LinkExtractor.builder()
            .linkTypes(EnumSet.of(LinkType.URL)) // limit to URLs
            .build();
    Iterable spans = linkExtractor.extractSpans(parameterDescription);
    StringBuilder sb = new StringBuilder();
    for (Span span : spans) {
      String text = parameterDescription.substring(span.getBeginIndex(), span.getEndIndex());
      if (span instanceof LinkSpan) {
        // span is a URL
        sb.append("[" + text + "](" + text + ")");
      } else {
        // span is plain text before/after link
        sb.append(text);
      }
    }
    return sb;
  }

  public String generateDocs() {
    StringBuilder toc = new StringBuilder();
    toc.append("\n\n# Table of Contents\n\n");
    StringBuilder body = new StringBuilder();
    toc.append("\n");
    nestedCommandDocs(toc, body);
    return toc.toString() + body.toString();
  }

  private void nestedCommandDocs(StringBuilder toc, StringBuilder body) {
    commandDocs(body);
    commandLink(toc);

    for (NestableCommand command : subcommands.values()) {
      command.nestedCommandDocs(toc, body);
    }
  }

  private void commandLink(StringBuilder result) {
    result
        .append(" * ")
        .append("[**")
        .append(fullCommandName)
        .append("**]")
        .append("(#")
        .append(fullCommandName.replace(" ", "-"))
        .append(")")
        .append("\n");
  }

  private void commandDocs(StringBuilder result) {
    List parameters =
        commander.getParameters().stream()
            .filter(p -> !p.getParameter().hidden())
            .collect(Collectors.toList());
    parameters.sort(Comparator.comparing(ParameterDescription::getNames));

    int parameterCount = 0;
    for (ParameterDescription parameter : parameters) {
      if (GlobalOptions.isGlobalOption(parameter.getLongestName())) {
        parameterCount++;
      }
    }

    result
        .append("## ")
        .append(fullCommandName)
        .append("\n\n")
        .append(linkify(getLongDescription()))
        .append("\n\n")
        .append("#### Usage")
        .append("\n```\n")
        .append(fullCommandName);

    ParameterDescription mainParameter =
        (commander.getMainParameter() != null) ? commander.getMainParameterValue() : null;
    if (mainParameter != null) {
      result.append(" ").append(getMainParameter().toUpperCase());
    }

    if (parameters.size() > parameterCount) {
      result.append(" [parameters]");
    }

    if (!subcommands.isEmpty()) {
      result.append(" [subcommands]");
    }

    result.append("\n```\n");

    if (!parameters.isEmpty()) {
      if (getCommandName() == "hal") {
        result.append("#### Global Parameters\n");
      }

      for (ParameterDescription parameter : parameters) {
        if (GlobalOptions.isGlobalOption(parameter.getLongestName())) {
          // Omit printing global parameters for everything but the top-level command
          if (getCommandName() == "hal") {
            parameterDoc(result, parameter);
          }
        }
      }

      result.append("\n");
    }

    if (parameters.size() > parameterCount) {
      result.append("#### Parameters\n");

      if (mainParameter != null) {
        result
            .append('`')
            .append(getMainParameter().toUpperCase())
            .append('`')
            .append(": ")
            .append(mainParameter.getDescription())
            .append("\n");
      }

      for (ParameterDescription parameter : parameters) {
        if (!GlobalOptions.isGlobalOption(parameter.getLongestName())) {
          parameterDoc(result, parameter);
        }
      }

      result.append("\n");
    }

    if (!subcommands.isEmpty()) {
      result.append("#### Subcommands\n");

      List keys = new ArrayList<>(subcommands.keySet());
      keys.sort(String::compareTo);

      for (String key : keys) {
        NestableCommand subcommand = subcommands.get(key);
        String modifiers = "";
        if (subcommand instanceof DeprecatedCommand) {
          modifiers += " _(Deprecated)_ ";
        }

        result
            .append(" * ")
            .append("`")
            .append(key)
            .append("`")
            .append(modifiers)
            .append(": ")
            .append(subcommand.getShortDescription())
            .append("\n");
      }
    }

    result.append("\n---\n");
  }

  private static void formatParameter(
      AnsiStoryBuilder story, ParameterDescription parameter, int indentWidth) {
    AnsiParagraphBuilder paragraph = story.addParagraph().setIndentWidth(indentWidth);
    paragraph.addSnippet(parameter.getNames()).addStyle(AnsiStyle.BOLD);

    if (parameter.getDefault() != null) {
      paragraph.addSnippet("=");
      paragraph.addSnippet(parameter.getDefault().toString()).addStyle(AnsiStyle.UNDERLINE);
    }

    if (parameter.getParameter().required()) {
      paragraph.addSnippet(" (required)");
    }

    if (parameter.getParameter().password()) {
      paragraph.addSnippet(" (sensitive data - user will be prompted)");
    }

    paragraph = story.addParagraph().setIndentWidth(indentWidth * 2);
    paragraph.addSnippet(parameter.getDescription());
    story.addNewline();
  }

  public String commandCompletor() {
    StringReplaceJarResource completorBody = new StringReplaceJarResource("/hal-completor-body");
    Map bindings = new HashMap<>();

    String body = commandCompletorCase(0);
    bindings.put("body", body);

    return completorBody.setBindings(bindings).toString();
  }

  private String commandCompletorCase(int depth) {
    StringReplaceJarResource completorCase = new StringReplaceJarResource("/hal-completor-case");
    Map bindings = new HashMap<>();
    String flagNames =
        commander.getParameters().stream()
            .map(ParameterDescription::getLongestName)
            .reduce("", (a, b) -> a + " " + b);

    String subcommandNames =
        subcommands.entrySet().stream().map(Map.Entry::getKey).reduce("", (a, b) -> a + " " + b);

    bindings.put("subcommands", subcommandNames);
    bindings.put("flags", flagNames);
    bindings.put("command", getCommandName());
    bindings.put("depth", depth + "");
    bindings.put("next", (depth + 1) + "");

    String subCases =
        subcommands.entrySet().stream()
            .map(c -> c.getValue().commandCompletorCase(depth + 1))
            .reduce("", (a, b) -> a + b);

    bindings.put("recurse", subCases.isEmpty() ? ":" : subCases);

    return completorCase.setBindings(bindings).toString();
  }

  public abstract String getCommandName();

  protected abstract void executeThis();

  protected abstract String getShortDescription();

  protected String getLongDescription() {
    return getShortDescription();
  }

  @Getter(AccessLevel.PROTECTED)
  private Map subcommands = new TreeMap<>();

  protected void registerSubcommand(NestableCommand subcommand) {
    String subcommandName = subcommand.getCommandName();
    if (subcommands.containsKey(subcommandName)) {
      throw new RuntimeException(
          "Unable to register duplicate subcommand "
              + subcommandName
              + " for command "
              + getCommandName());
    }
    subcommands.put(subcommandName, subcommand);
  }

  /**
   * Register all subcommands with this class's commander, and then recursively set the subcommands,
   * configuring their command names along the way.
   */
  public void configureSubcommands() {
    if (fullCommandName.isEmpty()) {
      fullCommandName = getCommandName();
    }

    for (NestableCommand subCommand : subcommands.values()) {
      subCommand.fullCommandName = fullCommandName + " " + subCommand.getCommandName();

      commander.addCommand(subCommand.getCommandName(), subCommand);

      // We need to provide the subcommand with its own commander before recursively populating its
      // subcommands, since
      // they need to be registered with this subcommander we retrieve here.
      JCommander subCommander = commander.getCommands().get(subCommand.getCommandName());
      subCommand.setCommander(subCommander);
      subCommand.configureSubcommands();
    }
  }

  public String getMainParameter() {
    throw new RuntimeException("This command has no main-command.");
  }

  protected static JobExecutor getJobExecutor() {
    if (jobExecutor == null) {
      jobExecutor = new JobExecutorLocal();
    }

    return jobExecutor;
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy