Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Build Docker CLIs Your Users Will Love

DZone's Guide to

Build Docker CLIs Your Users Will Love

It's important to perform checks to give you intelligence on how your CLI container has been configured to work with Docker, the network, and your host.

· DevOps Zone
Free Resource

Download “The DevOps Journey - From Waterfall to Continuous Delivery” to learn learn about the importance of integrating automated testing into the DevOps workflow, brought to you in partnership with Sauce Labs.

After nine months of development, we recently released Eclipse Che 5. It contains a new CLI authored in Bash and Docker. We have discussed how to use Docker to create cross-platform CLIs and it was a surprisingly well-read with thousands of views.

In this release, we have simplified the starting of Eclipse Che to docker run eclipse/che start. It’s a deceptively simple syntax that led Mike Milinkovich, Executive Director of the Eclipse Foundation, to tweet, “That is one damn cool way to install an IDE!”

This syntax is beguilingly simple but required many months of engineering effort to achieve it. The CLI has a number of features including offline installation, cross-platform operation, debugging mode, backup and restore, and version upgrade management. Custom assemblies can inherit from the Che CLI to create their own. For example, Codenvy’s implementation of the CLI keeps the same syntax but adds additional magic around adding and removing nodes from a cluster — all with the same simple syntax of docker run codenvy/cli start.

We are particularly proud of this achievement and think the community can benefit from thinking about using Docker as not just a runtime execution engine, but also a vehicle to standardize how OS-specific software is installed in a cross-platform way.

Simplicity Usually Conquers Complexity

Docker’s client syntax is particularly well-structured for starting and stopping a single container and a single task. The Docker client performs three basic functions:

  1. Identifies the image that should be used to initiate the container and its runtime including an optional entry point or command override.
  2. Provides a flexible volume mounting mechanism to map files from the user’s host system to directories and files within the container. This volume mounting system can be used uni-directional or bi-directional, with the movement of files from the host into the container, or even from files already contained within the container to be placed onto the user’s host system.
  3. Provides a way to send configuration options into the new container. Some of these options define the runtime behavior of the container such as the commonly seen -it. Or, they can be user-provided options such as those passed in with -e VAR=VALUE or --env-file=<filewithmanyparams>.

So, with Docker’s syntax the power is there, but at the cost of simplicity. It is not uncommon to see commands to start a simple service turn into many line behemoths. This line, for example, is a valid way to start Eclipse Che’s core server using a custom assembly and a local workspace storage directory.

docker run -p:8080:8080 \           
  --name che \           
  --restart always \           
  -v /var/run/docker.sock:/var/run/docker.sock \           
  -v /C/tmp:/data \           
  -v /C/conf:/conf \           
  -v /home/my_assembly:/assembly \           
  -e DOCKER_MACHINE_HOST=172.17.0.1 \           
  -e DOCKER_HOST=tcp://1.1.1.1:2375 \           
     eclipse/che-server:5.0.0

While Eclipse Che is a developer workspace server, it’s intended as both a solution for teams and individuals. Some of our users are unfamiliar with Docker and maybe even be new to programming. The failure rate of typing such commands is quite high, and the Docker client is not forgiving nor entirely helpful in telling you want you did wrong if you make a typo.

The sort of syntax that Eclipse Che uses is not uncommon to most systems that make use of Docker. Suddenly, developers or admins must become proficient in Docker’s run syntax, familiar with the nuances of volume mounting and syntax errors on different operating systems, and even learn to diagnose configurations issues that affect Docker. This presses individuals into learning about docker inspect, a particularly powerful examination utility for those that relish understanding what is happening within their containers, but perhaps frightening for the rest of us.

The complexity of this solution is compounded because Eclipse Che is a PaaS offering, meaning that the workspaces that we create each have their own Dockerized runtimes that the Che server (which is also a Docker runtime) needs to manage.

This means that the Che server needs to have privileged access to the Docker daemon so that we can interact with it similarly to how the Docker client on your host interacts with the daemon for starting and stopping containers. Since Che is running inside of a container, we have to allow an inside-the-container mechanism for the Docker client (which we install inside of our Che container) to have access to the daemon. This means that users need to know to volume mount the Docker Unix socket -v /var/run/docker.sock:/var/run/docker.sock or to know how to pass in DOCKER_HOST with a TCP address for connecting to the Docker daemon. This is not intuitive nor easily understood by most end users.

Simplicity

What if we could give a syntax that was universally easy to see, understand, and remember? We could open up the cloud IDE to a much broader ecosystem. We knew wanted to stick with Docker as both a runtime and as a configurator and desktop installation tactic. Docker has made tremendous strides is providing cross-platform usability with VMs that run Docker on Windows, Mac and native execution on most Linux systems. With a common Docker client operating on each of those systems, a Docker CLI that runs on one OS will largely be identical on another.

docker run eclipse/che

We wanted this simple command to work for users. This would enable a single syntax that we could provide in documentation within our marketing pages and even small enough to fit into a tweet so that anyone, anywhere could get started with Che.

Guided Installation and Startup

At this time, it is not yet possible to add volume mounts and configuration variables after-the-fact into a container based on automatically guessing the configuration values, so we did the next best thing — providing guided feedback on exactly what additional options a user should enter to get the Che server running.

# Output abbreviated for readability
USAGE:
  docker run -it --rm <DOCKERPARAMS> eclipse/che:<version> [COMMAND]
MANDATORY DOCKER PARAMETERS:
  -v <LOCAL_PATH>:/data                Where user data saved
OPTIONAL DOCKER PARAMETERS:
  -e CHE_HOST=<YOUR_HOST>              IP address or hostname 
  -e CHE_PORT=<YOUR_PORT>              Port where che will bind
  -v <LOCAL_PATH>:/data/instance       Where user data will be saved
  -v <LOCAL_PATH>:/data/backup         Backup location
  -v <LOCAL_PATH>:/repo                Use local Che binaries
  -v <LOCAL_PATH>:/sync                Sync ws files to desktop IDE
COMMANDS:
  action <action-name>                 Start action on che instance
  backup                               
  config                               
  destroy                              
  dir <command>                        
  download                             
  help                                 This message
  info                                 
  init                                 
  offline                              
  restart                              Restart che services
  restore                       
  rmi                                  
  ssh <wksp-name> [machine-name]       
  start                                Starts che services
  stop                                 Stops che services
  sync <wksp-name>                     
  test <test-name>                     Start test on che instance
  upgrade                              
  version                              
GLOBAL COMMAND OPTIONS:
  --fast                               Skips networking checks
  --debug                              Debug che server

The ability to intercept a parameterless container run is fairly straightforward. Add an ENTRYPOINTto your Dockerfile that launches a script.

ENTRYPOINT ["/scripts/entrypoint.sh"]
if [[ $# == 0 ]]; then  
  usage;  
fi

In this particular case, the script that gets launched is the beginning of the CLI and /scripts/entrypoint.sh contains.

The first thing to check in starting up a container is to see if your container has a valid Docker client and can gain access to the Docker daemon.

There are three steps here:

1. First, check if the container has a valid Docker client with:

 hash docker 2>/dev/null && return 0 || return 1 

2. Second, check that the Docker client that you have installed in your container is at or lower than the daemon that it is attempting to connect to. Newer Docker clients cannot work with older daemons, but the reverse is true. docker ps will return a particular error message if this is true.

CHECK_VERSION=$(docker ps 2>&1 || true)
  if [[ "$CHECK_VERSION" = \
        *"Error response from daemon: client is newer"* ]]; then
    error "Error - Docker engine 1.11+ required."
    return 2;
  fi

3. Third, check that your Docker client has valid access to the Docker daemon on the host. This would be true if the user has volume was mounted /var/run/docker.sock as a socket connection or if they have bound it to TCP by setting DOCKER_HOST as an environment variable. Again, you can use docker ps to check for this outcome.

if [ -z "${DOCKER_HOST+x}" ]; then
   if ! docker ps > /dev/null 2>&1; then
     # Oh boy, if you got here, neither option is set!
   fi
fi

Proxies

If the Docker daemon has been configured to work with a proxy, then it makes sense that your CLI container should inherit that as well. Some languages, like the Java JVM, need special parameters or environment variables set within your container. If the Docker daemon has these values set and a user is able to reach the Internet with a docker pull <image> , then let's implement an approach to inherit those values and then reset them as you see fit within your CLI container. Proxy configuration of the host daemon is available with docker info.

OUTPUT=$(docker info)
HTTP_PROXY=$(grep "Http Proxy" <<< "$OUTPUT" || true) 

if [ ! -z "$HTTP_PROXY" ]; then
  HTTP_PROXY=${HTTP_PROXY#"Http Proxy: "}
else 
  HTTP_PROXY=""
fi

# Use HTTP_PROXY here to set internal values or configuration!
# Repeat for HTTPS_PROXY and / or NO_PROXY as well

Does your CLI need to have -it set (interactive mode) so that your end users can get streaming terminal output and have a more natural interactive experience? If so, you can detect if users have not set -it as you would like them to.

if [ ! -t 1 ]; then  
  warning "Did not detect TTY - interactive mode disabled"
fi

Check Mounts

One of Docker’s great strengths is that you can use volume mounts to pass data from the host into the container. You can also use them in the reverse, taking files that are contained within a Docker image and then when the container is activated copy them onto a host volume mount.

We have found that volume mounts are an essential part of defining a good Docker CLI. Many end users improperly type a volume mount (they are long and the syntax is confusing to a new user). So, it’s important to check that the minimum viable mounts that you require are present and that if they are used, that the directory mounted is valid, writable, and has the proper SELinux settings, if necessary.

Volume mounts can also be used as flags for triggering a series of configuration options within your CLI. For example, in Eclipse Che, a user can optionally volume mount :/repo with a directory that points to a Che git source repository. If that is volume mounted, our startup containers will use binaries that developers have built on disk instead of the ones that are within the images that are downloaded from DockerHub.

Che has a single mandatory volume mount to the :/data folder. If this folder is not mounted, we should stop and print out a helpful message for the end user.

DATA_MOUNT=$(get_container_folder ":/data)
if [[ "${DATA_MOUNT}" = "not set" ]]; then
  info "Welcome to $CHE_FORMAL_PRODUCT_NAME!"
  info ""
  info "We could not detect a location to save data."
  info "Volume mount a local directory to ':/data'."
  info ""
  info "Syntax:"
  # We print out some helpful messages here
   return 2;
fi

You can do some clever introspection of your container to determine which host folder is volume mounted to :/data. We can use docker inspect with some parsing to extract the volume mount given an index.

get_container_folder() {
  THIS_ID=$(hostname)
  FOLDER=$(get_container_host_bind_folder ":/data" $THIS_ID)
  echo "${FOLDER:=not set}"
}
get_container_host_bind_folder() {
  # BINDS in the format of var/run/docker.sock:/var/run/docker.sock 
  BINDS=$(docker inspect --format="{{.HostConfig.Binds}}" "${2}" \
            | cut -d '[' -f 2 | cut -d ']' -f 1)
  # Remove /var/run/docker.sock:/var/run/docker.sock
  VALUE=${BINDS/\/var\/run\/docker\.sock\:\/var\/run\/docker\.sock/}
  # Remove leading and trailing spaces
  VALUE2=$(echo "${VALUE}" | xargs)
  # VALUE2 here has all of the remaining volume mounts
  # Parse all of the volume mounts searching for :/data
  MOUNT=""
  IFS=$' '
  for SINGLE_BIND in $VALUE2; do
    case $SINGLE_BIND in
      *$1)
        MOUNT="${MOUNT} ${SINGLE_BIND}"
        echo "${MOUNT}" | cut -f1 -d":" | xargs
      ;;
      *)
        # Since we parse by space, if the next parameter is not a
        # colon, then this existing path is one with a space and
        # the aggregated path includes the space and next param.
        if [[ ${SINGLE_BIND} != *":"* ]]; then
          MOUNT="${MOUNT} ${SINGLE_BIND}"
        else
          MOUNT=""
        fi
      ;;
    esac
  done
}

If there is a volume mount, it will be returned. Otherwise, we return "not set".

Once you have checked all of the volume mounts, you can then parse the return values of each one and then if they are not set to a proper value, you can interrupt execution. Or, if they are set to a proper value, you can use their contents (or contents on the file system) to set the configuration of your CLI without asking the user for additional properties.

Writable Mounts

Once you have verified that all of the mounts exist and have names, you can then check to see if they are read-only, writable, or have other flags for SE Linux. In Eclipse Che, we need the :/data folder to be writable, so just perform a quick write test to a file, check for its presence, and then destroy it.

echo 'test' > ${CHE_CONTAINER_ROOT}/test
if [[ ! -f ${CHE_CONTAINER_ROOT}/test ]]; then
    error "Unable to write files to your host."
    error "Have you enabled Docker to allow host mounting?"
    error "Does CLI have rights to create files on your host?"
    return 2;
  fi
  rm -rf ${CHE_CONTAINER_ROOT}/test
}

Initialize Logging

Only once we have verified that the correct mandatory volume mounts exist and they are writable, we activate logging. We log every CLI method and action within a cli.log that is written into the host. Messages sent to the console prior to the previous tests are not written into the cli.log.

init_logging() {
  # Initialize CLI folder
  CLI_DIR=":/data"
  test -d "${CLI_DIR}" || mkdir -p "${CLI_DIR}"
  # Ensure logs folder exists
  LOGS="${CLI_DIR}/cli.log"
  LOG_INITIALIZED=true
  # Log date of CLI execution
  log "$(date)"
}

Network Mode or Offline Mode?

At this point in the bootstrap cycle of the CLI, we’ve verified that we have access to Docker and volume mounts. We can now begin testing for valid network connections or, if not, whether we are going to operate in an offline manner, without access to the Internet.

With the Eclipse Che CLI, the CLI is a wrapper for creating a proper Docker Compose configuration to run Eclipse Che, which is launched in a separate container from an image that is eclipse/che-server. This image is quite different from the eclipse/che image that was used to launch the CLI. In fact, there are dozens of different Docker images that Che depends upon. Some of these images are utility images, such as eclipse/che-ip:nightly for grabbing the host IP address, or core runtime images like eclipse/che-server.

We store a list of the images that we require (and their tags and digests) within the CLI. We need these images to be available before execution can continue.

There are two ways which images can be verified available:

  1. We can verify that a network is available and then pull the images from DockerHub.
  2. If in offline mode, we can check to see if the user has provided the images in TAR files and then use docker load to load those files into Docker images available to the CLI. We have a function in the CLI that you run in a DMZ to save all of the images that we require to TAR files with eclipse/che offline.

With Eclipse Che, any time a user passes --offline as a parameter to any command, we assume that the CLI is to work in offline mode. Otherwise, we assume that images will be checked locally and if not present then a DockerHub pull.

if is_offline; then
  info "init" "Importing Docker images from tars..."
  if [ ! -d ${CHE_OFFLINE_FOLDER} ]; then
    info "init" "'${CHE_CONTAINER_OFFLINE_FOLDER}' not found"
    return 2;
  fi

  IFS=$'\n'
  for file in "${CHE_CONTAINER_OFFLINE_FOLDER}"/*.tar
  do
    if ! $(docker load < \
          "${CHE_OFFLINE_FOLDER}"/"${file##*/}" > /dev/null); then
      error "Failed to restore Docker images"
      return 2;
    fi
  done
else
  # Networking mode
  info "cli" "Checking network..."
  local HTTP_STATUS_CODE=$(curl -I -k dockerhub.com -s -o \
                     /dev/null --write-out '%{http_code}')
  if [[ ! $HTTP_STATUS_CODE -eq "301" ]]; then
    info "We could not resolve DockerHub using DNS."
    return 2;
  fi
fi

At this stage, we can verify that all of the Docker images that we need have been loaded or can be pulled from Docker Hub. We can now make use of docker pull or other utilities that depend upon Docker images.

Prior to this point in the bootstrap process, we have used the Docker client many times, but none of the utilities require any other Docker images other than the one we are operating in! At this stage, the number of utilities available to us has broadened.

Within Che, there are two mandatory images that our CLI depends upon. There are many other Docker images that are needed to run Che, but the CLI doesn’t have these as mandatory images. For the mandatory images, we pull them directly and for the others (like eclipse/che-server) we pull those later in the bootstrap process by looking up an image registry list from a file (also stored in the CLI container!).

# If alpine:3.4 is not cached locally, then pull it.
if [ "$(docker images -q alpine:3.4 2> /dev/null)" = "" ]; then
  TEST=""
  docker pull ${UTILITY_IMAGE_ALPINE} > /dev/null 2>&1 || TEST=$?
  if [ "$TEST" = "1" ]; then
    error "Image alpine:3.4 unavailable. Not on dockerhub."
    return 2;
  fi
fi
# We repeat this for eclipse/che-ip:nightly, too

Valid IP Address and Port Available

Eclipse Che is a server, so it needs a valid (external) IP address and port to bind itself to. If you are authoring server applications, there may be a variety of host-level networking elements that you need to check.

Getting the IP address of your host is a tricky endeavor, as Docker may be running natively (on Linux) or inside of a VM (Mac / Windows). So, depending on how Docker is running, the tactics for determining your IP address needs to change.

We have built a utility called eclipse/che-ip:nightly that does a discovery based upon the operating system that you are on and makes the best guess for the external-facing IP address that is available to the system.

Now that this image has been pulled and we have verified network and host access, we can pull the host’s IP address.

GLOBAL_HOST_IP=$(docker_run --net host eclipse/che-ip:nightly)

Also, you don’t want your users to find out too late if ports that they require are not available on the host. With Che, we are going to launch a container that is exposed on the host at port 8080 and want to check that it is possible to bind to this port. In the case of Codenvy, the ports that our services bind to are seven, so we perform all of these checks at once across all ports.

port_open() {
  docker run -d -p $1:$1 --name fake alpine:3.4 \
             httpd -f -p $1 -h /etc/ > /dev/null 2>&1
  # If the return value = 125, then the port is in use
  NETSTAT_EXIT=$?
  docker rm -f fake > /dev/null 2>&1
  if [ $NETSTAT_EXIT = 125 ]; then
    return 1
  else
    return 0
  fi
}

Ready for Action!

That's it! At this point, you have performed all of the checks necessary to give you intelligence on how your CLI container has been configured to work with Docker, the network, and your host.

In Eclipse Che, at this stage, we perform some additional version checks, verify that the configuration file is present, and then initiate the command line parser to see which command was passed into the CLI for execution. All of the command methods that are provided within the CLI no longer have to perform any of these checks and can just focus on their execution.

The Eclipse Che CLI is inheritable. You can build your own CLIs using our CLI as a base. We’ll be discussing how to build custom CLIs using the Che base CLI in another article. Codenvy and ARTIK IDE have already been ported to use this inheritance and their syntax is identical to the Che CLI, inheriting its user-friendly flow and common command infrastructure.

You can get started now building your own CLI by inspecting what we have done:

  1. The Che CLI base infrastructure.
  2. The Che CLI.
  3. The Codenvy CLI.

Discover how to optimize your DevOps workflows with our cloud-based automated testing infrastructure, brought to you in partnership with Sauce Labs

Topics:
cli ,docker ,che ,devops ,tutorial ,containers

Published at DZone with permission of Tyler Jewell, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

THE DZONE NEWSLETTER

Dev Resources & Solutions Straight to Your Inbox

Thanks for subscribing!

Awesome! Check your inbox to verify your email so you can start receiving the latest in tech news and resources.

X

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}